├── .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 | 19 | 20 | 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 | 40 | {samples.map(sample => ( 41 | 42 | {isPayloadSample(sample) ? ( 43 |
    44 | 45 |
    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 |
    40 | 41 |
    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 | {'redocly 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 | 40 | 41 | 54 | 55 | 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 |
    46 |
    49 |
    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 | --------------------------------------------------------------------------------