├── .formatter.exs ├── .gitattributes ├── .github └── workflows │ ├── artifacts.yml │ ├── ci.yml │ ├── release.yml │ └── release_pre_built │ └── action.yml ├── .gitignore ├── CHANGELOG.md ├── Cheatsheet.cheatmd ├── LICENSE ├── README.md ├── assets ├── .eslintignore ├── .eslintrc.js ├── README.md ├── build │ └── build.js ├── css │ ├── _epub.css │ ├── _html.css │ ├── autocomplete.css │ ├── content │ │ ├── admonition.css │ │ ├── bottom-actions.css │ │ ├── cheatsheet.css │ │ ├── code.css │ │ ├── epub-admonition.css │ │ ├── footer.css │ │ ├── functions.css │ │ ├── general.css │ │ └── summary.css │ ├── copy-button.css │ ├── custom-props │ │ ├── _elixir.css │ │ ├── _erlang.css │ │ ├── common.css │ │ ├── theme-dark.css │ │ └── theme-light.css │ ├── entry │ │ ├── epub-elixir.css │ │ ├── epub-erlang.css │ │ ├── html-elixir.css │ │ └── html-erlang.css │ ├── focus.css │ ├── icons.css │ ├── keyboard-shortcuts.css │ ├── layout.css │ ├── makeup.css │ ├── modal.css │ ├── preview.css │ ├── print-cheatsheet.css │ ├── print.css │ ├── quick-switch.css │ ├── screen-reader.css │ ├── search-bar.css │ ├── search.css │ ├── settings.css │ ├── sidebar.css │ ├── tabset.css │ ├── toast.css │ └── tooltips.css ├── fonts │ ├── RemixIconCollection.remixicon │ └── remixicon.woff2 ├── js │ ├── autocomplete │ │ ├── autocomplete-list.js │ │ └── suggestions.js │ ├── constants.js │ ├── content.js │ ├── copy-button.js │ ├── entry │ │ ├── epub.js │ │ ├── html.js │ │ └── inline_html.js │ ├── globals.js │ ├── handlebars │ │ ├── helpers.js │ │ └── templates │ │ │ ├── autocomplete-suggestions.handlebars │ │ │ ├── copy-button.html │ │ │ ├── modal-layout.html │ │ │ ├── quick-switch-modal-body.html │ │ │ ├── search-results.handlebars │ │ │ ├── settings-modal-body.handlebars │ │ │ ├── tooltip-body.handlebars │ │ │ └── versions-dropdown.handlebars │ ├── helpers.js │ ├── keyboard-shortcuts.js │ ├── makeup.js │ ├── modal.js │ ├── preview.js │ ├── quick-switch.js │ ├── search-bar.js │ ├── search-page.js │ ├── settings-store.js │ ├── settings.js │ ├── sidebar │ │ ├── constants.js │ │ ├── sidebar-drawer.js │ │ ├── sidebar-list.js │ │ └── sidebar-version-select.js │ ├── swup.js │ ├── tabsets.js │ ├── theme.js │ ├── toast.js │ └── tooltips │ │ ├── hint-page.js │ │ ├── hints.js │ │ └── tooltips.js ├── karma.conf.js ├── package-lock.json ├── package.json └── test │ ├── .eslintrc │ ├── autocomplete │ └── suggestions.spec.js │ ├── helpers.spec.js │ └── tooltips │ └── hints.spec.js ├── bin └── ex_doc ├── formatters ├── epub │ ├── dist │ │ ├── epub-4WIP524F.js │ │ ├── epub-elixir-FNUUKFP7.css │ │ └── epub-erlang-JBFPMY6T.css │ └── metainfo │ │ ├── com.apple.ibooks.display-options.xml │ │ └── container.xml └── html │ └── dist │ ├── html-Y223O6DN.js │ ├── html-elixir-KV3YOVJ3.css │ ├── html-erlang-DQDXQC7W.css │ ├── inline_html-4XT25SPW.js │ ├── lato-all-400-normal-MNITWADU.woff │ ├── lato-all-700-normal-XMT5XFBS.woff │ ├── lato-latin-400-normal-W7754I4D.woff2 │ ├── lato-latin-700-normal-2XVSBPG4.woff2 │ ├── lato-latin-ext-400-normal-N27NCBWW.woff2 │ ├── lato-latin-ext-700-normal-Q2L5DVMW.woff2 │ └── remixicon-QPNJX265.woff2 ├── lib ├── ex_doc.ex ├── ex_doc │ ├── application.ex │ ├── autolink.ex │ ├── cli.ex │ ├── config.ex │ ├── doc_ast.ex │ ├── formatter │ │ ├── epub.ex │ │ ├── epub │ │ │ ├── assets.ex │ │ │ ├── templates.ex │ │ │ └── templates │ │ │ │ ├── content_template.eex │ │ │ │ ├── extra_template.eex │ │ │ │ ├── head_template.eex │ │ │ │ ├── media-types.txt │ │ │ │ ├── module_template.eex │ │ │ │ ├── nav_grouped_item_template.eex │ │ │ │ ├── nav_template.eex │ │ │ │ ├── title_template.eex │ │ │ │ └── toc_item_template.eex │ │ ├── html.ex │ │ └── html │ │ │ ├── assets.ex │ │ │ ├── search_data.ex │ │ │ ├── templates.ex │ │ │ └── templates │ │ │ ├── api_reference_entry_template.eex │ │ │ ├── api_reference_template.eex │ │ │ ├── detail_template.eex │ │ │ ├── extra_template.eex │ │ │ ├── footer_template.eex │ │ │ ├── head_template.eex │ │ │ ├── module_template.eex │ │ │ ├── not_found_template.eex │ │ │ ├── redirect_template.eex │ │ │ ├── search_template.eex │ │ │ ├── sidebar_template.eex │ │ │ └── summary_template.eex │ ├── group_matcher.ex │ ├── language.ex │ ├── language │ │ ├── elixir.ex │ │ ├── erlang.ex │ │ └── source.ex │ ├── markdown.ex │ ├── markdown │ │ └── earmark.ex │ ├── nodes.ex │ ├── refs.ex │ ├── retriever.ex │ ├── shell_lexer.ex │ └── utils.ex └── mix │ └── tasks │ └── docs.ex ├── mix.exs ├── mix.lock └── test ├── ex_doc ├── cli_test.exs ├── config_test.exs ├── doc_ast_test.exs ├── formatter │ ├── epub │ │ └── templates_test.exs │ ├── epub_test.exs │ ├── html │ │ ├── erlang_test.exs │ │ ├── search_data_test.exs │ │ └── templates_test.exs │ └── html_test.exs ├── group_matcher_test.exs ├── language │ ├── elixir_test.exs │ └── erlang_test.exs ├── markdown │ └── earmark_test.exs ├── refs_test.exs ├── retriever │ ├── elixir_test.exs │ └── erlang_test.exs ├── retriever_test.exs └── utils_test.exs ├── ex_doc_test.exs ├── examples └── admonition.md ├── fixtures ├── ExtraPage.md ├── ExtraPageWithSettextHeader.md ├── LICENSE ├── LivebookFile.livemd ├── PlainText.txt ├── PlainTextFiles.md ├── README.md ├── behaviour.ex ├── callbacks_no_docs.ex ├── cheatsheets.cheatmd ├── common_nesting_prefix.ex ├── compiled_with_docs.ex ├── compiled_without_docs.ex ├── duplicate_headings.ex ├── elixir.png ├── overlapping_defaults.ex ├── protocol.ex ├── random_error.ex ├── task_with_docs.ex ├── types_and_specs.ex ├── umbrella │ ├── apps │ │ ├── bar │ │ │ ├── lib │ │ │ │ └── bar.ex │ │ │ └── mix.exs │ │ └── foo │ │ │ ├── lib │ │ │ └── foo.ex │ │ │ └── mix.exs │ └── mix.exs └── warnings.ex ├── mix └── tasks │ └── docs_test.exs ├── prerelease.sh ├── support └── with_without_module_doc.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{bin,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /formatters/epub/dist/* -diff 2 | /formatters/html/dist/* -diff 3 | -------------------------------------------------------------------------------- /.github/workflows/artifacts.yml: -------------------------------------------------------------------------------- 1 | name: Artifacts 2 | on: 3 | workflow_run: 4 | workflows: ["CI"] 5 | types: 6 | - completed 7 | branches-ignore: 8 | - main 9 | 10 | permissions: 11 | pull-requests: write 12 | 13 | jobs: 14 | notify: 15 | runs-on: ubuntu-latest 16 | if: ${{ github.event.workflow_run.event == 'pull_request' }} 17 | steps: 18 | - uses: actions/github-script@v7 19 | with: 20 | script: | 21 | const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | run_id: context.payload.workflow_run.id 25 | }); 26 | 27 | if (artifacts.data.total_count !== 1) { 28 | throw new Error('Expected one artifact') 29 | } 30 | 31 | const artifact = artifacts.data.artifacts[0]; 32 | const download = await github.rest.actions.downloadArtifact({ 33 | owner: context.repo.owner, 34 | repo: context.repo.repo, 35 | artifact_id: artifact.id, 36 | archive_format: 'zip' 37 | }); 38 | 39 | const fs = require('fs'); 40 | fs.writeFileSync('artifact.zip', Buffer.from(download.data)); 41 | require('child_process').execSync('unzip artifact.zip'); 42 | const ghInfo = JSON.parse(fs.readFileSync('gh.json', 'utf8')); 43 | const pullNumber = ghInfo.number; 44 | 45 | const artifactUrl = `${context.payload.workflow_run.html_url}/artifacts/${artifact.id}`; 46 | const commentBody = `\n📦 Docs artifacts are ready: ${artifactUrl}`; 47 | 48 | const comments = await github.rest.issues.listComments({ 49 | owner: context.repo.owner, 50 | repo: context.repo.repo, 51 | issue_number: pullNumber 52 | }); 53 | 54 | const botComment = comments.data.find(comment => 55 | comment.user.type === 'Bot' && 56 | comment.body.includes('') 57 | ); 58 | 59 | if (botComment) { 60 | await github.rest.issues.updateComment({ 61 | owner: context.repo.owner, 62 | repo: context.repo.repo, 63 | comment_id: botComment.id, 64 | body: commentBody 65 | }); 66 | } else { 67 | await github.rest.issues.createComment({ 68 | owner: context.repo.owner, 69 | repo: context.repo.repo, 70 | issue_number: pullNumber, 71 | body: commentBody 72 | }); 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | env: 6 | MIX_ENV: test 7 | 8 | jobs: 9 | assets: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | include: 14 | - elixir: "1.17" 15 | otp: "27" 16 | node: 18.x 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | # Setup Node 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{matrix.node}} 24 | 25 | - name: Cache npm dependencies 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-node-${{ hashFiles('asssets/package-lock.json') }} 30 | 31 | - run: npm ci --prefix assets 32 | - run: npm run build --prefix assets 33 | 34 | # Setup Elixir 35 | - uses: erlef/setup-beam@v1 36 | with: 37 | otp-version: ${{ matrix.otp }} 38 | elixir-version: ${{ matrix.elixir }} 39 | 40 | # Generate and upload artifacts 41 | - name: Generate docs 42 | run: test/prerelease.sh 43 | 44 | - name: Attach docs metadata 45 | run: | 46 | echo "{\"number\":${{ github.event.number }}}" > test/tmp/contents/doc/gh.json 47 | 48 | - name: Upload docs 49 | uses: actions/upload-artifact@v4 50 | id: docs-upload 51 | with: 52 | name: docs 53 | path: test/tmp/contents/doc/ 54 | 55 | # Test JS 56 | - run: npm run lint --prefix assets 57 | - name: npm run test --prefix assets 58 | run: | 59 | sudo apt-get install xvfb 60 | xvfb-run --auto-servernum npm run test --prefix assets 61 | env: 62 | CI: true 63 | 64 | # Push updated assets if all good 65 | - name: Push updated assets 66 | if: github.ref_name == 'main' 67 | uses: stefanzweifel/git-auto-commit-action@v4 68 | with: 69 | commit_message: Update assets 70 | file_pattern: formatters 71 | 72 | elixir: 73 | runs-on: ubuntu-latest 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | include: 78 | # Test very old Elixir and Erlang 79 | - elixir: "1.15" 80 | otp: "25" 81 | # Test Erlang without -doc attribute support 82 | - elixir: "1.16" 83 | otp: "26" 84 | # Test with initial Erlang doc attribute support 85 | - elixir: "1.17" 86 | otp: "27" 87 | - elixir: "1.18" 88 | otp: "27" 89 | lint: true 90 | steps: 91 | - uses: actions/checkout@v3 92 | 93 | - uses: erlef/setup-beam@v1 94 | with: 95 | otp-version: ${{ matrix.otp }} 96 | elixir-version: ${{ matrix.elixir }} 97 | 98 | - run: mix deps.get 99 | 100 | - run: mix compile --warnings-as-errors 101 | if: ${{ matrix.lint }} 102 | 103 | - run: mix test 104 | 105 | - run: mix deps.unlock --check-unused 106 | if: ${{ matrix.lint }} 107 | 108 | - run: mix format --check-formatted 109 | if: ${{ matrix.lint }} 110 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | create_release: 13 | continue-on-error: true 14 | runs-on: ubuntu-22.04 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | steps: 18 | - name: Create release 19 | run: | 20 | echo "Creating release..." 21 | gh release create \ 22 | --repo ${{ github.repository }} \ 23 | --title ${{ github.ref_name }} \ 24 | ${{ github.ref_name }} 25 | 26 | release_pre_built: 27 | needs: create_release 28 | strategy: 29 | fail-fast: true 30 | matrix: 31 | include: 32 | - otp: 25 33 | otp_version: "25.3.2.12" 34 | elixir_version: "1.16.2" 35 | - otp: 26 36 | otp_version: "26.2.5" 37 | elixir_version: "1.16.2" 38 | - otp: 27 39 | otp_version: "27.2" 40 | elixir_version: "1.18.2" 41 | 42 | runs-on: ubuntu-22.04 43 | steps: 44 | - uses: actions/checkout@v4 45 | with: 46 | fetch-depth: 50 47 | - uses: ./.github/workflows/release_pre_built 48 | with: 49 | otp_version: ${{ matrix.otp_version }} 50 | otp: ${{ matrix.otp }} 51 | elixir_version: ${{ matrix.elixir_version }} 52 | 53 | - name: Upload Pre-built 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | run: | 57 | gh release upload --clobber "${{ github.ref_name }}" \ 58 | ex_doc_otp_${{ matrix.otp }} \ 59 | ex-doc-otp-${{ matrix.otp }}.sha{1,256}sum \ 60 | -------------------------------------------------------------------------------- /.github/workflows/release_pre_built/action.yml: -------------------------------------------------------------------------------- 1 | name: "Release pre built" 2 | description: "Builds ex_doc scripts" 3 | inputs: 4 | otp: 5 | description: "The major OTP version" 6 | otp_version: 7 | description: "The exact OTP version (major.minor[.patch])" 8 | elixir_version: 9 | description: "The exact Elixir version (major.minor[.patch])" 10 | runs: 11 | using: "composite" 12 | steps: 13 | - uses: erlef/setup-beam@v1.16.0 14 | with: 15 | otp-version: ${{ inputs.otp_version }} 16 | elixir-version: ${{ inputs.elixir_version }} 17 | - name: Build ex_doc 18 | shell: bash 19 | run: | 20 | mix deps.get 21 | mix escript.build 22 | mv ex_doc ex_doc_otp_${{ inputs.otp }} 23 | shasum -a 1 ex_doc_otp_${{ inputs.otp }} > ex-doc-otp-${{ inputs.otp }}.sha1sum 24 | shasum -a 256 ex_doc_otp_${{ inputs.otp }} > ex-doc-otp-${{ inputs.otp }}.sha256sum 25 | echo "$PWD/bin" >> $GITHUB_PATH 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | ex_doc-*.tar 24 | 25 | node_modules/ 26 | /test/fixtures/umbrella/_build/ 27 | /test/tmp/ 28 | /tmp/ 29 | /npm-debug.log 30 | 31 | # Ignore artifacts from non-production builds 32 | formatters/epub/dist/epub.js 33 | formatters/html/dist/html.js 34 | 35 | # Ignore escript when built 36 | /ex_doc 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012 Plataformatec 2 | Copyright 2021 The Elixir Team 3 | https://github.com/elixir-lang/ex_doc/ 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | ========================================================================== 18 | 19 | Dependencies are externally maintained and have their own licenses; we 20 | recommend you read them as their terms may differ from the terms above. 21 | -------------------------------------------------------------------------------- /assets/.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts 2 | formatters/* 3 | doc/* 4 | test/* 5 | 6 | # Ignore JavaScript files in dependencies 7 | deps/* 8 | -------------------------------------------------------------------------------- /assets/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true 5 | }, 6 | extends: 'standard', 7 | overrides: [ 8 | ], 9 | parserOptions: { 10 | ecmaVersion: 'latest', 11 | sourceType: 'module' 12 | }, 13 | rules: { 14 | 'no-new': 0, 15 | 'no-path-concat': 0, 16 | 'no-throw-literal': 0, 17 | 'no-useless-escape': 0, 18 | 'object-curly-spacing': 0 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | All asset sources for `ExDoc` live in this directory. The built, ready-to-use versions are found in `formatters/{html,epub}/dist`. 4 | 5 | To work on these assets you need to have [Node.js] and [npm] installed. (npm is usually installed along with Node.js.) The build process is currently tested in Node 18 LTS. 6 | 7 | Assets are built with [esbuild], which, along with the JavaScript linter and test-runner, is set as a dependency in the assets `package.json` and installed via [npm]: 8 | 9 | ```bash 10 | $ npm install --prefix assets 11 | ``` 12 | 13 | ## `npm run` scripts 14 | 15 | The following scripts are available from the root folder of the project. 16 | 17 | ### `build` 18 | 19 | ```bash 20 | $ npm run --prefix assets build 21 | ``` 22 | 23 | Build a complete production bundle, including JavaScript and CSS. 24 | 25 | (Note that this is not required to be manually run when generating docs: if you run `mix build` at the `ExDoc` root after changing your assets, the assets will be recompiled and fresh docs with your changes will be generated.) 26 | 27 | ### `build:watch` 28 | 29 | ```bash 30 | $ npm run --prefix assets build:watch 31 | ``` 32 | 33 | Run the `build` command with watch mode set, providing for automatic assets rebuilds on every asset file change. 34 | 35 | Additionally, in watch mode, the docs are built after every asset rebuild, meaning the only action required to check results after changing asset sources is to refresh/reload the browser or EPUB reader. 36 | 37 | ### `lint` 38 | 39 | ```bash 40 | $ npm run --prefix assets lint 41 | ``` 42 | 43 | Lint all JavaScript files using [ESLint]. 44 | 45 | ### `lint:fix` 46 | 47 | ```bash 48 | $ npm run --prefix assets lint:fix 49 | ``` 50 | 51 | Lint and automatically fix all JavaScript files using [ESLint]. 52 | 53 | ### `test` 54 | 55 | ```bash 56 | $ npm run --prefix assets test 57 | ``` 58 | 59 | Run all the available JavaScript tests using [Karma]. 60 | 61 | 62 | [esbuild]: https://esbuild.github.io 63 | [Node.js]: https://nodejs.org/ 64 | [npm]: https://www.npmjs.com/ 65 | [ESLint]: https://eslint.org/ 66 | [Karma]: https://karma-runner.github.io/ 67 | -------------------------------------------------------------------------------- /assets/build/build.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path') 2 | const process = require('node:process') 3 | const cp = require('node:child_process') 4 | const esbuild = require('esbuild') 5 | const fsExtra = require('fs-extra') 6 | const fs = require('node:fs/promises') 7 | const handlebars = require('handlebars') 8 | const util = require('node:util') 9 | 10 | const exec = util.promisify(cp.exec) 11 | 12 | const watchMode = Boolean(process.env.npm_config_watch) 13 | 14 | /** @type {import('esbuild').BuildOptions[]} */ 15 | const formatters = [ 16 | { 17 | formatter: 'epub', 18 | outdir: path.resolve('../formatters/epub/dist'), 19 | entryPoints: [ 20 | 'js/entry/epub.js', 21 | 'css/entry/epub-elixir.css', 22 | 'css/entry/epub-erlang.css' 23 | ] 24 | }, 25 | { 26 | formatter: 'html', 27 | outdir: path.resolve('../formatters/html/dist'), 28 | entryPoints: [ 29 | 'js/entry/inline_html.js', 30 | 'js/entry/html.js', 31 | 'css/entry/html-elixir.css', 32 | 'css/entry/html-erlang.css' 33 | ], 34 | loader: { 35 | '.woff2': 'file', 36 | // TODO: Remove when @fontsource/* removes legacy .woff 37 | '.woff': 'file' 38 | } 39 | } 40 | ] 41 | 42 | Promise.all(formatters.map(async ({formatter, ...options}) => { 43 | // Clean outdir. 44 | await fsExtra.emptyDir(options.outdir) 45 | 46 | await esbuild.build({ 47 | entryNames: watchMode ? '[name]-dev' : '[name]-[hash]', 48 | bundle: true, 49 | minify: !watchMode, 50 | logLevel: watchMode ? 'warning' : 'info', 51 | watch: watchMode, 52 | ...options, 53 | plugins: [{ 54 | name: 'ex_doc', 55 | setup (build) { 56 | // Pre-compile handlebars templates. 57 | build.onLoad({ 58 | filter: /\.handlebars$/ 59 | }, async ({ path: filename }) => { 60 | try { 61 | const source = await fs.readFile(filename, 'utf-8') 62 | const template = handlebars.precompile(source) 63 | const contents = [ 64 | "import * as Handlebars from 'handlebars/runtime'", 65 | "import '../helpers'", 66 | `export default Handlebars.template(${template})` 67 | ].join('\n') 68 | return { contents } 69 | } catch (error) { 70 | return { errors: [{ text: error.message }] } 71 | } 72 | }) 73 | 74 | // Load html templates. 75 | build.onLoad({ 76 | filter: /\.html$/ 77 | }, async ({ path: filename }) => { 78 | try { 79 | const source = await fs.readFile(filename, 'utf-8') 80 | // Remove newlines and leading whitespace. 81 | // Shouldn't have any effect on content. 82 | const compressed = source.replace(/\n\s*/g, '') 83 | const contents = `export default ${JSON.stringify(compressed)}` 84 | return { contents } 85 | } catch (error) { 86 | return { errors: [{ text: error.message }] } 87 | } 88 | }) 89 | 90 | // Generate docs with new assets (watch mode only). 91 | if (watchMode) { 92 | build.onEnd(async result => { 93 | if (result.errors.length) return 94 | console.log(`${formatter} assets built`) 95 | await exec(`mix docs --formatter ${formatter}`, {cwd: '../'}) 96 | console.log(`${formatter} docs built`) 97 | }) 98 | } 99 | } 100 | }] 101 | }) 102 | })).catch((error) => { 103 | console.error(error) 104 | process.exit(1) 105 | }) 106 | -------------------------------------------------------------------------------- /assets/css/_epub.css: -------------------------------------------------------------------------------- 1 | @import 'custom-props/common.css'; 2 | @import 'custom-props/theme-light.css'; 3 | 4 | @import 'content/epub-admonition.css'; 5 | @import 'content/code.css'; 6 | @import 'content/functions.css'; 7 | @import 'screen-reader.css'; 8 | @import 'makeup.css'; 9 | 10 | body { 11 | display: block; 12 | font-size: 1em; 13 | line-height: 1.2; 14 | padding-left: 0; 15 | padding-right: 0; 16 | margin: 0 5pt; 17 | } 18 | 19 | nav > ol { 20 | list-style-type: square; 21 | } 22 | 23 | nav > ol ol { 24 | list-style-type: disc; 25 | } 26 | 27 | .title-container { 28 | text-align: center; 29 | } 30 | 31 | img[src*="#gh-dark-mode-only"] { 32 | display: none; 33 | } 34 | -------------------------------------------------------------------------------- /assets/css/_html.css: -------------------------------------------------------------------------------- 1 | @import "@fontsource/lato/400.css"; 2 | @import "@fontsource/lato/700.css"; 3 | 4 | @import "custom-props/common.css"; 5 | @import "custom-props/theme-light.css"; 6 | @import "custom-props/theme-dark.css"; 7 | 8 | @import "modern-normalize/modern-normalize.css"; 9 | 10 | @import "icons.css"; 11 | @import "layout.css"; 12 | @import "sidebar.css"; 13 | @import "search-bar.css"; 14 | @import "focus.css"; 15 | @import "content/general.css"; 16 | @import "content/admonition.css"; 17 | @import "content/summary.css"; 18 | @import "content/code.css"; 19 | @import "content/functions.css"; 20 | @import "content/footer.css"; 21 | @import "content/bottom-actions.css"; 22 | @import "content/cheatsheet.css"; 23 | @import "search.css"; 24 | @import "modal.css"; 25 | @import "keyboard-shortcuts.css"; 26 | @import "quick-switch.css"; 27 | @import "autocomplete.css"; 28 | @import "tooltips.css"; 29 | @import "copy-button.css"; 30 | @import "settings.css"; 31 | @import "toast.css"; 32 | @import "screen-reader.css"; 33 | @import "print.css"; 34 | @import "print-cheatsheet.css"; 35 | @import "makeup.css"; 36 | @import "tabset.css"; 37 | @import "preview.css"; 38 | 39 | body:not(.dark) .content-inner img[src*="#gh-dark-mode-only"], 40 | body.dark .content-inner img[src*="#gh-light-mode-only"] { 41 | display: none; 42 | } 43 | -------------------------------------------------------------------------------- /assets/css/content/admonition.css: -------------------------------------------------------------------------------- 1 | .content-inner section.admonition { 2 | border-radius: var(--borderRadius-base); 3 | border-left: 0; 4 | } 5 | 6 | .content-inner section.admonition.warning { 7 | background-color: var(--warningBackground); 8 | } 9 | 10 | .content-inner section.admonition.error { 11 | background-color: var(--errorBackground); 12 | } 13 | 14 | .content-inner section.admonition.info { 15 | background-color: var(--infoBackground); 16 | } 17 | 18 | .content-inner section.admonition.neutral { 19 | background-color: var(--neutralBackground); 20 | } 21 | 22 | .content-inner section.admonition.tip { 23 | background-color: var(--tipBackground); 24 | } 25 | 26 | .content-inner section.admonition > .admonition-title { 27 | color: var(--contrast); 28 | margin: 0 -1.2rem; 29 | padding: .7rem 1.2rem .7rem 3.3rem; 30 | font-weight: 700; 31 | font-style: normal; 32 | } 33 | .content-inner section.admonition > .admonition-title::before { 34 | color: var(--contrast); 35 | position: absolute; 36 | left: 1rem; 37 | font-size: 1.8rem; 38 | font-family: 'remixicon'; 39 | font-style: normal; 40 | -webkit-font-smoothing: antialiased; 41 | -moz-osx-font-smoothing: grayscale; 42 | } 43 | 44 | .content-inner section.admonition > .admonition-title.warning { 45 | background-color: var(--warningHeadingBackground); 46 | color: var(--warningHeading); 47 | } 48 | .content-inner section.admonition > .admonition-title.warning::before { 49 | content: var(--icon-error-warning); 50 | color: var(--warningHeading); 51 | } 52 | 53 | .content-inner section.admonition > .admonition-title.error { 54 | background-color: var(--errorHeadingBackground); 55 | color: var(--errorHeading); 56 | } 57 | .content-inner section.admonition > .admonition-title.error::before { 58 | content: var(--icon-error-warning); 59 | color: var(--errorHeading); 60 | } 61 | 62 | .content-inner section.admonition > .admonition-title.info { 63 | background-color: var(--infoHeadingBackground); 64 | color: var(--infoHeading); 65 | } 66 | .content-inner section.admonition > .admonition-title.info::before { 67 | content: var(--icon-information); 68 | color: var(--infoHeading); 69 | } 70 | 71 | .content-inner section.admonition > .admonition-title.neutral { 72 | background-color: var(--neutralHeadingBackground); 73 | color: var(--neutralHeading); 74 | } 75 | .content-inner section.admonition > .admonition-title.neutral::before { 76 | content: var(--icon-double-quotes-l); 77 | color: var(--neutralHeading); 78 | } 79 | 80 | .content-inner section.admonition > .admonition-title.tip { 81 | background-color: var(--tipHeadingBackground); 82 | color: var(--tipHeading); 83 | } 84 | .content-inner section.admonition > .admonition-title.tip::before { 85 | content: var(--icon-information); 86 | color: var(--tipHeading); 87 | } 88 | 89 | .content-inner section.admonition > .admonition-title code { 90 | margin: 0 0.5ch; 91 | } 92 | 93 | .content-inner section.admonition code { 94 | background-color: var(--admInlineCodeBackground); 95 | border: 1px solid var(--admInlineCodeBorder); 96 | color: var(--admInlineCodeColor); 97 | } 98 | 99 | .content-inner section.admonition pre code { 100 | background-color: var(--admCodeBackground); 101 | border: 1px solid var(--admCodeBorder); 102 | color: var(--admCodeColor); 103 | } 104 | 105 | .content-inner section.admonition > .admonition-title :is(a, a:visited) { 106 | color: inherit; 107 | text-decoration-color: currentColor; 108 | } 109 | 110 | @media screen and (max-width: 768px) { 111 | .content-inner section.admonition { 112 | margin-left: calc(-1 * var(--content-gutter)); 113 | margin-right: calc(-1 * var(--content-gutter)); 114 | padding-left: var(--content-gutter); 115 | padding-right: var(--content-gutter); 116 | border-radius: 0; 117 | } 118 | 119 | .content-inner section.admonition > .admonition-title { 120 | margin: 0 calc(-1 * var(--content-gutter)); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /assets/css/content/bottom-actions.css: -------------------------------------------------------------------------------- 1 | .content-inner .bottom-actions { 2 | display: flex; 3 | justify-content: space-between; 4 | margin-top: 4em; 5 | gap: 12px; 6 | } 7 | 8 | .bottom-actions-item { 9 | flex: 1 1 0%; 10 | } 11 | 12 | .content-inner .bottom-actions .bottom-actions-button { 13 | display: flex; 14 | text-decoration: none; 15 | flex-direction: column; 16 | border-radius: var(--borderRadius-sm); 17 | border: 1px solid var(--bottomActionsBtnBorder); 18 | padding: 12px 16px; 19 | min-width: 150px; 20 | transition: var(--transition-all); 21 | } 22 | 23 | .content-inner .bottom-actions .bottom-actions-button:hover { 24 | border-color: var(--mainLight); 25 | } 26 | .content-inner .bottom-actions .bottom-actions-button .subheader { 27 | font-size: .8em; 28 | color: var(--textHeaders); 29 | white-space: nowrap; 30 | } 31 | 32 | .content-inner .bottom-actions .bottom-actions-button .title { 33 | color: var(--bottomActionsBtnTitle); 34 | } 35 | 36 | .content-inner .bottom-actions .bottom-actions-button[rel="prev"] { 37 | text-align: start; 38 | } 39 | 40 | .content-inner .bottom-actions .bottom-actions-button[rel="next"] { 41 | text-align: end; 42 | } 43 | 44 | @media screen and (max-width: 768px) { 45 | .content-inner .bottom-actions { 46 | flex-direction: column-reverse; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /assets/css/content/code.css: -------------------------------------------------------------------------------- 1 | /* The Consolas font on Windows is too small compared to other ones */ 2 | @font-face { 3 | font-family: "Consolas"; 4 | src: local("Consolas"); 5 | size-adjust: 110%; 6 | } 7 | 8 | .content-inner.content-inner :is(a:has(code, img), pre a) { 9 | color: var(--link-color); 10 | text-shadow: none; 11 | text-decoration: none; 12 | background-image: none; 13 | } 14 | 15 | .content-inner.content-inner :is(a:has(code, img), pre a):is(:visited, :active, :focus, :hover) { 16 | color: var(--link-visited-color); 17 | } 18 | 19 | .content-inner code { 20 | background-color: var(--codeBackground); 21 | vertical-align: baseline; 22 | border-radius: var(--borderRadius-sm); 23 | padding: .1em .2em; 24 | border: 1px solid var(--codeBorder); 25 | text-transform: none; 26 | } 27 | 28 | .content-inner code.inline { 29 | border-radius: var(--borderRadius-sm); 30 | word-wrap: break-word; 31 | } 32 | 33 | .content-inner pre { 34 | margin: var(--baseLineHeight) 0; 35 | } 36 | 37 | .content-inner pre code { 38 | display: block; 39 | overflow-x: auto; 40 | white-space: inherit; 41 | padding: 1em; 42 | scrollbar-width: thin; 43 | } 44 | 45 | .content-inner pre code.output { 46 | margin: 0 12px; 47 | max-height: 400px; 48 | overflow: auto; 49 | } 50 | 51 | .content-inner pre code.output + .copy-button { 52 | margin-right: 12px; 53 | } 54 | 55 | .content-inner pre code.output:before { 56 | content: "Output"; 57 | display: block; 58 | position: absolute; 59 | top: -16px; 60 | left: 12px; 61 | padding: 2px 4px; 62 | font-size: var(--text-xs); 63 | font-family: var(--monoFontFamily); 64 | line-height: 1; 65 | color: var(--textHeaders); 66 | background-color: var(--codeBackground); 67 | border: 1px solid var(--codeBorder); 68 | border-bottom: 0; 69 | border-radius: 2px; 70 | } 71 | 72 | @media screen and (max-width: 768px) { 73 | .content-inner > pre:has(code), 74 | .content-inner section > pre:has(code) { 75 | margin-left: calc(-1 * var(--content-gutter)); 76 | margin-right: calc(-1 * var(--content-gutter)); 77 | } 78 | 79 | .content-inner > pre code, 80 | .content-inner section > pre code { 81 | padding-left: var(--content-gutter); 82 | padding-right: var(--content-gutter); 83 | border-radius: 0; 84 | border-left-width: 0; 85 | border-right-width: 0; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /assets/css/content/epub-admonition.css: -------------------------------------------------------------------------------- 1 | .content-inner section.admonition { 2 | border-left: solid 4px; 3 | color: var(--black); 4 | font-size: 0.9em; 5 | line-height: 1.4em; 6 | margin-bottom: 1.5em; 7 | margin-left: 5px; 8 | padding: 7px 15px; 9 | page-break-inside: avoid; 10 | } 11 | 12 | .content-inner section.admonition.warning { 13 | background-color: var(--warningBackground); 14 | border-left-color: var(--warningHeadingBackground); 15 | } 16 | 17 | .content-inner section.admonition.error { 18 | background-color: var(--errorBackground); 19 | border-left-color: var(--errorHeadingBackground); 20 | } 21 | 22 | .content-inner section.admonition.info { 23 | background-color: var(--infoBackground); 24 | border-left-color: var(--infoHeadingBackground); 25 | } 26 | 27 | .content-inner section.admonition.neutral { 28 | background-color: var(--neutralBackground); 29 | border-left-color: var(--neutralHeadingBackground); 30 | } 31 | 32 | .content-inner section.admonition.tip { 33 | background-color: var(--tipBackground); 34 | border-left-color: var(--tipHeadingBackground); 35 | } 36 | 37 | .content-inner section.admonition > .admonition-title { 38 | font-weight: bold; 39 | margin: 0px 10px 5px 0px; 40 | font-style: normal; 41 | font-weight: 700; 42 | } 43 | 44 | .content-inner section.admonition > .admonition-title.warning { 45 | color: var(--warningHeadingBackground); 46 | } 47 | .content-inner section.admonition > .admonition-title.error { 48 | color: var(--errorHeadingBackground); 49 | } 50 | .content-inner section.admonition > .admonition-title.info { 51 | color: var(--infoHeadingBackground); 52 | } 53 | .content-inner section.admonition > .admonition-title.neutral { 54 | color: var(--neutralHeadingBackground); 55 | } 56 | .content-inner section.admonition > .admonition-title.tip { 57 | color: var(--tipHeadingBackground); 58 | } 59 | 60 | .content-inner section.admonition > .admonition-title code { 61 | margin: 0 0.5ch; 62 | } 63 | 64 | .content-inner section.admonition code { 65 | background-color: var(--admInlineCodeBackground); 66 | border: 1px solid var(--admInlineCodeBorder); 67 | color: var(--admInlineCodeColor); 68 | } 69 | 70 | .content-inner section.admonition pre code { 71 | background-color: var(--admCodeBackground); 72 | border: 1px solid var(--admCodeBorder); 73 | color: var(--admCodeColor); 74 | } 75 | -------------------------------------------------------------------------------- /assets/css/content/footer.css: -------------------------------------------------------------------------------- 1 | .content-inner .footer { 2 | margin: 4em auto 1em; 3 | text-align: center; 4 | font-size: var(--text-sm); 5 | } 6 | 7 | .content-inner .footer .line { 8 | display: inline-block; 9 | } 10 | 11 | .content-inner .footer .footer-button { 12 | background-color: transparent; 13 | border: 0; 14 | cursor: pointer; 15 | padding: 0 4px; 16 | } 17 | 18 | .content-inner .footer .footer-hex-package { 19 | margin-right: 4px; 20 | } 21 | -------------------------------------------------------------------------------- /assets/css/content/functions.css: -------------------------------------------------------------------------------- 1 | @keyframes blink-background { 2 | 0%, 100% { 3 | background-color: var(--textDetailBackground); 4 | } 5 | 6 | 50% { 7 | background-color: var(--blink); 8 | } 9 | } 10 | 11 | .content-inner .detail:target .detail-header { 12 | animation-duration: 0.55s; 13 | animation-name: blink-background; 14 | animation-iteration-count: 1; 15 | animation-timing-function: ease-in-out; 16 | } 17 | 18 | .content-inner .detail-header { 19 | margin: 1em 0; 20 | padding: 0.5em 0.85em 0.5em 1em; 21 | background-color: var(--textDetailBackground); 22 | border-left: 3px solid var(--textDetailAccent); 23 | font-size: 1em; 24 | font-family: var(--monoFontFamily); 25 | position: relative; 26 | } 27 | 28 | .content-inner .detail-header .signature { 29 | font-family: var(--monoFontFamily); 30 | font-size: 13px; 31 | font-weight: 700; 32 | line-height: 2em; 33 | } 34 | 35 | .content-inner .detail-header:hover a.detail-link, 36 | .content-inner .detail-header a.detail-link:focus { 37 | opacity: 1; 38 | text-decoration: none; 39 | } 40 | 41 | .content-inner .detail-header a.detail-link { 42 | transition: var(--transition-opacity); 43 | position: absolute; 44 | top: 0; 45 | left: 0; 46 | display: block; 47 | opacity: 0; 48 | padding: 0.6em; 49 | line-height: 1.5em; 50 | margin-left: -2.5em; 51 | text-decoration: none; 52 | border: none; 53 | } 54 | 55 | @media screen and (max-width: 768px) { 56 | .content-inner .detail-header a.detail-link { 57 | margin-left: -30px; 58 | } 59 | } 60 | 61 | .content-inner .specs pre { 62 | font-family: var(--monoFontFamily); 63 | font-size: var(--text-xs); 64 | font-style: normal; 65 | line-height: 24px; 66 | white-space: pre-wrap; 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | .content-inner .specs .attribute { 72 | color: var(--fnSpecAttr); 73 | } 74 | 75 | .content-inner .docstring { 76 | margin: 1.2em 0 3em 1.2em; 77 | } 78 | 79 | @media screen and (max-width: 768px) { 80 | .content-inner .docstring { 81 | margin-left: 0; 82 | } 83 | } 84 | 85 | .content-inner .docstring:is(h2, h3, h4, h5) { 86 | font-weight: 700; 87 | } 88 | 89 | .content-inner .docstring h2 { 90 | font-size: 1.1em; 91 | } 92 | 93 | .content-inner .docstring h3 { 94 | font-size: 1em; 95 | } 96 | 97 | .content-inner .docstring h4 { 98 | font-size: 0.95em; 99 | } 100 | 101 | .content-inner .docstring h5 { 102 | font-size: 0.9em; 103 | } 104 | 105 | .content-inner div.deprecated { 106 | display: block; 107 | padding: 1em; 108 | background-color: var(--fnDeprecated); 109 | border-radius: var(--borderRadius-sm); 110 | margin: var(--baseLineHeight) 0; 111 | } 112 | -------------------------------------------------------------------------------- /assets/css/content/summary.css: -------------------------------------------------------------------------------- 1 | .content-inner .summary h2 a { 2 | text-decoration: none; 3 | border: none; 4 | color: var(--textHeaders) !important; 5 | } 6 | 7 | .content-inner .summary span.deprecated { 8 | color: var(--darkDeprecated); 9 | font-weight: normal; 10 | } 11 | 12 | .content-inner .summary .summary-row .summary-signature { 13 | font-family: var(--monoFontFamily); 14 | font-size: 13px; 15 | font-weight: 700; 16 | } 17 | 18 | .content-inner .summary .summary-row .summary-signature a { 19 | text-decoration: none; 20 | border: none; 21 | } 22 | 23 | .content-inner .summary .summary-row .summary-synopsis { 24 | padding: 0 1.2em; 25 | margin: 0 0 0.5em; 26 | } 27 | 28 | .content-inner .summary .summary-row .summary-synopsis p { 29 | margin: 0; 30 | padding: 0; 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/copy-button.css: -------------------------------------------------------------------------------- 1 | pre { 2 | position: relative; 3 | } 4 | 5 | pre:hover .copy-button, 6 | pre .copy-button:focus { 7 | opacity: 1; 8 | } 9 | 10 | .copy-button { 11 | display: flex; 12 | opacity: 0; 13 | position: absolute; 14 | top: 7px; 15 | right: 8px; 16 | padding: 8px; 17 | background-color: transparent; 18 | backdrop-filter: blur(8px); 19 | border-radius: var(--borderRadius-sm); 20 | border: 1px solid var(--codeBorder); 21 | cursor: pointer; 22 | transition: var(--transition-all); 23 | font-size: var(--text-sm); 24 | line-height: 24px; 25 | color: currentColor; 26 | 27 | & svg[aria-live="polite"] { 28 | display: none; 29 | } 30 | } 31 | 32 | .copy-button svg { 33 | opacity: 0.5; 34 | transition: var(--transition-all); 35 | } 36 | 37 | pre .copy-button:hover svg, 38 | pre .copy-button:focus-visible svg { 39 | opacity: 1; 40 | } 41 | 42 | .copy-button svg { 43 | width: 20px; 44 | } 45 | 46 | .copy-button.clicked { 47 | opacity: 1; 48 | color: var(--success); 49 | 50 | & svg[aria-live="polite"] { 51 | display: block; 52 | } 53 | } 54 | 55 | .copy-button.clicked svg { 56 | display: none; 57 | color: currentColor; 58 | } 59 | -------------------------------------------------------------------------------- /assets/css/custom-props/_elixir.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main: hsl(250, 68%, 69%); /* purple 600 */ 3 | --mainDark: hsl(250, 68%, 59%); 4 | --mainDarkest: hsl(250, 68%, 49%); 5 | --mainLight: hsl(250, 68%, 74%); 6 | --mainLightest: hsl(250, 68%, 79%); 7 | 8 | --searchBarFocusColor: #8E7CE6; 9 | --searchBarBorderColor: rgba(142, 124, 230, 0.25); 10 | 11 | --link-color: var(--mainDark); 12 | --link-visited-color: var(--mainDarkest); 13 | } 14 | 15 | body.dark { 16 | --link-color: var(--mainLightest); 17 | --link-visited-color: var(--mainLight); 18 | } 19 | -------------------------------------------------------------------------------- /assets/css/custom-props/_erlang.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main: hsl(0, 100%, 44%); 3 | --mainDark: hsl(0, 100%, 34%); 4 | --mainDarkest: hsl(0, 100%, 24%); 5 | --mainLight: hsl(0, 100%, 64%); 6 | --mainLightest: hsl(0, 100%, 74%); 7 | 8 | --searchBarFocusColor: hsl(0, 100%, 50%); 9 | --searchBarBorderColor: rgb(255, 71, 71, 0.1); 10 | 11 | --link-color: hsl(212, 96%, 45%); 12 | --link-visited-color: hsl(212, 96%, 40%); 13 | } 14 | 15 | body.dark { 16 | --link-color: hsl(212, 56%, 72%); 17 | --link-visited-color: hsl(212, 56%, 67%); 18 | } 19 | -------------------------------------------------------------------------------- /assets/css/custom-props/common.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Layout & Whitespace */ 3 | --content-width: 949px; 4 | --content-gutter: 60px; 5 | --borderRadius-lg: 14px; 6 | --borderRadius-base: 8px; 7 | --borderRadius-sm: 3px; 8 | --navTabBorderWidth: 2px; 9 | 10 | /* Font Families */ 11 | /* These mirror modern-normalize.css with "Lato" on top */ 12 | --sansFontFamily: "Lato", system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; 13 | --monoFontFamily: ui-monospace, SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 14 | 15 | /* Typography */ 16 | --baseLineHeight: 1.5em; 17 | 18 | /* Colours */ 19 | --gray25: hsl(207, 43%, 98%); 20 | --gray50: hsl(207, 43%, 96%); 21 | --gray100: hsl(212, 33%, 91%); 22 | --gray200: hsl(210, 29%, 88%); 23 | --gray300: hsl(210, 26%, 84%); 24 | --gray400: hsl(210, 21%, 64%); 25 | --gray450: hsl(210, 21%, 49%); 26 | --gray500: hsl(210, 21%, 34%); 27 | --gray600: hsl(210, 27%, 26%); 28 | --gray700: hsl(212, 35%, 17%); 29 | --gray750: hsl(214, 46%, 14%); 30 | --gray800: hsl(216, 52%, 11%); 31 | --gray800-opacity-0: hsla(216, 52%, 11%, 0%); 32 | --gray850: hsl(216, 63%, 8%); 33 | --gray900: hsl(218, 73%, 4%); 34 | --gray900-opacity-50: hsla(218, 73%, 4%, 50%); 35 | --gray900-opacity-0: hsla(218, 73%, 4%, 0%); 36 | --coldGrayFaint: hsl(240, 5%, 97%); 37 | --coldGrayLight: hsl(240, 5%, 88%); 38 | --coldGray-lightened-10: hsl(240, 5%, 56%); 39 | --coldGray: hsl(240, 5%, 46%); 40 | --coldGray-opacity-10: hsla(240, 5%, 46%, 10%); 41 | --coldGrayDark: hsl(240, 5%, 28%); 42 | --coldGrayDim: hsl(240, 5%, 18%); 43 | --yellowLight: hsl(43, 100%, 95%); 44 | --yellowDark: hsl(44, 100%, 15%); 45 | --yellow: hsl(60, 100%, 43%); 46 | --green-lightened-10: hsl(90, 100%, 45%); 47 | --green: hsl(90, 100%, 35%); 48 | --white: hsl(0, 0%, 100%); 49 | --white-opacity-50: hsla(0, 0%, 100%, 50%); 50 | --white-opacity-10: hsla(0, 0%, 100%, 10%); 51 | --white-opacity-0: hsla(0, 0%, 100%, 0%); 52 | --black: hsl(0, 0%, 0%); 53 | --black-opacity-10: hsla(0, 0%, 0%, 10%); 54 | --black-opacity-50: hsla(0, 0%, 0%, 50%); 55 | --orangeDark: hsl(30, 90%, 40%); 56 | --orangeLight: hsl(30, 80%, 50%); 57 | 58 | --text-xs: 0.75rem; /* 12px */ 59 | --text-sm: 0.875rem; /* 14px */ 60 | --text-md: 1rem; /* 16px */ 61 | --text-lg: 1.125rem; /* 18px */ 62 | --text-xl: 1.25rem; /* 20px */ 63 | 64 | --transition-duration: 150ms; 65 | --transition-timing: cubic-bezier(0.4, 0, 0.2, 1); 66 | 67 | --transition-all: all var(--transition-duration) var(--transition-timing); 68 | 69 | --transition-colors: color var(--transition-duration) var(--transition-timing), 70 | background-color var(--transition-duration) var(--transition-timing), 71 | border-color var(--transition-duration) var(--transition-timing), 72 | text-decoration-color var(--transition-duration) var(--transition-timing), 73 | fill var(--transition-duration) var(--transition-timing), 74 | stroke var(--transition-duration) var(--transition-timing); 75 | 76 | --transition-opacity: opacity var(--transition-duration) var(--transition-timing); 77 | } 78 | 79 | @media screen and (max-width: 768px) { 80 | :root { 81 | --content-width: 100%; 82 | --content-gutter: 20px; 83 | } 84 | } 85 | 86 | option { 87 | background-color: var(--sidebarBackground); 88 | } 89 | -------------------------------------------------------------------------------- /assets/css/custom-props/theme-dark.css: -------------------------------------------------------------------------------- 1 | body.dark { 2 | --background: var(--gray900); 3 | --contrast: var(--white); 4 | --textBody: var(--gray200); 5 | --textHeaders: var(--gray100); 6 | --textDetailAccent: var(--mainLight); 7 | --textDetailBackground: var(--gray700); 8 | 9 | --iconAction: var(--coldGray-lightened-10); 10 | --iconActionHover: var(--white); 11 | 12 | --blockquoteBackground: var(--coldGray-opacity-10); 13 | --blockquoteBorder: var(--coldGrayDim); 14 | 15 | --tableHeadBorder: var(--gray600); 16 | --tableBodyBorder: var(--gray700); 17 | 18 | --warningBackground: hsla( 33, 30%, 60%, 10%); 19 | --warningHeadingBackground: hsla( 33, 66%, 35%, 80%); 20 | --warningHeading: var(--white); 21 | --errorBackground: hsla( 7, 30%, 60%, 10%); 22 | --errorHeadingBackground: hsla( 6, 70%, 40%, 80%); 23 | --errorHeading: var(--white); 24 | --infoBackground: hsla(206, 30%, 60%, 10%); 25 | --infoHeadingBackground: hsla(213, 55%, 35%, 80%); 26 | --infoHeading: var(--white); 27 | --neutralBackground: hsl(210, 30%, 60%, 10%); 28 | --neutralHeadingBackground: var(--gray600); 29 | --neutralHeading: var(--white); 30 | --tipBackground: hsla(142, 30%, 60%, 10%); 31 | --tipHeadingBackground: hsla(134, 45%, 30%, 80%); 32 | --tipHeading: var(--white); 33 | 34 | --fnSpecAttr: var(--gray400); 35 | --fnDeprecated: var(--yellowDark); 36 | --blink: var(--gray600); 37 | 38 | --codeBackground: var(--gray750); 39 | --codeBorder: var(--gray600); 40 | --codeScrollThumb: var(--gray500); 41 | --codeScrollBackground: var(--codeBorder); 42 | --admCodeBackground: var(--gray750); 43 | --admCodeBorder: var(--gray600); 44 | --admCodeColor: var(--gray100); 45 | --admInlineCodeColor: var(--gray100); 46 | --admInlineCodeBackground: var(--gray750); 47 | --admInlineCodeBorder: var(--gray600); 48 | 49 | --tabBorder: var(--gray700); 50 | --tabBorderTop: var(--gray700); 51 | --tabShadow: var(--black); 52 | 53 | --bottomActionsBtnBorder: var(--white-opacity-10); 54 | --bottomActionsBtnTitle: var(--mainLightest); 55 | 56 | --modalBackground: var(--gray800); 57 | 58 | --settingsInput: var(--white); 59 | --settingsInputBackground: var(--gray700); 60 | --settingsInputBorder: var(--gray700); 61 | --settingsSectionBorder: var(--gray700); 62 | 63 | --quickSwitchInput: var(--gray300); 64 | --quickSwitchContour: var(--gray500); 65 | 66 | --success: var(--green-lightened-10); 67 | --progressBarColor: var(--gray300); 68 | 69 | --sidebarAccentMain: var(--gray50); 70 | --sidebarBackground: var(--gray800); 71 | --sidebarHeader: var(--gray700); 72 | --sidebarMuted: var(--gray300); 73 | --sidebarHover: var(--white); 74 | --sidebarStaleVersion: var(--orangeLight); 75 | --sidebarSubheadings: var(--gray400); 76 | --sidebarItem: var(--gray200); 77 | --sidebarInactiveItemBorder: var(--gray400); 78 | --sidebarInactiveItemMarker: var(--gray600); 79 | --sidebarLanguageAccentBar: var(--mainLight); 80 | --sidebarActiveItem: var(--mainLightest); 81 | --searchBarBorder: var(--gray500); 82 | --searchAccentMain: var(--gray300); 83 | --searchSearch: var(--gray900); 84 | --autocompleteBorder: rgba(28,42,60,.75); 85 | --autocompletePreview: var(--gray750); 86 | --autocompleteSelected: var(--gray750); 87 | --autocompleteHover: var(--gray700); 88 | --autocompleteBackground: var(--gray800); 89 | --suggestionBorder: var(--gray600); 90 | --autocompleteResults: var(--gray200); 91 | --autocompleteResultsBold: var(--gray100); 92 | --autocompleteLabelBack: var(--gray600); 93 | --autocompleteLabelFont: rgba(255, 255, 255, 0.8); 94 | } 95 | 96 | :root:has(body.dark) { 97 | color-scheme: dark; 98 | } 99 | -------------------------------------------------------------------------------- /assets/css/custom-props/theme-light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --background: var(--white); 3 | --contrast: var(--black); 4 | --textBody: var(--gray800); 5 | --textHeaders: var(--gray900); 6 | --textDetailAccent: var(--mainLight); 7 | --textDetailBackground: var(--coldGrayFaint); 8 | 9 | --iconAction: var(--coldGray); 10 | --iconActionHover: var(--gray800); 11 | 12 | --blockquoteBackground: var(--coldGrayFaint); 13 | --blockquoteBorder: var(--coldGrayLight); 14 | 15 | --tableHeadBorder: var(--gray100); 16 | --tableBodyBorder: var(--gray50); 17 | 18 | --warningBackground: hsl( 33, 100%, 97%); 19 | --warningHeadingBackground: hsl( 33, 87%, 64%); 20 | --warningHeading: var(--black); 21 | --errorBackground: hsl( 7, 81%, 96%); 22 | --errorHeadingBackground: hsl( 6, 80%, 60%); 23 | --errorHeading: var(--white); 24 | --infoBackground: hsl(206, 91%, 96%); 25 | --infoHeadingBackground: hsl(213, 92%, 62%); 26 | --infoHeading: var(--white); 27 | --neutralBackground: hsl(212, 29%, 92%); 28 | --neutralHeadingBackground: hsl(220, 43%, 11%); 29 | --neutralHeading: var(--white); 30 | --tipBackground: hsl(142, 31%, 93%); 31 | --tipHeadingBackground: hsl(134, 39%, 36%); 32 | --tipHeading: var(--white); 33 | 34 | --fnSpecAttr: var(--coldGray); 35 | --fnDeprecated: var(--yellowLight); 36 | --blink: var(--yellowLight); 37 | 38 | --codeBackground: var(--gray25); 39 | --codeBorder: var(--gray100); 40 | --codeScrollThumb: var(--gray400); 41 | --codeScrollBackground: var(--codeBorder); 42 | --admCodeBackground: var(--gray25); 43 | --admCodeBorder: var(--gray100); 44 | --admCodeColor: var(--black); 45 | --admInlineCodeColor: var(--black); 46 | --admInlineCodeBackground: var(--gray25); 47 | --admInlineCodeBorder: var(--gray100); 48 | 49 | --tabBorder: var(--gray300); 50 | --tabBorderTop: var(--gray100); 51 | --tabShadow: var(--gray25); 52 | 53 | --bottomActionsBtnBorder: var(--black-opacity-10); 54 | --bottomActionsBtnTitle: var(--mainDark); 55 | 56 | --modalBackground: var(--white); 57 | 58 | --settingsInput: var(--gray500); 59 | --settingsInputBackground: var(--white); 60 | --settingsInputBorder: var(--gray300); 61 | --settingsSectionBorder: var(--gray300); 62 | 63 | --quickSwitchInput: var(--gray500); 64 | --quickSwitchContour: var(--coldGray); 65 | 66 | --success: var(--green); 67 | --progressBarColor: var(--gray400); 68 | 69 | --sidebarAccentMain: var(--black); 70 | --sidebarBackground: var(--gray50); 71 | --sidebarHeader: var(--gray100); 72 | --sidebarMuted: var(--gray800); 73 | --sidebarHover: var(--black); 74 | --sidebarStaleVersion: var(--orangeDark); 75 | --sidebarSubheadings: var(--gray500); 76 | --sidebarItem: var(--black); 77 | --sidebarInactiveItemBorder: var(--gray500); 78 | --sidebarInactiveItemMarker: var(--gray200); 79 | --sidebarLanguageAccentBar: var(--mainDark); 80 | --sidebarActiveItem: var(--mainDarkest); 81 | --searchBarBorder: var(--gray200); 82 | --searchAccentMain: var(--gray600); 83 | --searchLanguageAccentBar: var(--main); 84 | --searchSearch: var(--white); 85 | --autocompleteBorder: rgba(3, 9, 19, 0.10); 86 | --autocompletePreview: var(--gray25); 87 | --autocompleteSelected: var(--gray25); 88 | --autocompleteHover: var(--gray50); 89 | --autocompleteBackground: var(--white); 90 | --suggestionBorder: var(--gray200); 91 | --autocompleteResults: var(--gray600); 92 | --autocompleteResultsBold: var(--gray800); 93 | --autocompleteLabelBack: var(--gray100); 94 | --autocompleteLabelFont: var(--gray600); 95 | } 96 | -------------------------------------------------------------------------------- /assets/css/entry/epub-elixir.css: -------------------------------------------------------------------------------- 1 | @import '../custom-props/_elixir.css'; 2 | @import '../_epub.css'; 3 | -------------------------------------------------------------------------------- /assets/css/entry/epub-erlang.css: -------------------------------------------------------------------------------- 1 | @import '../custom-props/_erlang.css'; 2 | @import '../_epub.css'; 3 | -------------------------------------------------------------------------------- /assets/css/entry/html-elixir.css: -------------------------------------------------------------------------------- 1 | @import '../custom-props/_elixir.css'; 2 | @import '../_html.css'; 3 | -------------------------------------------------------------------------------- /assets/css/entry/html-erlang.css: -------------------------------------------------------------------------------- 1 | @import '../custom-props/_erlang.css'; 2 | @import '../_html.css'; 3 | -------------------------------------------------------------------------------- /assets/css/focus.css: -------------------------------------------------------------------------------- 1 | /* For backwards compatibility, style :focus state so that it works in all browsers, 2 | but unstyle not-focus-visible focus state in browsers that support focus-visible */ 3 | 4 | /* extra button selectors necessary to overwrite normalize.css */ 5 | *:focus, 6 | button:focus, 7 | [type="button"]:focus, 8 | [type="reset"]:focus, 9 | [type="submit"]:focus { 10 | outline: 2px solid var(--main); 11 | /* negative offset to make outline visible when overflow hidden */ 12 | outline-offset: -2px; 13 | } 14 | 15 | *:focus:not(:focus-visible), 16 | button:focus:not(:focus-visible), 17 | [type="button"]:focus:not(:focus-visible), 18 | [type="reset"]:focus:not(:focus-visible), 19 | [type="submit"]:focus:not(:focus-visible) { 20 | outline: 0; 21 | } 22 | 23 | /* inputs you can type into don't need an extra focus style 24 | because they have a visible cursor */ 25 | input[type="text"], 26 | input[type="number"], 27 | input[type="date"], 28 | input[type="datetime"], 29 | input[type="datetime-local"], 30 | input[type="email"], 31 | input[type="month"], 32 | input[type="number"], 33 | input[type="password"], 34 | input[type="search"], 35 | input[type="tel"], 36 | input[type="time"], 37 | input[type="url"], 38 | input[type="week"], 39 | textarea { 40 | outline: 0; 41 | } 42 | -------------------------------------------------------------------------------- /assets/css/icons.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Only used icons are included in the font 3 | * Icons can be generated at https://remixicon.com/ 4 | * In assets/fonts/RemixIconCollection.remixicon there's easy to import on website list of icons 5 | */ 6 | 7 | @font-face { 8 | font-family: 'remixicon'; 9 | src: url('../fonts/remixicon.woff2') format('woff2'); 10 | font-display: swap; 11 | } 12 | 13 | [class^="ri-"], [class*=" ri-"], .remix-icon { 14 | font-family: 'remixicon'; 15 | font-style: normal; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | 20 | :root { 21 | --icon-arrow-up-s: "\ea78"; 22 | --icon-arrow-down-s: "\ea4e"; 23 | --icon-arrow-right-s: "\ea6e"; 24 | --icon-add: "\ea13"; 25 | --icon-subtract: "\f1af"; 26 | --icon-error-warning: "\eca1"; 27 | --icon-external-link-line: "\ecaf"; 28 | --icon-information: "\ee59"; 29 | --icon-alert: "\ea21"; 30 | --icon-double-quotes-l: "\ec51"; 31 | --icon-link-m: "\eeaf"; 32 | --icon-close-line: "\eb99"; 33 | --icon-code-s-slash-line: "\ebad"; 34 | --icon-menu-line: "\ef3e"; 35 | --icon-search-2-line: "\f0cd"; 36 | --icon-settings-3-line: "\f0e6"; 37 | --icon-printer-line: "\f029"; 38 | } 39 | 40 | .ri-lg { font-size: 1.3333em; line-height: .75em; vertical-align: -.0667em; } 41 | 42 | .ri-settings-3-line:before { content: var(--icon-settings-3-line); } 43 | .ri-add-line:before { content: var(--icon-add); } 44 | .ri-subtract-line:before { content: var(--icon-subtract); } 45 | .ri-arrow-up-s-line:before { content: var(--icon-arrow-up-s); } 46 | .ri-arrow-down-s-line:before { content: var(--icon-arrow-down-s); } 47 | .ri-arrow-right-s-line:before { content: var(--icon-arrow-right-s); } 48 | .ri-external-link-line:before { content: var(--icon-external-link-line); } 49 | .ri-search-2-line:before { content: var(--icon-search-2-line); } 50 | .ri-menu-line:before { content: var(--icon-menu-line); } 51 | .ri-close-line:before { content: var(--icon-close-line); } 52 | .ri-link-m:before { content: var(--icon-link-m); } 53 | .ri-code-s-slash-line:before { content: var(--icon-code-s-slash-line); } 54 | .ri-error-warning-line:before { content: var(--icon-error-warning); } 55 | .ri-information-line:before { content: var(--icon-information); } 56 | .ri-alert-line:before { content: var(--icon-alert); } 57 | .ri-double-quotes-l:before { content: var(--icon-double-quotes-l); } 58 | .ri-printer-line:before { content: var(--icon-printer-line); } 59 | -------------------------------------------------------------------------------- /assets/css/keyboard-shortcuts.css: -------------------------------------------------------------------------------- 1 | #keyboard-shortcuts-content dl.shortcut-row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | margin: 0; 6 | padding: 6px 0 8px; 7 | border-bottom: 1px solid var(--settingsSectionBorder); 8 | } 9 | 10 | #keyboard-shortcuts-content dl.shortcut-row:last-of-type { 11 | border-bottom-style: none; 12 | } 13 | 14 | #keyboard-shortcuts-content dl.shortcut-row:first-child { 15 | padding-top: 0; 16 | } 17 | 18 | #keyboard-shortcuts-content :is(.shortcut-keys, .shortcut-description) { 19 | display: inline-block; 20 | } 21 | 22 | #keyboard-shortcuts-content kbd > kbd { 23 | background-color: var(--settingsInputBorder); 24 | color: var(--contrast); 25 | border-radius: var(--borderRadius-sm); 26 | font-family: inherit; 27 | font-weight: bold; 28 | display: inline-block; 29 | line-height: 1; 30 | padding: 4px 7px 6px 7px; 31 | min-width: 26px; 32 | text-align: center; 33 | font-size: var(--text-sm); 34 | } 35 | 36 | #keyboard-shortcuts-content :is(.shortcut-keys, .shortcut-description) { 37 | margin: 0; 38 | } 39 | -------------------------------------------------------------------------------- /assets/css/layout.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | box-sizing: border-box; 4 | height: 100%; 5 | width: 100%; 6 | } 7 | 8 | body { 9 | --sidebarWidth: 300px; 10 | --sidebarMinWidth: 300px; 11 | --sidebarTransitionDuration: 0.3s; 12 | background-color: var(--background); 13 | color: var(--textBody); 14 | font-size: var(--text-md); 15 | line-height: 1.6875em; 16 | /* swup.js adds tabindex=-1 on Chrome, 17 | which then adds an outline when clicked */ 18 | outline: none !important; 19 | } 20 | 21 | *, 22 | *:before, 23 | *:after { 24 | box-sizing: inherit; 25 | } 26 | 27 | .body-wrapper { 28 | display: flex; 29 | height: 100%; 30 | } 31 | 32 | /* Sidebar is closed by default and opened with body.sidebar-opened. */ 33 | .sidebar { 34 | display: none; 35 | flex-direction: column; 36 | width: var(--sidebarWidth); 37 | min-width: var(--sidebarMinWidth); 38 | max-width: 50vw; 39 | height: 100%; 40 | position: fixed; 41 | top: 0; 42 | left: calc(-1 * var(--sidebarWidth)); 43 | z-index: 100; 44 | resize: horizontal; 45 | } 46 | 47 | .sidebar-button { 48 | padding: 26px 12px 18px 19px; 49 | position: fixed; 50 | z-index: 200; 51 | top: 0; 52 | left: 0; 53 | will-change: transform; 54 | transform: translateX(0); 55 | } 56 | 57 | .content { 58 | left: 0; 59 | width: 100%; 60 | height: 100%; 61 | position: absolute; 62 | } 63 | 64 | .content .content-inner { 65 | container: content / inline-size; 66 | max-width: var(--content-width); 67 | min-height: 100%; 68 | margin: 0 auto; 69 | padding: 0 var(--content-gutter) 10px; 70 | } 71 | 72 | .content-inner:focus { 73 | outline: none; 74 | } 75 | 76 | .sidebar-transition .sidebar, 77 | .sidebar-transition .sidebar-button, 78 | .sidebar-transition .content { 79 | transition: all var(--sidebarTransitionDuration) ease-in-out allow-discrete; 80 | } 81 | 82 | .sidebar-open .sidebar, 83 | .sidebar-transition .sidebar { 84 | display: flex; 85 | } 86 | 87 | .sidebar-open .sidebar { 88 | left: 0; 89 | } 90 | 91 | .sidebar-open .sidebar-button { 92 | transform: translateX(calc(var(--sidebarWidth) - 100%)); 93 | } 94 | 95 | .sidebar-open .content { 96 | width: calc(100% - var(--sidebarWidth)); 97 | left: var(--sidebarWidth); 98 | } 99 | 100 | @media screen and (max-width: 768px) { 101 | .sidebar-open .content { 102 | left: 0; 103 | width: 100%; 104 | } 105 | 106 | .sidebar { 107 | max-width: 90vw; 108 | } 109 | 110 | body:not(.sidebar-open) .sidebar-button { 111 | position: absolute; 112 | } 113 | } 114 | 115 | .swup-progress-bar { 116 | height: 2px; 117 | background-color: var(--progressBarColor); 118 | } 119 | -------------------------------------------------------------------------------- /assets/css/modal.css: -------------------------------------------------------------------------------- 1 | @keyframes keyboard-shortcuts-show { 2 | from { 3 | opacity: 0; 4 | } 5 | to { 6 | opacity: 1; 7 | } 8 | } 9 | 10 | .modal { 11 | animation-duration: .15s; 12 | animation-name: keyboard-shortcuts-show; 13 | animation-iteration-count: 1; 14 | animation-timing-function: ease-in-out; 15 | display: none; 16 | background-color: rgba(0, 0, 0, .75); 17 | position: fixed; 18 | inset: 0; 19 | z-index: 300; 20 | } 21 | 22 | .modal.shown { 23 | display: block; 24 | } 25 | 26 | .modal .modal-contents { 27 | margin: 75px auto 0 auto; 28 | max-width: 500px; 29 | background-color: var(--modalBackground); 30 | border-radius: var(--borderRadius-sm); 31 | box-shadow: 2px 2px 8px rgba(0, 0, 0, .2); 32 | padding: 25px 35px 35px; 33 | } 34 | 35 | @media screen and (max-width: 768px) { 36 | .modal .modal-contents { 37 | padding: 20px; 38 | } 39 | } 40 | 41 | .modal .modal-header { 42 | display: flex; 43 | align-items: start; 44 | } 45 | 46 | .modal .modal-title { 47 | display: inline-block; 48 | flex-grow: 1; 49 | font-size: 1.2rem; 50 | font-weight: bold; 51 | margin-bottom: 20px; 52 | } 53 | 54 | .modal .modal-title button { 55 | border: none; 56 | background-color: transparent; 57 | color: var(--textHeaders); 58 | font-weight: bold; 59 | margin-right: 30px; 60 | padding-left: 0; 61 | text-align: left; 62 | transition: var(--transition-colors); 63 | } 64 | .modal .modal-title button:hover { 65 | color: var(--main); 66 | cursor: pointer; 67 | } 68 | .modal .modal-title button.active { 69 | color: var(--main); 70 | } 71 | 72 | .modal .modal-close { 73 | cursor: pointer; 74 | display: block; 75 | font-size: 1.5rem; 76 | margin: -8px -8px 0 0; 77 | padding: 8px; 78 | opacity: .7; 79 | background-color: transparent; 80 | color: var(--textHeaders); 81 | border: none; 82 | transition: var(--transition-opacity); 83 | } 84 | .modal .modal-close:hover { 85 | opacity: 1; 86 | } 87 | -------------------------------------------------------------------------------- /assets/css/preview.css: -------------------------------------------------------------------------------- 1 | body.preview { 2 | --sidebarWidth: 0px; 3 | overflow: hidden; 4 | } 5 | 6 | body.preview .content { 7 | height: auto; 8 | } 9 | 10 | body.preview .content-inner { 11 | padding: 0; 12 | } 13 | 14 | body.preview .sidebar, body.preview #sidebar-menu { 15 | display: none; 16 | } 17 | 18 | body.preview .hover-link, body.preview .detail-link { 19 | display: none; 20 | } 21 | 22 | body.preview :is(h1, h2, h3):first-of-type { 23 | margin-top: 0; 24 | } 25 | -------------------------------------------------------------------------------- /assets/css/print-cheatsheet.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | 3 | /* Remove background colors and set border colors */ 4 | .page-cheatmd .content-inner * { 5 | background-color: transparent !important; 6 | border-color: var(--gray400) !important; 7 | } 8 | 9 | /* Basic layout and columns */ 10 | 11 | .page-cheatmd .content-inner { 12 | max-width: 100%; 13 | width: 100%; 14 | padding: 0; 15 | font-size: .7em; 16 | } 17 | 18 | .page-cheatmd .content-inner section:is(.col-2, .col-2-left, .col-3) { 19 | column-gap: 30px; 20 | } 21 | 22 | /* column-count and grid display required to be set again within @print media query to take effect when actually printing */ 23 | .page-cheatmd .content-inner section.col-2 { 24 | column-count: 2; 25 | } 26 | .page-cheatmd .content-inner section.col-2-left { 27 | display: grid; 28 | } 29 | .page-cheatmd .content-inner section.col-3 { 30 | column-count: 3; 31 | } 32 | 33 | /* h1 */ 34 | 35 | .page-cheatmd .content-inner h1 { 36 | margin-top: 0; 37 | margin-bottom: .5em; 38 | } 39 | 40 | /* h2 */ 41 | 42 | .page-cheatmd .content-inner h2.section-heading { 43 | font-weight: bold; 44 | margin-top: 1em; 45 | column-span: all; 46 | } 47 | 48 | .page-cheatmd .content-inner section.h2 { 49 | break-inside: avoid; 50 | } 51 | 52 | /* h3 */ 53 | 54 | .page-cheatmd .content-inner h3 { 55 | font-weight: bold; 56 | color: var(--mainDark); 57 | } 58 | 59 | .page-cheatmd .content-inner h3::after { 60 | height: 2px; 61 | background-color: var(--gray400); 62 | } 63 | 64 | .page-cheatmd .content-inner section.h3 { 65 | min-width: 300px; 66 | break-inside: avoid; 67 | } 68 | 69 | /* h4 */ 70 | 71 | .page-cheatmd .content-inner h4 { 72 | padding: .5em 0; 73 | border: none; 74 | font-weight: bold; 75 | color: black; 76 | } 77 | 78 | /* Paragraphs */ 79 | 80 | .page-cheatmd .content-inner .h2 p { 81 | padding-left: 0; 82 | padding-right: 0; 83 | border: none !important; 84 | } 85 | 86 | /* Code blocks */ 87 | 88 | .page-cheatmd .content-inner code { 89 | line-height: 1.5em; 90 | } 91 | 92 | /* Tables */ 93 | 94 | .page-cheatmd .content-inner .h2 table { 95 | font-variant-numeric: tabular-nums; 96 | break-inside: avoid; 97 | } 98 | 99 | .page-cheatmd .content-inner .h2 :is(th, td) { 100 | vertical-align: top; 101 | padding-left: 0; 102 | padding-right: 0; 103 | } 104 | 105 | .page-cheatmd .content-inner .h2 thead { 106 | border-style: solid none; 107 | border-width: 1px; 108 | } 109 | 110 | .page-cheatmd .content-inner .h2 tr { 111 | border-bottom: none; 112 | } 113 | 114 | .page-cheatmd .content-inner .h2 th { 115 | font-weight: bold; 116 | } 117 | 118 | /* Lists */ 119 | 120 | .page-cheatmd .content-inner .h2 li { 121 | padding-left: 0; 122 | padding-right: 0; 123 | vertical-align: middle; 124 | border-bottom: none; 125 | } 126 | 127 | /* Remove copy button from code blocks */ 128 | .page-cheatmd .content-inner pre:hover button.copy-button { 129 | display: none; 130 | } 131 | 132 | /* Remove hover tooltip from inline code references */ 133 | .page-cheatmd .content-inner div.tooltip { 134 | display: none; 135 | } 136 | 137 | .page-cheatmd .content-inner footer p:not(.built-using) { 138 | display: none; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /assets/css/print.css: -------------------------------------------------------------------------------- 1 | @media print { 2 | .body-wrapper { 3 | display: block; 4 | } 5 | 6 | .sidebar, 7 | .sidebar-button { 8 | display: none; 9 | } 10 | 11 | .top-search { 12 | display: none; 13 | } 14 | 15 | .content { 16 | padding-left: 0; 17 | overflow: visible; 18 | left: 0px; 19 | width: 100%; 20 | } 21 | 22 | .summary-row { 23 | break-inside: avoid; 24 | } 25 | 26 | #toast { 27 | display: none; 28 | } 29 | 30 | .content-inner { 31 | padding: 0; 32 | } 33 | 34 | .content-inner .section-heading a.hover-link { 35 | display: none; 36 | } 37 | 38 | .content-inner button.icon-action { 39 | display: none; 40 | } 41 | 42 | .content-inner a.icon-action { 43 | display: none; 44 | } 45 | 46 | .content-inner .bottom-actions { 47 | display: none; 48 | } 49 | 50 | /* Hide On Hex.pm text but keep Built Using ExDoc */ 51 | .footer p:first-of-type { 52 | display: none; 53 | } 54 | 55 | .content-inner section.admonition { 56 | border: 2px solid var(--gray400); 57 | } 58 | 59 | .content-inner section.admonition > .admonition-title { 60 | color: var(--textHeaders); 61 | border-bottom: 2px solid var(--gray400); 62 | } 63 | 64 | .content-inner pre code.makeup { 65 | border-color: var(--gray400); 66 | white-space: break-spaces; 67 | break-inside: avoid; 68 | } 69 | 70 | .content-inner blockquote code.inline { 71 | border-color: var(--gray400); 72 | } 73 | 74 | .content-inner code.inline { 75 | border-color: var(--gray400); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /assets/css/quick-switch.css: -------------------------------------------------------------------------------- 1 | #quick-switch-modal-body { 2 | width: 100%; 3 | position: relative; 4 | } 5 | 6 | #quick-switch-modal-body .ri-search-2-line { 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | padding: 4px 10px; 11 | color: var(--quickSwitchContour); 12 | font-weight: bold; 13 | } 14 | 15 | #quick-switch-modal-body #quick-switch-input { 16 | width: 100%; 17 | padding: 8px 6px 8px 38px; 18 | border: none; 19 | color: var(--quickSwitchInput); 20 | background-color: transparent; 21 | border-bottom: 1px solid var(--quickSwitchContour); 22 | box-sizing: border-box; 23 | transition: all .12s ease-out; 24 | } 25 | 26 | #quick-switch-modal-body #quick-switch-results { 27 | margin: 0; 28 | } 29 | 30 | #quick-switch-modal-body .quick-switch-result { 31 | padding: 2px 5px; 32 | border-bottom: 1px dotted var(--quickSwitchContour); 33 | transition: all .12s ease-out; 34 | } 35 | 36 | #quick-switch-modal-body .quick-switch-result:last-child { 37 | border-bottom: none; 38 | } 39 | 40 | #quick-switch-modal-body .quick-switch-result:hover { 41 | cursor: pointer; 42 | } 43 | 44 | #quick-switch-modal-body .quick-switch-result:is(:hover, .selected) { 45 | border-left: 4px solid var(--main); 46 | background-color: var(--codeBackground); 47 | } 48 | -------------------------------------------------------------------------------- /assets/css/screen-reader.css: -------------------------------------------------------------------------------- 1 | .sr-only { 2 | position: absolute; 3 | width: 1px; 4 | height: 1px; 5 | padding: 0; 6 | margin: -1px; 7 | overflow: hidden; 8 | clip: rect(0, 0, 0, 0); 9 | border: 0; 10 | user-select: none; 11 | } 12 | -------------------------------------------------------------------------------- /assets/css/search.css: -------------------------------------------------------------------------------- 1 | #search { 2 | min-height: 200px; 3 | position: relative; 4 | } 5 | 6 | #search .loading { 7 | height: 64px; 8 | width: 64px; 9 | position: absolute; 10 | top: 50%; 11 | left: calc(50% - 32px); 12 | } 13 | 14 | #search .loading div { 15 | box-sizing: border-box; 16 | display: block; 17 | position: absolute; 18 | width: 51px; 19 | height: 51px; 20 | margin: 6px; 21 | border: 6px solid var(--coldGray); 22 | border-radius: 50%; 23 | animation: loading 1.2s cubic-bezier(.5, 0, .5, 1) infinite; 24 | border-color: var(--coldGray) transparent transparent transparent; 25 | } 26 | 27 | #search .loading div:nth-child(1) { 28 | animation-delay: -.45s; 29 | } 30 | #search .loading div:nth-child(2) { 31 | animation-delay: -.3s; 32 | } 33 | #search .loading div:nth-child(3) { 34 | animation-delay: -.15s; 35 | } 36 | 37 | @keyframes loading { 38 | 0% { 39 | transform: rotate(0deg); 40 | } 41 | 100% { 42 | transform: rotate(360deg); 43 | } 44 | } 45 | 46 | #search .result { 47 | margin: 2em 0 2.5em; 48 | } 49 | 50 | #search .result p { 51 | margin: 0; 52 | } 53 | 54 | #search .result-id { 55 | font-size: 1.4em; 56 | margin: 0; 57 | } 58 | 59 | #search .result-id a { 60 | text-decoration: none; 61 | color: var(--textHeaders); 62 | transition: var(--transition-colors); 63 | } 64 | #search .result-id a:is(:visited, :active) { 65 | color: var(--textHeaders); 66 | } 67 | #search .result-id a:is(:hover, :focus) { 68 | color: var(--main); 69 | } 70 | 71 | #search :is(.result-id, .result-elem) em { 72 | font-style: normal; 73 | color: var(--main); 74 | } 75 | 76 | #search .result-id small { 77 | font-weight: normal; 78 | } 79 | -------------------------------------------------------------------------------- /assets/css/settings.css: -------------------------------------------------------------------------------- 1 | #settings-modal-content { 2 | margin-top: 10px; 3 | } 4 | 5 | #settings-modal-content .hidden { 6 | display: none; 7 | } 8 | 9 | #settings-modal-content .input { 10 | box-sizing: border-box; 11 | width: 80%; 12 | padding: 8px; 13 | font-size: var(--text-sm); 14 | background-color: var(--settingsInputBackground); 15 | color: var(--settingsInput); 16 | border: 1px solid var(--settingsInputBorder); 17 | border-radius: var(--borderRadius-base); 18 | transition: var(--transition-all); 19 | } 20 | #settings-modal-content .input:focus { 21 | border-color: var(--main); 22 | } 23 | #settings-modal-content .input::placeholder { 24 | color: var(--gray400); 25 | } 26 | 27 | #settings-modal-content .switch-button-container { 28 | display: flex; 29 | align-items: center; 30 | justify-content: space-between; 31 | border-top: 1px solid var(--settingsSectionBorder); 32 | padding: 10px 0; 33 | } 34 | #settings-modal-content .switch-button-container:first-of-type { 35 | border-top-style: none; 36 | padding-top: 0; 37 | } 38 | 39 | #settings-modal-content .switch-button-container > div > span { 40 | font-size: var(--text-md); 41 | } 42 | 43 | #settings-modal-content .switch-button-container > div > p { 44 | -webkit-font-smoothing: antialiased; 45 | -moz-osx-font-smoothing: grayscale; 46 | font-size: var(--text-sm); 47 | line-height: 1.4; 48 | margin: 0; 49 | padding-bottom: 6px; 50 | padding-right: 10px; 51 | } 52 | 53 | #settings-modal-content .switch-button { 54 | position: relative; 55 | display: inline-block; 56 | flex-shrink: 0; 57 | width: 40px; 58 | height: 20px; 59 | user-select: none; 60 | transition: var(--transition-all); 61 | } 62 | 63 | #settings-modal-content .switch-button__checkbox { 64 | appearance: none; 65 | position: absolute; 66 | display: block; 67 | width: 20px; 68 | height: 20px; 69 | border-radius: 1000px; 70 | background-color: #91a4b7; 71 | border: 3px solid #e5edf5; 72 | cursor: pointer; 73 | transition: var(--transition-all); 74 | } 75 | 76 | #settings-modal-content .switch-button__bg { 77 | display: block; 78 | width: 100%; 79 | height: 100%; 80 | border-radius: 1000px; 81 | background-color: #e5edf5; 82 | cursor: pointer; 83 | transition: var(--transition-all); 84 | } 85 | 86 | #settings-modal-content .switch-button__checkbox:checked { 87 | background-color: white; 88 | border-color: var(--main); 89 | transform: translateX(100%); 90 | } 91 | 92 | #settings-modal-content .switch-button__checkbox:checked + .switch-button__bg { 93 | background-color: var(--main); 94 | } 95 | 96 | #settings-modal-content .switch-button__checkbox:focus { 97 | outline: 0; 98 | } 99 | 100 | #settings-modal-content .switch-button__checkbox:focus + .switch-button__bg { 101 | outline: 2px solid var(--main); 102 | outline-offset: 2px; 103 | } 104 | 105 | #settings-modal-content .switch-button__checkbox:focus:not(:focus-visible) + .switch-button__bg { 106 | outline: 0; 107 | } 108 | 109 | #settings-modal-content .settings-select { 110 | cursor: pointer; 111 | position: relative; 112 | border: none; 113 | background-color: transparent; 114 | color: var(--textBody); 115 | } 116 | 117 | #settings-modal-content .settings-select option { 118 | color: initial; 119 | } 120 | -------------------------------------------------------------------------------- /assets/css/tabset.css: -------------------------------------------------------------------------------- 1 | .tabset { 2 | --borderWidth: 1px; 3 | --tabsetPadding: var(--baseLineHeight); 4 | margin: var(--baseLineHeight) 0; 5 | border: var(--borderWidth) solid var(--tabBorder); 6 | padding: 0 var(--tabsetPadding); 7 | border-radius: var(--borderRadius-lg); 8 | } 9 | 10 | .tabset-tablist { 11 | display: flex; 12 | overflow: auto; 13 | scrollbar-width: thin; 14 | border-bottom-width: 1px; 15 | border-bottom-style: solid; 16 | border-bottom-color: var(--tabBorderTop); 17 | } 18 | 19 | .tabset-tab { 20 | padding: 1.1rem var(--tabsetPadding); 21 | font-family: var(--sansFontFamily); 22 | color: var(--textColor); 23 | margin-right: calc(-1 * var(--borderWidth)); 24 | background-color: transparent; 25 | border:0; 26 | box-shadow: none; 27 | cursor: pointer; 28 | border-bottom-width: 2px; 29 | border-bottom-style: solid; 30 | border-bottom-color: transparent; 31 | transition: var(--transition-all); 32 | } 33 | 34 | :hover.tabset-tab { 35 | border-bottom-color: var(--tabBorderTop); 36 | color: var(--textHeaders); 37 | } 38 | 39 | .tabset-tab[aria-selected=true] { 40 | border-bottom-color: var(--mainLight); 41 | color: var(--textHeaders); 42 | } 43 | 44 | /* Keyboard navigation focus state (increased contrast) */ 45 | .tabset-tab[aria-selected=true]:focus-visible { 46 | background-color: var(--mainLight); 47 | border-color: var(--mainLight); 48 | color: var(--white); /* light works best for both light and dark themes given background colors */ 49 | } 50 | 51 | @media screen and (max-width: 768px) { 52 | .tabset { 53 | --tabsetPadding: calc(var(--baseLineHeight) / 2); 54 | } 55 | 56 | .tabset-panel { 57 | padding-top: calc(var(--tabsetPadding) / 2); 58 | padding-bottom: calc(var(--tabsetPadding) / 2); 59 | } 60 | 61 | .tabset-panel pre, 62 | .tabset-panel blockquote, 63 | .tabset-panel section.admonition { 64 | margin-left: calc(-1 * var(--tabsetPadding)) !important; 65 | margin-right: calc(-1 * var(--tabsetPadding)) !important; 66 | } 67 | 68 | .tabset-panel > pre code { 69 | border-left-width: 0; 70 | border-right-width: 0; 71 | } 72 | } 73 | 74 | /* tabpanel content: top margin of first and bottom margin of last */ 75 | @media screen and (max-width: 768px) { 76 | .tabset-panel > :is(:first-child) { 77 | &:is(table) { 78 | margin: .5em 0; 79 | } 80 | } 81 | } 82 | @media screen and (min-width: 769px) { 83 | .tabset-panel > :is(:first-child) { 84 | &:is(blockquote, .admonition) { 85 | margin-top: 1.5em; 86 | } 87 | &:is(p:has(img)) { 88 | margin-top: 1.25em; 89 | } 90 | &:is(table) { 91 | margin-top: .75em; 92 | } 93 | } 94 | .tabset-panel > :is(:last-child) { 95 | &:is(blockquote, .admonition) { 96 | margin-bottom: 1.5em; 97 | } 98 | &:is(p:not(:has(img)), ul, ol) { 99 | margin-bottom: 1.25em; 100 | } 101 | &:is(table) { 102 | margin-bottom: .75em; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /assets/css/toast.css: -------------------------------------------------------------------------------- 1 | #toast { 2 | visibility: hidden; 3 | opacity: 0; 4 | position: fixed; 5 | z-index: 1; 6 | left: 50%; 7 | bottom: 1rem; 8 | min-width: 3rem; 9 | margin: 0 -1.2rem; 10 | padding: .7rem 1.2rem; 11 | text-align: center; 12 | font-weight: 700; 13 | border-radius: var(--borderRadius-base); 14 | border: 1px solid var(--codeBorder); 15 | background-color: var(--codeBackground); 16 | color: var(--textBody); 17 | transition: opacity .4s ease-in-out, transform .3s ease-out; 18 | cursor: default; 19 | } 20 | 21 | #toast.show { 22 | visibility: visible; 23 | opacity: 1; 24 | transform: translateY(-.75rem); 25 | } 26 | 27 | @media (prefers-reduced-motion: reduce) { 28 | #toast { 29 | transition: none; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /assets/css/tooltips.css: -------------------------------------------------------------------------------- 1 | .tooltip { 2 | box-shadow: 0 0 10px var(--black-opacity-10); 3 | max-height: 300px; 4 | max-width: 500px; 5 | padding: 0; 6 | position: absolute; 7 | pointer-events: none; 8 | margin: 0; 9 | z-index: 99; 10 | top: 0; 11 | left: 0; 12 | visibility: hidden; 13 | transform: translateY(20px); 14 | opacity: 0; 15 | transition: .2s visibility ease-out, .2s transform ease-out, .2s opacity ease-out; 16 | } 17 | 18 | /* 19 | Show-up animation 20 | ====== 21 | Note: it's fine to hide the tooltip with `visibility: hidden` (rather than `display: none`) 22 | as it has absolute positioning, so doesn't impact the layout and click events pass through. 23 | */ 24 | .tooltip.tooltip-shown { 25 | visibility: visible; 26 | transform: translateY(0); 27 | opacity: 1; 28 | } 29 | 30 | .tooltip .tooltip-body { 31 | border: 1px solid var(--codeBorder); 32 | border-radius: var(--borderRadius-sm); 33 | overflow: auto; 34 | } 35 | 36 | .tooltip .tooltip-body .signature { 37 | min-width: 320px; 38 | width: 100%; 39 | line-height: 1em; 40 | } 41 | 42 | .tooltip .tooltip-body .detail-header { 43 | border-left: 0; 44 | margin-bottom: 0; 45 | margin-top: 0; 46 | } 47 | 48 | .tooltip .tooltip-body .docstring { 49 | background-color: var(--background); 50 | padding: 1.2em; 51 | margin: 0; 52 | width: 498px; /* Taking 2 * 1px of border into account */ 53 | } 54 | 55 | /* Used for simple tooltips having only description. */ 56 | .tooltip .tooltip-body .docstring-plain { 57 | max-width: 498px; 58 | width: auto; 59 | } 60 | 61 | .tooltip .tooltip-body .version-info { 62 | float: right; 63 | font-family: var(--monoFontFamily); 64 | font-weight: normal; 65 | opacity: .3; 66 | padding-left: .3em; 67 | } 68 | -------------------------------------------------------------------------------- /assets/fonts/RemixIconCollection.remixicon: -------------------------------------------------------------------------------- 1 | add-line,alert-line,arrow-down-s-line,arrow-right-s-line,arrow-up-s-line,close-line,code-s-slash-line,double-quotes-l,error-warning-line,external-link-line,information-line,link-m,menu-line,printer-line,search-2-line,settings-3-line,subtract-line 2 | -------------------------------------------------------------------------------- /assets/fonts/remixicon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/assets/fonts/remixicon.woff2 -------------------------------------------------------------------------------- /assets/js/constants.js: -------------------------------------------------------------------------------- 1 | // Constants separated to allow importing into inline_html.js without 2 | // bringing in other code. 3 | export const SETTINGS_KEY = 'ex_doc:settings' 4 | export const DARK_MODE_CLASS = 'dark' 5 | export const THEME_SYSTEM = 'system' 6 | export const THEME_DARK = 'dark' 7 | export const THEME_LIGHT = 'light' 8 | -------------------------------------------------------------------------------- /assets/js/content.js: -------------------------------------------------------------------------------- 1 | import { qsAll } from './helpers' 2 | import { settingsStore } from './settings-store' 3 | 4 | /** 5 | * Updates "Run in Livebook" badges to link to a notebook 6 | * corresponding to the current documentation page. 7 | */ 8 | 9 | window.addEventListener('exdoc:loaded', initialize) 10 | 11 | function initialize () { 12 | const notebookPath = window.location.pathname.replace(/(\.html)?$/, '.livemd') 13 | const notebookUrl = encodeURIComponent(new URL(notebookPath, window.location.href).toString()) 14 | 15 | settingsStore.getAndSubscribe(({livebookUrl}) => { 16 | const targetUrl = livebookUrl 17 | ? `${livebookUrl}/import?url=${notebookUrl}` 18 | : `https://livebook.dev/run?url=${notebookUrl}` 19 | 20 | for (const anchor of qsAll('.livebook-badge')) { 21 | anchor.href = targetUrl 22 | } 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /assets/js/copy-button.js: -------------------------------------------------------------------------------- 1 | import { qsAll } from './helpers' 2 | import buttonHtml from './handlebars/templates/copy-button.html' 3 | 4 | /** @type {HTMLButtonElement} */ 5 | let buttonTemplate 6 | 7 | /** 8 | * Initializes copy buttons. 9 | */ 10 | 11 | window.addEventListener('exdoc:loaded', initialize) 12 | 13 | function initialize () { 14 | if (!('clipboard' in navigator)) return 15 | 16 | qsAll('pre:has(> code:first-child):not(:has(.copy-button))').forEach(pre => { 17 | if (!buttonTemplate) { 18 | const div = document.createElement('div') 19 | div.innerHTML = buttonHtml 20 | buttonTemplate = div.firstChild 21 | } 22 | 23 | const button = buttonTemplate.cloneNode(true) 24 | pre.appendChild(button) 25 | 26 | let timeout 27 | button.addEventListener('click', () => { 28 | clearTimeout(timeout) 29 | 30 | const text = 31 | Array.from(pre.querySelectorAll('code > *:not(.unselectable)')) 32 | .map(elem => elem.textContent) 33 | .join('') 34 | 35 | navigator.clipboard.writeText(text) 36 | button.classList.add('clicked') 37 | button.disabled = true 38 | timeout = setTimeout(() => { 39 | button.classList.remove('clicked') 40 | button.disabled = false 41 | }, 3000) 42 | }) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /assets/js/entry/epub.js: -------------------------------------------------------------------------------- 1 | import { onDocumentReady } from '../helpers' 2 | import { initialize as initMakeup } from '../makeup' 3 | 4 | onDocumentReady(() => { 5 | initMakeup() 6 | }) 7 | -------------------------------------------------------------------------------- /assets/js/entry/html.js: -------------------------------------------------------------------------------- 1 | // Load preview & hint-page first because they could remove DOM. 2 | // This prevents later modules doing unnecessary work. 3 | import '../preview' 4 | import '../tooltips/hint-page' 5 | // The remaining modules are loaded in order of visible impact. 6 | import '../theme' 7 | import '../sidebar/sidebar-drawer' 8 | import '../sidebar/sidebar-version-select' 9 | import '../tabsets' 10 | import '../content' 11 | import '../makeup' 12 | import '../search-bar' 13 | import '../tooltips/tooltips' 14 | import '../copy-button' 15 | import '../search-page' 16 | import '../settings' 17 | import '../keyboard-shortcuts' 18 | import '../quick-switch' 19 | import '../swup' 20 | -------------------------------------------------------------------------------- /assets/js/entry/inline_html.js: -------------------------------------------------------------------------------- 1 | // CAREFUL 2 | // This file is inlined into each HTML document. 3 | // Only code that must be executed ASAP belongs here. 4 | // Imports should only bring in inlinable constants. 5 | // Check compiled output to make sure no unnecessary code is imported. 6 | import { DARK_MODE_CLASS, SETTINGS_KEY, THEME_DARK, THEME_LIGHT } from '../constants' 7 | import { SIDEBAR_CLASS_OPEN, SIDEBAR_PREF_CLOSED, SIDEBAR_STATE_KEY, SIDEBAR_WIDTH_KEY, SMALL_SCREEN_BREAKPOINT } from '../sidebar/constants' 8 | 9 | const params = new URLSearchParams(window.location.search) 10 | 11 | // Immediately apply night mode preference to avoid a flash effect. 12 | // Should match logic in theme.js. 13 | const theme = params.get('theme') || JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}').theme 14 | if (theme === THEME_DARK || 15 | (theme !== THEME_LIGHT && 16 | window.matchMedia('(prefers-color-scheme: dark)').matches) 17 | ) { 18 | document.body.classList.add(DARK_MODE_CLASS) 19 | } 20 | 21 | // Set sidebar state and width. 22 | // Should match logic in sidebar-drawer.js. 23 | const sidebarPref = sessionStorage.getItem(SIDEBAR_STATE_KEY) 24 | const open = sidebarPref !== SIDEBAR_PREF_CLOSED && !window.matchMedia(`screen and (max-width: ${SMALL_SCREEN_BREAKPOINT}px)`).matches 25 | document.body.classList.toggle(SIDEBAR_CLASS_OPEN, open) 26 | 27 | const sidebarWidth = sessionStorage.getItem(SIDEBAR_WIDTH_KEY) 28 | if (sidebarWidth) { 29 | document.body.style.setProperty('--sidebarWidth', `${sidebarWidth}px`) 30 | } 31 | 32 | // Set OS class. 33 | const isAppleOS = /(Macintosh|iPhone|iPad|iPod)/.test(window.navigator.userAgent) 34 | document.documentElement.classList.toggle('apple-os', isAppleOS) 35 | -------------------------------------------------------------------------------- /assets/js/globals.js: -------------------------------------------------------------------------------- 1 | const params = new URLSearchParams(window.location.search) 2 | const isFrame = window.self !== window.parent 3 | 4 | export const isPreview = isFrame && params.has('preview') 5 | export const isHint = isFrame && params.has('hint') 6 | export const isEmbedded = isPreview || isHint 7 | 8 | // These variables are set by other scripts (e.g. generated by the docs task). 9 | 10 | export function getSidebarNodes () { 11 | return window.sidebarNodes || {} 12 | } 13 | 14 | export function getVersionNodes () { 15 | return window.versionNodes || [] 16 | } 17 | -------------------------------------------------------------------------------- /assets/js/handlebars/helpers.js: -------------------------------------------------------------------------------- 1 | import * as Handlebars from 'handlebars/runtime' 2 | 3 | Handlebars.registerHelper('isArray', function (entry, options) { 4 | if (Array.isArray(entry)) { 5 | return options.fn(this) 6 | } else { 7 | return options.inverse(this) 8 | } 9 | }) 10 | 11 | Handlebars.registerHelper('isNonEmptyArray', function (entry, options) { 12 | if (Array.isArray(entry) && entry.length > 0) { 13 | return options.fn(this) 14 | } else { 15 | return options.inverse(this) 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/autocomplete-suggestions.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | Autocompletion results for "{{term}}" 7 | 8 | 9 | Press RETURN for full-text search, TAB for previews 10 | 11 |
12 |
13 | {{#each suggestions}} 14 | 15 |
16 | {{# if deprecated }} 17 | {{{title}}} 18 | {{ else }} 19 | {{{title}}} 20 | {{/if}} 21 | 22 | {{#each labels}} 23 | {{this}} 24 | {{/each}} 25 |
26 | 30 |
31 |
32 | 36 |
37 |
38 | 39 | {{#if description}} 40 |
41 | {{{description}}} 42 |
43 | {{/if}} 44 |
45 | {{/each}} 46 |
47 |
48 |
49 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/copy-button.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/modal-layout.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/quick-switch-modal-body.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/search-results.handlebars: -------------------------------------------------------------------------------- 1 |

2 | {{#if value}} 3 | Search results for {{value}} 4 | {{else}} 5 | Invalid search 6 | {{/if}} 7 |

8 | 9 | {{#isNonEmptyArray results}} 10 | {{#each results}} 11 |
12 |

13 | 14 | {{this.title}} ({{this.type}}) 15 | 16 |

17 | {{#each excerpts}} 18 |

{{{this}}}

19 | {{/each}} 20 |
21 | {{/each}} 22 | {{else}} 23 | {{#isArray results}} 24 |

Sorry, we couldn't find anything for {{value}}.

25 | {{else if value}} 26 |

Invalid search: {{errorMessage}}.

27 | {{else}} 28 |

Please type something into the search bar to perform a search.

29 | {{/isArray}} 30 | 31 |

The search functionality is full-text based. Here are some tips:

32 | 33 | 42 | 43 |

To quickly go to a module, type, or function, use the autocompletion feature in the sidebar search.

44 | {{/isNonEmptyArray}} 45 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/settings-modal-body.handlebars: -------------------------------------------------------------------------------- 1 |
2 |
3 | 16 | 26 | 36 | 37 |
38 | 56 |
57 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/tooltip-body.handlebars: -------------------------------------------------------------------------------- 1 | {{#if isPlain}} 2 |
3 | {{this.hint.description}} 4 |
5 | {{else}} 6 |
7 |

8 | {{this.hint.title}} 9 |
{{this.hint.version}}
10 |

11 |
12 | {{#if this.hint.description}} 13 |
14 | {{{this.hint.description}}} 15 |
16 | {{/if}} 17 | {{/if}} 18 | -------------------------------------------------------------------------------- /assets/js/handlebars/templates/versions-dropdown.handlebars: -------------------------------------------------------------------------------- 1 |
2 | 13 | {{#if latestVersion}} 14 | 19 | {{/if}} 20 |
21 | -------------------------------------------------------------------------------- /assets/js/keyboard-shortcuts.js: -------------------------------------------------------------------------------- 1 | import { isAppleOS, qs } from './helpers' 2 | import { isSidebarOpened, openSidebar, toggleSidebar } from './sidebar/sidebar-drawer' 3 | import { openVersionSelect } from './sidebar/sidebar-version-select' 4 | import { focusSearchInput } from './search-bar' 5 | import { cycleTheme } from './theme' 6 | import { openQuickSwitchModal } from './quick-switch' 7 | import { closeModal, isModalOpen } from './modal' 8 | import { openSettingsModal } from './settings' 9 | import { isEmbedded } from './globals' 10 | 11 | const HELP_MODAL_BODY_SELECTOR = '#settings-modal-content' 12 | 13 | export const keyboardShortcuts = [ 14 | { 15 | key: 'c', 16 | description: 'Toggle sidebar', 17 | action: toggleSidebar 18 | }, 19 | { 20 | key: 'n', 21 | description: 'Cycle themes', 22 | action: cycleTheme 23 | }, 24 | { 25 | key: 's', 26 | description: 'Focus search bar', 27 | displayAs: '/ or s', 28 | action: searchKeyAction 29 | }, 30 | { 31 | key: '/', 32 | action: searchKeyAction 33 | }, 34 | { 35 | key: 'k', 36 | hasModifier: true, 37 | action: searchKeyAction 38 | }, 39 | { 40 | key: 'v', 41 | description: 'Open/focus version select', 42 | action: versionKeyAction 43 | }, 44 | { 45 | key: 'g', 46 | description: 'Go to package docs', 47 | displayAs: 'g', 48 | action: openQuickSwitchModal 49 | }, 50 | { 51 | key: '?', 52 | displayAs: '?', 53 | description: 'Bring up this modal', 54 | action: toggleHelpModal 55 | } 56 | ] 57 | 58 | const state = { 59 | // Stores shortcut info to prevent multiple activations on long press (continuous keydown events) 60 | shortcutBeingPressed: null 61 | } 62 | 63 | /** 64 | * Registers keyboard shortcuts. 65 | */ 66 | if (!isEmbedded) { 67 | document.addEventListener('keydown', handleKeyDown) 68 | document.addEventListener('keyup', handleKeyUp) 69 | } 70 | 71 | function handleKeyDown (event) { 72 | if (state.shortcutBeingPressed) { return } 73 | if (event.target.matches('input, select, textarea')) { return } 74 | 75 | const matchingShortcut = keyboardShortcuts.find(shortcut => { 76 | if (shortcut.hasModifier) { 77 | if (isAppleOS() && event.metaKey) { return shortcut.key === event.key } 78 | if (event.ctrlKey) { return shortcut.key === event.key } 79 | 80 | return false 81 | } else { 82 | if (event.ctrlKey || event.metaKey || event.altKey) { return false } 83 | 84 | return shortcut.key === event.key 85 | } 86 | }) 87 | 88 | if (!matchingShortcut) { return } 89 | state.shortcutBeingPressed = matchingShortcut 90 | 91 | event.preventDefault() 92 | matchingShortcut.action(event) 93 | } 94 | 95 | function handleKeyUp (event) { 96 | state.shortcutBeingPressed = null 97 | } 98 | 99 | // Additional shortcut actions 100 | 101 | function searchKeyAction (event) { 102 | closeModal() 103 | focusSearchInput() 104 | } 105 | 106 | function versionKeyAction () { 107 | closeModal() 108 | 109 | if (isSidebarOpened()) { 110 | openVersionSelect() 111 | } else { 112 | openSidebar().then(openVersionSelect) 113 | } 114 | } 115 | 116 | function toggleHelpModal () { 117 | if (isHelpModalOpen()) { 118 | closeModal() 119 | } else { 120 | openSettingsModal() 121 | } 122 | } 123 | 124 | function isHelpModalOpen () { 125 | return isModalOpen() && qs(HELP_MODAL_BODY_SELECTOR) 126 | } 127 | -------------------------------------------------------------------------------- /assets/js/makeup.js: -------------------------------------------------------------------------------- 1 | import { qsAll } from './helpers' 2 | 3 | const HIGHLIGHT_CLASS = 'hll' 4 | 5 | /** 6 | * Sets up dynamic behaviour for code blocks processed with *makeup*. 7 | */ 8 | 9 | window.addEventListener('exdoc:loaded', initialize) 10 | 11 | export function initialize () { 12 | // Hovering over a delimiter (bracket, parenthesis, do/end) 13 | // highlights the relevant pair of delimiters. 14 | qsAll('[data-group-id]').forEach(delimiter => { 15 | delimiter.addEventListener('mouseenter', toggleDelimitersHighlight) 16 | delimiter.addEventListener('mouseleave', toggleDelimitersHighlight) 17 | }) 18 | } 19 | 20 | /** @param {MouseEvent} event */ 21 | function toggleDelimitersHighlight (event) { 22 | const element = event.currentTarget 23 | const force = event.type === 'mouseenter' 24 | const groupId = element.getAttribute('data-group-id') 25 | element.parentElement.querySelectorAll(`[data-group-id="${groupId}"]`).forEach(delimiter => { 26 | delimiter.classList.toggle(HIGHLIGHT_CLASS, force) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /assets/js/modal.js: -------------------------------------------------------------------------------- 1 | import { qs } from './helpers' 2 | import modalLayoutHtml from './handlebars/templates/modal-layout.html' 3 | 4 | const FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' 5 | 6 | // State 7 | 8 | /** @type {HTMLDivElement | null} */ 9 | let modal = null 10 | /** @type {HTMLElement | null} */ 11 | let prevFocus = null 12 | /** @type {HTMLElement | null} */ 13 | let lastFocus = null 14 | let ignoreFocusChanges = false 15 | 16 | /** 17 | * Adds the modal to DOM, initially it's hidden. 18 | */ 19 | function renderModal () { 20 | if (modal) return 21 | 22 | document.body.insertAdjacentHTML('beforeend', modalLayoutHtml) 23 | modal = qs('.modal') 24 | 25 | modal.addEventListener('keydown', event => { 26 | if (event.key === 'Escape') { 27 | closeModal() 28 | } 29 | }) 30 | 31 | modal.querySelector('.modal-close').addEventListener('click', closeModal) 32 | 33 | modal.addEventListener('click', event => { 34 | // if we clicked on the modal overlay/parent but not the modal content 35 | if (event.target === modal) { 36 | closeModal() 37 | } 38 | }) 39 | } 40 | 41 | /** 42 | * Trap focus in modal 43 | * Only called on open modals 44 | */ 45 | function handleFocus (event) { 46 | if (ignoreFocusChanges) return 47 | 48 | if (modal.contains(event.target)) { 49 | lastFocus = event.target 50 | } else { 51 | ignoreFocusChanges = true 52 | const focusable = modal.querySelectorAll(FOCUSABLE_SELECTOR) 53 | if (lastFocus === focusable[0]) { 54 | // Focus last 55 | focusable[focusable.length - 1].focus() 56 | } else { 57 | // Focus first 58 | focusable[0].focus() 59 | } 60 | ignoreFocusChanges = false 61 | lastFocus = document.activeElement 62 | } 63 | } 64 | 65 | /** 66 | * Shows modal with the given content. 67 | * 68 | * @param {{ title: string, body: string }} attrs 69 | */ 70 | export function openModal ({ title, body }) { 71 | renderModal() 72 | prevFocus = document.activeElement 73 | document.addEventListener('focus', handleFocus, true) 74 | 75 | modal.querySelector('.modal-title').innerHTML = title 76 | modal.querySelector('.modal-body').innerHTML = body 77 | 78 | modal.classList.add('shown') 79 | modal.focus() 80 | } 81 | 82 | /** 83 | * Closes the modal. 84 | */ 85 | export function closeModal () { 86 | modal?.classList.remove('shown') 87 | 88 | document.removeEventListener('focus', handleFocus, true) 89 | prevFocus?.focus() 90 | prevFocus = null 91 | } 92 | 93 | /** 94 | * Checks whether a modal is open. 95 | */ 96 | export function isModalOpen () { 97 | return Boolean(modal?.classList.contains('shown')) 98 | } 99 | -------------------------------------------------------------------------------- /assets/js/preview.js: -------------------------------------------------------------------------------- 1 | import { isEmbedded, isPreview } from './globals' 2 | import { descriptionElementFromHash } from './helpers' 3 | 4 | if (isPreview && isEmbedded) { 5 | const previewing = descriptionElementFromHash(true) 6 | 7 | if (previewing) { 8 | document.body.classList.add('preview') 9 | document.getElementById('content').replaceChildren(...previewing.childNodes) 10 | 11 | // Make links open in parent. 12 | const links = document.getElementsByTagName('a:not([target=_blank]') 13 | for (const element of links) { 14 | element.setAttribute('target', '_parent') 15 | } 16 | 17 | window.scrollTo(0, 0) 18 | // Stop iframe scrolling affecting parent by setting body position to fixed. 19 | document.body.style.position = 'fixed' 20 | // Defer preview message until all other scripts have run. 21 | setTimeout(sendPreviewInfoToParent) 22 | window.addEventListener('resize', sendPreviewInfoToParent) 23 | } 24 | } 25 | 26 | function sendPreviewInfoToParent () { 27 | const message = { 28 | type: 'preview', 29 | contentHeight: document.getElementById('content').parentElement.offsetHeight 30 | } 31 | window.parent.postMessage(message, '*') 32 | } 33 | -------------------------------------------------------------------------------- /assets/js/settings-store.js: -------------------------------------------------------------------------------- 1 | import { SETTINGS_KEY } from './constants' 2 | 3 | const DEFAULT_SETTINGS = { 4 | // Whether to show tooltips on function/module links 5 | tooltips: true, 6 | // Theme preference, null if never explicitly overridden by the user 7 | theme: null, 8 | // Livebook URL to point the badges directly to 9 | livebookUrl: null 10 | } 11 | 12 | /** 13 | * Stores configuration state and persists it across 14 | * browser sessions. 15 | */ 16 | class SettingsStore { 17 | constructor () { 18 | this._subscribers = [] 19 | this._settings = DEFAULT_SETTINGS 20 | 21 | this._loadSettings() 22 | } 23 | 24 | /** 25 | * Returns the current settings. 26 | */ 27 | get () { 28 | return this._settings 29 | } 30 | 31 | /** 32 | * Stores the given settings in local storage. 33 | * 34 | * The given attributes are merged into the current settings. 35 | */ 36 | update (newSettings) { 37 | const prevSettings = this._settings 38 | this._settings = { ...this._settings, ...newSettings } 39 | this._subscribers.forEach((callback) => 40 | callback(this._settings, prevSettings) 41 | ) 42 | this._storeSettings() 43 | } 44 | 45 | /** 46 | * Runs the given function with the current settings and then 47 | * whenever the settings change. 48 | */ 49 | getAndSubscribe (callback) { 50 | this._subscribers.push(callback) 51 | callback(this._settings) 52 | } 53 | 54 | _loadSettings () { 55 | try { 56 | const json = localStorage.getItem(SETTINGS_KEY) 57 | 58 | if (json) { 59 | const settings = JSON.parse(json) 60 | this._settings = { ...this._settings, ...settings } 61 | } 62 | 63 | this._loadSettingsLegacy() 64 | } catch (error) { 65 | console.error(`Failed to load settings: ${error}`) 66 | } 67 | } 68 | 69 | _storeSettings () { 70 | try { 71 | this._storeSettingsLegacy() 72 | localStorage.setItem(SETTINGS_KEY, JSON.stringify(this._settings)) 73 | } catch (error) { 74 | console.error(`Failed to persist settings: ${error}`) 75 | } 76 | } 77 | 78 | // Every package uses a specific ExDoc, so the JS used 79 | // across packages varies. We now store all local settings 80 | // under a single key, but to ensure compatibility across 81 | // pages, we also load/store settings from the legacy keys 82 | 83 | _loadSettingsLegacy () { 84 | const tooltipsDisabled = localStorage.getItem('tooltipsDisabled') 85 | if (tooltipsDisabled !== null) { 86 | this._settings = { ...this._settings, tooltips: false } 87 | } 88 | 89 | const nightMode = localStorage.getItem('night-mode') 90 | if (nightMode === 'true') { 91 | this._settings = { ...this._settings, nightMode: true } 92 | } 93 | 94 | if (this._settings.nightMode === true) { 95 | this._settings = { ...this._settings, theme: 'dark' } 96 | } 97 | } 98 | 99 | _storeSettingsLegacy () { 100 | if (this._settings.tooltips) { 101 | localStorage.removeItem('tooltipsDisabled') 102 | } else { 103 | localStorage.setItem('tooltipsDisabled', 'true') 104 | } 105 | 106 | if (this._settings.nightMode !== null) { 107 | localStorage.setItem('night-mode', this._settings.nightMode === true ? 'true' : 'false') 108 | } else { 109 | localStorage.removeItem('night-mode') 110 | } 111 | 112 | if (this._settings.theme !== null) { 113 | localStorage.setItem('night-mode', this._settings.theme === 'dark' ? 'true' : 'false') 114 | this._settings.nightMode = this._settings.theme === 'dark' 115 | } else { 116 | delete this._settings.nightMode 117 | localStorage.removeItem('night-mode') 118 | } 119 | } 120 | } 121 | 122 | export const settingsStore = new SettingsStore() 123 | -------------------------------------------------------------------------------- /assets/js/settings.js: -------------------------------------------------------------------------------- 1 | import { qs, qsAll } from './helpers' 2 | import { openModal } from './modal' 3 | import { settingsStore } from './settings-store' 4 | import { keyboardShortcuts } from './keyboard-shortcuts' 5 | import settingsModalBodyTemplate from './handlebars/templates/settings-modal-body.handlebars' 6 | 7 | const SETTINGS_LINK_SELECTOR = '.display-settings' 8 | const SETTINGS_MODAL_BODY_SELECTOR = '#settings-modal-content' 9 | const SETTINGS_TAB = '#modal-settings-tab' 10 | const KEYBOARD_SHORTCUTS_TAB = '#modal-keyboard-shortcuts-tab' 11 | const SETTINGS_CONTENT = '#settings-content' 12 | const KEYBOARD_SHORTCUTS_CONTENT = '#keyboard-shortcuts-content' 13 | 14 | const modalTabs = [ 15 | { 16 | title: 'Settings', 17 | id: 'modal-settings-tab' 18 | }, 19 | { 20 | title: 'Keyboard shortcuts', 21 | id: 'modal-keyboard-shortcuts-tab' 22 | } 23 | ] 24 | 25 | /** 26 | * Sets up the settings modal. 27 | */ 28 | 29 | window.addEventListener('exdoc:loaded', initialize) 30 | 31 | function initialize () { 32 | qsAll(SETTINGS_LINK_SELECTOR).forEach(element => { 33 | element.addEventListener('click', openSettingsModal) 34 | }) 35 | } 36 | 37 | function showSettingsTab () { 38 | qs(KEYBOARD_SHORTCUTS_TAB).classList.remove('active') 39 | qs(SETTINGS_TAB).classList.add('active') 40 | qs(SETTINGS_CONTENT).classList.remove('hidden') 41 | qs(KEYBOARD_SHORTCUTS_CONTENT).classList.add('hidden') 42 | } 43 | 44 | function showKeyboardShortcutsTab () { 45 | qs(KEYBOARD_SHORTCUTS_TAB).classList.add('active') 46 | qs(SETTINGS_TAB).classList.remove('active') 47 | qs(KEYBOARD_SHORTCUTS_CONTENT).classList.remove('hidden') 48 | qs(SETTINGS_CONTENT).classList.add('hidden') 49 | } 50 | 51 | export function openSettingsModal () { 52 | openModal({ 53 | title: modalTabs.map(({id, title}) => ``).join(''), 54 | body: settingsModalBodyTemplate({ shortcuts: keyboardShortcuts }) 55 | }) 56 | 57 | const modal = qs(SETTINGS_MODAL_BODY_SELECTOR) 58 | 59 | const themeInput = modal.querySelector('[name="theme"]') 60 | const tooltipsInput = modal.querySelector('[name="tooltips"]') 61 | const directLivebookUrlInput = modal.querySelector('[name="direct_livebook_url"]') 62 | const livebookUrlInput = modal.querySelector('[name="livebook_url"]') 63 | 64 | settingsStore.getAndSubscribe(settings => { 65 | themeInput.value = settings.theme || 'system' 66 | tooltipsInput.checked = settings.tooltips 67 | 68 | if (settings.livebookUrl === null) { 69 | directLivebookUrlInput.checked = false 70 | livebookUrlInput.classList.add('hidden') 71 | livebookUrlInput.tabIndex = -1 72 | } else { 73 | directLivebookUrlInput.checked = true 74 | livebookUrlInput.classList.remove('hidden') 75 | livebookUrlInput.tabIndex = 0 76 | livebookUrlInput.value = settings.livebookUrl 77 | } 78 | }) 79 | 80 | themeInput.addEventListener('change', event => { 81 | settingsStore.update({ theme: event.target.value }) 82 | }) 83 | 84 | tooltipsInput.addEventListener('change', event => { 85 | settingsStore.update({ tooltips: event.target.checked }) 86 | }) 87 | 88 | directLivebookUrlInput.addEventListener('change', event => { 89 | const livebookUrl = event.target.checked ? livebookUrlInput.value : null 90 | settingsStore.update({ livebookUrl }) 91 | }) 92 | 93 | livebookUrlInput.addEventListener('input', event => { 94 | settingsStore.update({ livebookUrl: event.target.value }) 95 | }) 96 | 97 | qs(SETTINGS_TAB).addEventListener('click', event => { 98 | showSettingsTab() 99 | }) 100 | qs(KEYBOARD_SHORTCUTS_TAB).addEventListener('click', event => { 101 | showKeyboardShortcutsTab() 102 | }) 103 | 104 | showSettingsTab() 105 | } 106 | -------------------------------------------------------------------------------- /assets/js/sidebar/constants.js: -------------------------------------------------------------------------------- 1 | export const SIDEBAR_STATE_KEY = 'sidebar_state' 2 | export const SIDEBAR_PREF_CLOSED = 'closed' 3 | export const SIDEBAR_PREF_OPEN = 'open' 4 | export const SIDEBAR_WIDTH_KEY = 'sidebar_width' 5 | export const SMALL_SCREEN_BREAKPOINT = 768 6 | export const SIDEBAR_CLASS_OPEN = 'sidebar-open' 7 | export const SIDEBAR_CLASS_TRANSITION = 'sidebar-transition' 8 | -------------------------------------------------------------------------------- /assets/js/sidebar/sidebar-version-select.js: -------------------------------------------------------------------------------- 1 | import { qs, checkUrlExists } from '../helpers' 2 | import { getVersionNodes, isEmbedded } from '../globals' 3 | import versionsDropdownTemplate from '../handlebars/templates/versions-dropdown.handlebars' 4 | 5 | const VERSIONS_CONTAINER_SELECTOR = '.sidebar-projectVersion' 6 | const VERSIONS_DROPDOWN_SELECTOR = '.sidebar-projectVersion select' 7 | const VERSIONS_LATEST_SELECTOR = '.sidebar-staleVersion a' 8 | 9 | /** 10 | * Initializes selectable version list if `versionNodes` have been configured. 11 | */ 12 | 13 | if (!isEmbedded) { 14 | const versionNodes = getVersionNodes() 15 | const versionsContainer = qs(VERSIONS_CONTAINER_SELECTOR) 16 | 17 | if (versionNodes.length > 0 || !versionsContainer) { 18 | // Initially the container contains only text with the current version 19 | const currentVersion = versionsContainer.textContent.trim() 20 | 21 | // Add the current version node to the list if not there. 22 | const withCurrentVersion = versionNodes.some((node) => node.version === currentVersion) 23 | ? versionNodes 24 | : [{ version: currentVersion, url: '#' }, ...versionNodes] 25 | 26 | // Add additional attributes to version nodes for rendering. 27 | const nodes = withCurrentVersion.map(node => ({ 28 | ...node, 29 | isCurrentVersion: node.version === currentVersion 30 | })) 31 | 32 | const latestVersionNode = versionNodes.find(node => node.latest) 33 | const latestVersion = latestVersionNode?.version !== currentVersion && !currentVersion.includes('-') ? latestVersionNode?.url : null 34 | 35 | versionsContainer.innerHTML = versionsDropdownTemplate({ nodes, latestVersion}) 36 | 37 | const select = qs(VERSIONS_DROPDOWN_SELECTOR) 38 | select.addEventListener('change', handleVersionSelected) 39 | adjustWidth(select) 40 | 41 | const versionsGoToLatest = qs(VERSIONS_LATEST_SELECTOR) 42 | 43 | if (versionsGoToLatest) { 44 | versionsGoToLatest.addEventListener('click', handleGoToLatestClicked) 45 | } 46 | } 47 | } 48 | 49 | // Function to adjust the width of the select element 50 | function adjustWidth (select) { 51 | // Create a temporary element to measure the width 52 | const temp = document.createElement('span') 53 | temp.style.visibility = 'hidden' 54 | temp.style.position = 'absolute' 55 | temp.style.whiteSpace = 'nowrap' 56 | temp.style.font = window.getComputedStyle(select).font 57 | temp.textContent = select.options[select.selectedIndex].text 58 | 59 | document.body.appendChild(temp) 60 | select.style.width = `${temp.offsetWidth + 20}px` 61 | document.body.removeChild(temp) 62 | } 63 | 64 | function handleVersionSelected (event) { 65 | const url = event.target.value 66 | const pathSuffix = window.location.pathname.split('/').pop() + window.location.hash 67 | const otherVersionWithPath = `${url}/${pathSuffix}` 68 | 69 | checkUrlExists(otherVersionWithPath) 70 | .then(exists => { 71 | if (exists) { 72 | window.location.href = otherVersionWithPath 73 | } else { 74 | window.location.href = url 75 | } 76 | }) 77 | } 78 | 79 | function handleGoToLatestClicked (event) { 80 | const url = this.href 81 | const pathSuffix = window.location.pathname.split('/').pop() + window.location.hash 82 | const otherVersionWithPath = `${url}/${pathSuffix}` 83 | event.preventDefault() 84 | 85 | checkUrlExists(otherVersionWithPath) 86 | .then(exists => { 87 | if (exists) { 88 | window.location.href = otherVersionWithPath 89 | } else { 90 | window.location.href = url 91 | } 92 | }) 93 | } 94 | 95 | /** 96 | * Opens the version select if available. 97 | * Only focuses the version select if 98 | * - the browser's HTMLSelectElement lacks the showPicker method 99 | * - there has been transient user interaction 100 | */ 101 | export function openVersionSelect () { 102 | const select = qs(VERSIONS_DROPDOWN_SELECTOR) 103 | 104 | if (select) { 105 | select.focus() 106 | 107 | // Prevent subsequent 'v' press from submitting form 108 | select.addEventListener('keydown', event => { 109 | if (event.key === 'Escape' || event.key === 'v') { 110 | event.preventDefault() 111 | select.blur() 112 | } 113 | }) 114 | 115 | if (navigator.userActivation.isActive && 'showPicker' in HTMLSelectElement.prototype) { 116 | select.showPicker() 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /assets/js/swup.js: -------------------------------------------------------------------------------- 1 | import Swup from 'swup' 2 | import SwupA11yPlugin from '@swup/a11y-plugin' 3 | import SwupProgressPlugin from '@swup/progress-plugin' 4 | import { isEmbedded } from './globals' 5 | 6 | // Emit exdoc:loaded each time content loads: 7 | // - on initial page load (DOMContentLoaded) 8 | // - on subsequent SWUP page loads (page:view) 9 | const emitExdocLoaded = () => { 10 | window.dispatchEvent(new Event('exdoc:loaded')) 11 | } 12 | 13 | const maybeMetaRedirect = (visit, {page}) => { 14 | const hasMetaRefresh = //i.test(page.html) 15 | 16 | if (hasMetaRefresh) { 17 | visit.abort() 18 | window.location.reload() 19 | } 20 | } 21 | 22 | window.addEventListener('DOMContentLoaded', emitExdocLoaded) 23 | 24 | if (!isEmbedded && window.location.protocol !== 'file:') { 25 | new Swup({ 26 | animationSelector: false, 27 | containers: ['#main'], 28 | ignoreVisit: (url) => { 29 | const path = url.split('#')[0] 30 | return path === window.location.pathname || 31 | path === window.location.pathname + '.html' 32 | }, 33 | linkSelector: 'a[href]:not([href^="/"]):not([href^="http"])', 34 | hooks: { 35 | 'page:load': maybeMetaRedirect, 36 | 'page:view': emitExdocLoaded 37 | }, 38 | plugins: [new SwupA11yPlugin(), new SwupProgressPlugin({delay: 500})] 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /assets/js/theme.js: -------------------------------------------------------------------------------- 1 | import { settingsStore } from './settings-store' 2 | import { showToast } from './toast' 3 | import { DARK_MODE_CLASS, THEME_SYSTEM, THEME_DARK, THEME_LIGHT } from './constants' 4 | 5 | const THEMES = [THEME_SYSTEM, THEME_DARK, THEME_LIGHT] 6 | 7 | const darkMediaQuery = window.matchMedia('(prefers-color-scheme: dark)') 8 | 9 | /** 10 | * Sets initial night mode state and registers to settings updates. 11 | */ 12 | 13 | settingsStore.getAndSubscribe(update) 14 | darkMediaQuery.addEventListener('change', update) 15 | 16 | function update () { 17 | const theme = currentTheme() 18 | const dark = theme === THEME_DARK || (theme !== THEME_LIGHT && darkMediaQuery.matches) 19 | document.body.classList.toggle(DARK_MODE_CLASS, dark) 20 | } 21 | 22 | /** 23 | * Cycles through themes and saves the preference. 24 | */ 25 | export function cycleTheme () { 26 | const nextTheme = THEMES[THEMES.indexOf(currentTheme()) + 1] || THEMES[0] 27 | settingsStore.update({ theme: nextTheme }) 28 | showToast(`Set theme to "${nextTheme}"`) 29 | } 30 | 31 | export function currentTheme () { 32 | const params = new URLSearchParams(window.location.search) 33 | return params.get('theme') || settingsStore.get().theme || THEME_SYSTEM 34 | } 35 | -------------------------------------------------------------------------------- /assets/js/toast.js: -------------------------------------------------------------------------------- 1 | let init = false 2 | let toastTimer = null 3 | let toast = null 4 | 5 | export function showToast (message) { 6 | if (!init) { 7 | init = true 8 | toast = document.getElementById('toast') 9 | toast?.addEventListener('click', () => { 10 | clearTimeout(toastTimer) 11 | toast.classList.remove('show') 12 | }) 13 | } 14 | 15 | if (toast) { 16 | clearTimeout(toastTimer) 17 | toast.innerText = message 18 | toast.classList.add('show') 19 | 20 | toastTimer = setTimeout(() => { 21 | toast.classList.remove('show') 22 | toastTimer = setTimeout(function () { toast.innerText = '' }, 1000) 23 | }, 5000) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /assets/js/tooltips/hint-page.js: -------------------------------------------------------------------------------- 1 | import { extractModuleHint, extractFunctionHint } from './hints' 2 | import { getCurrentPageSidebarType, descriptionElementFromHash, getProjectNameAndVersion, qs } from '../helpers' 3 | import { isEmbedded, isHint } from '../globals' 4 | 5 | /** 6 | * Checks the URL query parameter for a hint request (`?hint=true`), 7 | * and when present extracts the relevant hint from the page content 8 | * and sends to the parent window as an event. 9 | */ 10 | 11 | if (isHint && isEmbedded) { 12 | const infoElement = descriptionElementFromHash() 13 | 14 | const hint = infoElement 15 | ? extractFunctionHint(infoElement) 16 | // Tasks are modules. 17 | : ['modules', 'tasks'].includes(getCurrentPageSidebarType()) 18 | ? extractModuleHint(qs('.content-inner')) 19 | : null 20 | 21 | if (hint) { 22 | // Send hint to parent. 23 | const message = { 24 | hint: { 25 | ...hint, 26 | version: getProjectNameAndVersion() 27 | }, 28 | href: window.location.href 29 | } 30 | window.parent.postMessage(message, '*') 31 | } 32 | 33 | // Empty content to prevent other modules doing work. 34 | qs('.content-inner')?.replaceChildren() 35 | } 36 | -------------------------------------------------------------------------------- /assets/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: '', 4 | 5 | frameworks: ['mocha', 'chai'], 6 | 7 | files: [ 8 | 'test/**/*.spec.js' 9 | ], 10 | 11 | preprocessors: { 12 | 'test/**/*.spec.js': ['esbuild', 'sourcemap'] 13 | }, 14 | 15 | reporters: ['progress'], 16 | port: 9876, 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | autoWatch: false, 20 | browsers: ['Chrome'], 21 | singleRun: false 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ex_doc", 3 | "description": "ExDoc produces HTML and EPUB documentation for Elixir projects.", 4 | "directories": { 5 | "test": "test" 6 | }, 7 | "engines": { 8 | "node": ">= 18.0.0", 9 | "npm": ">= 9.0.0" 10 | }, 11 | "scripts": { 12 | "lint": "eslint './js/**/*.js'", 13 | "lint:fix": "eslint --fix './js/**/*.js'", 14 | "test": "karma start ./karma.conf.js --single-run", 15 | "build:watch": "npm run build --watch", 16 | "build": "node build/build.js" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/elixir-lang/ex_doc.git" 21 | }, 22 | "author": "The Elixir Team", 23 | "license": "Apache-2.0", 24 | "bugs": { 25 | "url": "https://github.com/elixir-lang/ex_doc/issues" 26 | }, 27 | "homepage": "https://github.com/elixir-lang/ex_doc#readme", 28 | "browserslist": "last 2 versions", 29 | "devDependencies": { 30 | "@fontsource-variable/inconsolata": "^5.1.1", 31 | "@fontsource/lato": "^4.5.10", 32 | "@swup/a11y-plugin": "^5.0.0", 33 | "@swup/progress-plugin": "^3.2.0", 34 | "esbuild": "^0.16.16", 35 | "eslint": "^8.31.0", 36 | "eslint-config-standard": "^17.0.0", 37 | "eslint-plugin-import": "^2.26.0", 38 | "eslint-plugin-n": "^15.6.0", 39 | "eslint-plugin-promise": "^6.1.1", 40 | "fs-extra": "^11.1.0", 41 | "handlebars": "^4.7.7", 42 | "karma": "^6.4.1", 43 | "karma-chai-plugins": "^0.9.0", 44 | "karma-chrome-launcher": "^3.1.1", 45 | "karma-esbuild": "^2.2.5", 46 | "karma-mocha": "^2.0.1", 47 | "karma-sourcemap-loader": "^0.3.5", 48 | "lodash.throttle": "^4.1.1", 49 | "lunr": "^2.3.8", 50 | "mocha": "^10.8.2", 51 | "modern-normalize": "^3.0.1", 52 | "swup": "^4.8.1" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /assets/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | 6 | "globals": { 7 | "expect": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/test/helpers.spec.js: -------------------------------------------------------------------------------- 1 | import { escapeRegexModifiers } from '../js/helpers' 2 | 3 | describe('helpers', () => { 4 | describe('escapeRegexModifiers', () => { 5 | it('escapes -', () => { 6 | expect(escapeRegexModifiers('hello-world')).to.be.equal('hello\\-world') 7 | }) 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /assets/test/tooltips/hints.spec.js: -------------------------------------------------------------------------------- 1 | import { extractModuleHint, extractFunctionHint } from '../../js/tooltips/hints' 2 | 3 | describe('hints extraction', () => { 4 | describe('extractModuleHint', () => { 5 | const modulePageObject = parseHTML(` 6 |
7 |

8 | 9 | 10 | View Source 11 | 12 | 13 | Some module (ExDoc v0.0.1) 14 |

15 |
16 |

17 | Module description here 18 |

19 |
20 |
List of functions with summaries
21 |
22 | `)[0] 23 | 24 | it('extracts hint info', () => { 25 | expect(extractModuleHint(modulePageObject).title).to.eql('Some module') 26 | expect(extractModuleHint(modulePageObject).description).to.eql('Module description here') 27 | expect(extractModuleHint(modulePageObject).kind).to.eql('module') 28 | }) 29 | }) 30 | 31 | describe('extractFunctionHint', () => { 32 | const functionDetailObject = parseHTML(` 33 |
34 |
35 | 36 | 37 | Link to this callback 38 | 39 | 40 |

configure(any)

41 | 42 | 43 | 44 | View Source 45 | 46 |
47 |
48 |

First line of description.

49 |

Second line of description.

50 |
51 |
52 | `)[0] 53 | 54 | it('extracts hint info', () => { 55 | const hint = extractFunctionHint(functionDetailObject) 56 | 57 | expect(hint.title).to.eql('configure(any)') 58 | expect(hint.description).to.eql('First line of description.') 59 | expect(hint.kind).to.eql('function') 60 | }) 61 | }) 62 | }) 63 | 64 | function parseHTML (html) { 65 | const doc = document.implementation.createHTMLDocument(); 66 | doc.body.innerHTML = html; 67 | return doc.body.children; 68 | } 69 | -------------------------------------------------------------------------------- /bin/ex_doc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env elixir 2 | mix_env = System.get_env()["MIX_ENV"] || "dev" 3 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/nimble_parsec/ebin", __DIR__) 4 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup/ebin", __DIR__) 5 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_elixir/ebin", __DIR__) 6 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_erlang/ebin", __DIR__) 7 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/makeup_html/ebin", __DIR__) 8 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/earmark_parser/ebin", __DIR__) 9 | Code.prepend_path Path.expand("../_build/#{mix_env}/lib/ex_doc/ebin", __DIR__) 10 | 11 | if Code.ensure_loaded?(ExDoc.CLI) do 12 | ExDoc.CLI.main(System.argv()) 13 | else 14 | IO.puts :stderr, "Error: cannot generate docs because ExDoc.CLI module is not available. " <> 15 | "Please run `mix compile` before or ensure ExDoc is available." 16 | exit(1) 17 | end 18 | -------------------------------------------------------------------------------- /formatters/epub/dist/epub-4WIP524F.js: -------------------------------------------------------------------------------- 1 | (()=>{var s=document.querySelector.bind(document),o=document.querySelectorAll.bind(document);function r(e){document.readyState!=="loading"?e():document.addEventListener("DOMContentLoaded",e)}var l="hll";window.addEventListener("exdoc:loaded",t);function t(){o("[data-group-id]").forEach(e=>{e.addEventListener("mouseenter",i),e.addEventListener("mouseleave",i)})}function i(e){let n=e.currentTarget,a=e.type==="mouseenter",c=n.getAttribute("data-group-id");n.parentElement.querySelectorAll(`[data-group-id="${c}"]`).forEach(u=>{u.classList.toggle(l,a)})}r(()=>{t()});})(); 2 | -------------------------------------------------------------------------------- /formatters/epub/metainfo/com.apple.ibooks.display-options.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /formatters/epub/metainfo/container.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /formatters/html/dist/inline_html-4XT25SPW.js: -------------------------------------------------------------------------------- 1 | (()=>{var t="ex_doc:settings",e="dark";var o="dark",s="light";var E="sidebar_state",n="closed";var r="sidebar_width";var a="sidebar-open";var i=new URLSearchParams(window.location.search),S=i.get("theme")||JSON.parse(localStorage.getItem(t)||"{}").theme;(S===o||S!==s&&window.matchMedia("(prefers-color-scheme: dark)").matches)&&document.body.classList.add(e);var d=sessionStorage.getItem(E),A=d!==n&&!window.matchMedia(`screen and (max-width: ${768}px)`).matches;document.body.classList.toggle(a,A);var c=sessionStorage.getItem(r);c&&document.body.style.setProperty("--sidebarWidth",`${c}px`);var p=/(Macintosh|iPhone|iPad|iPod)/.test(window.navigator.userAgent);document.documentElement.classList.toggle("apple-os",p);})(); 2 | -------------------------------------------------------------------------------- /formatters/html/dist/lato-all-400-normal-MNITWADU.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-all-400-normal-MNITWADU.woff -------------------------------------------------------------------------------- /formatters/html/dist/lato-all-700-normal-XMT5XFBS.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-all-700-normal-XMT5XFBS.woff -------------------------------------------------------------------------------- /formatters/html/dist/lato-latin-400-normal-W7754I4D.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-latin-400-normal-W7754I4D.woff2 -------------------------------------------------------------------------------- /formatters/html/dist/lato-latin-700-normal-2XVSBPG4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-latin-700-normal-2XVSBPG4.woff2 -------------------------------------------------------------------------------- /formatters/html/dist/lato-latin-ext-400-normal-N27NCBWW.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-latin-ext-400-normal-N27NCBWW.woff2 -------------------------------------------------------------------------------- /formatters/html/dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/lato-latin-ext-700-normal-Q2L5DVMW.woff2 -------------------------------------------------------------------------------- /formatters/html/dist/remixicon-QPNJX265.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/formatters/html/dist/remixicon-QPNJX265.woff2 -------------------------------------------------------------------------------- /lib/ex_doc.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc do 2 | @moduledoc false 3 | @ex_doc_version Mix.Project.config()[:version] 4 | 5 | @doc """ 6 | Returns the ExDoc version (used in templates). 7 | """ 8 | @spec version :: String.t() 9 | def version, do: @ex_doc_version 10 | 11 | @doc """ 12 | Generates documentation for the given `project`, `vsn` (version) 13 | and `options`. 14 | """ 15 | @spec generate_docs(String.t(), String.t(), Keyword.t()) :: atom 16 | def generate_docs(project, vsn, options) 17 | when is_binary(project) and is_binary(vsn) and is_list(options) do 18 | config = ExDoc.Config.build(project, vsn, options) 19 | 20 | if processor = options[:markdown_processor] do 21 | ExDoc.Markdown.put_markdown_processor(processor) 22 | end 23 | 24 | {module_nodes, filtered_nodes} = config.retriever.docs_from_dir(config.source_beam, config) 25 | find_formatter(config.formatter).run(module_nodes, filtered_nodes, config) 26 | end 27 | 28 | # Short path for programmatic interface 29 | defp find_formatter(modname) when is_atom(modname), do: modname 30 | 31 | defp find_formatter("ExDoc.Formatter." <> _ = name) do 32 | [name] 33 | |> Module.concat() 34 | |> check_formatter_module(name) 35 | end 36 | 37 | defp find_formatter(name) do 38 | [ExDoc.Formatter, String.upcase(name)] 39 | |> Module.concat() 40 | |> check_formatter_module(name) 41 | end 42 | 43 | defp check_formatter_module(modname, argname) do 44 | if Code.ensure_loaded?(modname) do 45 | modname 46 | else 47 | raise "formatter module #{inspect(argname)} not found" 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/ex_doc/application.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Application do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | Makeup.Registry.register_lexer(ExDoc.ShellLexer, 7 | options: [], 8 | names: ["shell", "console", "sh", "bash", "zsh"], 9 | extensions: [] 10 | ) 11 | 12 | # Load applications so we can find their modules in docs 13 | Enum.each([:eex, :ex_unit, :iex, :logger, :mix], &Application.load/1) 14 | 15 | # Start all applications with the makeup prefix 16 | for {app, _, _} <- Application.loaded_applications(), 17 | match?("makeup_" <> _, Atom.to_string(app)) do 18 | Application.ensure_all_started(app) 19 | end 20 | 21 | children = [ 22 | ExDoc.Refs 23 | ] 24 | 25 | Supervisor.start_link(children, strategy: :one_for_one) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/assets.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Formatter.EPUB.Assets do 2 | @moduledoc false 3 | 4 | defmacrop embed_pattern(pattern) do 5 | ["formatters/epub", pattern] 6 | |> Path.join() 7 | |> Path.wildcard() 8 | |> Enum.map(fn path -> 9 | Module.put_attribute(__CALLER__.module, :external_resource, path) 10 | {Path.basename(path), File.read!(path)} 11 | end) 12 | end 13 | 14 | defp dist_js(), do: embed_pattern("dist/*.js") 15 | defp dist_css(:elixir), do: embed_pattern("dist/epub-elixir-*.css") 16 | defp dist_css(:erlang), do: embed_pattern("dist/epub-erlang-*.css") 17 | 18 | ## Assets 19 | 20 | def dist(proglang), do: dist_js() ++ dist_css(proglang) 21 | def metainfo, do: embed_pattern("metainfo/*") 22 | 23 | ## Filenames 24 | 25 | def js_filename(), do: dist_js() |> extract_filename!() 26 | def css_filename(language), do: dist_css(language) |> extract_filename!() 27 | 28 | ## Helpers 29 | 30 | defp extract_filename!([{location, _}]), do: location 31 | end 32 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Formatter.EPUB.Templates do 2 | @moduledoc false 3 | 4 | require EEx 5 | 6 | import ExDoc.Utils, 7 | only: [before_closing_body_tag: 2, before_closing_head_tag: 2, h: 1, text_to_id: 1] 8 | 9 | alias ExDoc.Formatter.HTML.Templates, as: H 10 | alias ExDoc.Formatter.EPUB.Assets 11 | 12 | # The actual rendering happens here 13 | defp render_doc(ast), do: ast && ExDoc.DocAST.to_string(ast) 14 | 15 | @doc """ 16 | Generate content from the module template for a given `node` 17 | """ 18 | def module_page(config, module_node) do 19 | summary = H.module_summary(module_node) 20 | module_template(config, module_node, summary) 21 | end 22 | 23 | @doc """ 24 | Generated ID for static file 25 | """ 26 | def static_file_to_id(static_file) do 27 | static_file |> Path.basename() |> text_to_id() 28 | end 29 | 30 | @doc """ 31 | Creates the Package Document Definition. 32 | 33 | this definition encapsulates the publication metadata and the resource 34 | information that constitute the EPUB publication. This definition also 35 | includes the default reading order. 36 | 37 | See http://www.idpf.org/epub/30/spec/epub30-publications.html#sec-package-def. 38 | """ 39 | EEx.function_from_file( 40 | :def, 41 | :content_template, 42 | Path.expand("templates/content_template.eex", __DIR__), 43 | [:config, :nodes, :uuid, :datetime, :static_files], 44 | trim: true 45 | ) 46 | 47 | @doc """ 48 | Creates a chapter which contains all the details about an individual module. 49 | 50 | This chapter can include the following sections: *functions*, *types*, *callbacks*. 51 | """ 52 | EEx.function_from_file( 53 | :def, 54 | :module_template, 55 | Path.expand("templates/module_template.eex", __DIR__), 56 | [:config, :module, :summary], 57 | trim: true 58 | ) 59 | 60 | @doc """ 61 | Creates the table of contents. 62 | 63 | This template follows the EPUB Navigation Document Definition. 64 | 65 | See http://www.idpf.org/epub/30/spec/epub30-contentdocs.html#sec-xhtml-nav. 66 | """ 67 | EEx.function_from_file( 68 | :def, 69 | :nav_template, 70 | Path.expand("templates/nav_template.eex", __DIR__), 71 | [:config, :nodes], 72 | trim: true 73 | ) 74 | 75 | @doc """ 76 | Creates a new chapter when the user provides additional files. 77 | """ 78 | EEx.function_from_file( 79 | :def, 80 | :extra_template, 81 | Path.expand("templates/extra_template.eex", __DIR__), 82 | [:config, :node], 83 | trim: true 84 | ) 85 | 86 | @doc """ 87 | Creates the cover page for the EPUB document. 88 | """ 89 | EEx.function_from_file( 90 | :def, 91 | :title_template, 92 | Path.expand("templates/title_template.eex", __DIR__), 93 | [:config], 94 | trim: true 95 | ) 96 | 97 | EEx.function_from_file( 98 | :defp, 99 | :head_template, 100 | Path.expand("templates/head_template.eex", __DIR__), 101 | [:config, :title], 102 | trim: true 103 | ) 104 | 105 | EEx.function_from_file( 106 | :defp, 107 | :nav_grouped_item_template, 108 | Path.expand("templates/nav_grouped_item_template.eex", __DIR__), 109 | [:nodes], 110 | trim: true 111 | ) 112 | 113 | EEx.function_from_file( 114 | :defp, 115 | :toc_item_template, 116 | Path.expand("templates/toc_item_template.eex", __DIR__), 117 | [:nodes], 118 | trim: true 119 | ) 120 | 121 | "templates/media-types.txt" 122 | |> Path.expand(__DIR__) 123 | |> File.read!() 124 | |> String.split("\n", trim: true) 125 | |> Enum.each(fn line -> 126 | [extension, media] = String.split(line, ",") 127 | 128 | def media_type("." <> unquote(extension)) do 129 | unquote(media) 130 | end 131 | end) 132 | 133 | def media_type(_arg), do: nil 134 | end 135 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/content_template.eex: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | <%= config.project %> - <%= config.version %> 7 | <%= uuid %> 8 | <%= config.language %> 9 | <%= for {author, index} <- Enum.with_index(config.authors || [], 1) do %> 10 | <%= author %> 11 | <% end %> 12 | <%= datetime %> 13 | <%= if config.cover do %> 14 | 15 | <% end %> 16 | 17 | 18 | 19 | 20 | <%= for {_title, extras} <- config.extras, extra <- extras do %> 21 | 22 | <% end %> 23 | <%= for filter <- [:modules, :tasks], node <- nodes[filter] do %> 24 | 25 | <% end %> 26 | <%= for {static_file, media_type} <- static_files do %> 27 | 28 | <% end %> 29 | <%= if config.cover do %> 30 | 31 | <% end %> 32 | <%= if config.logo do %> 33 | 36 | 37 | 38 | 39 | <%= for {_title, extras} <- config.extras, extra <- extras do %> 40 | 41 | <% end %> 42 | <%= for filter <- [:modules, :tasks], node <- nodes[filter] do %> 43 | 44 | <% end %> 45 | 46 | 47 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/extra_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, node.title) %> 2 |

<%= ExDoc.DocAST.to_string(node.title_doc) %>

3 | <%= render_doc(node.doc) %> 4 | <%= before_closing_body_tag(config, :epub) %> 5 | 6 | 7 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/head_template.eex: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | <%= title %> - <%= config.project %> v<%= config.version %> 7 | 8 | 9 | 10 | <%= before_closing_head_tag(config, :epub) %> 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/media-types.txt: -------------------------------------------------------------------------------- 1 | gif,image/gif 2 | jpg,image/jpeg 3 | jpeg,image/jpeg 4 | png,image/png 5 | svg,image/svg+xml 6 | xhtml,application/xhtml+xml 7 | html,application/xhtml+xml 8 | ncx,application/x-dtbncx+xml 9 | otf,application/vnd.ms-opentype 10 | ttf,application/vnd.ms-opentype 11 | ttc,application/vnd.ms-opentype 12 | eot,application/vnd.ms-opentype 13 | woff,application/font-woff 14 | opf,application/oebps-package+xml 15 | mp3,audio/mpeg 16 | mp4,video/mp4 17 | css,text/css 18 | js,text/javascript 19 | license,text/plain 20 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/module_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, module.title) %> 2 |

3 | <%= module.title %> <%= H.module_type(module) %> 4 |

5 | 6 | <%= if deprecated = module.deprecated do %> 7 |
8 | This <%= module.type %> is deprecated. <%=h deprecated %>. 9 |
10 | <% end %> 11 | 12 | <%= if doc = module.doc do %> 13 |
14 | <%= render_doc(doc) %> 15 |
16 | <% end %> 17 | 18 | <%= if summary != [] do %> 19 |
20 |

Summary

21 | <%= for {name, nodes} <- summary, do: H.summary_template(name, nodes) %> 22 |
23 | <% end %> 24 | 25 | <%= for {name, nodes} <- summary, key = text_to_id(name) do %> 26 |
27 |

<%=h to_string(name) %>

28 |
29 | <%= for node <- nodes, do: H.detail_template(node, module) %> 30 |
31 |
32 | <% end %> 33 | <%= before_closing_body_tag(config, :epub) %> 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/nav_grouped_item_template.eex: -------------------------------------------------------------------------------- 1 | <%= for {title, nodes} <- nodes do %> 2 | <%= if title do %> 3 |
  • <%=h to_string(title) %> 4 |
      5 | <% end %> 6 | <%= for node <- nodes do %> 7 |
    1. <%=h node.title %>
    2. 8 | <% end %> 9 | <%= if title do %> 10 |
    11 |
  • 12 | <% end %> 13 | <% end %> 14 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/nav_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, "Table Of Contents") %> 2 |

    Table of contents

    3 | 26 | <%= before_closing_body_tag(config, :epub) %> 27 | 28 | 29 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/title_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, "Cover") %> 2 |
    3 | <%= if cover = config.cover do %> 4 |
    5 | <% else %> 6 |

    <%= config.project %>

    7 |

    v<%= config.version %>

    8 | <%= if logo = config.logo do %> 9 |
    Logo
    10 | <% end %> 11 | <% end %> 12 |
    13 | <%= before_closing_body_tag(config, :epub) %> 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/epub/templates/toc_item_template.eex: -------------------------------------------------------------------------------- 1 | <%= for node <- nodes do %> 2 | 3 | 4 | <%=URI.encode node.id %> 5 | 6 | 7 | 8 | <% end %> 9 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/assets.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Formatter.HTML.Assets do 2 | @moduledoc false 3 | 4 | defmacrop embed_pattern(pattern) do 5 | ["formatters/html", pattern] 6 | |> Path.join() 7 | |> Path.wildcard() 8 | |> Enum.map(fn path -> 9 | Module.put_attribute(__CALLER__.module, :external_resource, path) 10 | {Path.basename(path), File.read!(path)} 11 | end) 12 | end 13 | 14 | defp dist_js(), do: embed_pattern("dist/html-*.js") 15 | defp dist_inline_js(), do: embed_pattern("dist/inline_html-*.js") 16 | defp dist_css(:elixir), do: embed_pattern("dist/html-elixir-*.css") 17 | defp dist_css(:erlang), do: embed_pattern("dist/html-erlang-*.css") 18 | defp dist_license(), do: embed_pattern("dist/*.LICENSE.txt") 19 | 20 | ## Assets 21 | 22 | def dist(proglang), do: dist_js() ++ dist_css(proglang) ++ dist_license() 23 | def fonts, do: embed_pattern("dist/*.woff2") 24 | 25 | ## Sources 26 | 27 | def inline_js_source(), do: dist_inline_js() |> extract_source!() 28 | 29 | ## Filenames 30 | 31 | def js_filename(), do: dist_js() |> extract_filename!() 32 | def css_filename(language), do: dist_css(language) |> extract_filename!() 33 | 34 | ## Helpers 35 | 36 | @doc """ 37 | Some assets are generated automatically, so we find the revision at runtime. 38 | """ 39 | def rev(output, pattern) do 40 | output = Path.expand(output) 41 | 42 | matches = 43 | output 44 | |> Path.join(pattern) 45 | |> Path.wildcard() 46 | 47 | case matches do 48 | [] -> raise("could not find matching #{output}/#{pattern}") 49 | [asset | _] -> Path.relative_to(asset, output) 50 | end 51 | end 52 | 53 | defp extract_filename!([{location, _}]), do: location 54 | defp extract_source!([{_, source}]), do: source 55 | end 56 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/search_data.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Formatter.HTML.SearchData do 2 | @moduledoc false 3 | 4 | def create(nodes, extras, proglang) do 5 | items = Enum.flat_map(nodes, &module/1) ++ Enum.flat_map(extras, &extra/1) 6 | 7 | data = %{ 8 | items: items, 9 | content_type: "text/markdown", 10 | proglang: proglang, 11 | producer: %{ 12 | name: "ex_doc", 13 | version: to_string(Application.spec(:ex_doc)[:vsn]) 14 | } 15 | } 16 | 17 | ["searchData=" | ExDoc.Utils.to_json(data)] 18 | end 19 | 20 | defp extra(%{url: _}), do: [] 21 | 22 | defp extra(%{search_data: search_data} = map) when is_list(search_data) do 23 | Enum.map(search_data, fn item -> 24 | link = 25 | if item.anchor === "" do 26 | "#{URI.encode(map.id)}.html" 27 | else 28 | "#{URI.encode(map.id)}.html##{URI.encode(item.anchor)}" 29 | end 30 | 31 | encode(link, item.title <> " - #{map.id}", item.type, clean_markdown(item.body)) 32 | end) 33 | end 34 | 35 | defp extra(map) do 36 | page = URI.encode(map.id) <> ".html" 37 | {intro, sections} = extract_sections_from_markdown(map.source, "") 38 | 39 | intro = encode(page, map.title, :extras, intro) 40 | [intro | render_sections(sections, page, map.title, :extras)] 41 | end 42 | 43 | defp module(%ExDoc.ModuleNode{} = node) do 44 | page = URI.encode(node.id) <> ".html" 45 | {intro, sections} = extract_sections(node.source_format, node, "module-") 46 | module = encode(page, node.title, node.type, intro) 47 | docs = Enum.flat_map(node.docs, &node_child(&1, node, page)) 48 | [module] ++ render_sections(sections, page, node.title, node.type) ++ docs 49 | end 50 | 51 | defp node_child(node, module_node, page) do 52 | title = "#{module_node.id}.#{node.name}/#{node.arity}" 53 | {intro, sections} = extract_sections(module_node.source_format, node, node.id <> "-") 54 | 55 | child = encode("#{page}##{URI.encode(node.id)}", title, node.type, intro) 56 | [child | render_sections(sections, page, title, node.type)] 57 | end 58 | 59 | defp encode(ref, title, type, doc) do 60 | %{ref: ref, title: title, type: type, doc: doc} 61 | end 62 | 63 | defp extract_sections("text/markdown", %{source_doc: %{"en" => doc}}, prefix) do 64 | extract_sections_from_markdown(doc, prefix) 65 | end 66 | 67 | defp extract_sections(_format, %{doc: nil}, _prefix) do 68 | {"", []} 69 | end 70 | 71 | defp extract_sections(_format, %{doc: doc}, _prefix) do 72 | {ExDoc.DocAST.text(doc, " "), []} 73 | end 74 | 75 | defp extract_sections_from_markdown(string, prefix) do 76 | [intro | headers_sections] = 77 | Regex.split(~r/(?\b.+)/, string, include_captures: true) 78 | 79 | {headers, sections} = 80 | headers_sections 81 | |> Enum.chunk_every(2) 82 | |> Enum.map(fn [header, section] -> {header, section} end) 83 | |> Enum.unzip() 84 | 85 | # Now convert the headers into a single markdown document 86 | header_tags = 87 | headers 88 | |> Enum.join("\n\n") 89 | |> ExDoc.Markdown.to_ast() 90 | |> ExDoc.DocAST.add_ids_to_headers([:h2, :h3], prefix) 91 | 92 | sections = 93 | Enum.zip_with(header_tags, sections, fn {_, attrs, inner, _}, section -> 94 | {ExDoc.DocAST.text(inner), Keyword.fetch!(attrs, :id), clean_markdown(section)} 95 | end) 96 | 97 | {clean_markdown(intro), sections} 98 | end 99 | 100 | defp clean_markdown(text) do 101 | text 102 | |> ExDoc.Utils.strip_tags(" ") 103 | |> drop_ignorable_codeblocks() 104 | |> String.trim() 105 | end 106 | 107 | defp render_sections(sections, page, title, type) do 108 | for {header, anchor, body} <- sections do 109 | encode("#{page}##{anchor}", header <> " - " <> title, type, body) 110 | end 111 | end 112 | 113 | @ignored_codeblocks ~w[vega-lite] 114 | 115 | defp drop_ignorable_codeblocks(section) do 116 | block_names = Enum.join(@ignored_codeblocks, "|") 117 | String.replace(section, ~r/^```(?:#{block_names})\n(?:[\s\S]*?)```$/m, "") 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/api_reference_entry_template.eex: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | <%=h module_node.title %> 4 | <%= if deprecated = module_node.deprecated do %> 5 | deprecated 6 | <% end %> 7 |
    8 | <%= if doc = module_node.doc do %> 9 |
    <%= ExDoc.DocAST.synopsis(doc) %>
    10 | <% end %> 11 |
    12 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/api_reference_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, "API Reference", false) %> 2 | <%= sidebar_template(config, :extra) %> 3 | 4 |
    5 |
    6 |

    API Reference <%= config.project %> v#<%= config.version %>

    7 | <%= if config.source_url do %> 8 | 9 | 10 | View Source 11 | 12 | <% end %> 13 |
    14 | 15 | <%= if nodes_map.modules != [] do %> 16 |
    17 |

    Modules

    18 |
    19 | <%= for module_node <- Enum.sort_by(nodes_map.modules, & &1.id) do 20 | api_reference_entry_template(module_node) 21 | end %> 22 |
    23 |
    24 | <% end %> 25 | 26 | <%= if nodes_map.tasks != [] do %> 27 |
    28 |

    Mix Tasks

    29 |
    30 | <%= for task_node <- nodes_map.tasks do 31 | api_reference_entry_template(task_node) 32 | end %> 33 |
    34 |
    35 | <% end %> 36 |
    37 | 38 | <%= footer_template(config, nil) %> -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/detail_template.eex: -------------------------------------------------------------------------------- 1 |
    2 | <%= for {default_name, default_arity} <- node.defaults do %> 3 | "> 4 | <% end %> 5 |
    6 | 7 | 8 | 9 |
    10 |

    <%=h node.signature %>

    11 | <%= for annotation <- node.annotations do %> 12 | (<%= annotation %>) 13 | <% end %> 14 | <%= if node.source_url do %> 15 | 16 | 17 | 18 | <% end %> 19 |
    20 |
    21 | <%= if deprecated = node.deprecated do %> 22 |
    23 | This <%= node.type %> is deprecated. <%= h(deprecated) %>. 24 |
    25 | <% end %> 26 | 27 |
    28 | <%= if node.specs != [] do %> 29 |
    30 | <%= for spec <- node.specs do %> 31 |
    <%= format_spec_attribute(module, node) %> <%= spec %>
    32 | <% end %> 33 |
    34 | <% end %> 35 | 36 | <%= render_doc(node.doc) %> 37 |
    38 |
    39 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/extra_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, node.title, false) %> 2 | <%= sidebar_template(config, node.type) %> 3 | 4 |
    5 |
    6 |

    <%= ExDoc.DocAST.to_string(node.title_doc) %>

    7 | <%= if node.type == :cheatmd do %> 8 | 12 | <% end %> 13 | <%= if node.source_url do %> 14 | 15 | 16 | View Source 17 | 18 | <% end %> 19 |
    20 | 21 | <%= if node.type == :livemd do %> 22 |
    23 | 24 | Run in Livebook 25 | 26 |
    27 | <% end %> 28 | 29 | <%= if node.type == :cheatmd do %> 30 | <%= node.doc |> ExDoc.DocAST.sectionize([:h2, :h3]) |> render_doc() %> 31 | <% else %> 32 | <%= node[:content] || render_doc(node.doc) %> 33 | <% end %> 34 |
    35 | 36 |
    37 |
    38 | <%= if refs.prev do %> 39 | 47 | <% end %> 48 |
    49 |
    50 | <%= if refs.next do %> 51 | 59 | <% end %> 60 |
    61 |
    62 | 63 | <%= footer_template(config, node.source_path) %> 64 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/footer_template.eex: -------------------------------------------------------------------------------- 1 | 39 | 40 | 41 | 42 | <%= before_closing_body_tag(config, :html) %> 43 | 44 | 45 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/head_template.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= if config.authors do %> 10 | "> 11 | <% end %> 12 | <%= if noindex do %> 13 | 14 | <% end %> 15 | <%= title %> — <%= config.project %> v<%= config.version %> 16 | <%= if config.favicon do %> 17 | 18 | <% end %> 19 | 20 | <%= if config.canonical do %> 21 | 22 | <% end %> 23 | 24 | 25 | 26 | <%= before_closing_head_tag(config, :html) %> 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/module_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, module.title, false) %> 2 | <%= sidebar_template(config, module.type) %> 3 | 4 |
    5 |
    6 |

    7 | <%= module.title %> <%= module_type(module) %> 8 | (<%= config.project %> v<%= config.version %>) 9 | <%= for annotation <- module.annotations do %> 10 | (<%= annotation %>) 11 | <% end %> 12 |

    13 | <%= if module.source_url do %> 14 | 15 | 16 | View Source 17 | 18 | <% end %> 19 |
    20 | 21 | <%= if deprecated = module.deprecated do %> 22 |
    23 | This <%= module.type %> is deprecated. <%= h(deprecated) %>. 24 |
    25 | <% end %> 26 | 27 | <%= if doc = module.doc do %> 28 |
    29 | <%= render_doc(doc) %> 30 |
    31 | <% end %> 32 |
    33 | 34 | <%= if summary != [] do %> 35 |
    36 |

    37 | 38 | 39 | 40 | Summary 41 |

    42 | <%= for {name, nodes} <- summary, do: summary_template(name, nodes) %> 43 |
    44 | <% end %> 45 | 46 | <%= for {name, nodes} <- summary, key = text_to_id(name) do %> 47 |
    48 |

    49 | 50 | 51 | 52 | <%= name %> 53 |

    54 |
    55 | <%= for node <- nodes, do: detail_template(node, module) %> 56 |
    57 |
    58 | <% end %> 59 | 60 | <%= footer_template(config, module.source_path) %> 61 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/not_found_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, "404", true) %> 2 | <%= sidebar_template(config, :extra) %> 3 | 4 |

    5 | Page not found 6 |

    7 | 8 |

    Sorry, but the page you were trying to get to, does not exist. You 9 | may want to try searching this site using the sidebar 10 | <%= if config.api_reference do %> 11 | or using our API Reference page 12 | <% end %> 13 | to find what you were looking for.

    14 | 15 | <%= footer_template(config, nil) %> 16 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/redirect_template.eex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= config.project %> v<%= config.version %> — Documentation 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/search_template.eex: -------------------------------------------------------------------------------- 1 | <%= head_template(config, "Search", true) %> 2 | <%= sidebar_template(config, :search) %> 3 | 4 | 11 | 12 | <%= footer_template(config, nil) %> 13 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/sidebar_template.eex: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 6 | 7 | 28 | 29 | 30 | 31 |
    32 |
    33 | 55 | -------------------------------------------------------------------------------- /lib/ex_doc/formatter/html/templates/summary_template.eex: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | <%= name %> 4 |

    5 | <%= for node <- nodes do %> 6 |
    7 |
    8 | <%=h node.signature %> 9 | <%= if deprecated = node.deprecated do %> 10 | deprecated 11 | <% end %> 12 |
    13 | <%= if doc = node.doc do %> 14 |
    <%= ExDoc.DocAST.synopsis(doc) %>
    15 | <% end %> 16 |
    17 | <% end %> 18 |
    19 | -------------------------------------------------------------------------------- /lib/ex_doc/group_matcher.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.GroupMatcher do 2 | # General helpers for dealing with grouping functionality. 3 | # Extracted for organization and testability. 4 | @moduledoc false 5 | 6 | @type pattern :: Regex.t() | module() | String.t() 7 | @type patterns :: pattern | [pattern] 8 | @type group_patterns :: keyword(patterns) 9 | 10 | @doc """ 11 | Finds the index of a given group. 12 | """ 13 | def index(groups, group) do 14 | Enum.find_index(groups, fn {k, _v} -> k == group end) || -1 15 | end 16 | 17 | @doc """ 18 | Group the following entries and while preserving the order in `groups`. 19 | """ 20 | def group_by(groups, entries, by) do 21 | entries = Enum.group_by(entries, by) 22 | 23 | {groups, leftovers} = 24 | Enum.flat_map_reduce(groups, entries, fn group, grouped_nodes -> 25 | case Map.pop(grouped_nodes, group, []) do 26 | {[], grouped_nodes} -> {[], grouped_nodes} 27 | {entries, grouped_nodes} -> {[{group, entries}], grouped_nodes} 28 | end 29 | end) 30 | 31 | groups ++ Enum.sort(leftovers) 32 | end 33 | 34 | @doc """ 35 | Finds a matching group for the given module name, id, and metadata. 36 | """ 37 | def match_module(group_patterns, module, id, metadata) do 38 | match_group_patterns(group_patterns, fn pattern -> 39 | case pattern do 40 | %Regex{} = regex -> Regex.match?(regex, id) 41 | string when is_binary(string) -> id == string 42 | atom when is_atom(atom) -> atom == module 43 | function when is_function(function) -> function.(metadata) 44 | end 45 | end) 46 | end 47 | 48 | @doc """ 49 | Finds a matching group for the given filename or url. 50 | """ 51 | def match_extra(group_patterns, path) do 52 | match_group_patterns(group_patterns, fn pattern -> 53 | case pattern do 54 | %Regex{} = regex -> Regex.match?(regex, path) 55 | string when is_binary(string) -> path == string 56 | end 57 | end) 58 | end 59 | 60 | defp match_group_patterns(group_patterns, matcher) do 61 | Enum.find_value(group_patterns, fn {group, patterns} -> 62 | patterns = List.wrap(patterns) 63 | Enum.any?(patterns, matcher) && group 64 | end) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/ex_doc/markdown.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Markdown do 2 | @moduledoc """ 3 | Adapter behaviour and conveniences for converting Markdown to HTML. 4 | 5 | ExDoc is compatible with any markdown processor that implements the 6 | functions defined in this module. The markdown processor can be changed 7 | via the `:markdown_processor` option in your `mix.exs`. 8 | 9 | ExDoc supports the following Markdown parsers out of the box: 10 | 11 | * [EarmarkParser](https://github.com/robertdober/earmark_parser) 12 | 13 | ExDoc uses EarmarkParser by default. 14 | """ 15 | 16 | @doc """ 17 | Converts markdown into HTML. 18 | """ 19 | @callback to_ast(String.t(), Keyword.t()) :: term() 20 | 21 | @doc """ 22 | Returns true if all dependencies necessary are available. 23 | """ 24 | @callback available?() :: boolean() 25 | 26 | @markdown_processors [ 27 | ExDoc.Markdown.Earmark 28 | ] 29 | 30 | @markdown_processor_key :markdown_processor 31 | 32 | @doc """ 33 | Converts the given markdown document to HTML AST. 34 | """ 35 | def to_ast(text, opts \\ []) when is_binary(text) do 36 | {processor, options} = get_markdown_processor() 37 | processor.to_ast(text, options |> Keyword.merge(opts)) 38 | end 39 | 40 | @doc """ 41 | Gets the current markdown processor set globally. 42 | """ 43 | def get_markdown_processor do 44 | case Application.fetch_env(:ex_doc, @markdown_processor_key) do 45 | {:ok, {processor, options}} -> 46 | {processor, options} 47 | 48 | :error -> 49 | processor = find_markdown_processor() || raise_no_markdown_processor() 50 | put_markdown_processor({processor, []}) 51 | {processor, []} 52 | end 53 | end 54 | 55 | @doc """ 56 | Changes the markdown processor globally. 57 | """ 58 | def put_markdown_processor(processor) when is_atom(processor) do 59 | put_markdown_processor({processor, []}) 60 | end 61 | 62 | def put_markdown_processor({processor, options}) do 63 | Application.put_env(:ex_doc, @markdown_processor_key, {processor, options}) 64 | end 65 | 66 | defp find_markdown_processor do 67 | Enum.find(@markdown_processors, fn module -> 68 | Code.ensure_loaded?(module) && module.available?() 69 | end) 70 | end 71 | 72 | defp raise_no_markdown_processor do 73 | raise """ 74 | Could not find a markdown processor to be used by ex_doc. 75 | You can either: 76 | 77 | * Add {:earmark, ">= 0.0.0"} to your mix.exs deps 78 | to use an Elixir-based markdown processor 79 | """ 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/ex_doc/nodes.ex: -------------------------------------------------------------------------------- 1 | # TODO: source_doc should only be a string once we remove application/html+erlang. 2 | defmodule ExDoc.ModuleNode do 3 | @moduledoc false 4 | 5 | defstruct id: nil, 6 | title: nil, 7 | nested_context: nil, 8 | nested_title: nil, 9 | module: nil, 10 | group: nil, 11 | deprecated: nil, 12 | doc: nil, 13 | source_doc: nil, 14 | source_format: nil, 15 | moduledoc_line: nil, 16 | moduledoc_file: nil, 17 | source_path: nil, 18 | source_url: nil, 19 | docs_groups: [], 20 | docs: [], 21 | typespecs: [], 22 | type: nil, 23 | language: nil, 24 | annotations: [], 25 | metadata: nil 26 | 27 | @typep annotation :: atom() 28 | 29 | @type t :: %__MODULE__{ 30 | id: String.t(), 31 | title: String.t(), 32 | nested_context: String.t() | nil, 33 | nested_title: String.t() | nil, 34 | module: module(), 35 | group: atom() | nil, 36 | deprecated: String.t() | nil, 37 | doc: ExDoc.DocAST.t() | nil, 38 | source_doc: term() | nil, 39 | source_format: String.t() | nil, 40 | moduledoc_line: non_neg_integer(), 41 | moduledoc_file: String.t(), 42 | source_path: String.t() | nil, 43 | source_url: String.t() | nil, 44 | docs_groups: [atom()], 45 | docs: [ExDoc.DocNode.t()], 46 | typespecs: [ExDoc.DocNode.t()], 47 | type: atom(), 48 | language: module(), 49 | annotations: [annotation()], 50 | metadata: map() 51 | } 52 | end 53 | 54 | defmodule ExDoc.DocNode do 55 | @moduledoc false 56 | 57 | defstruct id: nil, 58 | name: nil, 59 | arity: 0, 60 | defaults: [], 61 | deprecated: nil, 62 | doc: nil, 63 | source_doc: nil, 64 | type: nil, 65 | signature: nil, 66 | specs: [], 67 | annotations: [], 68 | group: nil, 69 | doc_line: nil, 70 | doc_file: nil, 71 | source_url: nil 72 | 73 | @typep annotation :: String.t() 74 | @typep function_default :: {name :: atom(), arity :: non_neg_integer()} 75 | 76 | @type t :: %__MODULE__{ 77 | id: String.t(), 78 | name: atom(), 79 | arity: non_neg_integer(), 80 | defaults: [function_default()], 81 | deprecated: String.t() | nil, 82 | doc: ExDoc.DocAST.t() | nil, 83 | source_doc: term() | nil, 84 | type: atom(), 85 | signature: String.t(), 86 | specs: [ExDoc.Language.spec_ast()], 87 | annotations: [annotation()], 88 | group: atom() | nil, 89 | doc_file: String.t(), 90 | doc_line: non_neg_integer(), 91 | source_url: String.t() | nil 92 | } 93 | end 94 | -------------------------------------------------------------------------------- /lib/ex_doc/shell_lexer.ex: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.ShellLexer do 2 | # Makeup lexer for sh, bash, etc commands. 3 | # The only thing it does is making the `$ ` prompt not selectable. 4 | @moduledoc false 5 | 6 | @behaviour Makeup.Lexer 7 | 8 | @impl true 9 | def lex(text, _opts) do 10 | text 11 | |> String.split("\n") 12 | |> Enum.flat_map(fn 13 | "$ " <> rest -> 14 | [ 15 | {:generic_prompt, %{selectable: false}, "$ "}, 16 | {:text, %{}, rest <> "\n"} 17 | ] 18 | 19 | text -> 20 | [{:text, %{}, text <> "\n"}] 21 | end) 22 | end 23 | 24 | @impl true 25 | def match_groups(_arg0, _arg1) do 26 | raise "not implemented yet" 27 | end 28 | 29 | @impl true 30 | def postprocess(_arg0, _arg1) do 31 | raise "not implemented yet" 32 | end 33 | 34 | @impl true 35 | def root(_arg0) do 36 | raise "not implemented yet" 37 | end 38 | 39 | @impl true 40 | def root_element(_arg0) do 41 | raise "not implemented yet" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/elixir-lang/ex_doc" 5 | @version "0.38.2" 6 | 7 | def project do 8 | [ 9 | app: :ex_doc, 10 | version: @version, 11 | elixir: "~> 1.15", 12 | deps: deps(), 13 | aliases: aliases(), 14 | package: package(), 15 | escript: escript(), 16 | elixirc_paths: elixirc_paths(Mix.env()), 17 | source_url: @source_url, 18 | test_elixirc_options: [docs: true, debug_info: true], 19 | test_ignore_filters: [&String.starts_with?(&1, "test/fixtures/")], 20 | name: "ExDoc", 21 | description: "ExDoc is a documentation generation tool for Elixir", 22 | docs: docs() 23 | ] 24 | end 25 | 26 | def cli do 27 | [preferred_envs: ["hex.publish": :prod]] 28 | end 29 | 30 | def application do 31 | [ 32 | extra_applications: [:eex] ++ extra_applications(Mix.env()), 33 | mod: {ExDoc.Application, []} 34 | ] 35 | end 36 | 37 | defp extra_applications(:test), do: [:edoc, :xmerl] 38 | defp extra_applications(_), do: [] 39 | 40 | defp deps do 41 | [ 42 | {:earmark_parser, "~> 1.4.44"}, 43 | {:makeup_elixir, "~> 0.14 or ~> 1.0"}, 44 | {:makeup_erlang, "~> 0.1 or ~> 1.0"}, 45 | # Add other makeup lexers as optional for the executable 46 | {:makeup_c, ">= 0.1.0", optional: true}, 47 | {:makeup_html, ">= 0.1.0", optional: true}, 48 | {:jason, "~> 1.2", only: :test}, 49 | {:lazy_html, "~> 0.1.0", only: :test} 50 | ] 51 | end 52 | 53 | defp aliases do 54 | [ 55 | build: ["cmd --cd assets npm run build", "compile --force", &docs/1], 56 | clean: [&clean_test_fixtures/1, "clean"], 57 | fix: ["format", "cmd --cd assets npm run lint:fix"], 58 | lint: ["format --check-formatted", "cmd --cd assets npm run lint"], 59 | setup: ["deps.get", "cmd --cd assets npm install"] 60 | ] 61 | end 62 | 63 | defp package do 64 | [ 65 | licenses: ["Apache-2.0"], 66 | maintainers: ["José Valim", "Milton Mazzarri", "Wojtek Mach"], 67 | files: ~w(CHANGELOG.md Cheatsheet.cheatmd formatters lib LICENSE mix.exs README.md), 68 | links: %{ 69 | "GitHub" => @source_url, 70 | "Changelog" => "https://hexdocs.pm/ex_doc/changelog.html", 71 | "Writing documentation" => "https://hexdocs.pm/elixir/writing-documentation.html" 72 | } 73 | ] 74 | end 75 | 76 | defp escript do 77 | [ 78 | main_module: ExDoc.CLI 79 | ] 80 | end 81 | 82 | defp elixirc_paths(:test), do: ["lib", "test/support"] 83 | defp elixirc_paths(_), do: ["lib"] 84 | 85 | defp docs do 86 | [ 87 | main: "readme", 88 | extras: 89 | [ 90 | "README.md", 91 | "Cheatsheet.cheatmd", 92 | "CHANGELOG.md" 93 | ] ++ test_dev_examples(Mix.env()), 94 | source_ref: "v#{@version}", 95 | source_url: @source_url, 96 | groups_for_modules: [ 97 | Markdown: [ 98 | ExDoc.Markdown, 99 | ExDoc.Markdown.Earmark 100 | ] 101 | ], 102 | groups_for_extras: [ 103 | Examples: ~r"test/examples" 104 | ], 105 | skip_undefined_reference_warnings_on: [ 106 | "CHANGELOG.md" 107 | ] 108 | ] 109 | end 110 | 111 | defp docs(args) do 112 | Mix.Task.run("docs", args) 113 | {text_tags, 0} = System.cmd("git", ["tag"]) 114 | 115 | [latest | _] = 116 | versions = 117 | for("v" <> rest <- String.split(text_tags), do: Version.parse!(rest)) 118 | |> Enum.sort({:desc, Version}) 119 | 120 | list_contents = 121 | Enum.map_intersperse(versions, ", ", fn version -> 122 | string = Version.to_string(version) 123 | ~s[{"version":"v#{string}", "url":"https://hexdocs.pm/ex_doc/#{string}"}] 124 | end) 125 | 126 | File.write!("doc/docs_config.js", """ 127 | var versionNodes = [#{list_contents}]; 128 | var searchNodes = [{"name":"ex_doc","version":"#{Version.to_string(latest)}"}]; 129 | """) 130 | end 131 | 132 | defp test_dev_examples(:dev), do: Path.wildcard("test/examples/*") 133 | defp test_dev_examples(_), do: [] 134 | 135 | defp clean_test_fixtures(_args) do 136 | File.rm_rf("test/tmp") 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, 3 | "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 4 | "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, 5 | "fine": {:hex, :fine, "0.1.0", "9bb99a5ff9b968f12c3b458fa1277c39e9a620b23a9439103703a25917293871", [:mix], [], "hexpm", "1d6485bf811b95dc6ae3d197c0e6f994880b86167a827983bb29cbfc03a02684"}, 6 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 7 | "lazy_html": {:hex, :lazy_html, "0.1.0", "619c4c124a7375ecbf66263de90270d221ffc7479afde436717a4e5cceaac954", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "a212f417b0e546055b7d5d72d302fce747b63ac9dfe0cf491c1f9af6d198e256"}, 8 | "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 9 | "makeup_c": {:hex, :makeup_c, "0.1.1", "14250b1a69770b1892f4113129417a2df098e2a72b9e1477aa9096e9e6c473a6", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "89e9cf45372822d354c19a7e18d77f84cfd70e2d206ac987eb15a1b8357f2869"}, 10 | "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 11 | "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, 12 | "makeup_html": {:hex, :makeup_html, "0.2.0", "9f810da8d43d625ccd3f7ea25997e588fa541d80e0a8c6b895157ad5c7e9ca13", [:mix], [{:makeup, "~> 1.2", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "0856f7beb9a6a642ab1307e06d990fe39f0ba58690d0b8e662aa2e027ba331b2"}, 13 | "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 | } 15 | -------------------------------------------------------------------------------- /test/ex_doc/formatter/html/erlang_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.Formatter.HTML.ErlangTest do 2 | use ExUnit.Case 3 | import TestHelper 4 | 5 | @moduletag :otp_eep48 6 | @moduletag :tmp_dir 7 | 8 | test "smoke test", c do 9 | erlc(c, :foo, ~S""" 10 | %% @doc 11 | %% foo module. 12 | -module(foo). 13 | -export([foo/1, bar/0]). 14 | -export_type([t/0, t2/0]). 15 | 16 | %% @doc 17 | %% f/0 function. 18 | -spec foo(t()) -> t(). 19 | foo(X) -> X. 20 | 21 | -spec bar() -> baz. 22 | bar() -> baz. 23 | 24 | -type t() :: atom(). 25 | %% t/0 type. 26 | 27 | -record(rec, {k1 :: any(), k2 :: any()}). 28 | 29 | -type t2() :: #rec{k1 :: uri_string:uri_string(), k2 :: uri_string:uri_string() | undefined}. 30 | """) 31 | 32 | doc = generate_docs(c) 33 | html = LazyHTML.to_html(doc) 34 | 35 | assert html =~ 36 | ~s|-spec foo(t()) -> t().| 37 | 38 | assert html =~ 39 | ~s|-type t() :: atom().| 40 | 41 | assert html =~ 42 | ~s|-type t2() :: #rec{k1 :: uri_string:uri_string(), k2 :: uri_string:uri_string() \| undefined}.| 43 | end 44 | 45 | defp generate_docs(c) do 46 | config = [ 47 | version: "1.0.0", 48 | project: "Foo", 49 | formatter: "html", 50 | output: Path.join(c.tmp_dir, "doc"), 51 | source_beam: Path.join(c.tmp_dir, "ebin"), 52 | extras: [] 53 | ] 54 | 55 | ExDoc.generate_docs(config[:project], config[:version], config) 56 | 57 | [c.tmp_dir, "doc", "foo.html"] 58 | |> Path.join() 59 | |> File.read!() 60 | |> LazyHTML.from_document() 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/ex_doc/group_matcher_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.GroupMatcherTest do 2 | use ExUnit.Case, async: true 3 | import ExDoc.GroupMatcher 4 | 5 | describe "group_by" do 6 | test "group by given data with leftovers" do 7 | assert group_by([1, 3, 5], [%{key: 1}, %{key: 3}, %{key: 2}], & &1.key) == [ 8 | {1, [%{key: 1}]}, 9 | {3, [%{key: 3}]}, 10 | {2, [%{key: 2}]} 11 | ] 12 | end 13 | end 14 | 15 | describe "module matching" do 16 | test "by atom names" do 17 | patterns = [ 18 | Group: [MyApp.SomeModule, :lists] 19 | ] 20 | 21 | assert match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule", %{}) == 22 | :Group 23 | 24 | assert match_module(patterns, :lists, ":lists", %{}) == :Group 25 | 26 | assert match_module( 27 | patterns, 28 | MyApp.SomeOtherModule, 29 | "MyApp.SomeOtherModule", 30 | %{} 31 | ) == 32 | nil 33 | end 34 | 35 | test "by string names" do 36 | patterns = [ 37 | Group: ["MyApp.SomeModule", ":lists"] 38 | ] 39 | 40 | assert match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule", %{}) == 41 | :Group 42 | 43 | assert match_module(patterns, :lists, ":lists", %{}) == :Group 44 | 45 | assert match_module(patterns, MyApp.SomeOtherModule, "MyApp.SomeOtherModule", %{}) == 46 | nil 47 | end 48 | 49 | test "by regular expressions" do 50 | patterns = [ 51 | Group: ~r/MyApp\..?/ 52 | ] 53 | 54 | assert match_module(patterns, MyApp.SomeModule, "MyApp.SomeModule", %{}) == 55 | :Group 56 | 57 | assert match_module(patterns, MyApp.SomeOtherModule, "MyApp.SomeOtherModule", %{}) == 58 | :Group 59 | 60 | assert match_module(patterns, MyAppWeb.SomeOtherModule, "MyAppWeb.SomeOtherModule", %{}) == 61 | nil 62 | end 63 | end 64 | 65 | describe "extras matching" do 66 | test "by string names" do 67 | patterns = [Group: ["docs/handling/testing.md"]] 68 | 69 | assert match_extra(patterns, "docs/handling/testing.md") == :Group 70 | refute match_extra(patterns, "docs/handling/setup.md") 71 | end 72 | 73 | test "by regular expressions" do 74 | patterns = [Group: ~r/docs\/handling?/] 75 | 76 | assert match_extra(patterns, "docs/handling/testing.md") == :Group 77 | assert match_extra(patterns, "docs/handling/setup.md") == :Group 78 | refute match_extra(patterns, "docs/introduction.md") 79 | end 80 | 81 | test "for extras with a url" do 82 | patterns = [Group: ~r/elixir/i] 83 | 84 | assert match_extra(patterns, "https://elixir-lang.org") == :Group 85 | refute match_extra(patterns, "https://example.com") 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /test/ex_doc/refs_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.RefsTest do 2 | use ExUnit.Case, async: true 3 | alias ExDoc.Refs 4 | 5 | defmodule InMemory do 6 | @type a_type() :: any 7 | 8 | @callback a_callback() :: :ok 9 | @macrocallback a_macrocallback() :: :ok 10 | 11 | def no_doc(), do: :no_doc 12 | 13 | @doc false 14 | def doc_false(), do: :doc_false 15 | 16 | @doc "doc..." 17 | def with_doc(), do: :with_doc 18 | end 19 | 20 | test "get_visibility/1" do 21 | assert Refs.get_visibility({:module, Code}) == :public 22 | assert Refs.get_visibility({:module, Code.Typespec}) == :hidden 23 | assert Refs.get_visibility({:module, Unknown}) == :undefined 24 | assert Refs.get_visibility({:module, InMemory}) == :limited 25 | 26 | assert Refs.get_visibility({:function, Enum, :join, 1}) == :public 27 | assert Refs.get_visibility({:function, Enum, :join, 2}) == :public 28 | assert Refs.get_visibility({:function, Code.Typespec, :spec_to_quoted, 2}) == :hidden 29 | assert Refs.get_visibility({:function, Code.Typespec, :spec_to_quoted, 9}) == :undefined 30 | assert Refs.get_visibility({:function, Enum, :join, 9}) == :undefined 31 | assert Refs.get_visibility({:function, Enum, :_join, 9}) == :undefined 32 | assert Refs.get_visibility({:function, :lists, :all, 2}) == :public 33 | assert Refs.get_visibility({:function, :lists, :all, 9}) == :undefined 34 | assert Refs.get_visibility({:function, :lists, :_all, 9}) == :undefined 35 | assert Refs.get_visibility({:function, InMemory, :with_doc, 0}) == :public 36 | assert Refs.get_visibility({:function, InMemory, :non_existent, 0}) == :undefined 37 | assert Refs.get_visibility({:function, WithModuleDoc, :no_doc, 0}) == :public 38 | assert Refs.get_visibility({:function, WithModuleDoc, :_no_doc, 0}) == :hidden 39 | assert Refs.get_visibility({:function, WithModuleDoc, :_doc_false, 0}) == :hidden 40 | 41 | # unable to read documentation, visibility is set to :public 42 | assert Refs.get_visibility({:function, InMemory, :no_doc, 0}) == :public 43 | assert Refs.get_visibility({:function, InMemory, :doc_false, 0}) == :public 44 | assert Refs.get_visibility({:function, InMemory, :with_doc, 0}) == :public 45 | assert Refs.get_visibility({:function, InMemory, :non_existent, 0}) == :undefined 46 | 47 | # macros are classified as functions 48 | assert Refs.get_visibility({:function, Kernel, :def, 2}) == :public 49 | assert Refs.get_visibility({:function, Unknown, :unknown, 0}) == :undefined 50 | 51 | assert Refs.get_visibility({:type, String, :t, 0}) == :public 52 | assert Refs.get_visibility({:type, String, :t, 1}) == :undefined 53 | assert Refs.get_visibility({:type, :sets, :set, 0}) == :public 54 | assert Refs.get_visibility({:type, :sets, :set, 9}) == :undefined 55 | assert Refs.get_visibility({:type, WithoutModuleDoc, :a_type, 0}) == :public 56 | 57 | # types cannot be read for inmemory modules 58 | assert Refs.get_visibility({:type, InMemory, :a_type, 0}) == :undefined 59 | 60 | assert Refs.get_visibility({:callback, GenServer, :handle_call, 3}) == :public 61 | assert Refs.get_visibility({:callback, GenServer, :handle_call, 9}) == :undefined 62 | assert Refs.get_visibility({:callback, :gen_server, :handle_call, 3}) == :public 63 | assert Refs.get_visibility({:callback, :gen_server, :handle_call, 9}) == :undefined 64 | assert Refs.get_visibility({:callback, InMemory, :a_callback, 0}) == :public 65 | assert Refs.get_visibility({:callback, WithModuleDoc, :a_macrocallback, 0}) == :public 66 | assert Refs.get_visibility({:callback, WithoutModuleDoc, :a_callback, 0}) == :public 67 | assert Refs.get_visibility({:callback, WithoutModuleDoc, :a_macrocallback, 0}) == :public 68 | assert Refs.get_visibility({:callback, InMemory, :a_callback, 9}) == :undefined 69 | end 70 | 71 | test "insert_from_chunk/2 with module that doesn't exist" do 72 | result = Code.fetch_docs(DoesNotExists) 73 | assert :ok = ExDoc.Refs.insert_from_chunk(DoesNotExists, result) 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /test/ex_doc/utils_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDoc.UtilsTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest ExDoc.Utils 5 | alias ExDoc.Utils 6 | 7 | test "natural_sort_by" do 8 | input = ~w| 9 | reraise/2 10 | reraise/3 11 | send/2 12 | sigil_C/2 13 | sigil_D/2 14 | sigil_c/2 15 | λ/1 16 | spawn/1 17 | spawn/3 18 | Λ/1 19 | abc/1 20 | a2/1 21 | a1/1 22 | a0/1 23 | a10/1 24 | a1_0/1 25 | a_0/1 26 | äpfeln/1 27 | Äpfeln/1 28 | умножить/1 29 | Вычесть/1 30 | вычесть/1 31 | rm?/1 32 | rm!/1 33 | rm/1 34 | | 35 | 36 | assert Utils.natural_sort_by(input, & &1) == ~w| 37 | Äpfeln/1 38 | a0/1 39 | a1/1 40 | a1_0/1 41 | a2/1 42 | a10/1 43 | a_0/1 44 | abc/1 45 | äpfeln/1 46 | reraise/2 47 | reraise/3 48 | rm/1 49 | rm!/1 50 | rm?/1 51 | send/2 52 | sigil_C/2 53 | sigil_c/2 54 | sigil_D/2 55 | spawn/1 56 | spawn/3 57 | Λ/1 58 | λ/1 59 | Вычесть/1 60 | вычесть/1 61 | умножить/1 62 | | 63 | end 64 | 65 | test "to_json" do 66 | map = %{ 67 | nil: nil, 68 | true: true, 69 | false: false, 70 | atom: :hello, 71 | string: "world", 72 | string_with_quotes: "hello \" world", 73 | list: [ 74 | %{foo: "bar"}, 75 | %{baz: "bat"} 76 | ], 77 | integer: 1 78 | } 79 | 80 | assert map |> Utils.to_json() |> IO.iodata_to_binary() == Jason.encode!(map) 81 | 82 | string = for i <- 0..0x1F, do: <>, into: "" 83 | assert string |> Utils.to_json() |> IO.iodata_to_binary() == Jason.encode!(string) 84 | end 85 | 86 | test "strip_tags" do 87 | assert Utils.strip_tags("Hello World!
    ") == "Hello World!" 88 | assert Utils.strip_tags("Go back") == "Go back" 89 | assert Utils.strip_tags("Git opts (:git)") == "Git opts (:git)" 90 | assert Utils.strip_tags("

    P1.

    P2

    ") == "P1.P2" 91 | assert Utils.strip_tags("

    P1.

    P2

    ", " ") == " P1. P2 " 92 | assert Utils.strip_tags("<%= @inner_content %>", " ") == "<%= @inner_content %>" 93 | end 94 | 95 | test "text_to_id" do 96 | assert Utils.text_to_id("“Stale”") == "stale" 97 | assert Utils.text_to_id("José") == "josé" 98 | assert Utils.text_to_id(" a - b ") == "a-b" 99 | assert Utils.text_to_id("foo.img") == "foo-img" 100 | assert Utils.text_to_id(" ☃ ") == "" 101 | assert Utils.text_to_id(" ² ") == "" 102 | assert Utils.text_to_id(" ⏜ ") == "" 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /test/ex_doc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExDocTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :tmp_dir 5 | 6 | # Simple retriever that returns whatever is passed into it 7 | defmodule IdentityRetriever do 8 | def docs_from_dir(source, config) do 9 | {source, config} 10 | end 11 | end 12 | 13 | # Simple formatter that returns whatever is passed into it 14 | defmodule IdentityFormatter do 15 | def run(modules, _filtered, config) do 16 | {modules, config} 17 | end 18 | end 19 | 20 | test "uses custom markdown processor", %{tmp_dir: tmp_dir} do 21 | project = "Elixir" 22 | version = "1" 23 | 24 | options = [ 25 | apps: [:test_app], 26 | formatter: IdentityFormatter, 27 | markdown_processor: Sample, 28 | output: tmp_dir <> "/ex_doc", 29 | retriever: IdentityRetriever, 30 | source_beam: "beam_dir" 31 | ] 32 | 33 | ExDoc.generate_docs(project, version, options) 34 | assert Application.fetch_env!(:ex_doc, :markdown_processor) == {Sample, []} 35 | after 36 | Application.delete_env(:ex_doc, :markdown_processor) 37 | end 38 | 39 | test "uses custom markdown processor with custom options", %{tmp_dir: tmp_dir} do 40 | project = "Elixir" 41 | version = "1" 42 | 43 | options = [ 44 | apps: [:test_app], 45 | formatter: IdentityFormatter, 46 | markdown_processor: {Sample, [foo: :bar]}, 47 | output: tmp_dir <> "/ex_doc", 48 | retriever: IdentityRetriever, 49 | source_beam: "beam_dir" 50 | ] 51 | 52 | ExDoc.generate_docs(project, version, options) 53 | assert Application.fetch_env!(:ex_doc, :markdown_processor) == {Sample, [foo: :bar]} 54 | after 55 | Application.delete_env(:ex_doc, :markdown_processor) 56 | end 57 | 58 | test "source_beam sets source dir" do 59 | options = [ 60 | apps: [:test_app], 61 | formatter: IdentityFormatter, 62 | retriever: IdentityRetriever, 63 | source_beam: "beam_dir" 64 | ] 65 | 66 | assert {source_dir, _config} = ExDoc.generate_docs("Elixir", "1", options) 67 | assert source_dir == options[:source_beam] 68 | end 69 | 70 | test "formatter module not found" do 71 | project = "Elixir" 72 | version = "1" 73 | 74 | options = [ 75 | apps: [:test_app], 76 | formatter: "pdf", 77 | retriever: IdentityRetriever, 78 | source_beam: "beam_dir" 79 | ] 80 | 81 | assert_raise RuntimeError, 82 | "formatter module \"pdf\" not found", 83 | fn -> ExDoc.generate_docs(project, version, options) end 84 | end 85 | 86 | test "version" do 87 | assert {:ok, _version} = Version.parse(ExDoc.version()) 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /test/examples/admonition.md: -------------------------------------------------------------------------------- 1 | # Admonition 2 | 3 | ## Starting with header 3 4 | 5 | > ### Just a blockquote. 6 | > 7 | > Some `code` and a [link](#). 8 | > 9 | > ```elixir 10 | > foo + bar 11 | > ``` 12 | > 13 | > ```text 14 | > no highlight 15 | > ``` 16 | 17 | > ### Header 3 {: .info} 18 | > 19 | > #### Header 4 20 | > 21 | > Some `code` and a [link](#). 22 | > 23 | > ```elixir 24 | > foo + bar 25 | > ``` 26 | > 27 | > ```text 28 | > no highlight 29 | > ``` 30 | 31 | > ### Header 3 {: .tip} 32 | > 33 | > #### Header 4 34 | > 35 | > Some `code` and a [link](#). 36 | > 37 | > 38 | > ```elixir 39 | > foo + bar 40 | > ``` 41 | > 42 | > ```text 43 | > no highlight 44 | > ``` 45 | 46 | > ### Header 3 {: .neutral} 47 | > 48 | > #### Header 4 49 | > 50 | > Some `code` and a [link](#). 51 | > 52 | > ```elixir 53 | > foo + bar 54 | > ``` 55 | > 56 | > ```text 57 | > no highlight 58 | > ``` 59 | 60 | > ### Header 3 {: .warning} 61 | > 62 | > #### Header 4 63 | > 64 | > Some `code` and a [link](#). 65 | > 66 | > ```elixir 67 | > foo + bar 68 | > ``` 69 | > 70 | > ```text 71 | > no highlight 72 | > ``` 73 | 74 | > ### Header 3 {: .error} 75 | > 76 | > #### Header 4 77 | > 78 | > Some `code` and a [link](#). 79 | > 80 | > ```elixir 81 | > foo + bar 82 | > ``` 83 | > 84 | > ```text 85 | > no highlight 86 | > ``` 87 | 88 | ## Starting with header 4 89 | 90 | > #### Just a blockquote. 91 | > 92 | > Some `code` and a [link](#). 93 | > 94 | > ```erlang 95 | > Foo + Bar. 96 | > ``` 97 | 98 | > #### Header 4 {: .info} 99 | > 100 | > Some `code` and a [link](#). 101 | > 102 | > ```erlang 103 | > Foo + Bar. 104 | > ``` 105 | 106 | > #### Header 4 {: .tip} 107 | > 108 | > Some `code` and a [link](#). 109 | > 110 | > ```erlang 111 | > Foo + Bar. 112 | > ``` 113 | 114 | > #### Header 4 {: .neutral} 115 | > 116 | > Some `code` and a [link](#). 117 | > 118 | > ```erlang 119 | > Foo + Bar. 120 | > ``` 121 | 122 | > #### Header 4 {: .warning} 123 | > 124 | > Some `code` and a [link](#). 125 | > 126 | > ```erlang 127 | > Foo + Bar. 128 | > ``` 129 | 130 | > #### Header 4 {: .error} 131 | > 132 | > Some `code` and a [link](#). 133 | > 134 | > ```erlang 135 | > Foo + Bar. 136 | > ``` 137 | -------------------------------------------------------------------------------- /test/fixtures/ExtraPage.md: -------------------------------------------------------------------------------- 1 | # Extra Page Title 2 | 3 | some text 4 | 5 | ## Section One 6 | 7 | more text 8 | 9 | # Another H1 10 | 11 | even more text 12 | 13 | ## Section Two 14 | 15 | final text 16 | -------------------------------------------------------------------------------- /test/fixtures/ExtraPageWithSettextHeader.md: -------------------------------------------------------------------------------- 1 | Extra Page Title [![Build Status](https://travis-ci.org/username/project_name.svg?branch=master)](https://travis-ci.org/username/project_name) [![Coverage Status](https://coveralls.io/repos/github/username/project_name/badge.svg?branch=master&cache=1)](https://coveralls.io/github/username/project_name) [![hex.pm version](https://img.shields.io/hexpm/v/project_name.svg)](https://hex.pm/packages/project_name) [![hex.pm downloads](https://img.shields.io/hexpm/dt/project_name.svg)](https://hex.pm/packages/project_name) 2 | ================ 3 | 4 | some text 5 | 6 | Section One 7 | ----------- 8 | 9 | more text 10 | 11 | Another H1 12 | ========== 13 | 14 | even more text 15 | 16 | Section Two 17 | ----------- 18 | 19 | final text 20 | -------------------------------------------------------------------------------- /test/fixtures/LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this file except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /test/fixtures/LivebookFile.livemd: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Title for Livebook Files 4 | 5 | Read `.livemd` files generated by [livebook](https://github.com/livebook-dev/livebook). -------------------------------------------------------------------------------- /test/fixtures/PlainText.txt: -------------------------------------------------------------------------------- 1 | This is plain 2 | text and nothing 3 | should be linked `Kernel` 4 | 5 | `t:term/0` 6 | 7 | ## Neither formatted 8 | 9 | good bye 10 | -------------------------------------------------------------------------------- /test/fixtures/PlainTextFiles.md: -------------------------------------------------------------------------------- 1 | # Plain Text Files 2 | 3 | Read the [license](LICENSE) and the [plain-text file](PlainText.txt). -------------------------------------------------------------------------------- /test/fixtures/README.md: -------------------------------------------------------------------------------- 1 | `RandomError` 2 | `CustomBehaviourImpl.hello/1` 3 | `TypesAndSpecs.Sub` 4 | `t:atom/0` 5 | `mix compile.elixir` 6 | 7 | ## Heading without content 8 | 9 | ## `Header` sample 10 | 11 | hello 12 | 13 | 14 | 15 | ## more > than 16 | 17 |

    raw content

    18 | -------------------------------------------------------------------------------- /test/fixtures/behaviour.ex: -------------------------------------------------------------------------------- 1 | defmodule CustomBehaviourOne do 2 | # Even if we define a struct, this module should still be listed as a behaviour. 3 | defstruct [:a, :b] 4 | 5 | @doc """ 6 | This is a sample callback. 7 | """ 8 | @callback hello(%URI{}) :: integer 9 | @callback greet(integer | String.t()) :: integer 10 | end 11 | 12 | defmodule CustomBehaviourTwo do 13 | @doc """ 14 | This is a different sample callback. 15 | """ 16 | @macrocallback bye(integer) :: integer 17 | end 18 | 19 | defmodule CustomBehaviourImpl do 20 | @behaviour CustomBehaviourOne 21 | @behaviour CustomBehaviourTwo 22 | 23 | def hello(i), do: i 24 | 25 | @doc "A doc so it doesn't use 'Callback implementation for'" 26 | def greet(i), do: i 27 | 28 | defmacro bye(i), do: i 29 | end 30 | -------------------------------------------------------------------------------- /test/fixtures/callbacks_no_docs.ex: -------------------------------------------------------------------------------- 1 | defmodule CallbacksNoDocs do 2 | @callback connect(params :: map, String.t()) :: {:ok, String.t()} | :error 3 | 4 | @doc deprecated: "Use another id", since: "1.3.0" 5 | @callback id(String.t()) :: String.t() | nil 6 | 7 | @optional_callbacks id: 1 8 | end 9 | -------------------------------------------------------------------------------- /test/fixtures/cheatsheets.cheatmd: -------------------------------------------------------------------------------- 1 | ## Getting started 2 | 3 | ### Hello world 4 | 5 | ```elixir 6 | # hello.exs 7 | defmodule Greeter do 8 | def greet(name) do 9 | message = "Hello, " <> name <> "!" 10 | IO.puts message 11 | end 12 | end 13 | 14 | Greeter.greet("world") 15 | ``` 16 | 17 | ```bash 18 | elixir hello.exs 19 | # Hello, world! 20 | ``` 21 | 22 | ### Variables 23 | 24 | ```elixir 25 | age = 23 26 | ``` 27 | 28 | ## Types 29 | 30 | ### Operators 31 | 32 | ```elixir 33 | left != right # equal 34 | left !== right # match 35 | left ++ right # concat lists 36 | left <> right # concat string/binary 37 | left =~ right # regexp 38 | ``` 39 | -------------------------------------------------------------------------------- /test/fixtures/common_nesting_prefix.ex: -------------------------------------------------------------------------------- 1 | defmodule Common.Nesting.Prefix.B.A do 2 | @moduledoc "moduledoc" 3 | end 4 | 5 | defmodule Common.Nesting.Prefix.B.B.A do 6 | @moduledoc "moduledoc" 7 | end 8 | 9 | defmodule Common.Nesting.Prefix.B.C do 10 | @moduledoc "moduledoc" 11 | end 12 | 13 | defmodule Common.Nesting.Prefix.C do 14 | @moduledoc "moduledoc" 15 | end 16 | -------------------------------------------------------------------------------- /test/fixtures/compiled_with_docs.ex: -------------------------------------------------------------------------------- 1 | defmodule CompiledWithDocs do 2 | @moduledoc """ 3 | moduledoc 4 | 5 | ## Example ☃ Unicode > escaping 6 | CompiledWithDocs.example 7 | 8 | ### Example H3 heading 9 | 10 | example 11 | """ 12 | 13 | @moduledoc tags: :example_module_tag 14 | 15 | @doc "Some struct" 16 | defstruct [:field] 17 | 18 | @doc "Some example" 19 | @doc purpose: :example 20 | @deprecated "Use something else instead" 21 | def example(foo, bar \\ Baz), do: bar.baz(foo) 22 | 23 | @doc "Another example" 24 | @doc since: "1.3.0" 25 | defmacro example_1, do: 1 26 | 27 | @doc "A simple guard" 28 | defguard is_zero(number) when number == 0 29 | 30 | @doc """ 31 | Does example action. 32 | 33 | ### Examples 34 | """ 35 | @doc purpose: :example 36 | def example_with_h3, do: 1 37 | 38 | @deprecated "Use something else instead" 39 | def example_without_docs, do: nil 40 | 41 | # Check that delegate autogenerate docs 42 | defdelegate flatten(hello), to: List 43 | 44 | def unquote(:"name/with/slashes")(), do: :ok 45 | 46 | defmodule Nested do 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/fixtures/compiled_without_docs.ex: -------------------------------------------------------------------------------- 1 | defmodule CompiledWithoutDocs do 2 | @doc false 3 | def example, do: 1 4 | end 5 | -------------------------------------------------------------------------------- /test/fixtures/duplicate_headings.ex: -------------------------------------------------------------------------------- 1 | defmodule DuplicateHeadings do 2 | @moduledoc """ 3 | moduledoc 4 | 5 | ## One 6 | 7 | ## Two 8 | 9 | ## One 10 | 11 | ## Two 12 | 13 | ## One 14 | 15 | ## Two 16 | 17 | """ 18 | end 19 | -------------------------------------------------------------------------------- /test/fixtures/elixir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elixir-lang/ex_doc/16647077462448f4276707016634718b39b03397/test/fixtures/elixir.png -------------------------------------------------------------------------------- /test/fixtures/overlapping_defaults.ex: -------------------------------------------------------------------------------- 1 | defmodule OverlappingDefaults do 2 | @moduledoc """ 3 | Overlapping default functions 4 | """ 5 | 6 | @doc "Basic example" 7 | def overlapping_defaults(one, two) when is_list(two), 8 | do: {one, two} 9 | 10 | @doc "Third default arg overrides previous def clause" 11 | def overlapping_defaults(one, two, three \\ []), 12 | do: {one, two, three} 13 | 14 | def two_defaults(one, two) when is_atom(one) and is_atom(two), 15 | do: {one, two} 16 | 17 | @doc "Two default args" 18 | def two_defaults(one, two, three \\ [], four \\ []) 19 | when is_list(one) and is_list(two) and is_list(three) and is_list(four), 20 | do: {one, two, three, four} 21 | 22 | def special_case(one, two) when is_atom(one) and is_atom(two), 23 | do: {one, two} 24 | 25 | @doc "This function defines an arity that is less than the one in the previous clause" 26 | def special_case(one, two \\ [], three \\ [], four \\ []) 27 | when is_list(one) and is_list(two) and is_list(three) and is_list(four), 28 | do: {one, two, three, four} 29 | 30 | defmacro in_the_middle(foo, bar) when is_list(foo) and is_list(bar), 31 | do: quote(do: {unquote(foo), unquote(bar)}) 32 | 33 | @doc "default arg is in the middle" 34 | defmacro in_the_middle(foo, bar \\ Baz, baz), 35 | do: quote(do: {unquote(foo), unquote(bar), unquote(baz)}) 36 | end 37 | -------------------------------------------------------------------------------- /test/fixtures/protocol.ex: -------------------------------------------------------------------------------- 1 | defprotocol CustomProtocol do 2 | @moduledoc """ 3 | See `plus_one/1`. 4 | """ 5 | 6 | def plus_one(foo) 7 | def plus_two(bar) 8 | end 9 | 10 | defimpl CustomProtocol, for: Integer do 11 | @doc """ 12 | Special plus one docs 13 | """ 14 | def plus_one(int), do: int + 1 15 | def plus_two(int), do: int + 2 16 | end 17 | -------------------------------------------------------------------------------- /test/fixtures/random_error.ex: -------------------------------------------------------------------------------- 1 | defmodule RandomError do 2 | defexception [:message] 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/task_with_docs.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.TaskWithDocs do 2 | @moduledoc """ 3 | Very useful task 4 | """ 5 | 6 | def run(args) do 7 | args 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/fixtures/types_and_specs.ex: -------------------------------------------------------------------------------- 1 | defmodule TypesAndSpecs do 2 | defmodule Sub do 3 | @type t :: any 4 | end 5 | 6 | @moduledoc """ 7 | Types and tests fixture. 8 | 9 | Basic type: `t:atom/0`. 10 | """ 11 | 12 | @typedoc "A public type" 13 | @type public(t) :: {t, String.t(), Sub.t(), opaque, :ok | :error} 14 | @typep private :: any 15 | @opaque opaque :: {Dict.t()} 16 | @typedoc false 17 | @type internal :: any 18 | 19 | @spec add(integer, opaque) :: integer 20 | def add(x, _), do: x + x 21 | 22 | @spec minus(integer, integer) :: integer 23 | defp minus(x, y), do: x - y 24 | 25 | @spec macro_spec(any) :: {:ok, any} 26 | defmacro macro_spec(v), do: {:ok, v} 27 | 28 | @spec macro_with_spec(v) :: {:ok, v} when v: any() 29 | defmacro macro_with_spec(v), do: {:ok, v} 30 | 31 | @spec priv_macro_spec(any) :: {:no, any} 32 | defmacrop priv_macro_spec(v), do: {:no, v} 33 | 34 | # This is just to ignore warnings about unused private types/functions. 35 | @spec ignore(private) :: integer 36 | def ignore(_), do: priv_macro_spec(minus(0, 0)) 37 | end 38 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/bar/lib/bar.ex: -------------------------------------------------------------------------------- 1 | defmodule Bar do 2 | def bar do 3 | "hello world" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/bar/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Bar.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :bar, 7 | version: "0.1.0" 8 | ] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/foo/lib/foo.ex: -------------------------------------------------------------------------------- 1 | defmodule Foo do 2 | def foo do 3 | "bye world" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/apps/foo/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Foo.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :foo, 7 | version: "0.1.0" 8 | ] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /test/fixtures/umbrella/mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Umbrella.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | apps_path: "apps", 7 | deps: deps() 8 | ] 9 | end 10 | 11 | defp deps do 12 | [] 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/fixtures/warnings.ex: -------------------------------------------------------------------------------- 1 | defmodule Warnings do 2 | @moduledoc """ 3 | moduledoc `Warnings.bar/0` 4 | """ 5 | 6 | @moduledoc deprecated: "Use something without warnings" 7 | 8 | @typedoc """ 9 | typedoc `Warnings.bar/0` 10 | """ 11 | @type t() :: :ok 12 | 13 | @doc """ 14 | doc callback `Warnings.bar/0` 15 | """ 16 | @callback handle_foo() :: :ok 17 | 18 | @doc """ 19 | doc `Warnings.bar/0` 20 | """ 21 | def foo(), do: :ok 22 | end 23 | -------------------------------------------------------------------------------- /test/prerelease.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PKG=test/tmp 3 | 4 | # build package 5 | mix hex.build 6 | 7 | TAR=$(ls ex_doc-*.tar | head -n 1) 8 | 9 | # extract package 10 | rm -rf $PKG && mkdir -p $PKG/contents && tar xf $TAR -C $PKG && tar xzf $PKG/contents.tar.gz -C $PKG/contents 11 | 12 | # compile and build docs 13 | cd $PKG/contents 14 | MIX_ENV=prod mix do deps.get --only prod + compile + docs 15 | MIX_ENV=prod mix docs --proglang erlang --output doc/erlang 16 | 17 | # run assertions 18 | test -f doc/index.html || echo "doc/index.html missing" 19 | test -f doc/erlang/index.html || echo "doc/erlang/index.html missing" 20 | test -f doc/ExDoc.epub || echo "doc/ExDoc.epub missing" 21 | -------------------------------------------------------------------------------- /test/support/with_without_module_doc.ex: -------------------------------------------------------------------------------- 1 | defmodule WithModuleDoc do 2 | @moduledoc "Documentation for `WithModuleDoc`" 3 | 4 | @type a_type() :: any 5 | 6 | @callback a_callback() :: :ok 7 | @macrocallback a_macrocallback() :: :ok 8 | 9 | def no_doc(), do: :no_doc 10 | def _no_doc(), do: :no_doc 11 | 12 | @doc false 13 | def doc_false(), do: :doc_false 14 | 15 | @doc false 16 | def _doc_false(), do: :doc_false 17 | 18 | @doc "doc..." 19 | def with_doc(), do: :with_doc 20 | 21 | @doc "doc..." 22 | def _with_doc(), do: :with_doc 23 | end 24 | 25 | defmodule WithoutModuleDoc do 26 | @type a_type() :: any 27 | 28 | @callback a_callback() :: :ok 29 | @macrocallback a_macrocallback() :: :ok 30 | 31 | def no_doc(), do: :no_doc 32 | def _no_doc(), do: :no_doc 33 | 34 | @doc false 35 | def doc_false(), do: :doc_false 36 | 37 | @doc false 38 | def _doc_false(), do: :doc_false 39 | 40 | @doc "doc..." 41 | def with_doc(), do: :with_doc 42 | 43 | @doc "doc..." 44 | def _with_doc(), do: :with_doc 45 | end 46 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | otp_eep48? = Code.ensure_loaded?(:edoc_doclet_chunks) 2 | otp_eep59? = Code.ensure_loaded?(:beam_doc) 3 | 4 | exclude = [ 5 | otp_eep48: not otp_eep48?, 6 | otp_eep59: not otp_eep59?, 7 | otp_has_docs: not match?({:docs_v1, _, _, _, _, _, _}, Code.fetch_docs(:array)) 8 | ] 9 | 10 | ExUnit.start(exclude: Enum.filter(exclude, &elem(&1, 1))) 11 | 12 | # Prepare module fixtures 13 | File.rm_rf!("test/tmp") 14 | File.mkdir_p!("test/tmp/beam") 15 | Code.prepend_path("test/tmp/beam") 16 | 17 | # Compile module fixtures 18 | "test/fixtures/*.ex" 19 | |> Path.wildcard() 20 | |> Kernel.ParallelCompiler.compile_to_path("test/tmp/beam", return_diagnostics: true) 21 | 22 | defmodule TestHelper do 23 | def elixirc(context, filename \\ "nofile", code) do 24 | dir = context.tmp_dir 25 | 26 | src_path = Path.join([dir, filename]) 27 | src_path |> Path.dirname() |> File.mkdir_p!() 28 | File.write!(src_path, code) 29 | 30 | ebin_dir = Path.join(dir, "ebin") 31 | File.mkdir_p!(ebin_dir) 32 | 33 | {:ok, modules, _} = 34 | Kernel.ParallelCompiler.compile_to_path([src_path], ebin_dir, return_diagnostics: true) 35 | 36 | true = Code.prepend_path(ebin_dir) 37 | 38 | ExUnit.Callbacks.on_exit(fn -> 39 | for module <- modules do 40 | :code.purge(module) 41 | :code.delete(module) 42 | end 43 | 44 | File.rm_rf!(dir) 45 | end) 46 | 47 | modules 48 | end 49 | 50 | def erlc(context, module, code, opts \\ []) do 51 | dir = context.tmp_dir 52 | 53 | docs = Keyword.get(opts, :docs, true) 54 | 55 | src_path = Path.join([dir, "#{module}.erl"]) 56 | src_path |> Path.dirname() |> File.mkdir_p!() 57 | File.write!(src_path, code) 58 | 59 | ebin_dir = Path.join(dir, "ebin") 60 | File.mkdir_p!(ebin_dir) 61 | 62 | beam_docs = docstrings(docs, context) 63 | 64 | {:ok, module} = 65 | :compile.file( 66 | String.to_charlist(src_path), 67 | [ 68 | :return_errors, 69 | :debug_info, 70 | outdir: String.to_charlist(ebin_dir) 71 | ] ++ beam_docs 72 | ) 73 | 74 | true = Code.prepend_path(ebin_dir) 75 | {:module, ^module} = :code.load_file(module) 76 | 77 | ExUnit.Callbacks.on_exit(fn -> 78 | :code.purge(module) 79 | :code.delete(module) 80 | File.rm_rf!(dir) 81 | ExDoc.Refs.clear() 82 | end) 83 | 84 | if docs && !context[:otp_eep59] do 85 | edoc_to_chunk(module) 86 | end 87 | 88 | [module] 89 | end 90 | 91 | if otp_eep59? do 92 | def docstrings(docs, context) do 93 | if docs && context[:otp_eep59] do 94 | [] 95 | else 96 | [:no_docs] 97 | end 98 | end 99 | else 100 | def docstrings(docs, context) do 101 | if docs && context[:otp_eep59] do 102 | raise "not supported" 103 | else 104 | [] 105 | end 106 | end 107 | end 108 | 109 | if otp_eep48? do 110 | def edoc_to_chunk(module) do 111 | source_path = module.module_info(:compile)[:source] 112 | dir = :filename.dirname(source_path) 113 | 114 | :ok = 115 | :edoc.files([source_path], 116 | preprocess: true, 117 | doclet: :edoc_doclet_chunks, 118 | layout: :edoc_layout_chunks, 119 | dir: dir ++ ~c"/doc" 120 | ) 121 | 122 | module 123 | end 124 | else 125 | def edoc_to_chunk(_) do 126 | raise "not supported" 127 | end 128 | end 129 | end 130 | --------------------------------------------------------------------------------