├── .dockerignore
├── .editorconfig
├── .eslintrc.js
├── .github
├── CODEOWNERS
├── CONTRIBUTING.md
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── pull_request_template.md
├── styles
│ ├── Rules
│ │ ├── BritishEnglish.yml
│ │ ├── FutureTense.yml
│ │ ├── HeaderGerunds.yml
│ │ ├── InclusionGenderCulture.yml
│ │ └── OxfordComma.yml
│ └── config
│ │ └── vocabularies
│ │ └── Rules
│ │ ├── accept.txt
│ │ └── reject.txt
└── workflows
│ ├── docs-tests.yaml
│ ├── e2e-tests.yml
│ ├── main.yml
│ ├── publish.yml
│ └── unit-tests.yml
├── .gitignore
├── .husky
└── pre-commit
├── .markdownlint.yaml
├── .mlc.toml
├── .npmignore
├── .prettierignore
├── .vale.ini
├── CHANGELOG.md
├── LICENSE
├── README.md
├── benchmark
├── benchmark.js
├── index.html
└── index.tsx
├── config
├── docker
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-run.sh
│ ├── hooks
│ │ └── build
│ ├── index.tpl.html
│ └── nginx.conf
└── webpack-utils.ts
├── custom.d.ts
├── cypress.config.ts
├── demo
├── ComboBox.tsx
├── big-openapi.json
├── components
│ └── FileInput.tsx
├── favicon.png
├── index.html
├── index.tsx
├── museum-logo.png
├── museum.yaml
├── openapi-3-1.yaml
├── openapi.yaml
├── petstore-logo.png
├── playground
│ ├── hmr-playground.tsx
│ └── index.html
├── redoc-demo.png
├── ssr
│ └── index.ts
├── swagger.yaml
└── webpack.config.ts
├── docs
├── config.md
├── deployment
│ ├── cli.md
│ ├── docker.md
│ ├── html.md
│ ├── intro.md
│ └── react.md
├── images
│ ├── code-samples-demo.gif
│ ├── discriminator-demo.gif
│ ├── nested-demo.gif
│ ├── progressive-loading-demo.gif
│ ├── redoc-logo.png
│ └── redoc.png
├── index.md
├── quickstart.md
├── redoc-vendor-extensions.md
└── security-definitions-injection.md
├── e2e
├── e2e.html
├── index.html
├── integration
│ ├── menu.e2e.ts
│ ├── misc.e2e.ts
│ ├── search.e2e.ts
│ ├── standalone.e2e.ts
│ └── urls.e2e.ts
├── plugins
│ ├── cy-ts-preprocessor.js
│ └── index.js
├── standalone-3-1.html
├── standalone-compatibility.html
├── standalone.html
└── tsconfig.json
├── package-lock.json
├── package.json
├── scripts
├── invalidate-cache.sh
├── publish-cdn.sh
└── version.js
├── src
├── __tests__
│ ├── ssr.test.tsx
│ └── standalone.test.tsx
├── common-elements
│ ├── CopyButtonWrapper.tsx
│ ├── Dropdown
│ │ ├── Dropdown.tsx
│ │ ├── index.ts
│ │ ├── styled.ts
│ │ └── types.ts
│ ├── PrismDiv.tsx
│ ├── Tooltip.tsx
│ ├── fields-layout.ts
│ ├── fields.ts
│ ├── headers.ts
│ ├── index.ts
│ ├── linkify.tsx
│ ├── mixins.ts
│ ├── panels.ts
│ ├── perfect-scrollbar.tsx
│ ├── samples.tsx
│ ├── schema.ts
│ ├── shelfs.tsx
│ └── tabs.ts
├── components
│ ├── ApiInfo
│ │ ├── ApiInfo.tsx
│ │ ├── index.ts
│ │ └── styled.elements.ts
│ ├── ApiLogo
│ │ ├── ApiLogo.tsx
│ │ └── styled.elements.tsx
│ ├── CallbackSamples
│ │ ├── CallbackReqSamples.tsx
│ │ └── CallbackSamples.tsx
│ ├── Callbacks
│ │ ├── CallbackDetails.tsx
│ │ ├── CallbackOperation.tsx
│ │ ├── CallbackTitle.tsx
│ │ ├── CallbacksList.tsx
│ │ ├── index.ts
│ │ └── styled.elements.ts
│ ├── ContentItems
│ │ └── ContentItems.tsx
│ ├── DropdownOrLabel
│ │ └── DropdownOrLabel.tsx
│ ├── Endpoint
│ │ ├── Endpoint.tsx
│ │ └── styled.elements.ts
│ ├── ErrorBoundary.tsx
│ ├── ExternalDocumentation
│ │ └── ExternalDocumentation.tsx
│ ├── Fields
│ │ ├── ArrayItemDetails.tsx
│ │ ├── EnumValues.tsx
│ │ ├── Examples.tsx
│ │ ├── Extensions.tsx
│ │ ├── Field.tsx
│ │ ├── FieldConstraints.tsx
│ │ ├── FieldDetail.tsx
│ │ ├── FieldDetails.tsx
│ │ └── Pattern.tsx
│ ├── GenericChildrenSwitcher
│ │ └── GenericChildrenSwitcher.tsx
│ ├── JsonViewer
│ │ ├── JsonViewer.tsx
│ │ ├── index.tsx
│ │ └── style.ts
│ ├── Loading
│ │ ├── Loading.tsx
│ │ └── Spinner.svg.tsx
│ ├── Markdown
│ │ ├── AdvancedMarkdown.tsx
│ │ ├── Markdown.tsx
│ │ ├── SanitizedMdBlock.tsx
│ │ └── styled.elements.tsx
│ ├── MediaTypeSwitch
│ │ └── MediaTypesSwitch.tsx
│ ├── Operation
│ │ └── Operation.tsx
│ ├── OptionsProvider.ts
│ ├── Parameters
│ │ ├── Parameters.tsx
│ │ └── ParametersGroup.tsx
│ ├── PayloadSamples
│ │ ├── Example.tsx
│ │ ├── ExampleValue.tsx
│ │ ├── MediaTypeSamples.tsx
│ │ ├── PayloadSamples.tsx
│ │ ├── exernalExampleHook.ts
│ │ └── styled.elements.ts
│ ├── Redoc
│ │ ├── Redoc.tsx
│ │ └── styled.elements.tsx
│ ├── RedocStandalone.tsx
│ ├── RequestSamples
│ │ └── RequestSamples.tsx
│ ├── ResponseSamples
│ │ └── ResponseSamples.tsx
│ ├── Responses
│ │ ├── Response.tsx
│ │ ├── ResponseDetails.tsx
│ │ ├── ResponseHeaders.tsx
│ │ ├── ResponseTitle.tsx
│ │ ├── ResponsesList.tsx
│ │ └── styled.elements.ts
│ ├── Schema
│ │ ├── ArraySchema.tsx
│ │ ├── DiscriminatorDropdown.tsx
│ │ ├── ObjectSchema.tsx
│ │ ├── OneOfSchema.tsx
│ │ ├── RecursiveSchema.tsx
│ │ ├── Schema.tsx
│ │ └── index.ts
│ ├── SchemaDefinition
│ │ └── SchemaDefinition.tsx
│ ├── SearchBox
│ │ ├── SearchBox.tsx
│ │ └── styled.elements.tsx
│ ├── SecurityRequirement
│ │ ├── OAuthFlow.tsx
│ │ ├── RequiredScopesRow.tsx
│ │ ├── SecurityDetails.tsx
│ │ ├── SecurityHeader.tsx
│ │ ├── SecurityRequirement.tsx
│ │ └── styled.elements.ts
│ ├── SecuritySchemes
│ │ └── SecuritySchemes.tsx
│ ├── SeeMore
│ │ └── SeeMore.tsx
│ ├── SelectOnClick
│ │ └── SelectOnClick.tsx
│ ├── SideMenu
│ │ ├── Logo.tsx
│ │ ├── MenuItem.tsx
│ │ ├── MenuItems.tsx
│ │ ├── SideMenu.tsx
│ │ ├── index.ts
│ │ └── styled.elements.ts
│ ├── SourceCode
│ │ └── SourceCode.tsx
│ ├── StickySidebar
│ │ ├── ChevronSvg.tsx
│ │ └── StickyResponsiveSidebar.tsx
│ ├── StoreBuilder.ts
│ ├── __tests__
│ │ ├── Callbacks.test.tsx
│ │ ├── DiscriminatorDropdown.test.tsx
│ │ ├── FieldDetails.test.tsx
│ │ ├── JsonViewer.tsx
│ │ ├── OneOfSchema.test.tsx
│ │ ├── Schema.test.tsx
│ │ ├── SchemaDefinition.test.tsx
│ │ ├── SecurityRequirement.test.tsx
│ │ ├── __snapshots__
│ │ │ ├── DiscriminatorDropdown.test.tsx.snap
│ │ │ ├── FieldDetails.test.tsx.snap
│ │ │ ├── OneOfSchema.test.tsx.snap
│ │ │ └── SecurityRequirement.test.tsx.snap
│ │ └── fixtures
│ │ │ ├── simple-callback.json
│ │ │ ├── simple-discriminator.json
│ │ │ └── simple-security-fixture.json
│ ├── index.ts
│ └── testProviders.tsx
├── empty.js
├── index.ts
├── polyfills.ts
├── services
│ ├── AppStore.ts
│ ├── ClipboardService.ts
│ ├── HistoryService.ts
│ ├── Labels.ts
│ ├── MarkdownRenderer.ts
│ ├── MarkerService.ts
│ ├── MenuBuilder.ts
│ ├── MenuStore.ts
│ ├── OpenAPIParser.ts
│ ├── RedocNormalizedOptions.ts
│ ├── ScrollService.ts
│ ├── SearchStore.ts
│ ├── SearchWorker.worker.ts
│ ├── SpecStore.ts
│ ├── __tests__
│ │ ├── MarkdownRenderer.test.ts
│ │ ├── MarkerService.test.ts
│ │ ├── OpenAPIParser.test.ts
│ │ ├── __snapshots__
│ │ │ ├── OpenAPIParser.test.ts.snap
│ │ │ └── prism.test.ts.snap
│ │ ├── fixtures
│ │ │ ├── 3.1
│ │ │ │ ├── conditionalField.json
│ │ │ │ ├── conditionalSchema.json
│ │ │ │ ├── pathItems.json
│ │ │ │ ├── patternProperties.json
│ │ │ │ ├── prefixItems.json
│ │ │ │ ├── schemaDefinition.json
│ │ │ │ └── unevaluatedProperties.json
│ │ │ ├── arrayItems.json
│ │ │ ├── callback.json
│ │ │ ├── discriminator.json
│ │ │ ├── fields.json
│ │ │ ├── mergeAllOf.json
│ │ │ ├── nestedEnumDescroptionSample.json
│ │ │ ├── oneOfHoist.json
│ │ │ ├── oneOfTitles.json
│ │ │ └── siblingRefDescription.json
│ │ ├── history.service.test.ts
│ │ ├── models
│ │ │ ├── ApiInfo.test.ts
│ │ │ ├── Callback.test.ts
│ │ │ ├── FieldModel.test.ts
│ │ │ ├── MenuBuilder.test.ts
│ │ │ ├── RequestBody.test.ts
│ │ │ ├── Response.test.ts
│ │ │ ├── Schema.circular.test.ts
│ │ │ ├── Schema.test.ts
│ │ │ ├── __snapshots__
│ │ │ │ └── Schema.test.ts.snap
│ │ │ └── helpers.ts
│ │ └── prism.test.ts
│ ├── index.ts
│ ├── models
│ │ ├── ApiInfo.ts
│ │ ├── Callback.ts
│ │ ├── Example.ts
│ │ ├── Field.ts
│ │ ├── Group.model.ts
│ │ ├── MediaContent.ts
│ │ ├── MediaType.ts
│ │ ├── Operation.ts
│ │ ├── RequestBody.ts
│ │ ├── Response.ts
│ │ ├── Schema.ts
│ │ ├── SecurityRequirement.ts
│ │ ├── SecuritySchemes.ts
│ │ ├── Webhook.ts
│ │ └── index.ts
│ └── types.ts
├── setupTests.ts
├── standalone.tsx
├── styled-components.ts
├── theme.ts
├── types
│ ├── index.ts
│ └── open-api.ts
└── utils
│ ├── JsonPointer.ts
│ ├── __tests__
│ ├── __snapshots__
│ │ └── loadAndBundleSpec.test.ts.snap
│ ├── helpers.test.ts
│ ├── loadAndBundleSpec.test.ts
│ ├── object.test.ts
│ └── openapi.test.ts
│ ├── debug.ts
│ ├── decorators.ts
│ ├── dom.ts
│ ├── helpers.ts
│ ├── highlight.ts
│ ├── index.ts
│ ├── jsonToHtml.ts
│ ├── loadAndBundleSpec.ts
│ ├── memoize.ts
│ ├── object.ts
│ ├── openapi.ts
│ ├── sort.ts
│ └── test-utils.ts
├── tsconfig.json
├── tsconfig.lib.json
├── tslint.json
├── typings
└── styled-patch.d.ts
└── webpack.config.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !src/
3 | !config
4 | !demo/favicon.png
5 |
6 | !custom.d.ts
7 | !typings/styled-patch.d.ts
8 | !tsconfig.json
9 | !webpack.config.ts
10 |
11 | !package.json
12 | !package-lock.json
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 2
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | },
5 | parser: '@typescript-eslint/parser',
6 | extends: ['plugin:react/recommended', 'plugin:@typescript-eslint/recommended'],
7 | parserOptions: {
8 | project: 'tsconfig.json',
9 | sourceType: 'module',
10 | createDefaultProgram: true,
11 | ecmaFeatures: {
12 | jsx: true,
13 | },
14 | },
15 | settings: {
16 | react: {
17 | version: 'detect',
18 | },
19 | },
20 | plugins: ['react', 'react-hooks', '@typescript-eslint', 'import'],
21 | rules: {
22 | '@typescript-eslint/explicit-function-return-type': 'off',
23 | '@typescript-eslint/explicit-module-boundary-types': 'off',
24 | '@typescript-eslint/no-explicit-any': 'off',
25 | '@typescript-eslint/no-use-before-define': 'off',
26 | '@typescript-eslint/interface-name-prefix': 'off',
27 | '@typescript-eslint/no-inferrable-types': 'off',
28 | '@typescript-eslint/no-non-null-assertion': 'off',
29 | '@typescript-eslint/ban-ts-ignore': 'off',
30 | '@typescript-eslint/ban-types': ['error', { types: { object: false }, extendDefaults: true }],
31 | '@typescript-eslint/no-var-requires': 'off',
32 |
33 | 'react/prop-types': 'off',
34 | 'react-hooks/rules-of-hooks': 'error',
35 | 'react-hooks/exhaustive-deps': 'warn',
36 |
37 | 'import/no-extraneous-dependencies': 'error',
38 | 'import/no-internal-modules': [
39 | 'error',
40 | {
41 | allow: [
42 | 'prismjs/**',
43 | 'perfect-scrollbar/**',
44 | 'react-dom/*',
45 | 'core-js/**',
46 | 'memoize-one/**',
47 | 'unfetch/**',
48 | 'raf/polyfill',
49 | '**/fixtures/**', // for tests
50 | ],
51 | },
52 | ],
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @Redocly/keyboard-warriors
2 | /docs/ @Redocly/technical-writers
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: 'Type: Bug'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **Expected behavior**
14 | A clear and concise description of what you expected to happen.
15 |
16 | **Minimal reproducible OpenAPI snippet(if possible)**
17 |
18 | **Screenshots**
19 | If applicable, add screenshots to help explain your problem.
20 |
21 | **Additional context**
22 | Add any other context about the problem here.
23 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: 'Type: Enhancement'
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the problem to be solved**
11 | A clear and concise description of what problem to be solved
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/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What/Why/How?
2 |
3 | ## Reference
4 |
5 | ## Tests
6 |
7 | ## Screenshots (optional)
8 |
9 | ## Check yourself
10 |
11 | - [ ] Code is linted
12 | - [ ] Tested
13 | - [ ] All new/updated code is covered with tests
14 |
--------------------------------------------------------------------------------
/.github/styles/Rules/FutureTense.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: 'Avoid using future tense: "%s". Use present tense instead.'
3 | link: https://intranet.redoc.ly/contributing/documentation-style-guide/#tone-and-audience
4 | ignorecase: true
5 | level: error
6 | raw:
7 | - "(going to( |\n|[[:punct:]])[a-zA-Z]*|"
8 | - "will( |\n|[[:punct:]])[a-zA-Z]*|"
9 | - "won't( |\n|[[:punct:]])[a-zA-Z]*|"
10 | - "[a-zA-Z]*'ll( |\n|[[:punct:]])[a-zA-Z]*)"
11 |
--------------------------------------------------------------------------------
/.github/styles/Rules/HeaderGerunds.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: 'Do not start headings with with a gerund (ing word). Use an imperative verb instead.'
3 | link: https://intranet.redoc.ly/contributing/documentation-style-guide/#content-organization
4 | level: error
5 | scope: heading
6 | tokens:
7 | - '^\w*ing.*'
8 | exceptions:
9 | - expandSingleSchemaField
10 | - hideLoading
11 | - hideSingleRequestSampleTab
12 |
--------------------------------------------------------------------------------
/.github/styles/Rules/InclusionGenderCulture.yml:
--------------------------------------------------------------------------------
1 | extends: substitution
2 | message: 'Use inclusive language. Consider "%s" instead of "%s".'
3 | link: https://intranet.redoc.ly/contributing/documentation-style-guide/#grammar-and-syntax
4 | level: error
5 | ignorecase: true
6 | swap:
7 | he: they
8 | his: their
9 | she: they
10 | hers: their
11 | blacklist(?:ed|ing|s)?: blocklist
12 | whitelist(?:ed|ing|s)?: allowlist
13 | master: primary, main
14 | slave: replica
15 | he/she: they
16 | s/he: they
17 |
--------------------------------------------------------------------------------
/.github/styles/Rules/OxfordComma.yml:
--------------------------------------------------------------------------------
1 | extends: existence
2 | message: "Use the Oxford comma in '%s'."
3 | link: https://docs.microsoft.com/en-us/style-guide/punctuation/commas
4 | scope: sentence
5 | level: error
6 | nonword: true
7 | tokens:
8 | - '(?:[^\s,]+,){1,} \w+ (?:and|or) \w+[.?!]'
9 |
--------------------------------------------------------------------------------
/.github/styles/config/vocabularies/Rules/reject.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/.github/styles/config/vocabularies/Rules/reject.txt
--------------------------------------------------------------------------------
/.github/workflows/docs-tests.yaml:
--------------------------------------------------------------------------------
1 | name: Documentation tests
2 | on:
3 | pull_request:
4 | types: [opened, synchronize, reopened]
5 |
6 | jobs:
7 | markdownlint:
8 | name: markdownlint
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: DavidAnson/markdownlint-cli2-action@v15
13 | with:
14 | config: .markdownlint.yaml
15 | globs: |
16 | docs/**/*.md
17 | README.md
18 |
19 | vale:
20 | name: vale action
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: errata-ai/vale-action@reviewdog
25 | with:
26 | files: '["README.md", "docs"]'
27 | filter_mode: file
28 |
29 | linkcheck:
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout Repository
33 | uses: actions/checkout@v4
34 | - name: Markup Link Checker (mlc)
35 | uses: becheran/mlc@v0.16.1
36 | with:
37 | args: ./docs
38 |
--------------------------------------------------------------------------------
/.github/workflows/e2e-tests.yml:
--------------------------------------------------------------------------------
1 | name: Tests e2e
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build-and-e2e:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - run: npm ci
11 | - run: npm run bundle
12 | - run: npm run e2e
13 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Publish Docker image
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | dockerhub:
7 | name: Publish redoc image to DockerHub
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v3
12 |
13 | - name: Docker meta
14 | id: docker_meta
15 | uses: crazy-max/ghaction-docker-meta@v1
16 | with:
17 | images: redocly/redoc
18 |
19 | - name: Set up QEMU
20 | uses: docker/setup-qemu-action@v1
21 |
22 | - name: Set up Docker Buildx
23 | uses: docker/setup-buildx-action@v1
24 |
25 | - name: Login to DockerHub
26 | uses: docker/login-action@v1
27 | with:
28 | username: ${{ secrets.DOCKERHUB_USERNAME }}
29 | password: ${{ secrets.DOCKERHUB_TOKEN }}
30 |
31 | - name: Build and push
32 | uses: docker/build-push-action@v3
33 | with:
34 | context: .
35 | file: ./config/docker/Dockerfile
36 | platforms: linux/amd64,linux/arm64
37 | push: true
38 | tags: ${{ steps.docker_meta.outputs.tags }}
39 | labels: ${{ steps.docker_meta.outputs.labels }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/unit-tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build-and-unit:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - run: npm ci
11 | - run: npm run bundle
12 | - run: npm test
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Linux ###
2 | *~
3 |
4 | # KDE directory preferences
5 | .directory
6 | # OS X folder attributes
7 | .DS_Store
8 |
9 | # Linux trash folder which might appear on any partition or disk
10 | .Trash-*
11 |
12 | demo/dist/
13 |
14 | ### Node ###
15 | # Logs
16 | logs
17 | *.log
18 | npm-debug.log*
19 |
20 | # Dependency directory
21 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
22 | node_modules
23 |
24 | lib/
25 | stats.json
26 | cypress/
27 | bundles/
28 | typings/*
29 | !typings/styled-patch.d.ts
30 |
31 | /benchmark/revisions
32 |
33 | /coverage
34 | .ghpages-tmp
35 | stats.json
36 | yarn.lock
37 | .idea
38 | .vscode
39 | .eslintcache
40 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npm run pre-commit
5 |
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Default rules: https://github.com/github/super-linter/blob/master/TEMPLATES/.markdown-lint.yml
3 |
4 | # Rules by id
5 |
6 | # Unordered list style
7 | MD004: false
8 |
9 | # Unordered list indentation
10 | MD007:
11 | indent: 2
12 |
13 | MD013:
14 | # TODO: Consider to decrease allowed line length
15 | line_length: 800
16 | tables: false
17 |
18 | ## Allow same headers in siblings
19 | MD024:
20 | siblings_only: true
21 |
22 | # Multiple top level headings in the same document
23 | MD025:
24 | front_matter_title: ''
25 |
26 | # Trailing punctuation in heading
27 | MD026:
28 | punctuation: '.,;:。,;:'
29 |
30 | # Ordered list item prefix
31 | MD029: false
32 |
33 | # Unordered lists inside of ordered lists
34 | MD030: false
35 |
36 | # Inline HTML
37 | MD033: false
38 |
39 | # No bare urls
40 | MD034: false
41 |
42 | # Emphasis used instead of a heading
43 | MD036: false
44 |
45 | # Disable "First line in file should be a top level heading"
46 | # We use uncommon format to add metadata.
47 | # TODO: Consider to use "YAML front matter".
48 | MD041: false
49 |
50 | # Rules by tags
51 | blank_lines: false
52 |
53 | MD046: false
54 | # code-block-style
55 |
--------------------------------------------------------------------------------
/.mlc.toml:
--------------------------------------------------------------------------------
1 | # Ignore these links, we can't check them from this subproject
2 | ignore-links=["../*", "/docs/*"]
3 | # Path to the root folder used to resolve all relative paths
4 | root-dir="./docs"
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *
2 | !bundles/*
3 | !typings/**/*
4 | !package.json
5 | !README.md
6 | !LICENSE
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | *.md
2 |
--------------------------------------------------------------------------------
/.vale.ini:
--------------------------------------------------------------------------------
1 | # Vale configuration file.
2 | # See: https://docs.errata.ai/vale/config
3 |
4 | # The relative path to the folder containing linting rules (styles).
5 | StylesPath = .github/styles
6 |
7 | # Vocab define the exceptions to use in *all* `BasedOnStyles`.
8 | # spelling-exceptions.txt triggers `Vale.Terms`
9 | # reject.txt triggers `Vale.Avoid`
10 | # See: https://docs.errata.ai/vale/vocab
11 | Vocab = Rules
12 |
13 | # Minimum alert level
14 | # -------------------
15 | # The minimum alert level in the output (suggestion, warning, or error).
16 | # If integrated into CI, builds fail by default on error-level alerts, unless you run Vale with the --no-exit flag
17 | MinAlertLevel = suggestion
18 |
19 | # IgnoredScopes specifies inline-level HTML tags to ignore.
20 | # These tags may occur in an active scope (unlike SkippedScopes, skipped entirely) but their content still won't raise any alerts.
21 | # Default: ignore `code` and `tt`.
22 | IgnoredScopes = code, tt, img, url, a, body.id
23 | # SkippedScopes specifies block-level HTML tags to ignore. Ignore any content in these scopes.
24 | # Default: ignore `script`, `style`, `pre`, and `figure`.
25 | # For AsciiDoc: by default, listingblock, and literalblock.
26 | SkippedScopes = script, style, pre, figure, code, tt, listingblock, literalblock
27 |
28 | # Rules for matching file types. See: https://docs.errata.ai/vale/scoping
29 |
30 | [formats]
31 | properties = md
32 | mdx = md
33 |
34 | # Rules for .MD, .MDX
35 | [*.{md,mdx}]
36 |
37 | BasedOnStyles = Rules
38 | # Ignore code surrounded by backticks or plus sign, parameters defaults, URLs.
39 | TokenIgnores = (\x60[^\n\x60]+\x60), ([^\n]+=[^\n]*), (\+[^\n]+\+), (http[^\n]+\[)
40 | Vale.Repetition = NO
41 | Vale.SentenceSpacing = NO
42 | Vale.Spelling = NO
43 |
44 | # /End of rules for .MD, .MDX
45 |
46 |
47 | # Process .ini files
48 | [*.ini]
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015-present, Rebilly, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/benchmark/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ReDoc
7 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/config/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # To run:
2 | # docker build -t redoc .
3 | # docker run -it --rm -p 80:80 -e SPEC_URL='http://localhost:8000/swagger.yaml' redoc
4 | # Ensure http://localhost:8000/swagger.yaml is served with cors. A good solution is:
5 | # npm i -g http-server
6 | # http-server -p 8000 --cors
7 |
8 | FROM node:18-alpine
9 |
10 | RUN apk update && apk add --no-cache git
11 |
12 | # Install dependencies
13 | WORKDIR /build
14 | COPY package.json package-lock.json /build/
15 | RUN npm ci --no-optional --ignore-scripts
16 | RUN npm explore esbuild -- npm run postinstall
17 |
18 | # copy only required for the build files
19 | COPY src /build/src
20 | COPY webpack.config.ts tsconfig.json custom.d.ts /build/
21 | COPY config/webpack-utils.ts /build/config/
22 | COPY typings/styled-patch.d.ts /build/typings/styled-patch.d.ts
23 |
24 | RUN npm run bundle:standalone
25 |
26 | FROM nginx:alpine
27 |
28 | ENV PAGE_TITLE="ReDoc"
29 | ENV PAGE_FAVICON="favicon.png"
30 | ENV BASE_PATH=
31 | ENV SPEC_URL="http://petstore.swagger.io/v2/swagger.json"
32 | ENV PORT=80
33 | ENV REDOC_OPTIONS=
34 |
35 | # copy files to the nginx folder
36 | COPY --from=0 build/bundles /usr/share/nginx/html
37 | COPY config/docker/index.tpl.html /usr/share/nginx/html/index.html
38 | COPY demo/favicon.png /usr/share/nginx/html/
39 | COPY config/docker/nginx.conf /etc/nginx/
40 | COPY config/docker/docker-run.sh /usr/local/bin
41 |
42 | # Provide rights to the root group to write to nginx repositories (needed to run in OpenShift)
43 | RUN chgrp -R 0 /etc/nginx && \
44 | chgrp -R 0 /usr/share/nginx/html && \
45 | chgrp -R 0 /var/cache/nginx && \
46 | chgrp -R 0 /var/log/nginx && \
47 | chgrp -R 0 /var/run && \
48 | chmod -R g+rwX /etc/nginx && \
49 | chmod -R g+rwX /usr/share/nginx/html && \
50 | chmod -R g+rwX /var/cache/nginx && \
51 | chmod -R g+rwX /var/log/nginx && \
52 | chmod -R g+rwX /var/run
53 |
54 | EXPOSE 80
55 |
56 | CMD ["sh", "/usr/local/bin/docker-run.sh"]
57 |
--------------------------------------------------------------------------------
/config/docker/docker-run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | set -e
4 |
5 | sed -i -e "s|%PAGE_TITLE%|$PAGE_TITLE|g" /usr/share/nginx/html/index.html
6 | sed -i -e "s|%PAGE_FAVICON%|$PAGE_FAVICON|g" /usr/share/nginx/html/index.html
7 | sed -i -e "s|%BASE_PATH%|$BASE_PATH|g" /usr/share/nginx/html/index.html
8 | sed -i -e "s|%SPEC_URL%|$SPEC_URL|g" /usr/share/nginx/html/index.html
9 | sed -i -e "s|%REDOC_OPTIONS%|${REDOC_OPTIONS}|g" /usr/share/nginx/html/index.html
10 | sed -i -e "s|\(listen\s*\) [0-9]*|\1 ${PORT}|g" /etc/nginx/nginx.conf
11 |
12 | exec nginx -g 'daemon off;'
13 |
--------------------------------------------------------------------------------
/config/docker/hooks/build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # DockerHub cd into Dockerfile location before build
4 | # So we have to undo this.
5 | cd ../..
6 | docker build -f config/docker/Dockerfile -t $IMAGE_NAME .
7 |
--------------------------------------------------------------------------------
/config/docker/index.tpl.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | %PAGE_TITLE%
7 |
8 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/config/webpack-utils.ts:
--------------------------------------------------------------------------------
1 | import * as webpack from 'webpack';
2 |
3 | export function webpackIgnore(regexp) {
4 | return new webpack.NormalModuleReplacementPlugin(regexp, require.resolve('lodash.noop'));
5 | }
6 |
--------------------------------------------------------------------------------
/custom.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.json' {
4 | const content: any;
5 | export = content;
6 | }
7 |
8 | declare module '*.svg' {
9 | const content: string;
10 | export default content;
11 | }
12 |
13 | declare module '*.css' {
14 | const content: string;
15 | export default content;
16 | }
17 |
18 | declare var __REDOC_VERSION__: string;
19 | declare var __REDOC_REVISION__: string;
20 |
21 | declare var reactHotLoaderGlobal: any;
22 |
23 | interface Element {
24 | scrollIntoViewIfNeeded(centerIfNeeded?: boolean): void;
25 | }
26 |
27 | type GenericObject = Record;
28 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress';
2 |
3 | export default defineConfig({
4 | fixturesFolder: false,
5 | fileServerFolder: '.',
6 | video: true,
7 | projectId: 'z6eb6h',
8 | viewportWidth: 1440,
9 | viewportHeight: 720,
10 | e2e: {
11 | // We've imported your old cypress plugins here.
12 | // You may want to clean this up later by importing these.
13 | setupNodeEvents(on, config) {
14 | return require('./e2e/plugins/index.js')(on, config);
15 | },
16 | excludeSpecPattern: '*.js.map',
17 | specPattern: 'e2e/integration/**/*.{js,jsx,ts,tsx}',
18 | supportFile: false,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/demo/components/FileInput.tsx:
--------------------------------------------------------------------------------
1 | import * as yaml from 'js-yaml';
2 | import * as React from 'react';
3 | import { ChangeEvent, RefObject, useRef } from 'react';
4 | import styled from '../../src/styled-components';
5 |
6 | const Button = styled.button`
7 | background-color: #fff;
8 | color: #333;
9 | padding: 2px 10px;
10 | touch-action: manipulation;
11 | cursor: pointer;
12 | user-select: none;
13 | border: 1px solid #ccc;
14 | font-size: 16px;
15 | height: 28px;
16 | box-sizing: border-box;
17 | vertical-align: middle;
18 | line-height: 1;
19 | outline: none;
20 | white-space: nowrap;
21 | @media (max-width: 699px) {
22 | display: none;
23 | }
24 | `;
25 |
26 | function FileInput(props: { onUpload }) {
27 | const hiddenFileInput: RefObject = useRef(null);
28 |
29 | const handleClick = () => {
30 | if (hiddenFileInput && hiddenFileInput.current) {
31 | hiddenFileInput.current.click();
32 | }
33 | };
34 |
35 | const uploadFile = (event: ChangeEvent) => {
36 | const file = (event.target as HTMLInputElement).files![0];
37 | const reader = new FileReader();
38 | reader.onload = () => {
39 | props.onUpload(yaml.load(reader.result));
40 | };
41 | reader.readAsText(file);
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export default FileInput;
53 |
--------------------------------------------------------------------------------
/demo/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/demo/favicon.png
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Redoc Interactive Demo
6 |
10 |
11 |
12 |
13 |
17 |
21 |
22 |
23 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/demo/museum-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/demo/museum-logo.png
--------------------------------------------------------------------------------
/demo/petstore-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/demo/petstore-logo.png
--------------------------------------------------------------------------------
/demo/playground/hmr-playground.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import type { RedocRawOptions } from '../../src/services/RedocNormalizedOptions';
4 | import { RedocStandalone } from '../../src';
5 |
6 | const big = window.location.search.indexOf('big') > -1;
7 | const swagger = window.location.search.indexOf('swagger') > -1;
8 |
9 | const userUrl = window.location.search.match(/url=(.*)$/);
10 |
11 | const specUrl =
12 | (userUrl && userUrl[1]) || (swagger ? 'museum.yaml' : big ? 'big-openapi.json' : 'museum.yaml');
13 |
14 | const options: RedocRawOptions = {
15 | nativeScrollbars: false,
16 | maxDisplayedEnumValues: 3,
17 | schemaDefinitionsTagName: 'schemas',
18 | };
19 |
20 | const container = document.getElementById('example');
21 | const root = createRoot(container!);
22 | root.render();
23 |
--------------------------------------------------------------------------------
/demo/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Redoc
7 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demo/redoc-demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/demo/redoc-demo.png
--------------------------------------------------------------------------------
/docs/deployment/cli.md:
--------------------------------------------------------------------------------
1 | ---
2 | seo:
3 | title: Use the Redoc CLI
4 | ---
5 |
6 | # How to use the Redocly CLI
7 |
8 | With Redocly CLI, you can bundle your OpenAPI definition and API documentation
9 | (made with Redoc) into an HTML file and render it locally.
10 |
11 | ## Step 1 - Install Redocly CLI
12 |
13 | First, you need to install the `@redocly/cli` package.
14 |
15 | You can install it [globally](../../cli/installation#install-globally) using npm.
16 |
17 | Or you can install it during [runtime](../../cli/installation#use-npx-at-runtime) using npx or Docker.
18 |
19 | ## Step 2 - Build the HTML file
20 |
21 | The Redocly CLI `build-docs` command builds Redoc into an HTML file.
22 |
23 | To build an HTML file using Redocly CLI, enter the following command,
24 | replacing `apis/openapi.yaml` with your API definition file's name and path:
25 |
26 | ```bash
27 | redocly build-docs apis/openapi.yaml
28 | ```
29 |
30 | See the [build-docs](../../cli/commands/build-docs) documentation for more information
31 | on the different options and ways you can use the command.
32 |
33 | Also, check out [Redocly CLI commands](../../cli/commands), for more
34 | information on the different things you can do with Redocly CLI including
35 | linting, splitting, and bundling your API definition file.
36 |
--------------------------------------------------------------------------------
/docs/deployment/docker.md:
--------------------------------------------------------------------------------
1 | ---
2 | seo:
3 | title: Use the Redoc Docker image
4 | ---
5 |
6 | # How to use the Redoc Docker image
7 |
8 | Redoc is available as a pre-built Docker image in [Docker Hub](https://hub.docker.com/r/redocly/redoc/).
9 |
10 | If you have [Docker](https://docs.docker.com/get-docker/) installed, pull the image with the following command:
11 |
12 | ```docker
13 | docker pull redocly/redoc
14 | ```
15 |
16 | Then run the image with the following command:
17 |
18 | ```docker
19 | docker run -p 8080:80 redocly/redoc
20 | ```
21 |
22 | The preview starts on port 8080, based on the port used in the command,
23 | and can be accessed at `http://localhost:8080`.
24 | To exit the preview, use `control+C`.
25 |
26 | By default Redoc starts with a demo Swagger Petstore OpenAPI definition located at
27 | http://petstore.swagger.io/v2/swagger.json. You can update this URL using
28 | the environment variable `SPEC_URL`.
29 |
30 | For example:
31 |
32 | ```bash
33 | docker run -p 8080:80 -e SPEC_URL=https://api.example.com/openapi.json redocly/redoc
34 | ```
35 |
36 | ## Create a Dockerfile
37 |
38 | You can also create a Dockerfile with some predefined environment variables. Check out
39 | a sample [Dockerfile](https://github.com/Redocly/redoc/blob/main/config/docker/Dockerfile)
40 | in our code repo.
41 |
--------------------------------------------------------------------------------
/docs/images/code-samples-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/code-samples-demo.gif
--------------------------------------------------------------------------------
/docs/images/discriminator-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/discriminator-demo.gif
--------------------------------------------------------------------------------
/docs/images/nested-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/nested-demo.gif
--------------------------------------------------------------------------------
/docs/images/progressive-loading-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/progressive-loading-demo.gif
--------------------------------------------------------------------------------
/docs/images/redoc-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/redoc-logo.png
--------------------------------------------------------------------------------
/docs/images/redoc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Redocly/redoc/ce27184254c87d20b429a96f3090a8335ed2cef8/docs/images/redoc.png
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | ---
2 | seo:
3 | title: Redoc quickstart guide
4 | ---
5 |
6 | # Redoc quickstart guide
7 |
8 | To render your OpenAPI definition using Redoc, use the following HTML code sample and
9 | replace the `spec-url` attribute with the URL or local file address to your definition.
10 |
11 | ```html
12 |
13 |
14 |
15 | Redoc
16 |
17 |
18 |
19 |
23 |
24 |
27 |
33 |
34 |
35 |
38 |
39 |
42 |
43 |
44 |
45 | ```
46 |
47 | {% admonition type="info" name="Redoc requires an HTTP server to run locally" %}
48 | Loading local OpenAPI definitions is impossible without running a web server because of issues with
49 | [same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) and
50 | other security reasons. Refer to [Running Redoc locally](./deployment/intro.md#how-to-run-redoc-locally) for more information.
51 | {% /admonition %}
52 |
53 | For a more detailed explanation with step-by-step instructions and additional options for using Redoc, refer to the [Redoc deployment guide](./deployment/intro.md).
54 |
--------------------------------------------------------------------------------
/docs/security-definitions-injection.md:
--------------------------------------------------------------------------------
1 | # Injection security definitions
2 |
3 | You can inject the Security Definitions widget anywhere in your specification `description`:
4 |
5 | ```markdown
6 | ...
7 | ## Authorization
8 |
9 | Some description
10 |
11 |
12 | ...
13 | ```
14 | The inject instruction is wrapped in an HTML comment,
15 | so it is **visible only in Redoc** and not visible, for instance, in the SwaggerUI.
16 |
17 | ## Default behavior
18 |
19 | If the injection tag is not found in the description, it is appended to the end
20 | of description under the `Authentication` header.
21 |
22 | If the `Authentication` header is already present in the description,
23 | Security Definitions are not inserted or rendered.
24 |
--------------------------------------------------------------------------------
/e2e/e2e.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ;
7 |
--------------------------------------------------------------------------------
/e2e/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ;
7 |
--------------------------------------------------------------------------------
/e2e/integration/misc.e2e.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:no-implicit-dependencies
2 | import * as yaml from 'js-yaml';
3 |
4 | async function loadSpec(url: string): Promise {
5 | const spec = await (await fetch(url)).text();
6 | return yaml.load(spec);
7 | }
8 |
9 | function initReDoc(win, spec, options = {}) {
10 | (win as any).Redoc.init(spec, options, win.document.getElementById('redoc'));
11 | }
12 |
13 | describe('Servers', () => {
14 | beforeEach(() => {
15 | cy.visit('e2e/');
16 | });
17 |
18 | it('should have valid server', () => {
19 | cy.window().then(async win => {
20 | const spec = await loadSpec('/demo/openapi.yaml');
21 | initReDoc(win, spec, {});
22 |
23 | // TODO add cy-data attributes
24 | cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
25 | 'contain',
26 | 'http://petstore.swagger.io/v2/pet',
27 | );
28 |
29 | cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
30 | 'contain',
31 | 'http://petstore.swagger.io/sandbox/pet',
32 | );
33 | });
34 | });
35 |
36 | it('should have valid server for when servers not provided', () => {
37 | cy.window().then(async win => {
38 | const spec = await loadSpec('/demo/openapi.yaml');
39 | delete spec.servers;
40 | initReDoc(win, spec, {});
41 |
42 | // TODO add cy-data attributes
43 | cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
44 | 'contain',
45 | 'http://localhost:' + win.location.port + '/pet',
46 | );
47 | });
48 | });
49 |
50 | it('should have valid server for when servers not provided at .html pages', () => {
51 | cy.visit('e2e/e2e.html');
52 | cy.window().then(async win => {
53 | const spec = await loadSpec('/demo/openapi.yaml');
54 | delete spec.servers;
55 | initReDoc(win, spec, {});
56 |
57 | // TODO add cy-data attributes
58 | cy.get('[data-section-id="tag/pet/operation/addPet"]').should(
59 | 'contain',
60 | 'http://localhost:' + win.location.port + '/pet',
61 | );
62 | });
63 | });
64 | });
65 |
--------------------------------------------------------------------------------
/e2e/integration/standalone.e2e.ts:
--------------------------------------------------------------------------------
1 | describe('Standalone bundle test', () => {
2 | function baseCheck(name: string, url: string) {
3 | describe(name, () => {
4 | beforeEach(() => {
5 | cy.visit(url);
6 | });
7 |
8 | it('Render and check no errors', () => {
9 | cy.get('.api-info').should('exist');
10 | });
11 |
12 | it('Render and click all the menu items', () => {
13 | cy.get('.menu-content li').click({ multiple: true, force: true });
14 | });
15 | });
16 | }
17 |
18 | baseCheck('OAS3 mode', 'e2e/standalone.html');
19 | baseCheck('OAS3.1 mode', 'e2e/standalone-3-1.html');
20 | baseCheck('OAS2 compatibility mode', 'e2e/standalone-compatibility.html');
21 | });
22 |
--------------------------------------------------------------------------------
/e2e/integration/urls.e2e.ts:
--------------------------------------------------------------------------------
1 | describe('Supporting both operation/* and parent/*/operation* urls', () => {
2 | beforeEach(() => {
3 | cy.visit('e2e/standalone.html');
4 | });
5 |
6 | it('should supporting operation/* url', () => {
7 | cy.url().then(loc => {
8 | cy.visit(loc + '#operation/updatePet');
9 | cy.get('li[data-item-id="tag/pet/operation/updatePet"]').should('be.visible');
10 | });
11 | });
12 |
13 | it('should supporting parent/*/operation url', () => {
14 | cy.url().then(loc => {
15 | cy.visit(loc + '#tag/pet/operation/addPet');
16 | cy.get('li[data-item-id="tag/pet/operation/addPet"]').should('be.visible');
17 | });
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/e2e/plugins/cy-ts-preprocessor.js:
--------------------------------------------------------------------------------
1 | const wp = require('@cypress/webpack-preprocessor');
2 |
3 | const webpackOptions = {
4 | resolve: {
5 | extensions: ['.ts', '.js'],
6 | },
7 | performance: false,
8 | module: {
9 | rules: [
10 | {
11 | test: /\.ts$/,
12 | exclude: [/node_modules/],
13 | use: [
14 | {
15 | loader: 'esbuild-loader',
16 | options: {
17 | tsconfigRaw: require('../tsconfig.json'),
18 | },
19 | },
20 | ],
21 | },
22 | ],
23 | },
24 | };
25 |
26 | const options = {
27 | webpackOptions,
28 | };
29 |
30 | module.exports = wp(options);
31 |
--------------------------------------------------------------------------------
/e2e/plugins/index.js:
--------------------------------------------------------------------------------
1 | const cypressTypeScriptPreprocessor = require('./cy-ts-preprocessor');
2 |
3 | module.exports = on => {
4 | on('file:preprocessor', cypressTypeScriptPreprocessor);
5 | };
6 |
--------------------------------------------------------------------------------
/e2e/standalone-3-1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/e2e/standalone-compatibility.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/e2e/standalone.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "module": "commonjs",
5 | "moduleResolution": "node",
6 | "target": "es2015",
7 | "noImplicitAny": false,
8 | "noUnusedLocals": true,
9 | "noUnusedParameters": true,
10 | "strictNullChecks": true,
11 | "sourceMap": true,
12 | "pretty": true,
13 | "lib": [
14 | "es2015",
15 | "es2016",
16 | "es2017",
17 | "dom"
18 | ],
19 | "jsx": "react",
20 | "types": ["cypress"]
21 | },
22 | "compileOnSave": false,
23 | "include": [
24 | "integration/*.ts",
25 | "../node_modules/cypress"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/scripts/invalidate-cache.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e # exit on error
4 |
5 | echo jsdelivr clearing cache
6 | curl -i -X POST https://purge.jsdelivr.net/ \
7 | -H 'cache-control: no-cache' \
8 | -H 'content-type: application/json' \
9 | -d '{
10 | "path": [
11 | "npm/redoc@latest/bundles/redoc.browser.lib.js",
12 | "npm/redoc@latest/bundles/redoc.lib.js",
13 | "npm/redoc@latest/bundles/redoc.standalone.js"
14 | ]
15 | }'
16 |
17 | echo
18 | echo start invalidate cloudfront
19 |
20 | aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths "/redoc/*"
21 |
22 | echo Cache cleared successfully
23 |
24 | exit 0
--------------------------------------------------------------------------------
/scripts/publish-cdn.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e # exit on error
4 |
5 | # TODO: Update script!
6 |
7 | VERSION=$(node scripts/version.js)
8 | VERSION_TAG=v${VERSION:0:1}.x
9 |
10 | copy_to_s3 () {
11 | aws s3 cp --exclude "*" --include "*.js" --content-type "application/javascript; charset=utf-8" bundles "s3://redocly-cdn/redoc/$1/bundles" --recursive
12 | aws s3 cp --exclude "*" --include "*.map" --content-type "application/json" bundles "s3://redocly-cdn/redoc/$1/bundles" --recursive
13 | aws s3 cp --exclude "*" --include "*.txt" bundles "s3://redocly-cdn/redoc/$1/bundles" --recursive
14 | aws s3 cp CHANGELOG.md "s3://redocly-cdn/redoc/$1/CHANGELOG.md"
15 | aws s3 cp LICENSE "s3://redocly-cdn/redoc/$1/LICENSE"
16 | aws s3 cp package.json "s3://redocly-cdn/redoc/$1/package.json"
17 | aws s3 cp README.md "s3://redocly-cdn/redoc/$1/README.md"
18 | }
19 |
20 | if aws s3 ls "redocly-cdn/redoc/v$VERSION/" "$@"; then
21 | echo "Version $VERSION already exists"
22 | exit 1
23 | else
24 | echo Releasing $VERSION
25 |
26 | echo Uploading to S3 $VERSION
27 | copy_to_s3 "v$VERSION"
28 |
29 | echo Uploading to S3 $VERSION_TAG
30 | copy_to_s3 "$VERSION_TAG" $@
31 |
32 | if [[ "$VERSION_TAG" == "v2.x" ]]; then
33 | echo Uploading to S3 latest
34 | copy_to_s3 latest $@
35 | fi
36 |
37 | echo
38 | echo Deployed successfully
39 | exit 0
40 | fi
41 |
--------------------------------------------------------------------------------
/scripts/version.js:
--------------------------------------------------------------------------------
1 | console.log(require('../package.json').version);
2 |
--------------------------------------------------------------------------------
/src/__tests__/ssr.test.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-implicit-dependencies */
2 |
3 | import * as React from 'react';
4 | import { renderToString } from 'react-dom/server';
5 | import * as yaml from 'js-yaml';
6 | import { createStore, Redoc } from '../';
7 |
8 | import { readFileSync } from 'fs';
9 | import { resolve } from 'path';
10 |
11 | describe('SSR', () => {
12 | it('should render in SSR mode', async () => {
13 | const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'), 'utf-8'));
14 | const store = await createStore(spec, '');
15 | expect(() => {
16 | renderToString();
17 | }).not.toThrow();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/__tests__/standalone.test.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-implicit-dependencies */
2 | import { mount } from 'enzyme';
3 | import * as React from 'react';
4 | import * as yaml from 'js-yaml';
5 |
6 | import { readFileSync } from 'fs';
7 | import { resolve } from 'path';
8 |
9 | import { Loading, RedocStandalone } from '../components/';
10 |
11 | describe('Components', () => {
12 | describe('RedocStandalone', () => {
13 | test('should show loading first', () => {
14 | const spec = yaml.load(readFileSync(resolve(__dirname, '../../demo/openapi.yaml'), 'utf-8'));
15 |
16 | const inst = mount();
17 | expect(inst.find(Loading)).toHaveLength(1);
18 | });
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/common-elements/CopyButtonWrapper.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Tooltip } from '../common-elements/Tooltip';
3 |
4 | import { ClipboardService } from '../services/ClipboardService';
5 |
6 | export interface CopyButtonWrapperProps {
7 | data: any;
8 | children: (props: { renderCopyButton: () => React.ReactNode }) => React.ReactNode;
9 | }
10 |
11 | export const CopyButtonWrapper = (
12 | props: CopyButtonWrapperProps & { tooltipShown?: boolean },
13 | ): JSX.Element => {
14 | const [tooltipShown, setTooltipShown] = React.useState(false);
15 |
16 | const copy = () => {
17 | const content =
18 | typeof props.data === 'string' ? props.data : JSON.stringify(props.data, null, 2);
19 | ClipboardService.copyCustom(content);
20 | showTooltip();
21 | };
22 |
23 | const renderCopyButton = () => {
24 | return (
25 |
33 | );
34 | };
35 |
36 | const showTooltip = () => {
37 | setTooltipShown(true);
38 |
39 | setTimeout(() => {
40 | setTooltipShown(false);
41 | }, 1500);
42 | };
43 | return props.children({ renderCopyButton: renderCopyButton }) as JSX.Element;
44 | };
45 |
--------------------------------------------------------------------------------
/src/common-elements/Dropdown/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from '../../styled-components';
3 | import { ArrowIconProps, DropdownProps, DropdownOption } from './types';
4 |
5 | const ArrowSvg = ({ className, style }: ArrowIconProps): JSX.Element => (
6 |
21 | );
22 |
23 | const ArrowIcon = styled(ArrowSvg)`
24 | position: absolute;
25 | pointer-events: none;
26 | z-index: 1;
27 | top: 50%;
28 | -webkit-transform: translateY(-50%);
29 | -ms-transform: translateY(-50%);
30 | transform: translateY(-50%);
31 | right: 8px;
32 | margin: auto;
33 | text-align: center;
34 | polyline {
35 | color: ${props => props.variant === 'dark' && 'white'};
36 | }
37 | `;
38 |
39 | const DropdownComponent = (props: DropdownProps): JSX.Element => {
40 | const { options, onChange, placeholder, value = '', variant, className } = props;
41 |
42 | const handleOnChange = event => {
43 | const { selectedIndex } = event.target;
44 | const index = placeholder ? selectedIndex - 1 : selectedIndex;
45 | onChange(options[index]);
46 | };
47 |
48 | return (
49 |
50 |
51 |
63 |
64 |
65 | );
66 | };
67 |
68 | export const Dropdown = React.memo(DropdownComponent);
69 |
--------------------------------------------------------------------------------
/src/common-elements/Dropdown/index.ts:
--------------------------------------------------------------------------------
1 | export * from './styled';
2 | export * from './types';
3 |
--------------------------------------------------------------------------------
/src/common-elements/Dropdown/types.ts:
--------------------------------------------------------------------------------
1 | export interface DropdownOption {
2 | idx?: number;
3 | value: string;
4 | title?: string;
5 | serverUrl?: string;
6 | label?: string;
7 | }
8 |
9 | export interface DropdownProps {
10 | options: DropdownOption[];
11 | onChange: (option: DropdownOption) => void;
12 | ariaLabel?: string;
13 | className?: string;
14 | placeholder?: string;
15 | value?: string;
16 | dense?: boolean;
17 | fullWidth?: boolean;
18 | variant?: 'dark' | 'light';
19 | }
20 |
21 | export interface ArrowIconProps {
22 | className?: string;
23 | variant?: 'light' | 'dark';
24 | style?: React.CSSProperties;
25 | }
26 |
--------------------------------------------------------------------------------
/src/common-elements/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import styled from '../styled-components';
4 |
5 | const Wrapper = styled.div`
6 | position: relative;
7 | `;
8 |
9 | const Tip = styled.div`
10 | position: absolute;
11 | min-width: 80px;
12 | max-width: 500px;
13 | background: #fff;
14 | bottom: 100%;
15 | left: 50%;
16 | margin-bottom: 10px;
17 | transform: translateX(-50%);
18 |
19 | border-radius: 4px;
20 | padding: 0.3em 0.6em;
21 | text-align: center;
22 | box-shadow: 0px 0px 5px 0px rgba(204, 204, 204, 1);
23 | `;
24 |
25 | const Content = styled.div`
26 | background: #fff;
27 | color: #000;
28 | display: inline;
29 | font-size: 0.85em;
30 | white-space: nowrap;
31 | `;
32 |
33 | const Arrow = styled.div`
34 | position: absolute;
35 | width: 0;
36 | height: 0;
37 | bottom: -5px;
38 | left: 50%;
39 | margin-left: -5px;
40 | border-left: solid transparent 5px;
41 | border-right: solid transparent 5px;
42 | border-top: solid #fff 5px;
43 | `;
44 |
45 | const Gap = styled.div`
46 | position: absolute;
47 | width: 100%;
48 | height: 20px;
49 | bottom: -20px;
50 | `;
51 |
52 | export interface TooltipProps extends React.PropsWithChildren {
53 | open: boolean;
54 | title: string;
55 | }
56 |
57 | export class Tooltip extends React.Component {
58 | render() {
59 | const { open, title, children } = this.props;
60 | return (
61 |
62 | {children}
63 | {open && (
64 |
65 | {title}
66 |
67 |
68 |
69 | )}
70 |
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/common-elements/headers.ts:
--------------------------------------------------------------------------------
1 | import styled, { css, extensionsHook } from '../styled-components';
2 |
3 | const headerFontSize = {
4 | 1: '1.85714em',
5 | 2: '1.57143em',
6 | 3: '1.27em',
7 | };
8 |
9 | export const headerCommonMixin = level => css`
10 | font-family: ${({ theme }) => theme.typography.headings.fontFamily};
11 | font-weight: ${({ theme }) => theme.typography.headings.fontWeight};
12 | font-size: ${headerFontSize[level]};
13 | line-height: ${({ theme }) => theme.typography.headings.lineHeight};
14 | `;
15 |
16 | export const H1 = styled.h1`
17 | ${headerCommonMixin(1)};
18 | color: ${({ theme }) => theme.colors.text.primary};
19 |
20 | ${extensionsHook('H1')};
21 | `;
22 |
23 | export const H2 = styled.h2`
24 | ${headerCommonMixin(2)};
25 | color: ${({ theme }) => theme.colors.text.primary};
26 | margin: 0 0 20px;
27 |
28 | ${extensionsHook('H2')};
29 | `;
30 |
31 | export const H3 = styled.h2`
32 | ${headerCommonMixin(3)};
33 | color: ${({ theme }) => theme.colors.text.primary};
34 |
35 | ${extensionsHook('H3')};
36 | `;
37 |
38 | export const RightPanelHeader = styled.h3`
39 | color: ${({ theme }) => theme.rightPanel.textColor};
40 |
41 | ${extensionsHook('RightPanelHeader')};
42 | `;
43 |
44 | export const UnderlinedHeader = styled.h5`
45 | border-bottom: 1px solid rgba(38, 50, 56, 0.3);
46 | margin: 1em 0 1em 0;
47 | color: rgba(38, 50, 56, 0.5);
48 | font-weight: normal;
49 | text-transform: uppercase;
50 | font-size: 0.929em;
51 | line-height: 20px;
52 |
53 | ${extensionsHook('UnderlinedHeader')};
54 | `;
55 |
--------------------------------------------------------------------------------
/src/common-elements/index.ts:
--------------------------------------------------------------------------------
1 | export * from './panels';
2 | export * from './headers';
3 | export * from './linkify';
4 | export * from './shelfs';
5 | export * from './fields-layout';
6 | export * from './schema';
7 | export * from './mixins';
8 | export * from './tabs';
9 | export * from './samples';
10 | export * from './perfect-scrollbar';
11 | export * from './Dropdown';
12 |
--------------------------------------------------------------------------------
/src/common-elements/mixins.ts:
--------------------------------------------------------------------------------
1 | import { css } from '../styled-components';
2 |
3 | export const deprecatedCss = css`
4 | text-decoration: line-through;
5 | color: #707070;
6 | `;
7 |
--------------------------------------------------------------------------------
/src/common-elements/panels.ts:
--------------------------------------------------------------------------------
1 | import { SECTION_ATTR } from '../services/MenuStore';
2 | import styled, { media } from '../styled-components';
3 |
4 | export const MiddlePanel = styled.div<{ $compact?: boolean }>`
5 | width: calc(100% - ${props => props.theme.rightPanel.width});
6 | padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
7 |
8 | ${({ $compact, theme }) =>
9 | media.lessThan('medium', true)`
10 | width: 100%;
11 | padding: ${`${$compact ? 0 : theme.spacing.sectionVertical}px ${
12 | theme.spacing.sectionHorizontal
13 | }px`};
14 | `};
15 | `;
16 |
17 | export const Section = styled.div.attrs(props => ({
18 | [SECTION_ATTR]: props.id,
19 | }))<{ $underlined?: boolean }>`
20 | padding: ${props => props.theme.spacing.sectionVertical}px 0;
21 |
22 | &:last-child {
23 | min-height: calc(100vh + 1px);
24 | }
25 |
26 | & > &:last-child {
27 | min-height: initial;
28 | }
29 |
30 | ${media.lessThan('medium', true)`
31 | padding: 0;
32 | `}
33 | ${({ $underlined }) =>
34 | ($underlined &&
35 | `
36 | position: relative;
37 |
38 | &:not(:last-of-type):after {
39 | position: absolute;
40 | bottom: 0;
41 | width: 100%;
42 | display: block;
43 | content: '';
44 | border-bottom: 1px solid rgba(0, 0, 0, 0.2);
45 | }
46 | `) ||
47 | ''}
48 | `;
49 |
50 | export const RightPanel = styled.div`
51 | width: ${props => props.theme.rightPanel.width};
52 | color: ${({ theme }) => theme.rightPanel.textColor};
53 | background-color: ${props => props.theme.rightPanel.backgroundColor};
54 | padding: 0 ${props => props.theme.spacing.sectionHorizontal}px;
55 |
56 | ${media.lessThan('medium', true)`
57 | width: 100%;
58 | padding: ${props =>
59 | `${props.theme.spacing.sectionVertical}px ${props.theme.spacing.sectionHorizontal}px`};
60 | `};
61 | `;
62 |
63 | export const DarkRightPanel = styled(RightPanel)`
64 | background-color: ${props => props.theme.rightPanel.backgroundColor};
65 | `;
66 |
67 | export const Row = styled.div`
68 | display: flex;
69 | width: 100%;
70 | padding: 0;
71 |
72 | ${media.lessThan('medium', true)`
73 | flex-direction: column;
74 | `};
75 | `;
76 |
--------------------------------------------------------------------------------
/src/common-elements/samples.tsx:
--------------------------------------------------------------------------------
1 | import styled from '../styled-components';
2 | import { PrismDiv } from './PrismDiv';
3 |
4 | export const SampleControls = styled.div`
5 | opacity: 0.7;
6 | transition: opacity 0.3s ease;
7 | text-align: right;
8 | &:focus-within {
9 | opacity: 1;
10 | }
11 | > button {
12 | background-color: transparent;
13 | border: 0;
14 | color: inherit;
15 | padding: 2px 10px;
16 | font-family: ${({ theme }) => theme.typography.fontFamily};
17 | font-size: ${({ theme }) => theme.typography.fontSize};
18 | line-height: ${({ theme }) => theme.typography.lineHeight};
19 | cursor: pointer;
20 | outline: 0;
21 |
22 | :hover,
23 | :focus {
24 | background: rgba(255, 255, 255, 0.1);
25 | }
26 | }
27 | `;
28 |
29 | export const SampleControlsWrap = styled.div`
30 | &:hover ${SampleControls} {
31 | opacity: 1;
32 | }
33 | `;
34 |
35 | export const StyledPre = styled(PrismDiv).attrs({
36 | as: 'pre',
37 | })`
38 | font-family: ${props => props.theme.typography.code.fontFamily};
39 | font-size: ${props => props.theme.typography.code.fontSize};
40 | overflow-x: auto;
41 | margin: 0;
42 |
43 | white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
44 | `;
45 |
--------------------------------------------------------------------------------
/src/common-elements/schema.ts:
--------------------------------------------------------------------------------
1 | import styled from '../styled-components';
2 | import { darken } from 'polished';
3 | import { deprecatedCss } from './mixins';
4 |
5 | export const OneOfList = styled.div`
6 | margin: 0 0 3px 0;
7 | display: inline-block;
8 | `;
9 |
10 | export const OneOfLabel = styled.span`
11 | font-size: 0.9em;
12 | margin-right: 10px;
13 | color: ${props => props.theme.colors.primary.main};
14 | font-family: ${props => props.theme.typography.headings.fontFamily};
15 | }
16 | `;
17 |
18 | export const OneOfButton = styled.button<{ $active: boolean; $deprecated: boolean }>`
19 | display: inline-block;
20 | margin-right: 10px;
21 | margin-bottom: 5px;
22 | font-size: 0.8em;
23 | cursor: pointer;
24 | border: 1px solid ${props => props.theme.colors.primary.main};
25 | padding: 2px 10px;
26 | line-height: 1.5em;
27 | outline: none;
28 | &:focus {
29 | box-shadow: 0 0 0 1px ${props => props.theme.colors.primary.main};
30 | }
31 |
32 | ${({ $deprecated }) => ($deprecated && deprecatedCss) || ''};
33 |
34 | ${props => {
35 | if (props.$active) {
36 | return `
37 | color: white;
38 | background-color: ${props.theme.colors.primary.main};
39 | &:focus {
40 | box-shadow: none;
41 | background-color: ${darken(0.15, props.theme.colors.primary.main)};
42 | }
43 | `;
44 | } else {
45 | return `
46 | color: ${props.theme.colors.primary.main};
47 | background-color: white;
48 | `;
49 | }
50 | }}
51 | `;
52 |
53 | export const ArrayOpenningLabel = styled.div`
54 | font-size: 0.9em;
55 | font-family: ${props => props.theme.typography.code.fontFamily};
56 | &::after {
57 | content: ' [';
58 | }
59 | `;
60 |
61 | export const ArrayClosingLabel = styled.div`
62 | font-size: 0.9em;
63 | font-family: ${props => props.theme.typography.code.fontFamily};
64 | &::after {
65 | content: ']';
66 | }
67 | `;
68 |
--------------------------------------------------------------------------------
/src/common-elements/shelfs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from '../styled-components';
3 |
4 | const directionMap = {
5 | left: '90deg',
6 | right: '-90deg',
7 | up: '-180deg',
8 | down: '0',
9 | };
10 |
11 | const IntShelfIcon = (props: {
12 | className?: string;
13 | float?: 'left' | 'right';
14 | size?: string;
15 | color?: string;
16 | direction: 'left' | 'right' | 'up' | 'down';
17 | style?: React.CSSProperties;
18 | }): JSX.Element => {
19 | return (
20 |
32 | );
33 | };
34 |
35 | export const ShelfIcon = styled(IntShelfIcon)`
36 | height: ${props => props.size || '18px'};
37 | width: ${props => props.size || '18px'};
38 | min-width: ${props => props.size || '18px'};
39 | vertical-align: middle;
40 | float: ${props => props.float || ''};
41 | transition: transform 0.2s ease-out;
42 | transform: rotateZ(${props => directionMap[props.direction || 'down']});
43 |
44 | polygon {
45 | fill: ${({ color, theme }) =>
46 | (color && theme.colors.responses[color] && theme.colors.responses[color].color) || color};
47 | }
48 | `;
49 |
50 | export const Badge = styled.span<{ type: string; color?: string }>`
51 | display: inline-block;
52 | padding: 2px 8px;
53 | margin: 0;
54 | background-color: ${props => props.color || props.theme.colors[props.type].main};
55 | color: ${props => props.theme.colors[props.type].contrastText};
56 | font-size: ${props => props.theme.typography.code.fontSize};
57 | vertical-align: middle;
58 | line-height: 1.6;
59 | border-radius: 4px;
60 | font-weight: ${({ theme }) => theme.typography.fontWeightBold};
61 | font-size: 12px;
62 | + span[type] {
63 | margin-left: 4px;
64 | }
65 | `;
66 |
--------------------------------------------------------------------------------
/src/components/ApiInfo/index.ts:
--------------------------------------------------------------------------------
1 | export { ApiInfo } from './ApiInfo';
2 |
--------------------------------------------------------------------------------
/src/components/ApiInfo/styled.elements.ts:
--------------------------------------------------------------------------------
1 | import { H1, MiddlePanel } from '../../common-elements';
2 | import styled, { extensionsHook } from '../../styled-components';
3 |
4 | const delimiterWidth = 15;
5 |
6 | export const ApiInfoWrap = MiddlePanel;
7 |
8 | export const ApiHeader = styled(H1)`
9 | margin-top: 0;
10 | margin-bottom: 0.5em;
11 |
12 | ${extensionsHook('ApiHeader')};
13 | `;
14 |
15 | export const DownloadButton = styled.a`
16 | border: 1px solid ${props => props.theme.colors.primary.main};
17 | color: ${props => props.theme.colors.primary.main};
18 | font-weight: normal;
19 | margin-left: 0.5em;
20 | padding: 4px 8px 4px;
21 | display: inline-block;
22 | text-decoration: none;
23 | cursor: pointer;
24 |
25 | ${extensionsHook('DownloadButton')};
26 | `;
27 |
28 | export const InfoSpan = styled.span`
29 | &::before {
30 | content: '|';
31 | display: inline-block;
32 | opacity: 0.5;
33 | width: ${delimiterWidth}px;
34 | text-align: center;
35 | }
36 |
37 | &:last-child::after {
38 | display: none;
39 | }
40 | `;
41 |
42 | export const InfoSpanBoxWrap = styled.div`
43 | overflow: hidden;
44 | `;
45 |
46 | export const InfoSpanBox = styled.div`
47 | display: flex;
48 | flex-wrap: wrap;
49 | // hide separator on new lines: idea from https://stackoverflow.com/a/31732902/1749888
50 | margin-left: -${delimiterWidth}px;
51 | `;
52 |
--------------------------------------------------------------------------------
/src/components/ApiLogo/ApiLogo.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 | import { OpenAPIInfo } from '../../types';
4 | import { LinkWrap, LogoImgEl, LogoWrap } from './styled.elements';
5 |
6 | @observer
7 | export class ApiLogo extends React.Component<{ info: OpenAPIInfo }> {
8 | render() {
9 | const { info } = this.props;
10 | const logoInfo = info['x-logo'];
11 | if (!logoInfo || !logoInfo.url) {
12 | return null;
13 | }
14 |
15 | const logoHref = logoInfo.href || (info.contact && info.contact.url);
16 |
17 | // Use the english word logo if no alt text is provided
18 | const altText = logoInfo.altText ? logoInfo.altText : 'logo';
19 |
20 | const logo = ;
21 | return (
22 |
23 | {logoHref ? LinkWrap(logoHref)(logo) : logo}
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/ApiLogo/styled.elements.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from '../../styled-components';
3 |
4 | export const LogoImgEl = styled.img`
5 | max-height: ${props => props.theme.logo.maxHeight};
6 | max-width: ${props => props.theme.logo.maxWidth};
7 | padding: ${props => props.theme.logo.gutter};
8 | width: 100%;
9 | display: block;
10 | `;
11 |
12 | export const LogoWrap = styled.div`
13 | text-align: center;
14 | `;
15 |
16 | const Link = styled.a`
17 | display: inline-block;
18 | `;
19 |
20 | // eslint-disable-next-line react/display-name
21 | export const LinkWrap = url => Component => {Component};
22 |
--------------------------------------------------------------------------------
/src/components/CallbackSamples/CallbackReqSamples.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import styled from '../../styled-components';
4 | import { DropdownProps } from '../../common-elements';
5 | import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
6 | import { OperationModel } from '../../services/models';
7 | import { XPayloadSample } from '../../services/models/Operation';
8 | import { isPayloadSample } from '../../services';
9 |
10 | export interface PayloadSampleProps {
11 | callback: OperationModel;
12 | renderDropdown: (props: DropdownProps) => JSX.Element;
13 | }
14 |
15 | export class CallbackPayloadSample extends React.Component {
16 | render() {
17 | const payloadSample = this.props.callback.codeSamples.find(sample =>
18 | isPayloadSample(sample),
19 | ) as XPayloadSample | undefined;
20 |
21 | if (!payloadSample) {
22 | return null;
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | export const PayloadSampleWrapper = styled.div`
34 | margin-top: 15px;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/components/Callbacks/CallbackDetails.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { OperationModel } from '../../services/models';
5 | import styled from '../../styled-components';
6 | import { Endpoint } from '../Endpoint/Endpoint';
7 | import { ExternalDocumentation } from '../ExternalDocumentation/ExternalDocumentation';
8 | import { Extensions } from '../Fields/Extensions';
9 | import { Markdown } from '../Markdown/Markdown';
10 | import { Parameters } from '../Parameters/Parameters';
11 | import { ResponsesList } from '../Responses/ResponsesList';
12 | import { SecurityRequirements } from '../SecurityRequirement/SecurityRequirement';
13 | import { CallbackDetailsWrap } from './styled.elements';
14 |
15 | export interface CallbackDetailsProps {
16 | operation: OperationModel;
17 | }
18 |
19 | @observer
20 | export class CallbackDetails extends React.Component {
21 | render() {
22 | const { operation } = this.props;
23 | const { description, externalDocs } = operation;
24 | const hasDescription = !!(description || externalDocs);
25 |
26 | return (
27 |
28 | {hasDescription && (
29 |
30 | {description !== undefined && }
31 | {externalDocs && }
32 |
33 | )}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 | }
43 |
44 | const Description = styled.div`
45 | margin-bottom: ${({ theme }) => theme.spacing.unit * 3}px;
46 | `;
47 |
--------------------------------------------------------------------------------
/src/components/Callbacks/CallbackOperation.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { OperationModel } from '../../services/models';
5 | import { StyledCallbackTitle } from './styled.elements';
6 | import { CallbackDetails } from './CallbackDetails';
7 |
8 | @observer
9 | export class CallbackOperation extends React.Component<{ callbackOperation: OperationModel }> {
10 | toggle = () => {
11 | this.props.callbackOperation.toggle();
12 | };
13 |
14 | render() {
15 | const { name, expanded, httpVerb, deprecated } = this.props.callbackOperation;
16 |
17 | return (
18 | <>
19 |
26 | {expanded && }
27 | >
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Callbacks/CallbackTitle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { darken } from 'polished';
4 | import { ShelfIcon } from '../../common-elements';
5 | import { OperationBadge } from '../SideMenu/styled.elements';
6 | import { shortenHTTPVerb } from '../../utils/openapi';
7 | import styled from '../../styled-components';
8 | import { Badge } from '../../common-elements/';
9 | import { l } from '../../services/Labels';
10 |
11 | export interface CallbackTitleProps {
12 | name: string;
13 | opened?: boolean;
14 | httpVerb: string;
15 | deprecated?: boolean;
16 | className?: string;
17 | onClick?: () => void;
18 | }
19 |
20 | export const CallbackTitle = (props: CallbackTitleProps) => {
21 | const { name, opened, className, onClick, httpVerb, deprecated } = props;
22 |
23 | return (
24 |
25 | {shortenHTTPVerb(httpVerb)}
26 |
27 | {name}
28 | {deprecated ? {l('deprecated')} : null}
29 |
30 | );
31 | };
32 |
33 | const CallbackTitleWrapper = styled.button`
34 | border: 0;
35 | width: 100%;
36 | text-align: left;
37 | & > * {
38 | vertical-align: middle;
39 | }
40 |
41 | ${ShelfIcon} {
42 | polygon {
43 | fill: ${({ theme }) => darken(theme.colors.tonalOffset, theme.colors.gray[100])};
44 | }
45 | }
46 | `;
47 |
48 | const CallbackName = styled.span<{ $deprecated?: boolean }>`
49 | text-decoration: ${props => (props.$deprecated ? 'line-through' : 'none')};
50 | margin-right: 8px;
51 | `;
52 |
53 | const OperationBadgeStyled = styled(OperationBadge)`
54 | margin: 0 5px 0 0;
55 | `;
56 |
--------------------------------------------------------------------------------
/src/components/Callbacks/CallbacksList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { CallbackModel } from '../../services/models';
4 | import styled from '../../styled-components';
5 | import { CallbackOperation } from './CallbackOperation';
6 |
7 | export interface CallbacksListProps {
8 | callbacks: CallbackModel[];
9 | }
10 |
11 | export class CallbacksList extends React.PureComponent {
12 | render() {
13 | const { callbacks } = this.props;
14 |
15 | if (!callbacks || callbacks.length === 0) {
16 | return null;
17 | }
18 |
19 | return (
20 |
21 | Callbacks
22 | {callbacks.map(callback => {
23 | return callback.operations.map((operation, index) => {
24 | return (
25 |
26 | );
27 | });
28 | })}
29 |
30 | );
31 | }
32 | }
33 |
34 | const CallbacksHeader = styled.h3`
35 | font-size: 1.3em;
36 | padding: 0.2em 0;
37 | margin: 3em 0 1.1em;
38 | color: ${({ theme }) => theme.colors.text.primary};
39 | font-weight: normal;
40 | `;
41 |
--------------------------------------------------------------------------------
/src/components/Callbacks/index.ts:
--------------------------------------------------------------------------------
1 | export * from './CallbackOperation';
2 | export * from './CallbackTitle';
3 | export * from './CallbacksList';
4 |
--------------------------------------------------------------------------------
/src/components/Callbacks/styled.elements.ts:
--------------------------------------------------------------------------------
1 | import styled from '../../styled-components';
2 | import { CallbackTitle } from './CallbackTitle';
3 | import { darken } from 'polished';
4 |
5 | export const StyledCallbackTitle = styled(CallbackTitle)`
6 | padding: 10px;
7 | border-radius: 2px;
8 | margin-bottom: 4px;
9 | line-height: 1.5em;
10 | background-color: ${({ theme }) => theme.colors.gray[100]};
11 | cursor: pointer;
12 | outline-color: ${({ theme }) => darken(theme.colors.tonalOffset, theme.colors.gray[100])};
13 | `;
14 |
15 | export const CallbackDetailsWrap = styled.div`
16 | padding: 10px 25px;
17 | background-color: ${({ theme }) => theme.colors.gray[50]};
18 | margin-bottom: 5px;
19 | margin-top: 5px;
20 | `;
21 |
--------------------------------------------------------------------------------
/src/components/DropdownOrLabel/DropdownOrLabel.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { StyledComponent } from 'styled-components';
3 |
4 | import { DropdownProps, MimeLabel, SimpleDropdown } from '../../common-elements/Dropdown';
5 |
6 | export interface DropdownOrLabelProps extends DropdownProps {
7 | Label?: StyledComponent, never>;
8 | Dropdown?: StyledComponent<
9 | React.NamedExoticComponent,
10 | any,
11 | {
12 | fullWidth?: boolean | undefined;
13 | },
14 | never
15 | >;
16 | }
17 |
18 | export function DropdownOrLabel(props: DropdownOrLabelProps): JSX.Element {
19 | const { Label = MimeLabel, Dropdown = SimpleDropdown } = props;
20 | if (props.options.length === 1) {
21 | return ;
22 | }
23 | return ;
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from '../styled-components';
3 |
4 | const ErrorWrapper = styled.div`
5 | padding: 20px;
6 | color: red;
7 | `;
8 |
9 | export class ErrorBoundary extends React.Component<
10 | React.PropsWithChildren,
11 | { error?: Error }
12 | > {
13 | constructor(props) {
14 | super(props);
15 | this.state = { error: undefined };
16 | }
17 |
18 | componentDidCatch(error) {
19 | this.setState({ error });
20 | return false;
21 | }
22 |
23 | render() {
24 | if (this.state.error) {
25 | return (
26 |
27 | Something went wrong...
28 | {this.state.error.message}
29 |
30 |
31 | Stack trace
32 | {this.state.error.stack}
33 |
34 |
35 | ReDoc Version: {__REDOC_VERSION__}
36 | Commit: {__REDOC_REVISION__}
37 |
38 | );
39 | }
40 | return {React.Children.only(this.props.children)};
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/ExternalDocumentation/ExternalDocumentation.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 | import styled from '../../styled-components';
4 | import { OpenAPIExternalDocumentation } from '../../types';
5 | import { linksCss } from '../Markdown/styled.elements';
6 |
7 | const LinkWrap = styled.div<{ $compact?: boolean }>`
8 | ${linksCss};
9 | ${({ $compact }) => (!$compact ? 'margin: 1em 0' : '')}
10 | `;
11 |
12 | @observer
13 | export class ExternalDocumentation extends React.Component<{
14 | externalDocs: OpenAPIExternalDocumentation;
15 | compact?: boolean;
16 | }> {
17 | render() {
18 | const { externalDocs } = this.props;
19 | if (!externalDocs || !externalDocs.url) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 | {externalDocs.description || externalDocs.url}
26 |
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Fields/ArrayItemDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { TypeFormat, TypePrefix } from '../../common-elements/fields';
3 | import { ConstraintsView } from './FieldConstraints';
4 | import { Pattern } from './Pattern';
5 | import { SchemaModel } from '../../services';
6 | import styled from '../../styled-components';
7 | import { OptionsContext } from '../OptionsProvider';
8 |
9 | export function ArrayItemDetails({ schema }: { schema: SchemaModel }) {
10 | const { hideSchemaPattern } = React.useContext(OptionsContext);
11 | if (
12 | !schema ||
13 | ((!schema?.pattern || hideSchemaPattern) &&
14 | !schema.items &&
15 | !schema.displayFormat &&
16 | !schema.constraints?.length) // return null for cases where all constraints are empty
17 | ) {
18 | return null;
19 | }
20 |
21 | return (
22 |
23 | [ items
24 | {schema.displayFormat && <{schema.displayFormat} >}
25 |
26 |
27 | {schema.items && } ]
28 |
29 | );
30 | }
31 |
32 | const Wrapper = styled(TypePrefix)`
33 | margin: 0 5px;
34 | vertical-align: text-top;
35 | `;
36 |
--------------------------------------------------------------------------------
/src/components/Fields/Examples.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { FieldLabel, ExampleValue } from '../../common-elements/fields';
4 | import { getSerializedValue, isArray } from '../../utils';
5 |
6 | import { l } from '../../services/Labels';
7 | import { FieldModel } from '../../services';
8 | import styled from '../../styled-components';
9 |
10 | export function Examples({ field }: { field: FieldModel }) {
11 | if (!field.examples) {
12 | return null;
13 | }
14 |
15 | return (
16 | <>
17 | {l('examples')}:
18 | {isArray(field.examples) ? (
19 | field.examples.map((example, idx) => {
20 | const value = getSerializedValue(field, example);
21 | const stringifyValue = field.in ? String(value) : JSON.stringify(value);
22 | return (
23 |
24 | {stringifyValue}{' '}
25 |
26 | );
27 | })
28 | ) : (
29 |
30 | {Object.values(field.examples).map((example, idx) => (
31 |
32 | {getSerializedValue(field, example.value)} -{' '}
33 | {example.summary || example.description}
34 |
35 | ))}
36 |
37 | )}
38 | >
39 | );
40 | }
41 |
42 | const ExamplesList = styled.ul`
43 | margin-top: 1em;
44 | list-style-position: outside;
45 | `;
46 |
--------------------------------------------------------------------------------
/src/components/Fields/Extensions.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ExtensionValue, FieldLabel } from '../../common-elements/fields';
4 |
5 | import styled from '../../styled-components';
6 |
7 | import { OptionsContext } from '../OptionsProvider';
8 |
9 | import { StyledMarkdownBlock } from '../Markdown/styled.elements';
10 |
11 | const Extension = styled(StyledMarkdownBlock)`
12 | margin: 2px 0;
13 | `;
14 |
15 | export interface ExtensionsProps {
16 | extensions: {
17 | [k: string]: any;
18 | };
19 | }
20 |
21 | export class Extensions extends React.PureComponent {
22 | render() {
23 | const exts = this.props.extensions;
24 | return (
25 |
26 | {options => (
27 | <>
28 | {options.showExtensions &&
29 | Object.keys(exts).map(key => (
30 |
31 | {key.substring(2)}: {' '}
32 |
33 | {typeof exts[key] === 'string' ? exts[key] : JSON.stringify(exts[key])}
34 |
35 |
36 | ))}
37 | >
38 | )}
39 |
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Fields/FieldConstraints.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ConstraintItem } from '../../common-elements/fields';
3 |
4 | export interface ConstraintsViewProps {
5 | constraints: string[];
6 | }
7 |
8 | export class ConstraintsView extends React.PureComponent {
9 | render() {
10 | if (this.props.constraints.length === 0) {
11 | return null;
12 | }
13 | return (
14 |
15 | {' '}
16 | {this.props.constraints.map(constraint => (
17 | {constraint}
18 | ))}
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/Fields/FieldDetail.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ExampleValue, FieldLabel } from '../../common-elements/fields';
3 |
4 | export interface FieldDetailProps {
5 | value?: any;
6 | label: string;
7 | raw?: boolean;
8 | }
9 |
10 | function FieldDetailComponent({ value, label, raw }: FieldDetailProps) {
11 | if (value === undefined) {
12 | return null;
13 | }
14 |
15 | const stringifyValue = raw ? String(value) : JSON.stringify(value);
16 |
17 | return (
18 |
19 | {label} {stringifyValue}
20 |
21 | );
22 | }
23 |
24 | export const FieldDetail = React.memo(FieldDetailComponent);
25 |
--------------------------------------------------------------------------------
/src/components/Fields/Pattern.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { PatternLabel, ToggleButton } from '../../common-elements/fields';
3 | import { OptionsContext } from '../OptionsProvider';
4 | import { SchemaModel } from '../../services';
5 |
6 | const MAX_PATTERN_LENGTH = 45;
7 |
8 | export function Pattern(props: { schema: SchemaModel }) {
9 | const pattern = props.schema.pattern;
10 | const { hideSchemaPattern } = React.useContext(OptionsContext);
11 | const [isPatternShown, setIsPatternShown] = React.useState(false);
12 | const togglePattern = React.useCallback(
13 | () => setIsPatternShown(!isPatternShown),
14 | [isPatternShown],
15 | );
16 |
17 | if (!pattern || hideSchemaPattern) return null;
18 |
19 | return (
20 | <>
21 |
22 | {isPatternShown || pattern.length < MAX_PATTERN_LENGTH
23 | ? pattern
24 | : `${pattern.substr(0, MAX_PATTERN_LENGTH)}...`}
25 |
26 | {pattern.length > MAX_PATTERN_LENGTH && (
27 |
28 | {isPatternShown ? 'Hide pattern' : 'Show pattern'}
29 |
30 | )}
31 | >
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/GenericChildrenSwitcher/GenericChildrenSwitcher.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { DropdownProps, DropdownOption } from '../../common-elements/Dropdown';
5 | import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
6 |
7 | export interface GenericChildrenSwitcherProps {
8 | items?: T[];
9 | options: DropdownOption[];
10 | label?: string;
11 | renderDropdown: (props: DropdownProps) => JSX.Element;
12 | children: (activeItem: T) => JSX.Element;
13 | }
14 |
15 | export interface GenericChildrenSwitcherState {
16 | activeItemIdx: number;
17 | }
18 | /**
19 | * TODO: Refactor this component:
20 | * Implement rendering dropdown/label directly in this component
21 | * Accept as a parameter mapper-function for building dropdown option labels
22 | */
23 | @observer
24 | export class GenericChildrenSwitcher extends React.Component<
25 | GenericChildrenSwitcherProps,
26 | GenericChildrenSwitcherState
27 | > {
28 | constructor(props) {
29 | super(props);
30 | this.state = {
31 | activeItemIdx: 0,
32 | };
33 | }
34 |
35 | switchItem = ({ idx }: DropdownOption) => {
36 | if (this.props.items && idx !== undefined) {
37 | this.setState({
38 | activeItemIdx: idx,
39 | });
40 | }
41 | };
42 |
43 | render() {
44 | const { items } = this.props;
45 |
46 | if (!items || !items.length) {
47 | return null;
48 | }
49 |
50 | const Wrapper = ({ children }) =>
51 | this.props.label ? (
52 |
53 | {this.props.label}
54 | {children}
55 |
56 | ) : (
57 | children
58 | );
59 |
60 | return (
61 | <>
62 |
63 | {this.props.renderDropdown({
64 | value: this.props.options[this.state.activeItemIdx].value,
65 | options: this.props.options,
66 | onChange: this.switchItem,
67 | ariaLabel: this.props.label || 'Callback',
68 | })}
69 |
70 |
71 | {this.props.children(items[this.state.activeItemIdx])}
72 | >
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/JsonViewer/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './JsonViewer';
2 |
--------------------------------------------------------------------------------
/src/components/Loading/Loading.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from '../../styled-components';
3 |
4 | import { Spinner } from './Spinner.svg';
5 |
6 | const LoadingMessage = styled.div<{ color: string }>`
7 | font-family: helvetica, sans;
8 | width: 100%;
9 | text-align: center;
10 | font-size: 25px;
11 | margin: 30px 0 20px 0;
12 | color: ${props => props.color};
13 | `;
14 |
15 | export interface LoadingProps {
16 | color: string;
17 | }
18 |
19 | export class Loading extends React.PureComponent {
20 | render() {
21 | return (
22 |
23 | Loading ...
24 |
25 |
26 | );
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/Markdown/AdvancedMarkdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { AppStore, MarkdownRenderer, RedocNormalizedOptions } from '../../services';
4 | import { BaseMarkdownProps } from './Markdown';
5 | import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
6 |
7 | import { OptionsConsumer } from '../OptionsProvider';
8 | import { StoreConsumer } from '../StoreBuilder';
9 |
10 | export interface AdvancedMarkdownProps extends BaseMarkdownProps {
11 | htmlWrap?: (part: JSX.Element) => JSX.Element;
12 | parentId?: string;
13 | }
14 |
15 | export class AdvancedMarkdown extends React.Component {
16 | render() {
17 | return (
18 |
19 | {options => (
20 | {store => this.renderWithOptionsAndStore(options, store)}
21 | )}
22 |
23 | );
24 | }
25 |
26 | renderWithOptionsAndStore(options: RedocNormalizedOptions, store?: AppStore) {
27 | const { source, htmlWrap = i => i } = this.props;
28 | if (!store) {
29 | throw new Error('When using components in markdown, store prop must be provided');
30 | }
31 |
32 | const renderer = new MarkdownRenderer(options, this.props.parentId);
33 | const parts = renderer.renderMdWithComponents(source);
34 |
35 | if (!parts.length) {
36 | return null;
37 | }
38 |
39 | return parts.map((part, idx) => {
40 | if (typeof part === 'string') {
41 | return React.cloneElement(
42 | htmlWrap(),
43 | { key: idx },
44 | );
45 | }
46 | const PartComponent = part.component as React.FunctionComponent;
47 | return ;
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Markdown/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { MarkdownRenderer } from '../../services';
4 | import { SanitizedMarkdownHTML } from './SanitizedMdBlock';
5 |
6 | export interface StylingMarkdownProps {
7 | compact?: boolean;
8 | inline?: boolean;
9 | }
10 |
11 | export interface BaseMarkdownProps {
12 | sanitize?: boolean;
13 | source: string;
14 | }
15 |
16 | export type MarkdownProps = BaseMarkdownProps &
17 | StylingMarkdownProps & {
18 | source: string;
19 | className?: string;
20 | 'data-role'?: string;
21 | };
22 |
23 | export class Markdown extends React.Component {
24 | render() {
25 | const { source, inline, compact, className, 'data-role': dataRole } = this.props;
26 | const renderer = new MarkdownRenderer();
27 | return (
28 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Markdown/SanitizedMdBlock.tsx:
--------------------------------------------------------------------------------
1 | import * as DOMPurify from 'dompurify';
2 | import * as React from 'react';
3 |
4 | import { OptionsConsumer } from '../OptionsProvider';
5 | import { StylingMarkdownProps } from './Markdown';
6 | import { StyledMarkdownBlock } from './styled.elements';
7 | import styled from 'styled-components';
8 |
9 | // Workaround for DOMPurify type issues (https://github.com/cure53/DOMPurify/issues/1034)
10 | const dompurify = DOMPurify['default'] as DOMPurify.DOMPurify;
11 |
12 | const StyledMarkdownSpan = styled(StyledMarkdownBlock)`
13 | display: inline;
14 | `;
15 |
16 | const sanitize = (sanitize, html) => (sanitize ? dompurify.sanitize(html) : html);
17 |
18 | export function SanitizedMarkdownHTML({
19 | inline,
20 | compact,
21 | ...rest
22 | }: StylingMarkdownProps & { html: string; className?: string; 'data-role'?: string }) {
23 | const Wrap = inline ? StyledMarkdownSpan : StyledMarkdownBlock;
24 |
25 | return (
26 |
27 | {options => (
28 |
38 | )}
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/MediaTypeSwitch/MediaTypesSwitch.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { DropdownOption, DropdownProps } from '../../common-elements/Dropdown';
5 | import { MediaContentModel, MediaTypeModel, SchemaModel } from '../../services/models';
6 | import { DropdownLabel, DropdownWrapper } from '../PayloadSamples/styled.elements';
7 |
8 | export interface MediaTypeChildProps {
9 | schema: SchemaModel;
10 | mime?: string;
11 | }
12 |
13 | export interface MediaTypesSwitchProps {
14 | content?: MediaContentModel;
15 | withLabel?: boolean;
16 |
17 | renderDropdown: (props: DropdownProps) => JSX.Element;
18 | children: (activeMime: MediaTypeModel) => JSX.Element;
19 | }
20 |
21 | @observer
22 | export class MediaTypesSwitch extends React.Component {
23 | switchMedia = ({ idx }: DropdownOption) => {
24 | if (this.props.content && idx !== undefined) {
25 | this.props.content.activate(idx);
26 | }
27 | };
28 |
29 | render() {
30 | const { content } = this.props;
31 | if (!content || !content.mediaTypes || !content.mediaTypes.length) {
32 | return null;
33 | }
34 | const activeMimeIdx = content.activeMimeIdx;
35 |
36 | const options = content.mediaTypes.map((mime, idx) => {
37 | return {
38 | value: mime.name,
39 | idx,
40 | };
41 | });
42 |
43 | const Wrapper = ({ children }) =>
44 | this.props.withLabel ? (
45 |
46 | Content type
47 | {children}
48 |
49 | ) : (
50 | children
51 | );
52 |
53 | return (
54 | <>
55 |
56 | {this.props.renderDropdown({
57 | value: options[activeMimeIdx].value,
58 | options,
59 | onChange: this.switchMedia,
60 | ariaLabel: 'Content type',
61 | })}
62 |
63 | {this.props.children(content.active)}
64 | >
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/OptionsProvider.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { RedocNormalizedOptions } from '../services/RedocNormalizedOptions';
4 |
5 | export const OptionsContext = React.createContext(new RedocNormalizedOptions({}));
6 | export const OptionsProvider = OptionsContext.Provider;
7 | export const OptionsConsumer = OptionsContext.Consumer;
8 |
--------------------------------------------------------------------------------
/src/components/Parameters/ParametersGroup.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { UnderlinedHeader } from '../../common-elements';
4 | import { PropertiesTable } from '../../common-elements/fields-layout';
5 |
6 | import { FieldModel } from '../../services/models';
7 | import { Field } from '../Fields/Field';
8 |
9 | import { mapWithLast } from '../../utils';
10 |
11 | export interface ParametersGroupProps {
12 | place: string;
13 | parameters: FieldModel[];
14 | }
15 |
16 | export class ParametersGroup extends React.PureComponent {
17 | render() {
18 | const { place, parameters } = this.props;
19 | if (!parameters || !parameters.length) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 |
{place} Parameters
26 |
27 |
28 | {mapWithLast(parameters, (field, isLast) => (
29 |
30 | ))}
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/PayloadSamples/Example.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { StyledPre } from '../../common-elements/samples';
4 | import { ExampleModel } from '../../services/models';
5 | import { ExampleValue } from './ExampleValue';
6 | import { useExternalExample } from './exernalExampleHook';
7 |
8 | export interface ExampleProps {
9 | example: ExampleModel;
10 | mimeType: string;
11 | }
12 |
13 | export function Example({ example, mimeType }: ExampleProps) {
14 | if (example.value === undefined && example.externalValueUrl) {
15 | return ;
16 | } else {
17 | return ;
18 | }
19 | }
20 |
21 | export function ExternalExample({ example, mimeType }: ExampleProps) {
22 | const value = useExternalExample(example, mimeType);
23 |
24 | if (value === undefined) {
25 | return Loading...;
26 | }
27 |
28 | if (value instanceof Error) {
29 | return (
30 |
31 | Error loading external example:
32 |
38 | {example.externalValueUrl}
39 |
40 |
41 | );
42 | }
43 |
44 | return ;
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/PayloadSamples/ExampleValue.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { isJsonLike, langFromMime } from '../../utils/openapi';
4 | import { JsonViewer } from '../JsonViewer/JsonViewer';
5 | import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
6 |
7 | export interface ExampleValueProps {
8 | value: any;
9 | mimeType: string;
10 | }
11 |
12 | export function ExampleValue({ value, mimeType }: ExampleValueProps) {
13 | if (isJsonLike(mimeType)) {
14 | return ;
15 | } else {
16 | if (typeof value === 'object') {
17 | // just in case example was cached as json but used as non-json
18 | value = JSON.stringify(value, null, 2);
19 | }
20 | return ;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/PayloadSamples/PayloadSamples.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 | import { MediaTypeSamples } from './MediaTypeSamples';
4 |
5 | import { MediaContentModel } from '../../services/models';
6 | import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
7 | import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
8 | import { InvertedSimpleDropdown, MimeLabel } from './styled.elements';
9 |
10 | export interface PayloadSamplesProps {
11 | content: MediaContentModel;
12 | }
13 |
14 | @observer
15 | export class PayloadSamples extends React.Component {
16 | render() {
17 | const mimeContent = this.props.content;
18 | if (mimeContent === undefined) {
19 | return null;
20 | }
21 |
22 | return (
23 |
24 | {mediaType => (
25 |
30 | )}
31 |
32 | );
33 | }
34 |
35 | private renderDropdown = props => {
36 | return (
37 |
43 | );
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/PayloadSamples/exernalExampleHook.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useState } from 'react';
2 | import { ExampleModel } from '../../services/models/Example';
3 |
4 | export function useExternalExample(example: ExampleModel, mimeType: string) {
5 | const [, setIsLoading] = useState(true); // to trigger component reload
6 |
7 | const value = useRef(undefined);
8 | const prevRef = useRef(undefined);
9 |
10 | if (prevRef.current !== example) {
11 | value.current = undefined;
12 | }
13 |
14 | prevRef.current = example;
15 |
16 | useEffect(() => {
17 | const load = async () => {
18 | setIsLoading(true);
19 | try {
20 | value.current = await example.getExternalValue(mimeType);
21 | } catch (e) {
22 | value.current = e;
23 | }
24 | setIsLoading(false);
25 | };
26 |
27 | load();
28 | }, [example, mimeType]);
29 |
30 | return value.current;
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/PayloadSamples/styled.elements.ts:
--------------------------------------------------------------------------------
1 | import { transparentize } from 'polished';
2 | import styled from '../../styled-components';
3 | import { Dropdown } from '../../common-elements/Dropdown';
4 |
5 | export const MimeLabel = styled.div`
6 | padding: 0.9em;
7 | background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
8 | margin: 0 0 10px 0;
9 | display: block;
10 | font-family: ${({ theme }) => theme.typography.headings.fontFamily};
11 | font-size: 0.929em;
12 | line-height: 1.5em;
13 | `;
14 |
15 | export const DropdownLabel = styled.span`
16 | font-family: ${({ theme }) => theme.typography.headings.fontFamily};
17 | font-size: 12px;
18 | position: absolute;
19 | z-index: 1;
20 | top: -11px;
21 | left: 12px;
22 | font-weight: ${({ theme }) => theme.typography.fontWeightBold};
23 | color: ${({ theme }) => transparentize(0.3, theme.rightPanel.textColor)};
24 | `;
25 |
26 | export const DropdownWrapper = styled.div`
27 | position: relative;
28 | `;
29 |
30 | export const InvertedSimpleDropdown = styled(Dropdown)`
31 | label {
32 | color: ${({ theme }) => theme.rightPanel.textColor};
33 | text-overflow: ellipsis;
34 | white-space: nowrap;
35 | overflow: hidden;
36 | font-size: 1em;
37 | text-transform: none;
38 | border: none;
39 | }
40 | margin: 0 0 10px 0;
41 | display: block;
42 | background-color: ${({ theme }) => transparentize(0.6, theme.rightPanel.backgroundColor)};
43 | border: none;
44 | padding: 0.9em 1.6em 0.9em 0.9em;
45 | box-shadow: none;
46 | &:hover,
47 | &:focus-within {
48 | border: none;
49 | box-shadow: none;
50 | background-color: ${({ theme }) => transparentize(0.3, theme.rightPanel.backgroundColor)};
51 | }
52 | `;
53 |
54 | export const NoSampleLabel = styled.div`
55 | font-family: ${props => props.theme.typography.code.fontFamily};
56 | font-size: 12px;
57 | color: #ee807f;
58 | `;
59 |
--------------------------------------------------------------------------------
/src/components/Redoc/styled.elements.tsx:
--------------------------------------------------------------------------------
1 | import styled, { media } from '../../styled-components';
2 |
3 | export const RedocWrap = styled.div`
4 | ${({ theme }) => `
5 | font-family: ${theme.typography.fontFamily};
6 | font-size: ${theme.typography.fontSize};
7 | font-weight: ${theme.typography.fontWeightRegular};
8 | line-height: ${theme.typography.lineHeight};
9 | color: ${theme.colors.text.primary};
10 | display: flex;
11 | position: relative;
12 | text-align: left;
13 |
14 | -webkit-font-smoothing: ${theme.typography.smoothing};
15 | font-smoothing: ${theme.typography.smoothing};
16 | ${(theme.typography.optimizeSpeed && 'text-rendering: optimizeSpeed !important') || ''};
17 |
18 | tap-highlight-color: rgba(0, 0, 0, 0);
19 | text-size-adjust: 100%;
20 |
21 | * {
22 | box-sizing: border-box;
23 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
24 | }
25 | `};
26 | `;
27 |
28 | export const ApiContentWrap = styled.div`
29 | z-index: 1;
30 | position: relative;
31 | overflow: hidden;
32 | width: calc(100% - ${props => props.theme.sidebar.width});
33 | ${media.lessThan('small', true)`
34 | width: 100%;
35 | `};
36 |
37 | contain: layout;
38 | `;
39 |
40 | export const BackgroundStub = styled.div`
41 | background: ${({ theme }) => theme.rightPanel.backgroundColor};
42 | position: absolute;
43 | top: 0;
44 | bottom: 0;
45 | right: 0;
46 | width: ${({ theme }) => {
47 | if (theme.rightPanel.width.endsWith('%')) {
48 | const percents = parseInt(theme.rightPanel.width, 10);
49 | return `calc((100% - ${theme.sidebar.width}) * ${percents / 100})`;
50 | } else {
51 | return theme.rightPanel.width;
52 | }
53 | }};
54 | ${media.lessThan('medium', true)`
55 | display: none;
56 | `};
57 | `;
58 |
--------------------------------------------------------------------------------
/src/components/RedocStandalone.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import {
4 | argValueToBoolean,
5 | RedocNormalizedOptions,
6 | RedocRawOptions,
7 | } from '../services/RedocNormalizedOptions';
8 | import { ErrorBoundary } from './ErrorBoundary';
9 | import { Loading } from './Loading/Loading';
10 | import { Redoc } from './Redoc/Redoc';
11 | import { StoreBuilder } from './StoreBuilder';
12 |
13 | export interface RedocStandaloneProps {
14 | spec?: object;
15 | specUrl?: string;
16 | options?: RedocRawOptions;
17 | onLoaded?: (e?: Error) => any;
18 | }
19 |
20 | declare let __webpack_nonce__: string;
21 |
22 | export const RedocStandalone = function (props: RedocStandaloneProps) {
23 | const { spec, specUrl, options = {}, onLoaded } = props;
24 | const hideLoading = argValueToBoolean(options.hideLoading, false);
25 |
26 | const normalizedOpts = new RedocNormalizedOptions(options);
27 |
28 | if (normalizedOpts.nonce !== undefined) {
29 | try {
30 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
31 | __webpack_nonce__ = normalizedOpts.nonce;
32 | } catch {} // If we have exception, Webpack was not used to run this.
33 | }
34 |
35 | return (
36 |
37 |
43 | {({ loading, store }) =>
44 | !loading ? (
45 |
46 | ) : hideLoading ? null : (
47 |
48 | )
49 | }
50 |
51 |
52 | );
53 | };
54 |
--------------------------------------------------------------------------------
/src/components/RequestSamples/RequestSamples.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 | import { isPayloadSample, OperationModel, RedocNormalizedOptions } from '../../services';
4 | import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
5 | import { SourceCodeWithCopy } from '../SourceCode/SourceCode';
6 |
7 | import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
8 | import { OptionsContext } from '../OptionsProvider';
9 | import { l } from '../../services/Labels';
10 |
11 | export interface RequestSamplesProps {
12 | operation: OperationModel;
13 | }
14 |
15 | @observer
16 | export class RequestSamples extends React.Component {
17 | static contextType = OptionsContext;
18 | context: RedocNormalizedOptions;
19 | operation: OperationModel;
20 |
21 | render() {
22 | const { operation } = this.props;
23 | const samples = operation.codeSamples;
24 |
25 | const hasSamples = samples.length > 0;
26 | const hideTabList = samples.length === 1 ? this.context.hideSingleRequestSampleTab : false;
27 | return (
28 | (hasSamples && (
29 |
30 |
{l('requestSamples')}
31 |
32 |
33 |
34 | {samples.map(sample => (
35 |
36 | {sample.label !== undefined ? sample.label : sample.lang}
37 |
38 | ))}
39 |
40 | {samples.map(sample => (
41 |
42 | {isPayloadSample(sample) ? (
43 |
46 | ) : (
47 |
48 | )}
49 |
50 | ))}
51 |
52 |
53 | )) ||
54 | null
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/ResponseSamples/ResponseSamples.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { OperationModel } from '../../services/models';
5 |
6 | import { RightPanelHeader, Tab, TabList, TabPanel, Tabs } from '../../common-elements';
7 | import { PayloadSamples } from '../PayloadSamples/PayloadSamples';
8 | import { l } from '../../services/Labels';
9 |
10 | export interface ResponseSamplesProps {
11 | operation: OperationModel;
12 | }
13 |
14 | @observer
15 | export class ResponseSamples extends React.Component {
16 | operation: OperationModel;
17 |
18 | render() {
19 | const { operation } = this.props;
20 | const responses = operation.responses.filter(response => {
21 | return response.content && response.content.hasSample;
22 | });
23 |
24 | return (
25 | (responses.length > 0 && (
26 |
27 |
{l('responseSamples')}
28 |
29 |
30 |
31 | {responses.map(response => (
32 |
33 | {response.code}
34 |
35 | ))}
36 |
37 | {responses.map(response => (
38 |
39 |
42 |
43 | ))}
44 |
45 |
46 | )) ||
47 | null
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/Responses/Response.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { observer } from 'mobx-react';
3 |
4 | import type { ResponseModel, MediaTypeModel } from '../../services/models';
5 | import { ResponseDetails } from './ResponseDetails';
6 | import { ResponseDetailsWrap, StyledResponseTitle } from './styled.elements';
7 |
8 | export interface ResponseViewProps {
9 | response: ResponseModel;
10 | }
11 |
12 | export const ResponseView = observer(({ response }: ResponseViewProps): React.ReactElement => {
13 | const { extensions, headers, type, summary, description, code, expanded, content } = response;
14 |
15 | const mimes = React.useMemo(
16 | () =>
17 | content === undefined ? [] : content.mediaTypes.filter(mime => mime.schema !== undefined),
18 | [content],
19 | );
20 |
21 | const empty = React.useMemo(
22 | () =>
23 | (!extensions || Object.keys(extensions).length === 0) &&
24 | headers.length === 0 &&
25 | mimes.length === 0 &&
26 | !description,
27 | [extensions, headers, mimes, description],
28 | );
29 |
30 | return (
31 |
32 | response.toggle()}
34 | type={type}
35 | empty={empty}
36 | title={summary || ''}
37 | code={code}
38 | opened={expanded}
39 | />
40 | {expanded && !empty && (
41 |
42 |
43 |
44 | )}
45 |
46 | );
47 | });
48 |
--------------------------------------------------------------------------------
/src/components/Responses/ResponseDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ResponseModel } from '../../services/models';
4 |
5 | import { UnderlinedHeader } from '../../common-elements';
6 | import { DropdownOrLabel } from '../DropdownOrLabel/DropdownOrLabel';
7 | import { MediaTypesSwitch } from '../MediaTypeSwitch/MediaTypesSwitch';
8 | import { Schema } from '../Schema';
9 |
10 | import { Extensions } from '../Fields/Extensions';
11 | import { Markdown } from '../Markdown/Markdown';
12 | import { ResponseHeaders } from './ResponseHeaders';
13 | import { ConstraintsView } from '../Fields/FieldConstraints';
14 |
15 | export class ResponseDetails extends React.PureComponent<{ response: ResponseModel }> {
16 | render() {
17 | const { description, extensions, headers, content } = this.props.response;
18 | return (
19 | <>
20 | {description && }
21 |
22 |
23 |
24 | {({ schema }) => {
25 | return (
26 | <>
27 | {schema?.type === 'object' && (
28 |
29 | )}
30 |
31 | >
32 | );
33 | }}
34 |
35 | >
36 | );
37 | }
38 |
39 | private renderDropdown = props => {
40 | return (
41 |
42 | Response Schema:
43 |
44 | );
45 | };
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/Responses/ResponseHeaders.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { PropertiesTable } from '../../common-elements/fields-layout';
3 |
4 | import { FieldModel } from '../../services/models';
5 | import { mapWithLast } from '../../utils';
6 | import { Field } from '../Fields/Field';
7 | import { HeadersCaption } from './styled.elements';
8 |
9 | export interface ResponseHeadersProps {
10 | headers?: FieldModel[];
11 | }
12 |
13 | export class ResponseHeaders extends React.PureComponent {
14 | render() {
15 | const { headers } = this.props;
16 | if (headers === undefined || headers.length === 0) {
17 | return null;
18 | }
19 | return (
20 |
21 | Response Headers
22 |
23 | {mapWithLast(headers, (header, isLast) => (
24 |
25 | ))}
26 |
27 |
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Responses/ResponseTitle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Code } from './styled.elements';
4 | import { ShelfIcon } from '../../common-elements';
5 | import { Markdown } from '../Markdown/Markdown';
6 |
7 | export interface ResponseTitleProps {
8 | code: string;
9 | title: string;
10 | type: string;
11 | empty?: boolean;
12 | opened?: boolean;
13 | className?: string;
14 | onClick?: () => void;
15 | }
16 |
17 | function ResponseTitleComponent({
18 | title,
19 | type,
20 | empty,
21 | code,
22 | opened,
23 | className,
24 | onClick,
25 | }: ResponseTitleProps): React.ReactElement {
26 | return (
27 |
44 | );
45 | }
46 |
47 | export const ResponseTitle = React.memo(ResponseTitleComponent);
48 |
--------------------------------------------------------------------------------
/src/components/Responses/ResponsesList.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { l } from '../../services/Labels';
3 | import { ResponseModel } from '../../services/models';
4 | import styled from '../../styled-components';
5 | import { ResponseView } from './Response';
6 |
7 | const ResponsesHeader = styled.h3`
8 | font-size: 1.3em;
9 | padding: 0.2em 0;
10 | margin: 3em 0 1.1em;
11 | color: ${({ theme }) => theme.colors.text.primary};
12 | font-weight: normal;
13 | `;
14 |
15 | export interface ResponseListProps {
16 | responses: ResponseModel[];
17 | isCallback?: boolean;
18 | }
19 |
20 | export class ResponsesList extends React.PureComponent {
21 | render() {
22 | const { responses, isCallback } = this.props;
23 |
24 | if (!responses || responses.length === 0) {
25 | return null;
26 | }
27 |
28 | return (
29 |
30 | {isCallback ? l('callbackResponses') : l('responses')}
31 | {responses.map(response => {
32 | return ;
33 | })}
34 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Responses/styled.elements.ts:
--------------------------------------------------------------------------------
1 | import { UnderlinedHeader } from '../../common-elements';
2 | import styled from '../../styled-components';
3 | import { ResponseTitle } from './ResponseTitle';
4 |
5 | export const StyledResponseTitle = styled(ResponseTitle)`
6 | display: block;
7 | border: 0;
8 | width: 100%;
9 | text-align: left;
10 | padding: 10px;
11 | border-radius: 2px;
12 | margin-bottom: 4px;
13 | line-height: 1.5em;
14 | cursor: pointer;
15 |
16 | color: ${props => props.theme.colors.responses[props.type].color};
17 | background-color: ${props => props.theme.colors.responses[props.type].backgroundColor};
18 | &:focus {
19 | outline: auto ${props => props.theme.colors.responses[props.type].color};
20 | }
21 | ${props =>
22 | (props.empty &&
23 | `
24 | cursor: default;
25 | &::before {
26 | content: "—";
27 | font-weight: bold;
28 | width: 1.5em;
29 | text-align: center;
30 | display: inline-block;
31 | vertical-align: top;
32 | }
33 | &:focus {
34 | outline: 0;
35 | }
36 | `) ||
37 | ''};
38 | `;
39 |
40 | export const ResponseDetailsWrap = styled.div`
41 | padding: 10px;
42 | `;
43 |
44 | export const HeadersCaption = styled(UnderlinedHeader).attrs({
45 | as: 'caption',
46 | })`
47 | text-align: left;
48 | margin-top: 1em;
49 | caption-side: top;
50 | `;
51 |
52 | export const Code = styled.strong`
53 | vertical-align: top;
54 | `;
55 |
--------------------------------------------------------------------------------
/src/components/Schema/ArraySchema.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { Schema, SchemaProps } from './Schema';
4 |
5 | import { ArrayClosingLabel, ArrayOpenningLabel } from '../../common-elements';
6 | import styled from '../../styled-components';
7 | import { humanizeConstraints } from '../../utils';
8 | import { TypeName } from '../../common-elements/fields';
9 | import { ObjectSchema } from './ObjectSchema';
10 |
11 | const PaddedSchema = styled.div`
12 | padding-left: ${({ theme }) => theme.spacing.unit * 2}px;
13 | `;
14 |
15 | export class ArraySchema extends React.PureComponent {
16 | render() {
17 | const schema = this.props.schema;
18 | const itemsSchema = schema.items;
19 | const fieldParentsName = this.props.fieldParentsName;
20 |
21 | const minMaxItems =
22 | schema.minItems === undefined && schema.maxItems === undefined
23 | ? ''
24 | : `(${humanizeConstraints(schema)})`;
25 |
26 | const updatedParentsArray = fieldParentsName
27 | ? [...fieldParentsName.slice(0, -1), fieldParentsName[fieldParentsName.length - 1] + '[]']
28 | : fieldParentsName;
29 | if (schema.fields) {
30 | return (
31 |
36 | );
37 | }
38 | if (schema.displayType && !itemsSchema && !minMaxItems.length) {
39 | return (
40 |
41 | {schema.displayType}
42 |
43 | );
44 | }
45 |
46 | return (
47 |
48 |
Array {minMaxItems}
49 |
50 |
51 |
52 |
53 |
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/components/Schema/DiscriminatorDropdown.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { DropdownOption, Dropdown } from '../../common-elements/Dropdown';
5 | import { SchemaModel } from '../../services/models';
6 |
7 | @observer
8 | export class DiscriminatorDropdown extends React.Component<{
9 | parent: SchemaModel;
10 | enumValues: string[];
11 | }> {
12 | sortOptions(options: DropdownOption[], enumValues: string[]): void {
13 | if (enumValues.length === 0) {
14 | return;
15 | }
16 |
17 | const enumOrder = {};
18 |
19 | enumValues.forEach((enumItem, idx) => {
20 | enumOrder[enumItem] = idx;
21 | });
22 |
23 | options.sort((a, b) => {
24 | return enumOrder[a.value] > enumOrder[b.value] ? 1 : -1;
25 | });
26 | }
27 |
28 | render() {
29 | const { parent, enumValues } = this.props;
30 | if (parent.oneOf === undefined) {
31 | return null;
32 | }
33 |
34 | const options = parent.oneOf.map((subSchema, idx) => {
35 | return {
36 | value: subSchema.title,
37 | idx,
38 | };
39 | });
40 |
41 | const activeValue = options[parent.activeOneOf].value;
42 |
43 | this.sortOptions(options, enumValues);
44 |
45 | return (
46 |
52 | );
53 | }
54 |
55 | changeActiveChild = (option: DropdownOption) => {
56 | if (option.idx !== undefined) {
57 | this.props.parent.activateOneOf(option.idx);
58 | }
59 | };
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/Schema/OneOfSchema.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import {
5 | OneOfButton as StyledOneOfButton,
6 | OneOfLabel,
7 | OneOfList,
8 | } from '../../common-elements/schema';
9 | import { Badge } from '../../common-elements/shelfs';
10 | import { SchemaModel } from '../../services/models';
11 | import { ConstraintsView } from '../Fields/FieldConstraints';
12 | import { Schema, SchemaProps } from './Schema';
13 |
14 | export interface OneOfButtonProps {
15 | subSchema: SchemaModel;
16 | idx: number;
17 | schema: SchemaModel;
18 | }
19 |
20 | @observer
21 | export class OneOfButton extends React.Component {
22 | render() {
23 | const { idx, schema, subSchema } = this.props;
24 | return (
25 |
30 | {subSchema.title || subSchema.typePrefix + subSchema.displayType}
31 |
32 | );
33 | }
34 |
35 | activateOneOf = () => {
36 | this.props.schema.activateOneOf(this.props.idx);
37 | };
38 | }
39 |
40 | @observer
41 | export class OneOfSchema extends React.Component {
42 | render() {
43 | const {
44 | schema: { oneOf },
45 | schema,
46 | } = this.props;
47 |
48 | if (oneOf === undefined) {
49 | return null;
50 | }
51 | const activeSchema = oneOf[schema.activeOneOf];
52 |
53 | return (
54 |
55 |
{schema.oneOfType}
56 |
57 | {oneOf.map((subSchema, idx) => (
58 |
59 | ))}
60 |
61 |
62 | {oneOf[schema.activeOneOf].deprecated && Deprecated}
63 |
64 |
65 |
66 |
67 | );
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/Schema/RecursiveSchema.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { observer } from 'mobx-react';
3 |
4 | import { RecursiveLabel, TypeName, TypeTitle } from '../../common-elements/fields';
5 | import { l } from '../../services/Labels';
6 | import type { SchemaProps } from '.';
7 |
8 | export const RecursiveSchema = observer(({ schema }: SchemaProps) => {
9 | return (
10 |
11 | {schema.displayType}
12 | {schema.title && {schema.title} }
13 | {l('recursive')}
14 |
15 | );
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/Schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Schema';
2 | export * from './ObjectSchema';
3 | export * from './OneOfSchema';
4 | export * from './ArraySchema';
5 | export * from './DiscriminatorDropdown';
6 |
--------------------------------------------------------------------------------
/src/components/SecurityRequirement/RequiredScopesRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export const RequiredScopesRow = ({ scopes }: { scopes: string[] }): JSX.Element | null => {
4 | if (!scopes.length) return null;
5 |
6 | return (
7 |
8 | Required scopes:
9 | {scopes.map((scope, idx) => {
10 | return (
11 |
12 | {scope}
{' '}
13 |
14 | );
15 | })}
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/components/SecurityRequirement/SecurityDetails.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { SecuritySchemeModel } from '../../services';
3 | import { titleize } from '../../utils';
4 | import { StyledMarkdownBlock } from '../Markdown/styled.elements';
5 | import { SecurityRow } from './styled.elements';
6 | import { OAuthFlow } from './OAuthFlow';
7 |
8 | interface SecuritySchemaProps {
9 | RequiredScopes?: JSX.Element;
10 | scheme: SecuritySchemeModel;
11 | }
12 | export function SecurityDetails(props: SecuritySchemaProps) {
13 | const { RequiredScopes, scheme } = props;
14 |
15 | return (
16 |
17 | {scheme.apiKey ? (
18 | <>
19 |
20 | {titleize(scheme.apiKey.in || '')} parameter name:
21 | {scheme.apiKey.name}
22 |
23 | {RequiredScopes}
24 | >
25 | ) : scheme.http ? (
26 | <>
27 |
28 | HTTP Authorization Scheme:
29 | {scheme.http.scheme}
30 |
31 |
32 | {scheme.http.scheme === 'bearer' && scheme.http.bearerFormat && (
33 | <>
34 | Bearer format:
35 | {scheme.http.bearerFormat}
36 | >
37 | )}
38 |
39 | {RequiredScopes}
40 | >
41 | ) : scheme.openId ? (
42 | <>
43 |
44 | Connect URL:
45 |
46 |
47 | {scheme.openId.connectUrl}
48 |
49 |
50 |
51 | {RequiredScopes}
52 | >
53 | ) : scheme.flows ? (
54 | Object.keys(scheme.flows).map(type => (
55 |
61 | ))
62 | ) : null}
63 |
64 | );
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/SecurityRequirement/SecurityHeader.tsx:
--------------------------------------------------------------------------------
1 | import { SecurityRequirementModel } from '../../services/models/SecurityRequirement';
2 | import {
3 | ScopeName,
4 | SecurityRequirementAndWrap,
5 | SecurityRequirementOrWrap,
6 | } from './styled.elements';
7 | import * as React from 'react';
8 | import { AUTH_TYPES } from '../SecuritySchemes/SecuritySchemes';
9 |
10 | export interface SecurityRequirementProps {
11 | security: SecurityRequirementModel;
12 | showSecuritySchemeType?: boolean;
13 | expanded: boolean;
14 | }
15 |
16 | export function SecurityHeader(props: SecurityRequirementProps) {
17 | const { security, showSecuritySchemeType, expanded } = props;
18 |
19 | const grouping = security.schemes.length > 1;
20 | if (security.schemes.length === 0)
21 | return None;
22 | return (
23 |
24 | {grouping && '('}
25 | {security.schemes.map(scheme => {
26 | return (
27 |
28 | {showSecuritySchemeType && `${AUTH_TYPES[scheme.type] || scheme.type}: `}
29 | {scheme.displayName}
30 | {expanded && scheme.scopes.length
31 | ? [
32 | ' (',
33 | scheme.scopes.map(scope => (
34 | {scope}
35 | )),
36 | ') ',
37 | ]
38 | : null}
39 |
40 | );
41 | })}
42 | {grouping && ') '}
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/SecuritySchemes/SecuritySchemes.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { SecuritySchemesModel } from '../../services';
4 | import { H2, Row, ShareLink, MiddlePanel, Section } from '../../common-elements';
5 | import { Markdown } from '../Markdown/Markdown';
6 | import { SecurityDetails } from '../SecurityRequirement/SecurityDetails';
7 | import { SecurityDetailsStyle, SecurityRow } from '../SecurityRequirement/styled.elements';
8 |
9 | export const AUTH_TYPES = {
10 | oauth2: 'OAuth2',
11 | apiKey: 'API Key',
12 | http: 'HTTP',
13 | openIdConnect: 'OpenID Connect',
14 | };
15 |
16 | export interface SecurityDefsProps {
17 | securitySchemes: SecuritySchemesModel;
18 | }
19 |
20 | export class SecurityDefs extends React.PureComponent {
21 | render() {
22 | return this.props.securitySchemes.schemes.map(scheme => (
23 |
24 |
25 |
26 |
27 |
28 | {scheme.displayName}
29 |
30 |
31 |
32 |
33 | Security Scheme Type:
34 | {AUTH_TYPES[scheme.type] || scheme.type}
35 |
36 |
37 |
38 |
39 |
40 |
41 | ));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/SeeMore/SeeMore.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const TOLERANCE_PX = 20;
5 |
6 | interface SeeMoreProps {
7 | children?: React.ReactNode;
8 | height: string;
9 | }
10 |
11 | export function SeeMore({ children, height }: SeeMoreProps): JSX.Element {
12 | const ref = React.createRef() as React.RefObject;
13 | const [showMore, setShowMore] = React.useState(false);
14 | const [showLink, setShowLink] = React.useState(false);
15 |
16 | React.useEffect(() => {
17 | if (ref.current && ref.current.clientHeight + TOLERANCE_PX < ref.current.scrollHeight) {
18 | setShowLink(true);
19 | }
20 | }, [ref]);
21 |
22 | const onClickMore = () => {
23 | setShowMore(!showMore);
24 | };
25 |
26 | return (
27 | <>
28 |
33 | {children}
34 |
35 |
36 | {showLink && (
37 |
38 | {showMore ? 'See less' : 'See more'}
39 |
40 | )}
41 |
42 | >
43 | );
44 | }
45 |
46 | const Container = styled.div`
47 | overflow-y: hidden;
48 | `;
49 |
50 | const ButtonContainer = styled.div<{ $dimmed?: boolean }>`
51 | text-align: center;
52 | line-height: 1.5em;
53 | ${({ $dimmed }) =>
54 | $dimmed &&
55 | `background-image: linear-gradient(to bottom, transparent,rgb(255 255 255));
56 | position: relative;
57 | top: -0.5em;
58 | padding-top: 0.5em;
59 | background-position-y: -1em;
60 | `}
61 | `;
62 |
63 | const ButtonLinkStyled = styled.a`
64 | cursor: pointer;
65 | `;
66 |
--------------------------------------------------------------------------------
/src/components/SelectOnClick/SelectOnClick.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { ClipboardService } from '../../services';
4 |
5 | export class SelectOnClick extends React.PureComponent> {
6 | private child: HTMLDivElement | null;
7 | selectElement = () => {
8 | ClipboardService.selectElement(this.child);
9 | };
10 |
11 | render() {
12 | const { children } = this.props;
13 | return (
14 | (this.child = el)}
16 | onClick={this.selectElement}
17 | onFocus={this.selectElement}
18 | tabIndex={0}
19 | role="button"
20 | >
21 | {children}
22 |
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/SideMenu/Logo.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import * as React from 'react';
3 |
4 | export default function RedoclyLogo(): JSX.Element | null {
5 | const [isDisplay, setDisplay] = useState(false);
6 |
7 | useEffect(() => {
8 | setDisplay(true);
9 | }, []);
10 |
11 | return isDisplay ? (
12 |
setDisplay(false)}
15 | src={'https://cdn.redoc.ly/redoc/logo-mini.svg'}
16 | />
17 | ) : null;
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/SideMenu/MenuItems.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import type { IMenuItem } from '../../services';
5 |
6 | import { MenuItem } from './MenuItem';
7 | import { MenuItemUl } from './styled.elements';
8 |
9 | export interface MenuItemsProps {
10 | items: IMenuItem[];
11 | expanded?: boolean;
12 | onActivate?: (item: IMenuItem) => void;
13 | style?: React.CSSProperties;
14 | root?: boolean;
15 |
16 | className?: string;
17 | }
18 |
19 | @observer
20 | export class MenuItems extends React.Component {
21 | render() {
22 | const { items, root, className } = this.props;
23 | const expanded = this.props.expanded == null ? true : this.props.expanded;
24 | return (
25 |
31 | {items.map((item, idx) => (
32 |
33 | ))}
34 |
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/SideMenu/SideMenu.tsx:
--------------------------------------------------------------------------------
1 | import { observer } from 'mobx-react';
2 | import * as React from 'react';
3 |
4 | import { MenuStore } from '../../services';
5 | import type { IMenuItem } from '../../services';
6 | import { OptionsContext } from '../OptionsProvider';
7 | import { MenuItems } from './MenuItems';
8 |
9 | import { PerfectScrollbarWrap } from '../../common-elements/perfect-scrollbar';
10 | import { RedocAttribution } from './styled.elements';
11 | import RedoclyLogo from './Logo';
12 |
13 | @observer
14 | export class SideMenu extends React.Component<{ menu: MenuStore; className?: string }> {
15 | static contextType = OptionsContext;
16 | declare context: React.ContextType;
17 | private _updateScroll?: () => void;
18 |
19 | render() {
20 | const store = this.props.menu;
21 | return (
22 |
29 |
30 |
31 |
32 |
33 | API docs by Redocly
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | activate = (item: IMenuItem) => {
41 | if (item && item.active && this.context.menuToggle) {
42 | return item.expanded ? item.collapse() : item.expand();
43 | }
44 | this.props.menu.activateAndScroll(item, true);
45 | setTimeout(() => {
46 | if (this._updateScroll) {
47 | this._updateScroll();
48 | }
49 | });
50 | };
51 |
52 | private saveScrollUpdate = upd => {
53 | this._updateScroll = upd;
54 | };
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/SideMenu/index.ts:
--------------------------------------------------------------------------------
1 | export * from './MenuItem';
2 | export * from './MenuItems';
3 | export * from './SideMenu';
4 | export * from './styled.elements';
5 |
--------------------------------------------------------------------------------
/src/components/SourceCode/SourceCode.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { highlight } from '../../utils';
3 |
4 | import { SampleControls, SampleControlsWrap, StyledPre } from '../../common-elements';
5 | import { CopyButtonWrapper } from '../../common-elements/CopyButtonWrapper';
6 |
7 | export interface SourceCodeProps {
8 | source: string;
9 | lang: string;
10 | }
11 |
12 | export const SourceCode = (props: SourceCodeProps) => {
13 | const { source, lang } = props;
14 | return ;
15 | };
16 |
17 | export const SourceCodeWithCopy = (props: SourceCodeProps) => {
18 | const { source, lang } = props;
19 | return (
20 |
21 | {({ renderCopyButton }) => (
22 |
23 | {renderCopyButton()}
24 |
25 |
26 | )}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/StickySidebar/ChevronSvg.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import styled from '../../styled-components';
4 |
5 | export const AnimatedChevronButton = ({ open }: { open: boolean }) => {
6 | const iconOffset = open ? 8 : -4;
7 |
8 | return (
9 |
10 |
17 |
24 |
25 | );
26 | };
27 |
28 | // adapted from reactjs.org
29 | const ChevronSvg = ({ size = 10, className = '', style }) => (
30 |
56 | );
57 |
58 | const ChevronContainer = styled.div`
59 | user-select: none;
60 | width: 20px;
61 | height: 20px;
62 | align-self: center;
63 | display: flex;
64 | flex-direction: column;
65 | color: ${props => props.theme.colors.primary.main};
66 | `;
67 |
--------------------------------------------------------------------------------
/src/components/__tests__/Schema.test.tsx:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-implicit-dependencies */
2 |
3 | import { shallow } from 'enzyme';
4 | import * as React from 'react';
5 |
6 | import { Schema } from '../';
7 | import { OpenAPIParser, SchemaModel } from '../../services';
8 | import { RedocNormalizedOptions } from '../../services/RedocNormalizedOptions';
9 | import { withTheme } from '../testProviders';
10 |
11 | const options = new RedocNormalizedOptions({});
12 | describe('Components', () => {
13 | describe('SchemaView', () => {
14 | const parser = new OpenAPIParser(
15 | { openapi: '3.0', info: { title: 'test', version: '0' }, paths: {} },
16 | undefined,
17 | options,
18 | );
19 |
20 | describe('Show minProperties/maxProperties constraints', () => {
21 | const schema = new SchemaModel(
22 | parser,
23 | {
24 | properties: {
25 | name: {
26 | type: 'object',
27 | minProperties: 1,
28 | properties: {
29 | address: {
30 | type: 'string',
31 | },
32 | },
33 | },
34 | },
35 | },
36 | '',
37 | options,
38 | );
39 | const component = shallow(withTheme());
40 | expect(component.html().includes('non-empty')).toBe(true);
41 | });
42 |
43 | describe('Show range minProperties/maxProperties constraints', () => {
44 | const schema = new SchemaModel(
45 | parser,
46 | {
47 | properties: {
48 | name: {
49 | type: 'object',
50 | minProperties: 2,
51 | maxProperties: 10,
52 | additionalProperties: {
53 | type: 'string',
54 | },
55 | },
56 | },
57 | },
58 | '',
59 | options,
60 | );
61 | it('should includes [ 2 .. 10 ] properties', () => {
62 | const component = shallow(withTheme());
63 | expect(component.html().includes('[ 2 .. 10 ] properties')).toBe(true);
64 | });
65 | });
66 | });
67 | });
68 |
--------------------------------------------------------------------------------
/src/components/__tests__/__snapshots__/OneOfSchema.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`Components SchemaView OneOf deprecated should match snapshot 1`] = `
4 |
5 |
8 | One of
9 |
10 |
13 |
18 |
23 |
24 |
25 |
29 | Deprecated
30 |
31 |
32 |
33 |
34 |
35 |
38 |
41 | string
42 |
43 |
44 |
45 |
50 |
51 |
52 |
53 | `;
54 |
--------------------------------------------------------------------------------
/src/components/__tests__/fixtures/simple-callback.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "callbacks": {
9 | "Test": {
10 | "/test": {
11 | "post": {
12 | "operationId": "testCallback",
13 | "description": "Test callback.",
14 | "requestBody": {
15 | "content": {
16 | "application/json": {
17 | "schema": {
18 | "title": "TestTitle",
19 | "type": "object",
20 | "description": "Test description",
21 | "properties": {
22 | "type": {
23 | "type": "string",
24 | "description": "The type of response.",
25 | "enum": [
26 | "TestResponse.Complete"
27 | ]
28 | },
29 | "status": {
30 | "type": "string",
31 | "enum": [
32 | "FAILURE",
33 | "SUCCESS"
34 | ]
35 | }
36 | },
37 | "required": [
38 | "status"
39 | ]
40 | }
41 | }
42 | }
43 | },
44 | "parameters": [
45 | {
46 | "name": "X-Test-Header",
47 | "in": "header",
48 | "required": true,
49 | "example": "1",
50 | "description": "This is a test header parameter",
51 | "schema": {
52 | "type": "string"
53 | }
54 | }
55 | ],
56 | "responses": {
57 | "204": {
58 | "description": "Test response."
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/src/components/__tests__/fixtures/simple-discriminator.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "2.0.0",
3 | "components": {
4 | "schemas": {
5 | "Pet": {
6 | "type": "object",
7 | "required": [
8 | "type"
9 | ],
10 | "discriminator": {
11 | "propertyName": "type"
12 | },
13 | "properties": {
14 | "type": {
15 | "type": "string"
16 | }
17 | }
18 | },
19 | "Dog": {
20 | "type": "object",
21 | "allOf": [
22 | {
23 | "$ref": "#/components/schemas/Pet"
24 | }
25 | ],
26 | "properties": {
27 | "packSize": {
28 | "type": "number"
29 | }
30 | }
31 | },
32 | "Cat": {
33 | "type": "object",
34 | "allOf": [
35 | {
36 | "$ref": "#/components/schemas/Pet"
37 | },
38 | {
39 | "properties": {
40 | "packSize": {
41 | "type": "number"
42 | }
43 | }
44 | }
45 | ]
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/__tests__/fixtures/simple-security-fixture.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0",
3 | "info": {
4 | "title": "test",
5 | "version": "0"
6 | },
7 | "paths": {
8 | "/pet": {
9 | "put": {
10 | "summary": "Add a new pet to the store",
11 | "description": "Add new pet to the store inventory.",
12 | "operationId": "updatePet",
13 | "responses": {
14 | "405": {
15 | "description": "Invalid input"
16 | }
17 | },
18 | "security": [
19 | {
20 | "GitLab_PersonalAccessToken": [],
21 | "GitLab_OpenIdConnect": [],
22 | "basicAuth": []
23 | },
24 | {
25 | "petstore_auth": ["write:pets", "read:pets"]
26 | }
27 | ]
28 | }
29 | }
30 | },
31 | "components": {
32 | "securitySchemes": {
33 | "petstore_auth": {
34 | "description": "Get access to data while protecting your account credentials.\nOAuth2 is also a safer and more secure way to give you access.\n",
35 | "type": "oauth2",
36 | "bearerFormat": "",
37 | "flows": {
38 | "implicit": {
39 | "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog",
40 | "scopes": {
41 | "write:pets": "modify pets in your account",
42 | "read:pets": "read your pets"
43 | }
44 | }
45 | }
46 | },
47 | "GitLab_PersonalAccessToken": {
48 | "description": "GitLab Personal Access Token description",
49 | "type": "apiKey",
50 | "name": "PRIVATE-TOKEN",
51 | "in": "header",
52 | "bearerFormat": "",
53 | "flows": {}
54 | },
55 | "GitLab_OpenIdConnect": {
56 | "description": "GitLab OpenIdConnect description",
57 | "bearerFormat": "",
58 | "type": "openIdConnect",
59 | "openIdConnectUrl": "https://gitlab.com/.well-known/openid-configuration"
60 | },
61 | "basicAuth": {
62 | "type": "http",
63 | "scheme": "basic"
64 | }
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './RedocStandalone';
2 | export * from './Redoc/Redoc';
3 | export * from './ApiInfo/ApiInfo';
4 | export * from './ApiLogo/ApiLogo';
5 | export * from './ContentItems/ContentItems';
6 | export { ApiContentWrap, BackgroundStub, RedocWrap } from './Redoc/styled.elements';
7 | export * from './Schema/';
8 | export * from './SearchBox/SearchBox';
9 | export * from './Operation/Operation';
10 | export * from './Loading/Loading';
11 | export * from './JsonViewer';
12 | export * from './Markdown/Markdown';
13 | export { StyledMarkdownBlock } from './Markdown/styled.elements';
14 | export * from './SecuritySchemes/SecuritySchemes';
15 |
16 | export * from './Responses/Response';
17 | export * from './Responses/ResponseDetails';
18 | export * from './Responses/ResponseHeaders';
19 | export * from './Responses/ResponsesList';
20 | export * from './Responses/ResponseTitle';
21 | export * from './ResponseSamples/ResponseSamples';
22 | export * from './PayloadSamples/PayloadSamples';
23 | export * from './PayloadSamples/styled.elements';
24 | export * from './MediaTypeSwitch/MediaTypesSwitch';
25 | export * from './Parameters/Parameters';
26 | export * from './PayloadSamples/Example';
27 | export * from './DropdownOrLabel/DropdownOrLabel';
28 |
29 | export * from './ErrorBoundary';
30 | export * from './StoreBuilder';
31 | export * from './OptionsProvider';
32 | export * from './SideMenu/';
33 | export * from './StickySidebar/StickyResponsiveSidebar';
34 | export * from './SearchBox/SearchBox';
35 | export * from './SchemaDefinition/SchemaDefinition';
36 | export * from './SourceCode/SourceCode';
37 |
--------------------------------------------------------------------------------
/src/components/testProviders.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ThemeProvider } from 'styled-components';
3 | import defaultTheme, { resolveTheme } from '../theme';
4 |
5 | import { PropsWithChildren } from 'react';
6 |
7 | export default class TestThemeProvider extends React.Component> {
8 | render() {
9 | return (
10 |
11 | {React.Children.only(this.props.children as any)}
12 |
13 | );
14 | }
15 | }
16 |
17 | export function withTheme(children) {
18 | return {children};
19 | }
20 |
--------------------------------------------------------------------------------
/src/empty.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './components';
2 | export {
3 | MiddlePanel,
4 | Row,
5 | RightPanel,
6 | Section,
7 | Dropdown,
8 | SimpleDropdown,
9 | } from './common-elements/';
10 | export type { DropdownOption } from './common-elements';
11 | export type { OpenAPIEncoding } from './types';
12 | export * from './services';
13 | export * from './utils';
14 |
15 | export * from './styled-components';
16 | export { default as styled } from './styled-components';
17 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | import 'unfetch/polyfill/index';
2 | import 'core-js/es/symbol';
3 |
--------------------------------------------------------------------------------
/src/services/HistoryService.ts:
--------------------------------------------------------------------------------
1 | import { bind, debounce } from 'decko';
2 | import { EventEmitter } from 'eventemitter3';
3 | import { IS_BROWSER } from '../utils/';
4 |
5 | const EVENT = 'hashchange';
6 |
7 | export class HistoryService {
8 | private _emiter;
9 |
10 | constructor() {
11 | this._emiter = new EventEmitter();
12 | this.bind();
13 | }
14 |
15 | get currentId(): string {
16 | return IS_BROWSER ? decodeURIComponent(window.location.hash.substring(1)) : '';
17 | }
18 |
19 | linkForId(id: string) {
20 | if (!id) {
21 | return '';
22 | }
23 | return '#' + id;
24 | }
25 |
26 | subscribe(cb): () => void {
27 | const emmiter = this._emiter.addListener(EVENT, cb);
28 | return () => emmiter.removeListener(EVENT, cb);
29 | }
30 |
31 | emit = () => {
32 | this._emiter.emit(EVENT, this.currentId);
33 | };
34 |
35 | bind() {
36 | if (IS_BROWSER) {
37 | window.addEventListener('hashchange', this.emit, false);
38 | }
39 | }
40 |
41 | dispose() {
42 | if (IS_BROWSER) {
43 | window.removeEventListener('hashchange', this.emit);
44 | }
45 | }
46 |
47 | @bind
48 | @debounce
49 | replace(id: string | null, rewriteHistory: boolean = false) {
50 | if (!IS_BROWSER) {
51 | return;
52 | }
53 |
54 | if (id == null || id === this.currentId) {
55 | return;
56 | }
57 | if (rewriteHistory) {
58 | window.history.replaceState(
59 | null,
60 | '',
61 | window.location.href.split('#')[0] + this.linkForId(id),
62 | );
63 |
64 | return;
65 | }
66 | window.history.pushState(null, '', window.location.href.split('#')[0] + this.linkForId(id));
67 | this.emit();
68 | }
69 | }
70 |
71 | export const history = new HistoryService();
72 |
73 | if (module.hot) {
74 | module.hot.dispose(() => {
75 | history.dispose();
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/src/services/Labels.ts:
--------------------------------------------------------------------------------
1 | import type { LabelsConfig, LabelsConfigRaw } from './types';
2 |
3 | const labels: LabelsConfig = {
4 | enum: 'Enum',
5 | enumSingleValue: 'Value',
6 | enumArray: 'Items',
7 | default: 'Default',
8 | deprecated: 'Deprecated',
9 | example: 'Example',
10 | examples: 'Examples',
11 | recursive: 'Recursive',
12 | arrayOf: 'Array of ',
13 | webhook: 'Event',
14 | const: 'Value',
15 | noResultsFound: 'No results found',
16 | download: 'Download',
17 | downloadSpecification: 'Download OpenAPI specification',
18 | responses: 'Responses',
19 | callbackResponses: 'Callback responses',
20 | requestSamples: 'Request samples',
21 | responseSamples: 'Response samples',
22 | };
23 |
24 | export function setRedocLabels(_labels?: LabelsConfigRaw) {
25 | Object.assign(labels, _labels);
26 | }
27 |
28 | export function l(key: keyof LabelsConfig, idx?: number): string {
29 | const label = labels[key];
30 | if (idx !== undefined) {
31 | return label[idx];
32 | }
33 | return label;
34 | }
35 |
--------------------------------------------------------------------------------
/src/services/MarkerService.ts:
--------------------------------------------------------------------------------
1 | import * as Mark from 'mark.js';
2 |
3 | export class MarkerService {
4 | map: Map = new Map();
5 |
6 | private prevTerm: string = '';
7 |
8 | add(el: HTMLElement) {
9 | this.map.set(el, new Mark(el));
10 | }
11 |
12 | delete(el: Element) {
13 | this.map.delete(el);
14 | }
15 |
16 | addOnly(elements: Element[]) {
17 | this.map.forEach((inst, elem) => {
18 | if (elements.indexOf(elem) === -1) {
19 | inst.unmark();
20 | this.map.delete(elem);
21 | }
22 | });
23 |
24 | for (const el of elements) {
25 | if (!this.map.has(el)) {
26 | this.map.set(el, new Mark(el as HTMLElement));
27 | }
28 | }
29 | }
30 |
31 | clearAll() {
32 | this.unmark();
33 | this.map.clear();
34 | }
35 |
36 | mark(term?: string) {
37 | if (!term && !this.prevTerm) {
38 | return;
39 | }
40 | this.map.forEach(val => {
41 | val.unmark();
42 | val.mark(term || this.prevTerm);
43 | });
44 | this.prevTerm = term || this.prevTerm;
45 | }
46 |
47 | unmark() {
48 | this.map.forEach(val => val.unmark());
49 | this.prevTerm = '';
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/services/SearchStore.ts:
--------------------------------------------------------------------------------
1 | import { IS_BROWSER } from '../utils/';
2 | import type { IMenuItem } from './types';
3 | import type { OperationModel } from './models';
4 |
5 | import Worker from './SearchWorker.worker';
6 |
7 | function getWorker() {
8 | let worker: new () => Worker;
9 | if (IS_BROWSER) {
10 | try {
11 | // tslint:disable-next-line
12 | worker = require('workerize-loader?inline&fallback=false!./SearchWorker.worker');
13 | } catch (e) {
14 | worker = require('./SearchWorker.worker').default;
15 | }
16 | } else {
17 | worker = require('./SearchWorker.worker').default;
18 | }
19 | return new worker();
20 | }
21 |
22 | export class SearchStore {
23 | searchWorker = getWorker();
24 |
25 | indexItems(groups: Array) {
26 | const recurse = items => {
27 | items.forEach(group => {
28 | if (group.type !== 'group') {
29 | this.add(group.name, (group.description || '').concat(' ', group.path || ''), group.id);
30 | }
31 | recurse(group.items);
32 | });
33 | };
34 |
35 | recurse(groups);
36 | this.searchWorker.done();
37 | }
38 |
39 | add(title: string, body: string, meta?: T) {
40 | this.searchWorker.add(title, body, meta);
41 | }
42 |
43 | dispose() {
44 | (this.searchWorker as any).terminate();
45 | (this.searchWorker as any).dispose();
46 | }
47 |
48 | search(q: string) {
49 | return this.searchWorker.search(q);
50 | }
51 |
52 | async toJS() {
53 | return this.searchWorker.toJS();
54 | }
55 |
56 | load(state: any) {
57 | this.searchWorker.load(state);
58 | }
59 |
60 | fromExternalJS(path?: string, exportName?: string) {
61 | if (path && exportName) {
62 | this.searchWorker.fromExternalJS(path, exportName);
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/services/SpecStore.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIExternalDocumentation, OpenAPIPath, OpenAPISpec, Referenced } from '../types';
2 |
3 | import { MenuBuilder } from './MenuBuilder';
4 | import { ApiInfoModel } from './models/ApiInfo';
5 | import { WebhookModel } from './models/Webhook';
6 | import { SecuritySchemesModel } from './models/SecuritySchemes';
7 | import { OpenAPIParser } from './OpenAPIParser';
8 | import type { RedocNormalizedOptions } from './RedocNormalizedOptions';
9 | import type { ContentItemModel } from './types';
10 | /**
11 | * Store that contains all the specification related information in the form of tree
12 | */
13 | export class SpecStore {
14 | parser: OpenAPIParser;
15 |
16 | info: ApiInfoModel;
17 | externalDocs?: OpenAPIExternalDocumentation;
18 | contentItems: ContentItemModel[];
19 | securitySchemes: SecuritySchemesModel;
20 | webhooks?: WebhookModel;
21 |
22 | constructor(
23 | spec: OpenAPISpec,
24 | specUrl: string | undefined,
25 | private options: RedocNormalizedOptions,
26 | ) {
27 | this.parser = new OpenAPIParser(spec, specUrl, options);
28 | this.info = new ApiInfoModel(this.parser, this.options);
29 | this.externalDocs = this.parser.spec.externalDocs;
30 | this.contentItems = MenuBuilder.buildStructure(this.parser, this.options);
31 | this.securitySchemes = new SecuritySchemesModel(this.parser);
32 | const webhookPath: Referenced = {
33 | ...this.parser?.spec?.['x-webhooks'],
34 | ...this.parser?.spec.webhooks,
35 | };
36 | this.webhooks = new WebhookModel(this.parser, options, webhookPath);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/services/__tests__/__snapshots__/prism.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`prism.js helpers highlight js code 1`] = `"const t = 10;"`;
4 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/conditionalField.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Schema definition field with conditional operators",
5 | "version": "1.0.0"
6 | },
7 | "components": {
8 | "schemas": {
9 | "Test": {
10 | "type": "object",
11 | "properties": {
12 | "test": {
13 | "type": ["string", "integer", "null"],
14 | "minItems": 1,
15 | "maxItems": 20,
16 | "items": {
17 | "type": "string",
18 | "format": "url"
19 | },
20 | "if": {
21 | "x-displayName": "isString",
22 | "type": "string"
23 | },
24 | "then": {
25 | "type": "string",
26 | "minItems": 1,
27 | "maxItems": 20
28 | },
29 | "else": {
30 | "x-displayName": "notString",
31 | "minItems": 1,
32 | "maxItems": 10,
33 | "pattern": "\\d+"
34 | }
35 | }
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/conditionalSchema.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Schema definition with conditional operators",
5 | "version": "1.0.0"
6 | },
7 | "components": {
8 | "schemas": {
9 | "Test": {
10 | "type": "object",
11 | "properties": {
12 | "test": {
13 | "description": "The list of URL to a cute photos featuring pet",
14 | "type": ["string", "integer", "null"],
15 | "minItems": 1,
16 | "maxItems": 20,
17 | "items": {
18 | "type": "string",
19 | "format": "url"
20 | }
21 | }
22 | },
23 | "if": {
24 | "title": "=== 10",
25 | "properties": {
26 | "test": {
27 | "enum": [10]
28 | }
29 | }
30 | },
31 | "then": {
32 | "maxItems": 2
33 | },
34 | "else": {
35 | "maxItems": 20
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/pathItems.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "version": "1.0.0",
5 | "title": "Swagger Petstore"
6 | },
7 | "webhooks": {
8 | "myWebhook": {
9 | "$ref": "#/components/pathItems/catsWebhook",
10 | "description": "Overriding description",
11 | "summary": "Overriding summary"
12 | }
13 | },
14 | "components": {
15 | "pathItems": {
16 | "catsWebhook": {
17 | "put": {
18 | "summary": "Get a cat details after update",
19 | "description": "Get a cat details after update",
20 | "operationId": "updatedCat",
21 | "tags": [
22 | "pet"
23 | ],
24 | "requestBody": {
25 | "description": "Information about cat in the system",
26 | "content": {
27 | "multipart/form-data": {
28 | "schema": {
29 | "$ref": "#/components/schemas/Pet"
30 | }
31 | }
32 | }
33 | },
34 | "responses": {
35 | "200": {
36 | "description": "update Cat details"
37 | }
38 | }
39 | },
40 | "post": {
41 | "summary": "Create new cat",
42 | "description": "Info about new cat",
43 | "operationId": "createdCat",
44 | "tags": [
45 | "pet"
46 | ],
47 | "requestBody": {
48 | "description": "Information about cat in the system",
49 | "content": {
50 | "multipart/form-data": {
51 | "schema": {
52 | "$ref": "#/components/schemas/Pet"
53 | }
54 | }
55 | }
56 | },
57 | "responses": {
58 | "200": {
59 | "description": "create Cat details"
60 | }
61 | }
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/patternProperties.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Schema definition with unevaluatedProperties",
5 | "version": "1.0.0"
6 | },
7 | "servers": [
8 | {
9 | "url": "example.com"
10 | }
11 | ],
12 | "components": {
13 | "schemas": {
14 | "Patterns": {
15 | "type": "object",
16 | "patternProperties": {
17 | "^S_\\w+\\.[1-9]{2,4}$": {
18 | "type": "string"
19 | },
20 | "^O_\\w+\\.[1-9]{2,4}$": {
21 | "type": "object",
22 | "properties": {
23 | "x-nestedProperty": {
24 | "type": "string"
25 | }
26 | }
27 | }
28 | },
29 | "properties": {
30 | "nestedObjectProp": {
31 | "type": "object",
32 | "patternProperties": {
33 | ".*": {
34 | "type": "integer"
35 | }
36 | }
37 | },
38 | "nestedArrayProp": {
39 | "type": "array",
40 | "items": {
41 | "patternProperties": {
42 | ".*": {
43 | "type": "string"
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/schemaDefinition.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Schema definition double $ref",
5 | "version": "1.0.0"
6 | },
7 | "servers": [
8 | {
9 | "url": "example.com"
10 | }
11 | ],
12 | "tags": [
13 | {
14 | "name": "test",
15 | "x-displayName": "The test Model",
16 | "description": "\n"
17 | }
18 | ],
19 | "paths": {
20 | "/newPath": {
21 | "post": {
22 | "requestBody": {
23 | "content": {
24 | "application/json": {
25 | "schema": {
26 | "$ref": "#/components/schemas/Child"
27 | }
28 | }
29 | }
30 | },
31 | "responses": {
32 | "200": {
33 | "description": "all ok"
34 | }
35 | }
36 | }
37 | }
38 | },
39 | "components": {
40 | "schemas": {
41 | "Parent": {
42 | "$ref": "#/components/schemas/Child"
43 | },
44 | "Child": {
45 | "type": "object",
46 | "properties": {
47 | "test": {
48 | "type": "string"
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/3.1/unevaluatedProperties.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "Schema definition with unevaluatedProperties",
5 | "version": "1.0.0"
6 | },
7 | "servers": [
8 | {
9 | "url": "example.com"
10 | }
11 | ],
12 | "components": {
13 | "schemas": {
14 | "Test": {
15 | "type": "object",
16 | "unevaluatedProperties": true,
17 | "properties": {
18 | "$ref": "#/components/schemas/Cat"
19 | }
20 | },
21 | "Test2": {
22 | "type": "object",
23 | "unevaluatedProperties": true,
24 | "anyOf": [
25 | {
26 | "$ref": "#/components/schemas/Cat"
27 | },
28 | {
29 | "$ref": "#/components/schemas/Dog"
30 | }
31 | ]
32 | },
33 | "Test3": {
34 | "type": "object",
35 | "unevaluatedProperties": {
36 | "type": "boolean"
37 | },
38 | "properties": {
39 | "$ref": "#/components/schemas/Cat"
40 | }
41 | },
42 | "Cat": {
43 | "type": "object",
44 | "properties": {
45 | "color": {
46 | "type": "string"
47 | }
48 | }
49 | },
50 | "Dog": {
51 | "type": "object",
52 | "properties": {
53 | "size": {
54 | "type": "string"
55 | }
56 | }
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/callback.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "callbacks": {
9 | "Test": {
10 | "post": {
11 | "operationId": "testCallback",
12 | "description": "Test callback.",
13 | "requestBody": {
14 | "content": {
15 | "application/json": {
16 | "schema": {
17 | "title": "TestTitle",
18 | "type": "object",
19 | "description": "Test description",
20 | "properties": {
21 | "type": {
22 | "type": "string",
23 | "description": "The type of response.",
24 | "enum": [
25 | "TestResponse.Complete"
26 | ]
27 | },
28 | "status": {
29 | "type": "string",
30 | "enum": [
31 | "FAILURE",
32 | "SUCCESS"
33 | ]
34 | }
35 | },
36 | "required": [
37 | "status"
38 | ]
39 | }
40 | }
41 | }
42 | },
43 | "parameters": [
44 | {
45 | "name": "X-Test-Header",
46 | "in": "header",
47 | "required": true,
48 | "example": "1",
49 | "description": "This is a test header parameter",
50 | "schema": {
51 | "type": "string"
52 | }
53 | }
54 | ],
55 | "responses": {
56 | "204": {
57 | "description": "Test response."
58 | }
59 | }
60 | }
61 | }
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/discriminator.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "servers": [],
4 | "info": {
5 | "title": "Broken Redoc Discriminator",
6 | "version": ""
7 | },
8 | "paths": {
9 | "/foo": {
10 | "get": {
11 | "responses": {
12 | "200": {
13 | "description": "OK",
14 | "content": {
15 | "*/*": {
16 | "schema": {
17 | "$ref": "#/components/schemas/FooTopLevel"
18 | }
19 | }
20 | }
21 | }
22 | }
23 | }
24 | }
25 | },
26 | "components": {
27 | "schemas": {
28 | "JsonApiResource": {
29 | "type": "object",
30 | "description": "A related resource.",
31 | "required": [
32 | "type"
33 | ],
34 | "discriminator": {
35 | "propertyName": "type"
36 | },
37 | "properties": {
38 | "type": {
39 | "type": "string",
40 | "description": "The type of object this resource represents."
41 | }
42 | }
43 | },
44 | "FooTopLevel": {
45 | "type": "object",
46 | "required": [
47 | "data"
48 | ],
49 | "properties": {
50 | "data": {
51 | "$ref": "#/components/schemas/Foo"
52 | }
53 | }
54 | },
55 | "Foo": {
56 | "allOf": [
57 | {
58 | "type": "object"
59 | },
60 | {
61 | "$ref": "#/components/schemas/JsonApiResource"
62 | }
63 | ]
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/fields.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "parameters": {
9 | "testParam": {
10 | "in": "path",
11 | "name": "test_name",
12 | "schema": { "type": "string" }
13 | },
14 | "serializationParam": {
15 | "in": "query",
16 | "name": "serialization_test_name",
17 | "schema": { "type": "array" },
18 | "style": "form",
19 | "explode": true
20 | },
21 | "queryParamWithNoStyle": {
22 | "in": "query",
23 | "name": "serialization_test_name",
24 | "schema": { "type": "array" }
25 | },
26 | "pathParamWithNoStyle": {
27 | "in": "path",
28 | "name": "serialization_test_name",
29 | "schema": { "type": "array" }
30 | },
31 | "cookieParamWithNoStyle": {
32 | "in": "cookie",
33 | "name": "serialization_test_name",
34 | "schema": { "type": "array" }
35 | }
36 | },
37 | "headers": {
38 | "testHeader": {
39 | "description": "The response content language",
40 | "schema": {
41 | "type": "string"
42 | }
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/mergeAllOf.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "schemas": {
9 | "Case1": {
10 | "allOf": [
11 | {
12 | "title": "Bar"
13 | },
14 | {
15 | "title": "Baz"
16 | }
17 | ]
18 | },
19 | "Case2": {
20 | "properties": {
21 | "a": {
22 | "allOf": [
23 | {
24 | "title": "Bar"
25 | },
26 | {
27 | "title": "Baz"
28 | }
29 | ]
30 | }
31 | }
32 | },
33 | "Case3": {
34 | "schemas": {
35 | "Foo": {
36 | "title": "Foo",
37 | "allOf": [
38 | {
39 | "title": "Bar"
40 | },
41 | {
42 | "title": "Baz"
43 | }
44 | ]
45 | }
46 | }
47 | },
48 | "Case4": {
49 | "allOf": [
50 | {
51 | "title": "Foo"
52 | },
53 | {
54 | "$ref": "#/components/schemas/Ref"
55 | }
56 | ]
57 | },
58 | "Ref": {
59 | "oneOf": [
60 | {
61 | "title": "Bar"
62 | },
63 | {
64 | "title": "Baz"
65 | }
66 | ]
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/nestedEnumDescroptionSample.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Test"
6 | },
7 | "components": {
8 | "schemas": {
9 | "Test": {
10 | "type": "array",
11 | "description": "test description",
12 | "items": {
13 | "type": "string",
14 | "description": "test description",
15 | "enum": ["authorize", "do-nothing"],
16 | "x-enumDescriptions": {
17 | "authorize-and-void": "Will create an authorize transaction in the amount/currency of the request, followed by a void",
18 | "do-nothing": "Will do nothing, and return an approved `setup` transaction. This is the default behavior."
19 | }
20 | }
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/oneOfHoist.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "schemas": {
9 | "test": {
10 | "allOf": [
11 | {
12 | "properties": {
13 | "id": {
14 | "description": "The user's ID",
15 | "type": "integer"
16 | }
17 | },
18 | "oneOf": [
19 | {
20 | "properties": {
21 | "username": {
22 | "description": "The user's name",
23 | "type": "string"
24 | }
25 | }
26 | },
27 | {
28 | "properties": {
29 | "email": {
30 | "description": "The user's email",
31 | "type": "string"
32 | }
33 | }
34 | },
35 | {
36 | "properties": {
37 | "id": {
38 | "description": "The user's ID",
39 | "type": "string",
40 | "format": "uuid"
41 | }
42 | }
43 | }
44 | ]
45 | },
46 | {
47 | "properties": {
48 | "extra": {
49 | "type": "string"
50 | }
51 | }
52 | },
53 | {
54 | "oneOf": [
55 | {
56 | "properties": {
57 | "password": {
58 | "description": "The user's password",
59 | "type": "string"
60 | }
61 | }
62 | },
63 | {
64 | "properties": {
65 | "mobile": {
66 | "description": "The user's mobile",
67 | "type": "string"
68 | }
69 | }
70 | }
71 | ]
72 | }
73 | ]
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/oneOfTitles.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "version": "1.0",
5 | "title": "Foo"
6 | },
7 | "components": {
8 | "schemas": {
9 | "Test": {
10 | "type": "object",
11 | "properties": {
12 | "any": {
13 | "anyOf": [
14 | {
15 | "$ref": "#/components/schemas/Foo"
16 | },
17 | {
18 | "$ref": "#/components/schemas/Bar"
19 | }
20 | ]
21 | },
22 | "one": {
23 | "oneOf": [
24 | {
25 | "$ref": "#/components/schemas/Foo"
26 | },
27 | {
28 | "$ref": "#/components/schemas/Bar"
29 | }
30 | ]
31 | },
32 | "all": {
33 | "allOf": [
34 | {
35 | "$ref": "#/components/schemas/Foo"
36 | },
37 | {
38 | "$ref": "#/components/schemas/Bar"
39 | }
40 | ]
41 | }
42 | }
43 | },
44 | "Foo": {
45 | "type": "object",
46 | "properties": {
47 | "foo": {
48 | "type": "string"
49 | }
50 | }
51 | },
52 | "Bar": {
53 | "type": "object",
54 | "properties": {
55 | "bar": {
56 | "type": "string"
57 | }
58 | }
59 | },
60 | "WithArray": {
61 | "oneOf": [{
62 | "type" : "array",
63 | "items": {
64 | "oneOf": [
65 | {
66 | "type": "string"
67 | },
68 | {
69 | "type": "number"
70 | }
71 | ]
72 | }
73 | }, {
74 | "type": "string"
75 | }]
76 | }
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/src/services/__tests__/fixtures/siblingRefDescription.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.0",
3 | "info": {
4 | "title": "AA",
5 | "version": "1.0"
6 | },
7 | "paths": {
8 | "/test": {
9 | "get": {
10 | "operationId": "test",
11 | "responses": {
12 | "200": {
13 | "content": {
14 | "application/json": {
15 | "schema": {
16 | "type": "object",
17 | "properties": {
18 | "testAttr": {
19 | "description": "Overriden description",
20 | "$ref": "#/components/schemas/Test"
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | }
30 | },
31 | "components": {
32 | "schemas": {
33 | "Test": {
34 | "type": "object",
35 | "description": "Refed description"
36 | }
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/services/__tests__/history.service.test.ts:
--------------------------------------------------------------------------------
1 | import { history } from '../HistoryService';
2 |
3 | describe('History service', () => {
4 | test('should be an instance', () => {
5 | expect(typeof history).not.toBe('function');
6 | expect(history.subscribe).toBeDefined();
7 | });
8 |
9 | test('History subscribe', () => {
10 | const fn = jest.fn();
11 | history.subscribe(fn);
12 | history.emit();
13 | expect(fn).toHaveBeenCalled();
14 | });
15 |
16 | test('History subscribe should return unsubscribe function', () => {
17 | const fn = jest.fn();
18 | const unsubscribe = history.subscribe(fn);
19 | history.emit();
20 | expect(fn).toHaveBeenCalled();
21 | unsubscribe();
22 | history.emit();
23 | expect(fn).toHaveBeenCalledTimes(1);
24 | });
25 |
26 | test('currentId should return correct id', () => {
27 | window.location.hash = '#testid';
28 | expect(history.currentId).toEqual('testid');
29 | });
30 |
31 | test('should return correct link for id', () => {
32 | expect(history.linkForId('testid')).toEqual('#testid');
33 | });
34 |
35 | test('should return empty link for empty id', () => {
36 | expect(history.linkForId('')).toEqual('');
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/src/services/__tests__/models/Callback.test.ts:
--------------------------------------------------------------------------------
1 | import { CallbackModel } from '../../models/Callback';
2 | import { OpenAPIParser } from '../../OpenAPIParser';
3 | import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
4 |
5 | const opts = new RedocNormalizedOptions({});
6 |
7 | describe('Models', () => {
8 | describe('CallbackModel', () => {
9 | // eslint-disable-next-line @typescript-eslint/no-var-requires
10 | const spec = require('../fixtures/callback.json');
11 | const parser = new OpenAPIParser(spec, undefined, opts);
12 |
13 | test('basic callback details', () => {
14 | const callback = new CallbackModel(
15 | parser,
16 | 'Test.Callback',
17 | { $ref: '#/components/callbacks/Test' },
18 | '',
19 | opts,
20 | );
21 | expect(callback.name).toEqual('Test.Callback');
22 | expect(callback.operations.length).toEqual(0);
23 | expect(callback.expanded).toBeFalsy();
24 | });
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/services/__tests__/models/MenuBuilder.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 | import { MenuBuilder } from '../../MenuBuilder';
3 | import { OpenAPIParser } from '../../OpenAPIParser';
4 |
5 | import { RedocNormalizedOptions } from '../../RedocNormalizedOptions';
6 |
7 | const opts = new RedocNormalizedOptions({});
8 |
9 | describe('Models', () => {
10 | describe('MenuBuilder', () => {
11 | let parser;
12 |
13 | test('should resolve pathItems', () => {
14 | const spec = require('../fixtures/3.1/pathItems.json');
15 | parser = new OpenAPIParser(spec, undefined, opts);
16 | const contentItems = MenuBuilder.buildStructure(parser, opts);
17 | expect(contentItems).toHaveLength(1);
18 | expect(contentItems[0].items).toHaveLength(2);
19 | expect(contentItems[0].id).toEqual('tag/pet');
20 | expect(contentItems[0].name).toEqual('pet');
21 | expect(contentItems[0].type).toEqual('tag');
22 | });
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/src/services/__tests__/prism.test.ts:
--------------------------------------------------------------------------------
1 | import { highlight, mapLang } from '../../utils/highlight';
2 |
3 | describe('prism.js helpers', () => {
4 | test('mapLang should map "json" to "js"', () => {
5 | expect(mapLang('json')).toBe('js');
6 | });
7 |
8 | test('mapLang should map to "clike" by default', () => {
9 | expect(mapLang('non-existring')).toBe('clike');
10 | });
11 |
12 | test('highlight js code', () => {
13 | expect(highlight('const t = 10;', 'js')).toMatchSnapshot();
14 | });
15 |
16 | test('highlight raw text should just return text', () => {
17 | expect(highlight('Hello world', 'clike')).toBe('Hello world');
18 | });
19 |
20 | test('highlight should not throw with lang undefined', () => {
21 | expect(highlight('Hello world', undefined)).toBe('Hello world');
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/src/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './AppStore';
2 | export * from './OpenAPIParser';
3 | export * from './MarkdownRenderer';
4 | export * from './MenuStore';
5 | export * from './ScrollService';
6 | export * from './SpecStore';
7 | export * from './ClipboardService';
8 | export * from './HistoryService';
9 | export * from './models';
10 | export * from './RedocNormalizedOptions';
11 | export * from './MenuBuilder';
12 | export * from './SearchStore';
13 | export * from './MarkerService';
14 | export * from './types';
15 |
--------------------------------------------------------------------------------
/src/services/models/Callback.ts:
--------------------------------------------------------------------------------
1 | import { action, observable, makeObservable } from 'mobx';
2 |
3 | import { isOperationName, JsonPointer } from '../../utils';
4 | import { OperationModel } from './Operation';
5 | import type { OpenAPIParser } from '../OpenAPIParser';
6 | import type { OpenAPICallback, Referenced } from '../../types';
7 | import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
8 |
9 | export class CallbackModel {
10 | @observable
11 | expanded: boolean = false;
12 |
13 | name: string;
14 | operations: OperationModel[] = [];
15 |
16 | constructor(
17 | parser: OpenAPIParser,
18 | name: string,
19 | infoOrRef: Referenced,
20 | pointer: string,
21 | options: RedocNormalizedOptions,
22 | ) {
23 | makeObservable(this);
24 |
25 | this.name = name;
26 | const { resolved: paths } = parser.deref(infoOrRef);
27 |
28 | for (const pathName of Object.keys(paths)) {
29 | const path = paths[pathName];
30 | const operations = Object.keys(path).filter(isOperationName);
31 | for (const operationName of operations) {
32 | const operationInfo = path[operationName];
33 |
34 | const operation = new OperationModel(
35 | parser,
36 | {
37 | ...operationInfo,
38 | pathName,
39 | pointer: JsonPointer.compile([pointer, name, pathName, operationName]),
40 | httpVerb: operationName,
41 | pathParameters: path.parameters || [],
42 | pathServers: path.servers,
43 | },
44 | undefined,
45 | options,
46 | true,
47 | );
48 |
49 | this.operations.push(operation);
50 | }
51 | }
52 | }
53 |
54 | @action
55 | toggle() {
56 | this.expanded = !this.expanded;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/services/models/Example.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIEncoding, OpenAPIExample, Referenced } from '../../types';
2 | import { isFormUrlEncoded, isJsonLike, urlFormEncodePayload } from '../../utils/openapi';
3 | import type { OpenAPIParser } from '../OpenAPIParser';
4 |
5 | const externalExamplesCache: { [url: string]: Promise } = {};
6 |
7 | export class ExampleModel {
8 | value: any;
9 | summary?: string;
10 | description?: string;
11 | externalValueUrl?: string;
12 |
13 | constructor(
14 | parser: OpenAPIParser,
15 | infoOrRef: Referenced,
16 | public mime: string,
17 | encoding?: { [field: string]: OpenAPIEncoding },
18 | ) {
19 | const { resolved: example } = parser.deref(infoOrRef);
20 | this.value = example.value;
21 | this.summary = example.summary;
22 | this.description = example.description;
23 | if (example.externalValue) {
24 | this.externalValueUrl = new URL(example.externalValue, parser.specUrl).href;
25 | }
26 |
27 | if (isFormUrlEncoded(mime) && this.value && typeof this.value === 'object') {
28 | this.value = urlFormEncodePayload(this.value, encoding);
29 | }
30 | }
31 |
32 | getExternalValue(mimeType: string): Promise {
33 | if (!this.externalValueUrl) {
34 | return Promise.resolve(undefined);
35 | }
36 |
37 | if (this.externalValueUrl in externalExamplesCache) {
38 | return externalExamplesCache[this.externalValueUrl];
39 | }
40 |
41 | externalExamplesCache[this.externalValueUrl] = fetch(this.externalValueUrl).then(res => {
42 | return res.text().then(txt => {
43 | if (!res.ok) {
44 | return Promise.reject(new Error(txt));
45 | }
46 |
47 | if (isJsonLike(mimeType)) {
48 | try {
49 | return JSON.parse(txt);
50 | } catch (e) {
51 | return txt;
52 | }
53 | } else {
54 | return txt;
55 | }
56 | });
57 | });
58 |
59 | return externalExamplesCache[this.externalValueUrl];
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/services/models/MediaContent.ts:
--------------------------------------------------------------------------------
1 | import { action, computed, observable, makeObservable } from 'mobx';
2 |
3 | import type { OpenAPIMediaType } from '../../types';
4 | import { MediaTypeModel } from './MediaType';
5 |
6 | import { mergeSimilarMediaTypes } from '../../utils';
7 | import type { OpenAPIParser } from '../OpenAPIParser';
8 | import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
9 |
10 | /**
11 | * MediaContent model ready to be sued by React components
12 | * Contains multiple MediaTypes and keeps track of the currently active one
13 | */
14 | export class MediaContentModel {
15 | mediaTypes: MediaTypeModel[];
16 |
17 | @observable
18 | activeMimeIdx = 0;
19 |
20 | /**
21 | * @param isRequestType needed to know if skipe RO/RW fields in objects
22 | */
23 | constructor(
24 | parser: OpenAPIParser,
25 | info: Record,
26 | public isRequestType: boolean,
27 | options: RedocNormalizedOptions,
28 | ) {
29 | makeObservable(this);
30 |
31 | if (options.unstable_ignoreMimeParameters) {
32 | info = mergeSimilarMediaTypes(info);
33 | }
34 | this.mediaTypes = Object.keys(info).map(name => {
35 | const mime = info[name];
36 | // reset deref cache just in case something is left there
37 | return new MediaTypeModel(parser, name, isRequestType, mime, options);
38 | });
39 | }
40 |
41 | /**
42 | * Set active media type by index
43 | * @param idx media type index
44 | */
45 | @action
46 | activate(idx: number) {
47 | this.activeMimeIdx = idx;
48 | }
49 |
50 | @computed
51 | get active() {
52 | return this.mediaTypes[this.activeMimeIdx];
53 | }
54 |
55 | get hasSample(): boolean {
56 | return this.mediaTypes.filter(mime => !!mime.examples).length > 0;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/services/models/RequestBody.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIRequestBody, Referenced } from '../../types';
2 |
3 | import type { OpenAPIParser } from '../OpenAPIParser';
4 | import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
5 | import { MediaContentModel } from './MediaContent';
6 | import { getContentWithLegacyExamples } from '../../utils';
7 |
8 | type RequestBodyProps = {
9 | parser: OpenAPIParser;
10 | infoOrRef: Referenced;
11 | options: RedocNormalizedOptions;
12 | isEvent: boolean;
13 | };
14 |
15 | export class RequestBodyModel {
16 | description: string;
17 | required?: boolean;
18 | content?: MediaContentModel;
19 |
20 | constructor({ parser, infoOrRef, options, isEvent }: RequestBodyProps) {
21 | const isRequest = !isEvent;
22 | const { resolved: info } = parser.deref(infoOrRef);
23 | this.description = info.description || '';
24 | this.required = info.required;
25 |
26 | const mediaContent = getContentWithLegacyExamples(info);
27 | if (mediaContent !== undefined) {
28 | this.content = new MediaContentModel(parser, mediaContent, isRequest, options);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/services/models/SecurityRequirement.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPISecurityRequirement, OpenAPISecurityScheme } from '../../types';
2 | import type { OpenAPIParser } from '../OpenAPIParser';
3 |
4 | export interface SecurityScheme extends OpenAPISecurityScheme {
5 | id: string;
6 | sectionId: string;
7 | displayName: string;
8 | scopes: string[];
9 | }
10 |
11 | export class SecurityRequirementModel {
12 | schemes: SecurityScheme[];
13 |
14 | constructor(requirement: OpenAPISecurityRequirement, parser: OpenAPIParser) {
15 | const schemes = (parser.spec.components && parser.spec.components.securitySchemes) || {};
16 |
17 | this.schemes = Object.keys(requirement || {})
18 | .map(id => {
19 | const { resolved: scheme } = parser.deref(schemes[id]);
20 | const scopes = requirement[id] || [];
21 |
22 | if (!scheme) {
23 | console.warn(`Non existing security scheme referenced: ${id}. Skipping`);
24 | return undefined;
25 | }
26 | const displayName = scheme['x-displayName'] || id;
27 |
28 | return {
29 | ...scheme,
30 | id,
31 | sectionId: id,
32 | displayName,
33 | scopes,
34 | };
35 | })
36 | .filter(scheme => scheme !== undefined) as SecurityScheme[];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/services/models/SecuritySchemes.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPISecurityScheme, Referenced } from '../../types';
2 | import { SECURITY_SCHEMES_SECTION_PREFIX } from '../../utils';
3 | import type { OpenAPIParser } from '../OpenAPIParser';
4 |
5 | export class SecuritySchemeModel {
6 | id: string;
7 | sectionId: string;
8 | type: OpenAPISecurityScheme['type'];
9 | description: string;
10 | displayName: string;
11 | apiKey?: {
12 | name: string;
13 | in: OpenAPISecurityScheme['in'];
14 | };
15 |
16 | http?: {
17 | scheme: string;
18 | bearerFormat?: string;
19 | };
20 |
21 | flows: OpenAPISecurityScheme['flows'];
22 | openId?: {
23 | connectUrl: string;
24 | };
25 |
26 | constructor(parser: OpenAPIParser, id: string, scheme: Referenced) {
27 | const { resolved: info } = parser.deref(scheme);
28 | this.id = id;
29 | this.sectionId = SECURITY_SCHEMES_SECTION_PREFIX + id;
30 | this.type = info.type;
31 | this.displayName = info['x-displayName'] || id;
32 | this.description = info.description || '';
33 | if (info.type === 'apiKey') {
34 | this.apiKey = {
35 | name: info.name!,
36 | in: info.in,
37 | };
38 | }
39 |
40 | if (info.type === 'http') {
41 | this.http = {
42 | scheme: info.scheme!,
43 | bearerFormat: info.bearerFormat,
44 | };
45 | }
46 |
47 | if (info.type === 'openIdConnect') {
48 | this.openId = {
49 | connectUrl: info.openIdConnectUrl!,
50 | };
51 | }
52 |
53 | if (info.type === 'oauth2' && info.flows) {
54 | this.flows = info.flows;
55 | }
56 | }
57 | }
58 |
59 | export class SecuritySchemesModel {
60 | schemes: SecuritySchemeModel[];
61 |
62 | constructor(parser: OpenAPIParser) {
63 | const schemes = (parser.spec.components && parser.spec.components.securitySchemes) || {};
64 | this.schemes = Object.keys(schemes).map(
65 | name => new SecuritySchemeModel(parser, name, schemes[name]),
66 | );
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/services/models/Webhook.ts:
--------------------------------------------------------------------------------
1 | import type { OpenAPIPath, Referenced } from '../../types';
2 | import type { OpenAPIParser } from '../OpenAPIParser';
3 | import { OperationModel } from './Operation';
4 | import type { RedocNormalizedOptions } from '../RedocNormalizedOptions';
5 | import { isOperationName } from '../..';
6 |
7 | export class WebhookModel {
8 | operations: OperationModel[] = [];
9 |
10 | constructor(
11 | parser: OpenAPIParser,
12 | options: RedocNormalizedOptions,
13 | infoOrRef?: Referenced,
14 | ) {
15 | const { resolved: webhooks } = parser.deref(infoOrRef || {});
16 | this.initWebhooks(parser, webhooks, options);
17 | }
18 |
19 | initWebhooks(parser: OpenAPIParser, webhooks: OpenAPIPath, options: RedocNormalizedOptions) {
20 | for (const webhookName of Object.keys(webhooks)) {
21 | const webhook = webhooks[webhookName];
22 | const operations = Object.keys(webhook).filter(isOperationName);
23 | for (const operationName of operations) {
24 | const operationInfo = webhook[operationName];
25 | if (webhook.$ref) {
26 | const resolvedWebhook = parser.deref(webhook || {});
27 | this.initWebhooks(parser, { [operationName]: resolvedWebhook }, options);
28 | }
29 |
30 | if (!operationInfo) continue;
31 | const operation = new OperationModel(
32 | parser,
33 | {
34 | ...operationInfo,
35 | httpVerb: operationName,
36 | },
37 | undefined,
38 | options,
39 | false,
40 | );
41 |
42 | this.operations.push(operation);
43 | }
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/services/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from '../SpecStore';
2 | export * from './Group.model';
3 | export * from './Operation';
4 | export * from './RequestBody';
5 | export * from './Example';
6 | export * from './MediaContent';
7 | export * from './MediaType';
8 | export * from './Response';
9 | export * from './Schema';
10 | export * from './Field';
11 | export * from './ApiInfo';
12 | export * from './SecuritySchemes';
13 | export * from './Callback';
14 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import * as Enzyme from 'enzyme';
2 | import Adapter from '@cfaester/enzyme-adapter-react-18';
3 | import { TextEncoder, TextDecoder } from 'util';
4 |
5 | Object.assign(global, { TextDecoder, TextEncoder });
6 |
7 | import 'raf/polyfill';
8 |
9 | Enzyme.configure({ adapter: new Adapter() });
10 |
--------------------------------------------------------------------------------
/src/styled-components.ts:
--------------------------------------------------------------------------------
1 | import * as styledComponents from 'styled-components';
2 |
3 | import type { ResolvedThemeInterface } from './theme';
4 |
5 | export type { ResolvedThemeInterface };
6 |
7 | const {
8 | default: styled,
9 | css,
10 | createGlobalStyle,
11 | keyframes,
12 | ThemeProvider,
13 | } = styledComponents as unknown as styledComponents.ThemedStyledComponentsModule;
14 |
15 | export const media = {
16 | lessThan(breakpoint, print?: boolean, extra?: string) {
17 | return (...args) => css`
18 | @media ${print ? 'print, ' : ''} screen and (max-width: ${props =>
19 | props.theme.breakpoints[breakpoint]}) ${extra || ''} {
20 | ${(css as any)(...args)};
21 | }
22 | `;
23 | },
24 |
25 | greaterThan(breakpoint) {
26 | return (...args) => css`
27 | @media (min-width: ${props => props.theme.breakpoints[breakpoint]}) {
28 | ${(css as any)(...args)};
29 | }
30 | `;
31 | },
32 |
33 | between(firstBreakpoint, secondBreakpoint) {
34 | return (...args) => css`
35 | @media (min-width: ${props =>
36 | props.theme.breakpoints[firstBreakpoint]}) and (max-width: ${props =>
37 | props.theme.breakpoints[secondBreakpoint]}) {
38 | ${(css as any)(...args)};
39 | }
40 | `;
41 | },
42 | };
43 |
44 | export { css, createGlobalStyle, keyframes, ThemeProvider };
45 | export default styled;
46 |
47 | export function extensionsHook(styledName: string) {
48 | return props => {
49 | if (!props.theme.extensionsHook) {
50 | return;
51 | }
52 | return props.theme.extensionsHook(styledName, props);
53 | };
54 | }
55 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export * from './open-api';
2 |
3 | export type Omit = Pick>;
4 |
--------------------------------------------------------------------------------
/src/utils/__tests__/loadAndBundleSpec.test.ts:
--------------------------------------------------------------------------------
1 | import * as yaml from 'js-yaml';
2 | import { readFileSync } from 'fs';
3 | import { resolve } from 'path';
4 | import { loadAndBundleSpec } from '../loadAndBundleSpec';
5 |
6 | describe('#loadAndBundleSpec', () => {
7 | it('should load And Bundle Spec demo/openapi.yaml', async () => {
8 | const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/openapi.yaml'), 'utf-8'));
9 | const bundledSpec = await loadAndBundleSpec(spec);
10 | expect(bundledSpec).toMatchSnapshot();
11 | });
12 |
13 | it('should load And Bundle Spec demo/openapi-3-1.yaml', async () => {
14 | const spec = yaml.load(
15 | readFileSync(resolve(__dirname, '../../../demo/openapi-3-1.yaml'), 'utf-8'),
16 | );
17 | const bundledSpec = await loadAndBundleSpec(spec);
18 | expect(bundledSpec).toMatchSnapshot();
19 | });
20 |
21 | it('should load And Bundle Spec demo/swagger.yaml', async () => {
22 | const spec = yaml.load(readFileSync(resolve(__dirname, '../../../demo/swagger.yaml'), 'utf-8'));
23 | const bundledSpec = await loadAndBundleSpec(spec);
24 | expect(bundledSpec).toMatchSnapshot();
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/utils/__tests__/object.test.ts:
--------------------------------------------------------------------------------
1 | import { objectHas, objectSet } from '../object';
2 |
3 | describe('object utils', () => {
4 | let obj;
5 |
6 | beforeEach(() => {
7 | obj = {
8 | a: {
9 | b: {
10 | c: {
11 | d: 'd',
12 | },
13 | c1: 'c1',
14 | },
15 | b1: 'b1',
16 | },
17 | a1: 'a1',
18 | };
19 | });
20 |
21 | describe('objectHas function', () => {
22 | it('should check if the obj has path as string', () => {
23 | expect(objectHas(obj, 'a.b.c')).toBeTruthy();
24 | expect(objectHas(obj, 'a.b.c1')).toBeTruthy();
25 | expect(objectHas(obj, 'a.b.c.d')).toBeTruthy();
26 | expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
27 | });
28 |
29 | it('should check if the obj has path as array', () => {
30 | expect(objectHas(obj, ['a', 'b', 'c'])).toBeTruthy();
31 | expect(objectHas(obj, ['a', 'b', 'c1'])).toBeTruthy();
32 | expect(objectHas(obj, ['a', 'b', 'c', 'd'])).toBeTruthy();
33 | expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
34 | });
35 | });
36 |
37 | describe('objectSet function', () => {
38 | it('should set value by path as string', () => {
39 | expect(objectHas(obj, 'a.b.c1.d')).toBeFalsy();
40 | objectSet(obj, 'a.b.c1', { d: 'd' });
41 | expect(objectHas(obj, 'a.b.c1.d')).toBeTruthy();
42 | });
43 |
44 | it('should set value by path as array', () => {
45 | expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeFalsy();
46 | objectSet(obj, ['a', 'b', 'c1'], { d: 'd' });
47 | expect(objectHas(obj, ['a', 'b', 'c1', 'd'])).toBeTruthy();
48 | });
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/utils/debug.ts:
--------------------------------------------------------------------------------
1 | export function debugTime(label: string) {
2 | if (process.env.NODE_ENV !== 'production') {
3 | console.time(label);
4 | }
5 | }
6 |
7 | export function debugTimeEnd(label: string) {
8 | if (process.env.NODE_ENV !== 'production') {
9 | console.timeEnd(label);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/decorators.ts:
--------------------------------------------------------------------------------
1 | function throttle(func, wait) {
2 | let context;
3 | let args;
4 | let result;
5 | let timeout: any = null;
6 | let previous = 0;
7 | const later = () => {
8 | previous = new Date().getTime();
9 | timeout = null;
10 | result = func.apply(context, args);
11 | if (!timeout) {
12 | context = args = null;
13 | }
14 | };
15 | return function () {
16 | const now = new Date().getTime();
17 | const remaining = wait - (now - previous);
18 | // eslint-disable-next-line @typescript-eslint/no-this-alias
19 | context = this;
20 | // eslint-disable-next-line prefer-rest-params
21 | args = arguments;
22 | if (remaining <= 0 || remaining > wait) {
23 | if (timeout) {
24 | clearTimeout(timeout);
25 | timeout = null;
26 | }
27 | previous = now;
28 | result = func.apply(context, args);
29 | if (!timeout) {
30 | context = args = null;
31 | }
32 | } else if (!timeout) {
33 | timeout = setTimeout(later, remaining);
34 | }
35 | return result;
36 | };
37 | }
38 |
39 | export function Throttle(delay: number) {
40 | return (_, _2, desc: PropertyDescriptor) => {
41 | desc.value = throttle(desc.value, delay);
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './JsonPointer';
2 |
3 | export * from './openapi';
4 | export * from './helpers';
5 | export * from './highlight';
6 | export * from './loadAndBundleSpec';
7 | export * from './dom';
8 | export * from './decorators';
9 | export * from './debug';
10 | export * from './memoize';
11 | export * from './sort';
12 |
--------------------------------------------------------------------------------
/src/utils/loadAndBundleSpec.ts:
--------------------------------------------------------------------------------
1 | import type { Source, Document } from '@redocly/openapi-core';
2 | // eslint-disable-next-line import/no-internal-modules
3 | import type { ResolvedConfig } from '@redocly/openapi-core/lib/config';
4 |
5 | // eslint-disable-next-line import/no-internal-modules
6 | import { bundle } from '@redocly/openapi-core/lib/bundle';
7 | // eslint-disable-next-line import/no-internal-modules
8 | import { Config } from '@redocly/openapi-core/lib/config/config';
9 |
10 | /* tslint:disable-next-line:no-implicit-dependencies */
11 | import { convertObj } from 'swagger2openapi';
12 | import { OpenAPISpec } from '../types';
13 | import { IS_BROWSER } from './dom';
14 |
15 | export async function loadAndBundleSpec(specUrlOrObject: object | string): Promise {
16 | const config = new Config({} as ResolvedConfig);
17 | const bundleOpts = {
18 | config,
19 | base: IS_BROWSER ? window.location.href : process.cwd(),
20 | };
21 |
22 | if (IS_BROWSER) {
23 | config.resolve.http.customFetch = global.fetch;
24 | }
25 |
26 | if (typeof specUrlOrObject === 'object' && specUrlOrObject !== null) {
27 | bundleOpts['doc'] = {
28 | source: { absoluteRef: '' } as Source,
29 | parsed: specUrlOrObject,
30 | } as Document;
31 | } else {
32 | bundleOpts['ref'] = specUrlOrObject;
33 | }
34 |
35 | const {
36 | bundle: { parsed },
37 | } = await bundle(bundleOpts);
38 | return parsed.swagger !== undefined ? convertSwagger2OpenAPI(parsed) : parsed;
39 | }
40 |
41 | export function convertSwagger2OpenAPI(spec: any): Promise {
42 | console.warn('[ReDoc Compatibility mode]: Converting OpenAPI 2.0 to OpenAPI 3.0');
43 | return new Promise((resolve, reject) =>
44 | convertObj(spec, { patch: true, warnOnly: true, text: '{}', anchors: true }, (err, res) => {
45 | // TODO: log any warnings
46 | if (err) {
47 | return reject(err);
48 | }
49 | resolve(res && (res.openapi as any));
50 | }),
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/utils/memoize.ts:
--------------------------------------------------------------------------------
1 | // source: https://github.com/andreypopp/memoize-decorator
2 | const SENTINEL = {};
3 |
4 | export function memoize(target: any, name: string, descriptor: TypedPropertyDescriptor) {
5 | if (typeof descriptor.value === 'function') {
6 | return _memoizeMethod(target, name, descriptor) as any as TypedPropertyDescriptor;
7 | } else if (typeof descriptor.get === 'function') {
8 | return _memoizeGetter(target, name, descriptor) as TypedPropertyDescriptor;
9 | } else {
10 | throw new Error(
11 | '@memoize decorator can be applied to methods or getters, got ' +
12 | String(descriptor.value) +
13 | ' instead',
14 | );
15 | }
16 | }
17 |
18 | function _memoizeGetter(target: any, name: string, descriptor: PropertyDescriptor) {
19 | const memoizedName = `_memoized_${name}`;
20 | const get = descriptor.get!;
21 | target[memoizedName] = SENTINEL;
22 | return {
23 | ...descriptor,
24 | get() {
25 | if (this[memoizedName] === SENTINEL) {
26 | this[memoizedName] = get.call(this);
27 | }
28 | return this[memoizedName];
29 | },
30 | };
31 | }
32 |
33 | function _memoizeMethod(target: any, name: string, descriptor: TypedPropertyDescriptor) {
34 | if (!descriptor.value || (descriptor.value as any).length > 0) {
35 | throw new Error('@memoize decorator can only be applied to methods of zero arguments');
36 | }
37 | const memoizedName = `_memoized_${name}`;
38 | const value = descriptor.value;
39 | target[memoizedName] = SENTINEL;
40 | return {
41 | ...descriptor,
42 | value() {
43 | if (this[memoizedName] === SENTINEL) {
44 | this[memoizedName] = (value as any).call(this);
45 | }
46 | return this[memoizedName] as any;
47 | },
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/src/utils/object.ts:
--------------------------------------------------------------------------------
1 | export function objectHas(object: object, path: string | Array): boolean {
2 | let _path = >path;
3 |
4 | if (typeof path === 'string') {
5 | _path = path.split('.');
6 | }
7 |
8 | return _path.every((key: string) => {
9 | if (typeof object != 'object' || object === null || !(key in object)) return false;
10 | object = object[key];
11 | return true;
12 | });
13 | }
14 |
15 | export function objectSet(object: object, path: string | Array, value: any): void {
16 | let _path = >path;
17 |
18 | if (typeof path === 'string') {
19 | _path = path.split('.');
20 | }
21 | const limit = _path.length - 1;
22 | for (let i = 0; i < limit; ++i) {
23 | const key = _path[i];
24 | object = object[key] ?? (object[key] = {});
25 | }
26 | const key = _path[limit];
27 | object[key] = value;
28 | }
29 |
--------------------------------------------------------------------------------
/src/utils/sort.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Function that returns a comparator for sorting objects by some specific key alphabetically.
3 | *
4 | * @param {String} property key of the object to sort, if starts from `-` - reverse
5 | */
6 | export function alphabeticallyByProp(property: string): (a: T, b: T) => number {
7 | let sortOrder = 1;
8 |
9 | if (property[0] === '-') {
10 | sortOrder = -1;
11 | property = property.substr(1);
12 | }
13 |
14 | return (a: T, b: T) => {
15 | if (sortOrder == -1) {
16 | return b[property].localeCompare(a[property]);
17 | } else {
18 | return a[property].localeCompare(b[property]);
19 | }
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/src/utils/test-utils.ts:
--------------------------------------------------------------------------------
1 | import { objectHas, objectSet } from './object';
2 |
3 | function traverseComponent(root, fn) {
4 | if (!root) {
5 | return;
6 | }
7 |
8 | fn(root);
9 |
10 | if (root.children) {
11 | for (const child of root.children) {
12 | traverseComponent(child, fn);
13 | }
14 | }
15 | }
16 |
17 | export function filterPropsDeep(component: T, paths: string[]): T {
18 | traverseComponent(component, comp => {
19 | if (comp.props) {
20 | for (const path of paths) {
21 | if (objectHas(comp.props, path)) {
22 | objectSet(comp.props, path, '<<>>');
23 | }
24 | }
25 | }
26 | });
27 |
28 | return component;
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "moduleResolution": "node",
5 | "target": "es5",
6 | "noImplicitAny": false,
7 | "noUnusedParameters": true,
8 | "noUnusedLocals": true,
9 | "strictNullChecks": true,
10 | "sourceMap": true,
11 | "declaration": true,
12 | "noEmitHelpers": true,
13 | "importHelpers": true,
14 | "outDir": "lib",
15 | "pretty": true,
16 | "lib": ["es2015", "es2016", "es2017", "dom", "WebWorker.ImportScripts"],
17 | "jsx": "react",
18 | "types": ["webpack", "webpack-env", "jest"]
19 | },
20 | "compileOnSave": false,
21 | "exclude": ["node_modules", ".tmp", "lib", "e2e/**"],
22 | "include": [
23 | "./custom.d.ts",
24 | "./demo/playground/hmr-playground.tsx",
25 | "./src/**/*.ts?",
26 | "demo/*.tsx",
27 | "src/empty.js"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "declarationDir": "typings",
5 | "skipLibCheck": true
6 | },
7 | "include": [
8 | "./custom.d.ts",
9 | "src/index.ts"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-react"],
3 | "rules": {
4 | "array-type": false,
5 | "interface-name": false,
6 | "object-literal-sort-keys": false,
7 | "jsx-no-multiline-js": false,
8 | "jsx-wrap-multiline": false,
9 | "max-classes-per-file": false,
10 | "forin": false,
11 | "prefer-conditional-expression": false,
12 | "no-var-requires": false,
13 | "no-object-literal-type-assertion": false,
14 | "no-console": false,
15 | "jsx-curly-spacing": false,
16 | "max-line-length": false,
17 |
18 | "quotemark": [true, "single", "avoid-template", "jsx-double"],
19 | "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore", "allow-pascal-case"],
20 | "arrow-parens": [true, "ban-single-arg-parens"],
21 | "no-submodule-imports": [true, "prismjs", "perfect-scrollbar", "react-dom", "core-js", "memoize-one"],
22 | "object-literal-key-quotes": [true, "as-needed"],
23 | "no-unused-expression": [true, "allow-tagged-template"],
24 | "semicolon": [true, "always", "ignore-bound-class-methods"],
25 | "member-access": [true, "no-public"]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/typings/styled-patch.d.ts:
--------------------------------------------------------------------------------
1 | import * as styledComponents from 'styled-components';
2 |
3 | // FIXME
4 | declare module 'styled-components' {
5 | interface ThemedStyledComponentsModule {
6 | keyframes(
7 | strings: TemplateStringsArray | string[],
8 | ...interpolations: SimpleInterpolation[]
9 | ): Keyframes;
10 | }
11 |
12 | export interface BaseThemedCssFunction {
13 | (
14 | first:
15 | | TemplateStringsArray
16 | | CSSObject
17 | | InterpolationFunction>
18 | | string[],
19 | ...interpolations: Array>>
20 | ): FlattenInterpolation>;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------