├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .husky └── pre-commit ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── eslint.config.mjs ├── lib ├── tldr-lint-cli.js ├── tldr-lint.js └── tldr-parser.js ├── package-lock.json ├── package.json ├── specs ├── .eslintrc.json ├── pages │ ├── failing │ │ ├── 107 │ │ ├── 001.md │ │ ├── 002.md │ │ ├── 003.md │ │ ├── 004.md │ │ ├── 005.md │ │ ├── 006.md │ │ ├── 007.md │ │ ├── 008.md │ │ ├── 009.md │ │ ├── 010.md │ │ ├── 011.md │ │ ├── 012.md │ │ ├── 013.md │ │ ├── 014.md │ │ ├── 015.md │ │ ├── 016.md │ │ ├── 017.md │ │ ├── 018.md │ │ ├── 019.md │ │ ├── 020.md │ │ ├── 021.md │ │ ├── 101.md │ │ ├── 102.md │ │ ├── 103.md │ │ ├── 104.md │ │ ├── 105.md │ │ ├── 106.md │ │ ├── 108 .md │ │ ├── 109A.md │ │ ├── 110.md │ │ └── 111.md │ └── passing │ │ ├── !.md │ │ ├── bracket.md │ │ ├── descriptions.md │ │ ├── lower-case.md │ │ ├── special-characters.md │ │ └── title++.md ├── tldr-lint-helper.js └── tldr-lint.spec.js ├── tldr.l └── tldr.yy /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | # Required for TLDR010 (Only Unix-style line endings allowed) 4 | specs/pages/failing/010.md binary 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Enable version updates for npm 5 | - package-ecosystem: "npm" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | 10 | # Enable version updates for GitHub actions 11 | - package-ecosystem: "github-actions" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow automatically publishes the package to NPM and GHCR when a new release is created. 2 | # Before, creating a new release, make sure to update the package version in package.json 3 | # and add a Granular Access Token (with read and write packages scope) 4 | # to the repository secrets with the name NPM_TOKEN. 5 | # Once, the release has been published remove it from the repository secrets. 6 | 7 | name: Publish 8 | on: 9 | release: 10 | types: [published] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | publish-npm: 15 | runs-on: ubuntu-latest 16 | name: npm 17 | permissions: 18 | contents: read 19 | id-token: write 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | # Setup .npmrc file to publish to npm 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: '20.x' 28 | registry-url: 'https://registry.npmjs.org' 29 | 30 | - run: npm ci 31 | - run: npm publish --provenance 32 | env: 33 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | 35 | publish-ghcr: 36 | runs-on: ubuntu-latest 37 | 38 | permissions: 39 | contents: read 40 | packages: write # Allow pushing images to GHCR 41 | attestations: write # To create and write attestations 42 | id-token: write # Additional permissions for the persistence of the attestations 43 | 44 | env: 45 | BUILDX_NO_DEFAULT_ATTESTATIONS: 1 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Set image name 51 | run: | 52 | echo "IMAGE_URL=ghcr.io/tldr-pages/tldr-lint">> "$GITHUB_ENV" 53 | 54 | - name: Docker meta 55 | id: docker_meta 56 | uses: docker/metadata-action@v5 57 | with: 58 | images: | 59 | ${{ env. IMAGE_URL }} 60 | tags: | 61 | type=raw,value=latest 62 | 63 | - name: Set up Docker Buildx 64 | uses: docker/setup-buildx-action@v3 65 | 66 | - name: Login to GitHub Package Registry 67 | uses: docker/login-action@v3 68 | with: 69 | registry: ghcr.io 70 | username: ${{ github.repository_owner }} 71 | password: ${{ secrets.GITHUB_TOKEN }} 72 | 73 | - name: Build and Push the Docker image 74 | id: push 75 | uses: docker/build-push-action@v6 76 | with: 77 | context: . 78 | file: Dockerfile 79 | push: true 80 | tags: ${{ steps.docker_meta.outputs.tags }} 81 | labels: ${{ steps.docker_meta.outputs.labels }} 82 | cache-from: type=gha 83 | cache-to: type=gha,mode=max 84 | platforms: linux/amd64 85 | provenance: false 86 | 87 | - name: Attest pushed image 88 | uses: actions/attest-build-provenance@v2 89 | id: attest 90 | with: 91 | subject-name: ${{ env.IMAGE_URL }} 92 | subject-digest: ${{ steps.push.outputs.digest }} 93 | push-to-registry: false 94 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, macos-latest, windows-latest] 12 | node-version: [18.x, 20.x] 13 | 14 | name: Node ${{ matrix.node-version }} - ${{ matrix.os }} 15 | 16 | steps: 17 | - name: Cancel Previous Runs 18 | uses: styfle/cancel-workflow-action@0.12.1 19 | if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.event.pull_request.base.repo.id }} 20 | with: 21 | access_token: ${{ github.token }} 22 | 23 | - uses: actions/checkout@v4 24 | 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: npm 30 | 31 | - run: npm ci 32 | - run: npm run lint 33 | - run: npm run test 34 | env: 35 | FORCE_COLOR: true 36 | 37 | build-image: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - uses: actions/checkout@v4 41 | 42 | - name: Set up Docker Buildx 43 | uses: docker/setup-buildx-action@v3 44 | 45 | - name: Build the Docker image 46 | uses: docker/build-push-action@v6 47 | with: 48 | context: . 49 | file: Dockerfile 50 | push: false 51 | tags: tldr-lint:latest 52 | cache-from: type=gha 53 | cache-to: type=gha,mode=max 54 | platforms: linux/amd64 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | All notable changes to this project will be documented in this file. 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com). 7 | 8 | ## [v0.0.18 - 2025-06-02](https://github.com/tldr-pages/tldr-lint/compare/v0.0.17...v0.0.18) 9 | 10 | ### Added 11 | 12 | - Add `!` as allowed characters in page titles ([#386](https://github.com/tldr-pages/tldr-lint/pull/386)) 13 | 14 | ## [v0.0.17 - 2025-03-31](https://github.com/tldr-pages/tldr-lint/compare/v0.0.16...v0.0.17) 15 | 16 | ### Added 17 | 18 | - Add `]` and `}` as allowed characters in page titles ([#378](https://github.com/tldr-pages/tldr-lint/pull/378)) 19 | 20 | ## [v0.0.16 - 2024-10-04](https://github.com/tldr-pages/tldr-lint/compare/v0.0.15...v0.0.16) 21 | 22 | ### Added 23 | 24 | - Add rule `TLDR111` ([#353](https://github.com/tldr-pages/tldr-lint/pull/353)) 25 | 26 | ## [v0.0.15 - 2024-04-03](https://github.com/tldr-pages/tldr-lint/compare/v0.0.14...v0.0.15) 27 | 28 | ### Added 29 | 30 | - Add rule `TLDR020` ([#308](https://github.com/tldr-pages/tldr-lint/pull/308)) 31 | - Add rule `TLDR021` ([#310](https://github.com/tldr-pages/tldr-lint/pull/310)) 32 | 33 | ## [v0.0.14 - 2024-03-20](https://github.com/tldr-pages/tldr-lint/compare/v0.0.13...v0.0.14) 34 | 35 | ### Added 36 | 37 | - Add rule `TLDR110` ([#306](https://github.com/tldr-pages/tldr-lint/pull/306)) 38 | 39 | ## [v0.0.13 - 2021-10-12](https://github.com/tldr-pages/tldr-lint/compare/v0.0.12...v0.0.13) 40 | 41 | ### Changed 42 | 43 | - Allow exceptions to rule `TLDR003` ([#104](https://github.com/tldr-pages/tldr-lint/pull/104)) 44 | 45 | ## [v0.0.12 - 2021-10-04](https://github.com/tldr-pages/tldr-lint/compare/v0.0.11...v0.0.12) 46 | 47 | ### Added 48 | 49 | - Allow special uppercased characters ([#101](https://github.com/tldr-pages/tldr-lint/pull/101)) 50 | 51 | ### Changed 52 | 53 | - Update to test against Node 16.x ([#90](https://github.com/tldr-pages/tldr-lint/pull/90)) 54 | 55 | ## [v0.0.11 - 2021-04-14](https://github.com/tldr-pages/tldr-lint/compare/v0.0.10...v0.0.11) 56 | 57 | ### Added 58 | 59 | - Add CLI flag to ignore specific errors ([#69](https://github.com/tldr-pages/tldr-lint/pull/69)) 60 | 61 | ### Changed 62 | 63 | - Allow words at the beginning of example descriptions to end with "ys" ([#60](https://github.com/tldr-pages/tldr-lint/pull/60)) 64 | - Print out filename above parse errors ([#62](https://github.com/tldr-pages/tldr-lint/pull/62)) 65 | 66 | ### Fixed 67 | 68 | - Fix passing options to CLI ([#59](https://github.com/tldr-pages/tldr-lint/pull/59)) 69 | 70 | ## [v0.0.10 - 2021-03-01](https://github.com/tldr-pages/tldr-lint/compare/v0.0.9...v0.0.10) 71 | 72 | ### Added 73 | 74 | - Add rule `TLDR107` ([#34](https://github.com/tldr-pages/tldr-lint/pull/34)) 75 | - Add rule `TLDR108` ([#39](https://github.com/tldr-pages/tldr-lint/pull/39)) 76 | - Add rule `TLDR109` ([#42](https://github.com/tldr-pages/tldr-lint/pull/42)) 77 | 78 | ### Changed 79 | 80 | - Allow pages with `+` character in title ([#33](https://github.com/tldr-pages/tldr-lint/pull/33)) 81 | - Allow pages with `[` chacacter in title ([#44](https://github.com/tldr-pages/tldr-lint/pull/44)) 82 | - Add ESLint for JS linting ([#50](https://github.com/tldr-pages/tldr-lint/pull/50)) 83 | 84 | ## [v0.0.9 - 2020-12-23](https://github.com/tldr-pages/tldr-lint/compare/v0.0.8...v0.0.9) 85 | 86 | ### Added 87 | 88 | - Add rule `TLDR016` ([#20](https://github.com/tldr-pages/tldr-lint/pull/20)) 89 | - Add rule `TLDR017` ([#20](https://github.com/tldr-pages/tldr-lint/pull/20)) 90 | - Add rule `TLDR018` ([#22](https://github.com/tldr-pages/tldr-lint/pull/22)) 91 | - Add rule `TLDR019` ([#23](https://github.com/tldr-pages/tldr-lint/pull/23)) 92 | 93 | ### Changed 94 | 95 | - Replace Grunt with npm scripts locally ([#21](https://github.com/tldr-pages/tldr-lint/pull/21)) 96 | - Update CI to always use colour output ([#24](https://github.com/tldr-pages/tldr-lint/pull/24)) 97 | - Remove TODOs from the README into issues ([#27](https://github.com/tldr-pages/tldr-lint/pull/27)) 98 | - Update Node versions in CI ([#28](https://github.com/tldr-pages/tldr-lint/pull/28)) 99 | 100 | ## [v0.0.8 - 2020-10-02](https://github.com/tldr-pages/tldr-lint/compare/v0.0.7...v0.0.8) 101 | 102 | ### Added 103 | 104 | - Add rule `TLDR014` 105 | - Add rule `TLDR015` 106 | - Add rule `TLDR105` 107 | 108 | ### Changed 109 | 110 | - Only allow one command per example 111 | - Disallow trailing characters in `TLDR005` 112 | 113 | ## [v0.0.7 - 2016-01-19](https://github.com/tldr-pages/tldr-lint/compare/v0.0.6...v0.0.7) 114 | 115 | ### Added 116 | 117 | - Add rule `TLDR104` 118 | - Add rule `TLDR106` 119 | 120 | ### Changed 121 | 122 | - Improve checking on `TLDR103` 123 | 124 | ## [v0.0.6 - 2016-01-14](https://github.com/tldr-pages/tldr-lint/compare/v0.0.5...v0.0.6) 125 | 126 | ### Added 127 | 128 | - Add rule `TLDR101` 129 | - Add rule `TLDR102` 130 | - Add rule `TLDR103` 131 | 132 | ## [v0.0.5 - 2016-01-13](https://github.com/tldr-pages/tldr-lint/compare/v0.0.4...v0.0.5) 133 | 134 | ### Added 135 | 136 | - Add `tldrl` binary, as well as `tldr-lint` 137 | 138 | ### Changed 139 | 140 | - Allow periods in title, just not at the end 141 | 142 | ### Fixed 143 | 144 | - Fix example description regex 145 | 146 | ## [v0.0.4 - 2016-01-13](https://github.com/tldr-pages/tldr-lint/compare/v0.0.3...v0.0.4) 147 | 148 | ### Added 149 | 150 | - Add support for directories 151 | 152 | ## [v0.0.3 - 2016-01-13](https://github.com/tldr-pages/tldr-lint/compare/v0.0.2...v0.0.3) 153 | 154 | ### Fixed 155 | 156 | - Add exception to `TLDR003` 157 | 158 | ## [v0.0.2 - 2016-01-13](https://github.com/tldr-pages/tldr-lint/compare/v0.0.1...v0.0.2) 159 | 160 | ### Added 161 | 162 | - Add rule `TLDR002` 163 | - Add rule `TLDR003` 164 | - Add rule `TLDR004` 165 | - Add rule `TLDR005` 166 | - Add rule `TLDR006` 167 | - Add rule `TLDR007` 168 | - Add rule `TLDR008` 169 | - Add rule `TLDR009` 170 | - Add rule `TLDR010` 171 | - Add rule `TLDR011` 172 | - Add rule `TLDR012` 173 | - Add rule `TLDR013` 174 | 175 | ### Fixed 176 | 177 | - Fix writing to files with `-o` 178 | 179 | ## [v0.0.1 - 2016-01-07](https://github.com/tldr-pages/tldr-lint/commit/4570c2fe189e5fcc0ebd42b4cd4f63ac171ae07e) 180 | 181 | ### Added 182 | 183 | - Initial release 184 | - Add rule `TLDR001` 185 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use LTS version of node as base 2 | FROM node:lts-alpine AS build 3 | 4 | # Image metadata 5 | LABEL org.opencontainers.image.title="tldr-lint Image" 6 | LABEL org.opencontainers.image.description="This image contains the latest version \ 7 | of the tldr-lint package preinstalled." 8 | LABEL org.opencontainers.image.source="https://github.com/tldr-pages/tldr-lint" 9 | LABEL org.opencontainers.image.authors="tldr-pages maintainers and contributors" 10 | LABEL org.opencontainers.image.vendor="tldr.sh" 11 | LABEL org.opencontainers.image.licenses="MIT" 12 | 13 | # Create app directory 14 | WORKDIR /app 15 | 16 | # Change ownership to non-root user for security 17 | RUN chown -R node:node . 18 | USER node 19 | 20 | # Install app dependencies 21 | 22 | # A wildcard is used to ensure both package.json AND package-lock.json are copied 23 | COPY --chown=node:node package*.json ./ 24 | 25 | # Install dependencies in a separate layer to take advantage of Docker layer caching 26 | RUN npm ci 27 | 28 | # Bundle package sources 29 | COPY --chown=node:node lib/ ./lib/ 30 | COPY --chown=node:node specs/ ./specs/ 31 | COPY --chown=node:node tldr.l tldr.yy ./ 32 | 33 | # Build and test the application 34 | RUN npm run jison 35 | RUN npm run test 36 | 37 | # Set the command to be executed when running the Docker container. This will start the application. 38 | ENTRYPOINT ["node", "lib/tldr-lint-cli.js"] 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ruben Vereecken 4 | Copyright (c) 2016-present The [tldr-pages team](https://github.com/orgs/tldr-pages/people) 5 | and [contributors](https://github.com/tldr-pages/tldr-lint/graphs/contributors) 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tldr-lint 2 | 3 | [![npm version][npm-image]][npm-url] 4 | [![Build Status][github-actions-image]][github-actions-url] 5 | [![Matrix chat][matrix-image]][matrix-url] 6 | 7 | `tldr-lint` is a linting tool for validating [tldr](https://github.com/tldr-pages/tldr) pages. 8 | It can also format your pages for you! 9 | 10 | ## Installation 11 | 12 | `tldr-lint` and its alias `tldrl` can be installed via `npm`: 13 | 14 | ```sh 15 | npm install --global tldr-lint 16 | ``` 17 | 18 | ## Usage 19 | 20 | It's really simple. 21 | 22 | ```txt 23 | Usage: tldr-lint [options] 24 | 25 | Options: 26 | -V, --version output the version number 27 | -f, --format also attempt formatting (to stdout, or as specified by -o) 28 | -o, --output output to formatted file 29 | -i, --in-place formats in place 30 | -t, --tabular format errors in a tabular format 31 | -v, --verbose print verbose output 32 | -I, --ignore ignore comma separated tldr-lint error codes (e.g. "TLDR001,TLDR0014") 33 | -h, --help display help for command 34 | ``` 35 | 36 | ### Usage via Docker 37 | 38 | We provide a Dockerfile for reproducibly building and testing `tldr-lint` even without having NodeJS installed. 39 | 40 | For building the Docker image, run this command inside the cloned `tldr-lint` repository: 41 | 42 | `docker build -t tldr-lint .` 43 | 44 | For running a `tldr-lint` container, you need to mount a volume containing the page(s) you want to lint to the container. 45 | For checking a single page, run (replacing `{{/path/to/page.md}}` with the path to the page you want to check): 46 | 47 | `docker run --rm -v {{/path/to/page.md}}:/app/page.md tldr-lint page.md` 48 | 49 | In order to run the container on a directory, mount this directory as follows: 50 | 51 | `docker run --rm -v {{/path/to/directory}}:/app/pages tldr-lint pages/` 52 | 53 | > [!NOTE] 54 | > For Windows users, specify the full path to the directory or page you want to check along with the `docker run` command above. 55 | 56 | ## Linter errors 57 | 58 | All of the errors can be found in [`lib/tldr-lint.js`](./lib/tldr-lint.js). 59 | 60 | Error Code | Description 61 | :---------- | :----------- 62 | TLDR001 | File should contain no leading whitespace 63 | TLDR002 | A single space should precede a sentence 64 | TLDR003 | Descriptions should start with a capital letter 65 | TLDR004 | Command descriptions should end in a period 66 | TLDR005 | Example descriptions should end in a colon with no trailing characters 67 | TLDR006 | Command name and description should be separated by an empty line 68 | TLDR007 | Example descriptions should be surrounded by empty lines 69 | TLDR008 | File should contain no trailing whitespace 70 | TLDR009 | Page should contain a newline at end of file 71 | TLDR010 | Only Unix-style line endings allowed 72 | TLDR011 | Page never contains more than a single empty line 73 | TLDR012 | Page should contain no tabs 74 | TLDR013 | Title should be alphanumeric with dashes, underscores or spaces 75 | TLDR014 | Page should contain no trailing whitespace 76 | TLDR015 | Example descriptions should start with a capital letter 77 | TLDR016 | Label for information link should be spelled exactly `More information: ` 78 | TLDR017 | Information link should be surrounded with angle brackets 79 | TLDR018 | Page should only include a single information link 80 | TLDR019 | Page should only include a maximum of 8 examples 81 | TLDR020 | Label for additional notes should be spelled exactly `Note:` (with a succeeding whitespace) 82 | TLDR021 | Command example should not begin or end in whitespace 83 | TLDR101 | Command description probably not properly annotated 84 | TLDR102 | Example description probably not properly annotated 85 | TLDR103 | Command example is missing its closing backtick 86 | TLDR104 | Example descriptions should prefer infinitive tense (e.g. write) over present (e.g. writes) or gerund (e.g. writing) 87 | TLDR105 | There should be only one command per example 88 | TLDR106 | Page title should start with a hash (`#`) 89 | TLDR107 | File name should end with `.md` extension 90 | TLDR108 | File name should not contain whitespace 91 | TLDR109 | File name should be lowercase 92 | TLDR110 | Command example should not be empty 93 | TLDR111 | File name should not contain any Windows-forbidden character 94 | 95 | [npm-url]: https://www.npmjs.com/package/tldr-lint 96 | [npm-image]: https://img.shields.io/npm/v/tldr-lint.svg 97 | 98 | [github-actions-url]: https://github.com/tldr-pages/tldr-lint/actions 99 | [github-actions-image]: https://img.shields.io/github/actions/workflow/status/tldr-pages/tldr-lint/test.yml?branch=main 100 | 101 | [matrix-url]: https://matrix.to/#/#tldr-pages:matrix.org 102 | [matrix-image]: https://img.shields.io/matrix/tldr-pages:matrix.org?label=chat+on+matrix 103 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslintRecommended from 'eslint-config-eslint'; 2 | 3 | export default [ 4 | { 5 | "files": ["*.js"], 6 | "ignores": ["lib/tldr-parser.js"], 7 | "rules": { 8 | "indent": ["error", 2], 9 | "quotes": ["error", "single"], 10 | "linebreak-style": ["error", "unix"], 11 | "semi": ["error", "always"], 12 | "no-console": "off", 13 | "arrow-parens": ["error", "always"], 14 | "arrow-body-style": ["error", "always"], 15 | "array-callback-return": "error", 16 | "no-magic-numbers": ["error", { 17 | "ignore": [-1, 0, 1, 2], 18 | "ignoreArrayIndexes": true, 19 | "detectObjects": true 20 | }], 21 | "no-var": "error", 22 | "no-warning-comments": "warn", 23 | "handle-callback-err": "error" 24 | }, 25 | "languageOptions": { 26 | "ecmaVersion": "latest", 27 | "sourceType": "module", 28 | "globals": { 29 | "node": true, 30 | "mocha": true, 31 | "es6": true 32 | } 33 | }, 34 | "extends": eslintRecommended 35 | } 36 | ]; 37 | -------------------------------------------------------------------------------- /lib/tldr-lint-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const linter = require('./tldr-lint.js'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const cli = module.exports; 7 | const util = require('util'); 8 | 9 | cli.writeErrors = function(file, linterResult, args) { 10 | const format = args.tabular ? '%s\t%s\t%s\t%s\t' : '%s:%s: %s %s'; 11 | linterResult.errors.forEach(function(error) { 12 | console.error(util.format(format, file, (error.locinfo.first_line || 13 | error.locinfo.last_line - 1), 14 | error.code, error.description)); 15 | }); 16 | if (args.format) { 17 | if (!linterResult.success) { 18 | console.error('Refraining from formatting because of fatal error'); 19 | } else { 20 | const formattedPage = linterResult.formatted; 21 | let err; 22 | if (args.output) { 23 | err = fs.writeFileSync(args.output, formattedPage, 'utf8'); 24 | if (err) throw err; 25 | } else if (args.inPlace) { 26 | err = fs.writeFileSync(file, formattedPage, 'utf8'); 27 | if (err) throw err; 28 | } else { 29 | console.log(formattedPage); 30 | } 31 | } 32 | } 33 | }; 34 | 35 | cli.processFile = function(file, args) { 36 | const linterResult = linter.processFile(file, args.verbose, args.format, args.ignore); 37 | cli.writeErrors(file, linterResult, args); 38 | return linterResult; 39 | }; 40 | 41 | cli.processDirectory = function(dir, args) { 42 | const files = fs.readdirSync(dir); 43 | let stats; 44 | const result = { 45 | success: true, 46 | errors: [] 47 | }; 48 | files.forEach(function(file) { 49 | file = path.join(dir, file); 50 | try { 51 | stats = fs.statSync(file); 52 | } catch(err) { 53 | console.error(err.toString()); 54 | process.exit(1); 55 | } 56 | if (stats.isFile()) { 57 | // Only treat files ending in .md 58 | if (!file.match(/\.md$/)) return; 59 | const linterResult = cli.processFile(file, args); 60 | result.success &= linterResult.success; 61 | result.errors = result.errors.concat(linterResult.errors); 62 | } else { 63 | const aggregateResult = cli.processDirectory(file, args); 64 | result.success &= aggregateResult.success; 65 | result.errors = result.errors.concat(aggregateResult.errors); 66 | } 67 | }); 68 | return result; 69 | }; 70 | 71 | cli.process = function(file, args) { 72 | if (args.output && !args.format) { 73 | console.error('--output only makes sense when used with --format'); 74 | process.exit(1); 75 | } 76 | let stats; 77 | try { 78 | stats = fs.statSync(file); 79 | } catch(err) { 80 | console.error(err.toString()); 81 | process.exit(1); 82 | } 83 | const isdir = stats.isDirectory(); 84 | if (args.output && isdir) { 85 | console.error('--output only makes sense when used with a file'); 86 | } 87 | const result = isdir ? cli.processDirectory(file, args) : cli.processFile(file, args); 88 | if (!result.success || result.errors.length >= 1) return process.exit(1); 89 | }; 90 | 91 | if (require.main === module) { 92 | const { program } = require('commander'); 93 | const pkg = require('../package.json'); 94 | program 95 | .version(pkg.version) 96 | .description(pkg.description) 97 | .arguments('') 98 | .option('-f, --format', 'also attempt formatting (to stdout, or as specified by -o)') 99 | .option('-o, --output ', 'output to formatted file') 100 | .option('-i, --in-place', 'formats in place') 101 | .option('-t, --tabular', 'format errors in a tabular format') 102 | .option('-v, --verbose', 'print verbose output') 103 | .option('-I, --ignore ', 'ignore comma separated tldr-lint error codes (e.g. "TLDR001,TLDR0014")') 104 | .parse(process.argv); 105 | 106 | if (program.args.length !== 1) { 107 | program.help({ error: true }); 108 | } 109 | cli.process(program.args[0], program.opts()); 110 | process.exit(0); 111 | } 112 | -------------------------------------------------------------------------------- /lib/tldr-lint.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const parser = require('./tldr-parser.js').parser; 4 | const util = require('util'); 5 | 6 | const MAX_EXAMPLES = 8; 7 | 8 | module.exports.ERRORS = parser.ERRORS = { 9 | 'TLDR001': 'File should contain no leading whitespace', 10 | 'TLDR002': 'A single space should precede a sentence', 11 | 'TLDR003': 'Descriptions should start with a capital letter', 12 | 'TLDR004': 'Command descriptions should end in a period', 13 | 'TLDR005': 'Example descriptions should end in a colon with no trailing characters', 14 | 'TLDR006': 'Command name and description should be separated by an empty line', 15 | 'TLDR007': 'Example descriptions should be surrounded by empty lines', 16 | 'TLDR008': 'File should contain no trailing whitespace', 17 | 'TLDR009': 'Page should contain a newline at end of file', 18 | 'TLDR010': 'Only Unix-style line endings allowed', 19 | 'TLDR011': 'Page never contains more than a single empty line', 20 | 'TLDR012': 'Page should contain no tabs', 21 | 'TLDR013': 'Title should be alphanumeric with dashes, underscores or spaces', 22 | 'TLDR014': 'Page should contain no trailing whitespace', 23 | 'TLDR015': 'Example descriptions should start with a capital letter', 24 | 'TLDR016': 'Label for information link should be spelled exactly `More information: `', 25 | 'TLDR017': 'Information link should be surrounded with angle brackets', 26 | 'TLDR018': 'Page should only include a single information link', 27 | 'TLDR019': 'Page should only include a maximum of 8 examples', 28 | 'TLDR020': 'Label for additional notes should be spelled exactly `Note: `', 29 | 'TLDR021': 'Command example should not begin or end in whitespace', 30 | 31 | 32 | 'TLDR101': 'Command description probably not properly annotated', 33 | 'TLDR102': 'Example description probably not properly annotated', 34 | 'TLDR103': 'Command example is missing its closing backtick', 35 | 'TLDR104': 'Example descriptions should prefer infinitive tense (e.g. write) over present (e.g. writes) or gerund (e.g. writing)', 36 | 'TLDR105': 'There should be only one command per example', 37 | 'TLDR106': 'Page title should start with a hash (\'#\')', 38 | 'TLDR107': 'File name should end with .md extension', 39 | 'TLDR108': 'File name should not contain whitespace', 40 | 'TLDR109': 'File name should be lowercase', 41 | 'TLDR110': 'Command example should not be empty', 42 | 'TLDR111': 'File name should not contain any Windows-forbidden character' 43 | }; 44 | 45 | (function(parser) { 46 | // Prepares state for a single page. Should be called before a run. 47 | parser.init = function() { 48 | this.yy.errors = []; 49 | this.yy.page = { 50 | description: [], // can be multiple lines 51 | informationLink: [], 52 | examples: [] 53 | }; 54 | }; 55 | parser.finish = function() { 56 | 57 | }; 58 | parser.yy.ERRORS = parser.ERRORS; 59 | parser.yy.error = function(location, error) { 60 | if (!parser.ERRORS[error]) { 61 | throw new Error('Linter done goofed. \'' + error + '\' does not exist.'); 62 | } 63 | parser.yy.errors.push({ 64 | locinfo: location, 65 | code: error, 66 | description: parser.ERRORS[error] 67 | }); 68 | }; 69 | parser.yy.setTitle = function(title) { 70 | parser.yy.page.title = title; 71 | }; 72 | parser.yy.addDescription = function(description) { 73 | parser.yy.page.description.push(description); 74 | }; 75 | parser.yy.addInformationLink = function(url) { 76 | parser.yy.page.informationLink.push(url); 77 | }; 78 | parser.yy.addExample = function(description, commands) { 79 | parser.yy.page.examples.push({ 80 | description: description, 81 | commands: commands 82 | }); 83 | }; 84 | // parser.yy.parseError = function(error, hash) { 85 | // console.log(arguments); 86 | // }; 87 | parser.yy.createToken = function(token) { 88 | return { 89 | type: 'token', 90 | content: token 91 | }; 92 | }; 93 | parser.yy.createCommandText = function(text) { 94 | return { 95 | type: 'text', 96 | content: text 97 | }; 98 | }; 99 | parser.yy.initLexer = function(lexer) { 100 | lexer.pushState = function(key, condition) { 101 | if (!condition) { 102 | condition = { 103 | ctx: key, 104 | rules: lexer._currentRules() 105 | }; 106 | } 107 | lexer.conditions[key] = condition; 108 | lexer.conditionStack.push(key); 109 | }; 110 | 111 | lexer.checkNewline = function(nl, locinfo) { 112 | if (nl.match(/\r/)) { 113 | parser.yy.error(locinfo, 'TLDR010'); 114 | } 115 | }; 116 | 117 | lexer.checkTrailingWhitespace = function(nl, locinfo) { 118 | if (nl !== '') { 119 | parser.yy.error(locinfo, 'TLDR014'); 120 | } 121 | }; 122 | }; 123 | })(parser); 124 | 125 | const linter = module.exports; 126 | 127 | linter.parse = function(page) { 128 | parser.init(); 129 | parser.parse(page); 130 | parser.finish(); 131 | return parser.yy.page; 132 | }; 133 | 134 | linter.formatDescription = function(str) { 135 | return str[0].toUpperCase() + str.slice(1) + '.'; 136 | }; 137 | 138 | linter.formatExampleDescription = function(str) { 139 | return str[0].toUpperCase() + str.slice(1) + ':'; 140 | }; 141 | 142 | linter.format = function(parsedPage) { 143 | let str = ''; 144 | str += util.format('# %s', parsedPage.title); 145 | str += '\n\n'; 146 | parsedPage.description.forEach(function(line) { 147 | str += util.format('> %s', linter.formatDescription(line)); 148 | str += '\n'; 149 | }); 150 | parsedPage.informationLink.forEach(function(informationLink) { 151 | str += util.format('> More information: %s.', informationLink); 152 | str += '\n'; 153 | }); 154 | parsedPage.examples.forEach(function(example) { 155 | str += '\n'; 156 | str += util.format('- %s', linter.formatExampleDescription(example.description)); 157 | str += '\n\n'; 158 | example.commands.forEach(function(command) { 159 | 160 | str += '`'; 161 | command.forEach(function(textOrToken) { 162 | str += textOrToken.type === 'token' ? util.format('{{%s}}', textOrToken.content) : textOrToken.content; 163 | }); 164 | str += '`\n'; 165 | }); 166 | }); 167 | return str; 168 | }; 169 | 170 | linter.process = function(file, page, verbose, alsoFormat) { 171 | let success, result; 172 | try { 173 | linter.parse(page); 174 | success = true; 175 | } catch(err) { 176 | console.error(`${file}:`); 177 | console.error(err.toString()); 178 | success = false; 179 | } 180 | if (verbose) { 181 | console.log(parser.yy.page.description.length + ' line(s) of description'); 182 | console.log(parser.yy.page.examples.length + ' examples'); 183 | console.log(parser.yy.page.informationLink.length + ' link(s)'); 184 | } 185 | 186 | result = { 187 | page: parser.yy.page, 188 | errors: parser.yy.errors, 189 | success: success 190 | }; 191 | 192 | if (parser.yy.page.examples.length > MAX_EXAMPLES) { 193 | result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR019', 'description': this.ERRORS.TLDR019 }); 194 | } 195 | 196 | if (alsoFormat) 197 | result.formatted = linter.format(parser.yy.page); 198 | 199 | return result; 200 | }; 201 | 202 | linter.processFile = function(file, verbose, alsoFormat, ignoreErrors) { 203 | const result = linter.process(file, fs.readFileSync(file, 'utf8'), verbose, alsoFormat); 204 | if (path.extname(file) !== '.md') { 205 | result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR107', description: this.ERRORS.TLDR107 }); 206 | } 207 | 208 | if (RegExp(/\s/).test(path.basename(file))) { 209 | result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR108', description: this.ERRORS.TLDR108 }); 210 | } 211 | 212 | if (/[A-Z]/.test(path.basename(file))) { 213 | result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR109', description: this.ERRORS.TLDR109 }); 214 | } 215 | 216 | if (/[<>:"/\\|?*]/.test(path.basename(file))) { 217 | result.errors.push({ locinfo: { first_line: '0' }, code: 'TLDR111', description: this.ERRORS.TLDR111 }); 218 | } 219 | 220 | if (ignoreErrors) { 221 | ignoreErrors = ignoreErrors.split(',').map(function(val) { 222 | return val.trim(); 223 | }); 224 | result.errors = result.errors.filter(function(error) { 225 | return !ignoreErrors.includes(error.code); 226 | }); 227 | } 228 | 229 | return result; 230 | }; 231 | -------------------------------------------------------------------------------- /lib/tldr-parser.js: -------------------------------------------------------------------------------- 1 | /* parser generated by jison 0.4.18 */ 2 | /* 3 | Returns a Parser object of the following structure: 4 | 5 | Parser: { 6 | yy: {} 7 | } 8 | 9 | Parser.prototype: { 10 | yy: {}, 11 | trace: function(), 12 | symbols_: {associative list: name ==> number}, 13 | terminals_: {associative list: number ==> name}, 14 | productions_: [...], 15 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$), 16 | table: [...], 17 | defaultActions: {...}, 18 | parseError: function(str, hash), 19 | parse: function(input), 20 | 21 | lexer: { 22 | EOF: 1, 23 | parseError: function(str, hash), 24 | setInput: function(input), 25 | input: function(), 26 | unput: function(str), 27 | more: function(), 28 | less: function(n), 29 | pastInput: function(), 30 | upcomingInput: function(), 31 | showPosition: function(), 32 | test_match: function(regex_match_array, rule_index), 33 | next: function(), 34 | lex: function(), 35 | begin: function(condition), 36 | popState: function(), 37 | _currentRules: function(), 38 | topState: function(), 39 | pushState: function(condition), 40 | 41 | options: { 42 | ranges: boolean (optional: true ==> token location info will include a .range[] member) 43 | flex: boolean (optional: true ==> flex-like lexing behaviour where the rules are tested exhaustively to find the longest match) 44 | backtrack_lexer: boolean (optional: true ==> lexer regexes are tested in order and for each matching regex the action code is invoked; the lexer terminates the scan when a token is returned by the action code) 45 | }, 46 | 47 | performAction: function(yy, yy_, $avoiding_name_collisions, YY_START), 48 | rules: [...], 49 | conditions: {associative list: name ==> set}, 50 | } 51 | } 52 | 53 | 54 | token location info (@$, _$, etc.): { 55 | first_line: n, 56 | last_line: n, 57 | first_column: n, 58 | last_column: n, 59 | range: [start_number, end_number] (where the numbers are indexes into the input string, regular zero-based) 60 | } 61 | 62 | 63 | the parseError function receives a 'hash' object with these members for lexer and parser errors: { 64 | text: (matched text) 65 | token: (the produced terminal token, if any) 66 | line: (yylineno) 67 | } 68 | while parser (grammar) errors will also provide these members, i.e. parser errors deliver a superset of attributes: { 69 | loc: (yylloc) 70 | expected: (string describing the set of expected tokens) 71 | recoverable: (boolean: TRUE when the parser has a error recovery rule available for this particular error) 72 | } 73 | */ 74 | var tldrParser = (function(){ 75 | var o=function(k,v,o,l){for(o=o||{},l=k.length;l--;o[k[l]]=v);return o},$V0=[1,8],$V1=[5,13],$V2=[1,5,8,23],$V3=[2,13],$V4=[8,23],$V5=[2,16],$V6=[1,20],$V7=[1,5,8,13,23],$V8=[5,26],$V9=[1,36],$Va=[1,5,8,23,26],$Vb=[26,28,29]; 76 | var parser = {trace: function trace () { }, 77 | yy: {}, 78 | symbols_: {"error":2,"page":3,"title":4,"NEWLINE":5,"info":6,"examples":7,"TEXT":8,"HASH":9,"TITLE":10,"description":11,"information_link":12,"GREATER_THAN":13,"DESCRIPTION_LINE":14,"INFORMATION_LINK":15,"ANGLE_BRACKETED_URL":16,"END_INFORMATION_LINK_URL":17,"END_INFORMATION_LINK":18,"example":19,"maybe_newline":20,"example_description":21,"example_commands":22,"DASH":23,"EXAMPLE_DESCRIPTION":24,"example_command":25,"BACKTICK":26,"example_command_inner":27,"COMMAND_TEXT":28,"COMMAND_TOKEN":29,"$accept":0,"$end":1}, 79 | terminals_: {2:"error",5:"NEWLINE",8:"TEXT",9:"HASH",10:"TITLE",13:"GREATER_THAN",14:"DESCRIPTION_LINE",15:"INFORMATION_LINK",16:"ANGLE_BRACKETED_URL",17:"END_INFORMATION_LINK_URL",18:"END_INFORMATION_LINK",23:"DASH",24:"EXAMPLE_DESCRIPTION",26:"BACKTICK",28:"COMMAND_TEXT",29:"COMMAND_TOKEN"}, 80 | productions_: [0,[3,4],[3,3],[3,4],[4,2],[4,1],[6,1],[6,2],[11,2],[11,3],[12,4],[12,3],[12,5],[7,0],[7,2],[19,4],[20,0],[20,1],[21,2],[21,1],[22,1],[22,2],[25,2],[25,3],[27,1],[27,1],[27,2],[27,2]], 81 | performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate /* action[1] */, $$ /* vstack */, _$ /* lstack */) { 82 | /* this == yyval */ 83 | 84 | var $0 = $$.length - 1; 85 | switch (yystate) { 86 | case 2: 87 | this.$ = yy.error(this._$, 'TLDR006'); 88 | break; 89 | case 3: 90 | this.$ = yy.error(this._$, 'TLDR101') || yy.addDescription($$[$0-1]);; 91 | break; 92 | case 4: 93 | this.$ = yy.setTitle($$[$0]); 94 | break; 95 | case 5: 96 | this.$ = yy.error(_$[$0], 'TLDR106') || yy.setTitle($$[$0]); 97 | break; 98 | case 8: case 9: 99 | this.$ = yy.addDescription($$[$0]); 100 | break; 101 | case 10: 102 | this.$ = yy.addInformationLink($$[$0-1]); 103 | break; 104 | case 11: 105 | this.$ = yy.error(this._$, 'TLDR017') || yy.addDescription($$[$0-1] + $$[$0].trim()); 106 | break; 107 | case 12: 108 | this.$ = yy.error(this._$, 'TLDR018'); 109 | break; 110 | case 15: 111 | 112 | yy.addExample($$[$0-2], $$[$0]); 113 | // Just use the description line's location, easy to find 114 | if (!$$[$0-3]) 115 | yy.error(_$[$0-2], 'TLDR007'); 116 | if (!$$[$0-1]) 117 | yy.error(_$[$0], 'TLDR007'); 118 | 119 | break; 120 | case 18: case 24: case 25: 121 | this.$ = $$[$0]; 122 | break; 123 | case 19: 124 | this.$ = yy.error(this._$, 'TLDR102') || $$[$0]; 125 | break; 126 | case 20: 127 | this.$ = [$$[$0]]; 128 | break; 129 | case 21: 130 | this.$ = yy.error(_$[$0], 'TLDR105') || $$[$0-1]; 131 | break; 132 | case 22: 133 | this.$ = yy.error(this._$, 'TLDR110'); 134 | break; 135 | case 23: 136 | this.$ = $$[$0-1]; 137 | break; 138 | case 26: 139 | this.$ = [].concat($$[$0-1], yy.createCommandText($$[$0])); 140 | break; 141 | case 27: 142 | this.$ = [].concat($$[$0-1], yy.createToken($$[$0])); 143 | break; 144 | } 145 | }, 146 | table: [{3:1,4:2,8:[1,4],9:[1,3]},{1:[3]},{5:[1,5],6:6,11:7,13:$V0},{10:[1,9]},o($V1,[2,5]),{6:10,8:[1,11],11:7,13:$V0},o($V2,$V3,{7:12}),o($V2,[2,6],{12:13,13:[1,14]}),{14:[1,15]},o($V1,[2,4]),o($V2,$V3,{7:16}),o($V2,$V3,{7:17}),o($V4,$V5,{19:18,20:19,1:[2,2],5:$V6}),o($V2,[2,7],{13:[1,21]}),{14:[1,22],15:[1,23]},o($V7,[2,8]),o($V4,$V5,{19:18,20:19,1:[2,1],5:$V6}),o($V4,$V5,{19:18,20:19,1:[2,3],5:$V6}),o($V2,[2,14]),{8:[1,26],21:24,23:[1,25]},o([8,23,26],[2,17]),{15:[1,27]},o($V7,[2,9]),{16:[1,28],18:[1,29]},{5:$V6,20:30,26:$V5},{24:[1,31]},o($V8,[2,19]),{16:[1,32]},{17:[1,33]},o($V7,[2,11]),{22:34,25:35,26:$V9},o($V8,[2,18]),{17:[1,37]},o($V7,[2,10]),o($V2,[2,15],{25:38,26:$V9}),o($Va,[2,20]),{26:[1,39],27:40,28:[1,41],29:[1,42]},o($V7,[2,12]),o($Va,[2,21]),o($Va,[2,22]),{26:[1,43],28:[1,44],29:[1,45]},o($Vb,[2,24]),o($Vb,[2,25]),o($Va,[2,23]),o($Vb,[2,26]),o($Vb,[2,27])], 147 | defaultActions: {}, 148 | parseError: function parseError (str, hash) { 149 | if (hash.recoverable) { 150 | this.trace(str); 151 | } else { 152 | var error = new Error(str); 153 | error.hash = hash; 154 | throw error; 155 | } 156 | }, 157 | parse: function parse(input) { 158 | var self = this, stack = [0], tstack = [], vstack = [null], lstack = [], table = this.table, yytext = '', yylineno = 0, yyleng = 0, recovering = 0, TERROR = 2, EOF = 1; 159 | var args = lstack.slice.call(arguments, 1); 160 | var lexer = Object.create(this.lexer); 161 | var sharedState = { yy: {} }; 162 | for (var k in this.yy) { 163 | if (Object.prototype.hasOwnProperty.call(this.yy, k)) { 164 | sharedState.yy[k] = this.yy[k]; 165 | } 166 | } 167 | lexer.setInput(input, sharedState.yy); 168 | sharedState.yy.lexer = lexer; 169 | sharedState.yy.parser = this; 170 | if (typeof lexer.yylloc == 'undefined') { 171 | lexer.yylloc = {}; 172 | } 173 | var yyloc = lexer.yylloc; 174 | lstack.push(yyloc); 175 | var ranges = lexer.options && lexer.options.ranges; 176 | if (typeof sharedState.yy.parseError === 'function') { 177 | this.parseError = sharedState.yy.parseError; 178 | } else { 179 | this.parseError = Object.getPrototypeOf(this).parseError; 180 | } 181 | function popStack(n) { 182 | stack.length = stack.length - 2 * n; 183 | vstack.length = vstack.length - n; 184 | lstack.length = lstack.length - n; 185 | } 186 | _token_stack: 187 | var lex = function () { 188 | var token; 189 | token = lexer.lex() || EOF; 190 | if (typeof token !== 'number') { 191 | token = self.symbols_[token] || token; 192 | } 193 | return token; 194 | }; 195 | var symbol, preErrorSymbol, state, action, a, r, yyval = {}, p, len, newState, expected; 196 | while (true) { 197 | state = stack[stack.length - 1]; 198 | if (this.defaultActions[state]) { 199 | action = this.defaultActions[state]; 200 | } else { 201 | if (symbol === null || typeof symbol == 'undefined') { 202 | symbol = lex(); 203 | } 204 | action = table[state] && table[state][symbol]; 205 | } 206 | if (typeof action === 'undefined' || !action.length || !action[0]) { 207 | var errStr = ''; 208 | expected = []; 209 | for (p in table[state]) { 210 | if (this.terminals_[p] && p > TERROR) { 211 | expected.push('\'' + this.terminals_[p] + '\''); 212 | } 213 | } 214 | if (lexer.showPosition) { 215 | errStr = 'Parse error on line ' + (yylineno + 1) + ':\n' + lexer.showPosition() + '\nExpecting ' + expected.join(', ') + ', got \'' + (this.terminals_[symbol] || symbol) + '\''; 216 | } else { 217 | errStr = 'Parse error on line ' + (yylineno + 1) + ': Unexpected ' + (symbol == EOF ? 'end of input' : '\'' + (this.terminals_[symbol] || symbol) + '\''); 218 | } 219 | this.parseError(errStr, { 220 | text: lexer.match, 221 | token: this.terminals_[symbol] || symbol, 222 | line: lexer.yylineno, 223 | loc: yyloc, 224 | expected: expected 225 | }); 226 | } 227 | if (action[0] instanceof Array && action.length > 1) { 228 | throw new Error('Parse Error: multiple actions possible at state: ' + state + ', token: ' + symbol); 229 | } 230 | switch (action[0]) { 231 | case 1: 232 | stack.push(symbol); 233 | vstack.push(lexer.yytext); 234 | lstack.push(lexer.yylloc); 235 | stack.push(action[1]); 236 | symbol = null; 237 | if (!preErrorSymbol) { 238 | yyleng = lexer.yyleng; 239 | yytext = lexer.yytext; 240 | yylineno = lexer.yylineno; 241 | yyloc = lexer.yylloc; 242 | if (recovering > 0) { 243 | recovering--; 244 | } 245 | } else { 246 | symbol = preErrorSymbol; 247 | preErrorSymbol = null; 248 | } 249 | break; 250 | case 2: 251 | len = this.productions_[action[1]][1]; 252 | yyval.$ = vstack[vstack.length - len]; 253 | yyval._$ = { 254 | first_line: lstack[lstack.length - (len || 1)].first_line, 255 | last_line: lstack[lstack.length - 1].last_line, 256 | first_column: lstack[lstack.length - (len || 1)].first_column, 257 | last_column: lstack[lstack.length - 1].last_column 258 | }; 259 | if (ranges) { 260 | yyval._$.range = [ 261 | lstack[lstack.length - (len || 1)].range[0], 262 | lstack[lstack.length - 1].range[1] 263 | ]; 264 | } 265 | r = this.performAction.apply(yyval, [ 266 | yytext, 267 | yyleng, 268 | yylineno, 269 | sharedState.yy, 270 | action[1], 271 | vstack, 272 | lstack 273 | ].concat(args)); 274 | if (typeof r !== 'undefined') { 275 | return r; 276 | } 277 | if (len) { 278 | stack = stack.slice(0, -1 * len * 2); 279 | vstack = vstack.slice(0, -1 * len); 280 | lstack = lstack.slice(0, -1 * len); 281 | } 282 | stack.push(this.productions_[action[1]][0]); 283 | vstack.push(yyval.$); 284 | lstack.push(yyval._$); 285 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 286 | stack.push(newState); 287 | break; 288 | case 3: 289 | return true; 290 | } 291 | } 292 | return true; 293 | }}; 294 | /* generated by jison-lex 0.3.4 */ 295 | var lexer = (function(){ 296 | var lexer = ({ 297 | 298 | EOF:1, 299 | 300 | parseError:function parseError(str, hash) { 301 | if (this.yy.parser) { 302 | this.yy.parser.parseError(str, hash); 303 | } else { 304 | throw new Error(str); 305 | } 306 | }, 307 | 308 | // resets the lexer, sets new input 309 | setInput:function (input, yy) { 310 | this.yy = yy || this.yy || {}; 311 | this._input = input; 312 | this._more = this._backtrack = this.done = false; 313 | this.yylineno = this.yyleng = 0; 314 | this.yytext = this.matched = this.match = ''; 315 | this.conditionStack = ['INITIAL']; 316 | this.yylloc = { 317 | first_line: 1, 318 | first_column: 0, 319 | last_line: 1, 320 | last_column: 0 321 | }; 322 | if (this.options.ranges) { 323 | this.yylloc.range = [0,0]; 324 | } 325 | this.offset = 0; 326 | return this; 327 | }, 328 | 329 | // consumes and returns one char from the input 330 | input:function () { 331 | var ch = this._input[0]; 332 | this.yytext += ch; 333 | this.yyleng++; 334 | this.offset++; 335 | this.match += ch; 336 | this.matched += ch; 337 | var lines = ch.match(/(?:\r\n?|\n).*/g); 338 | if (lines) { 339 | this.yylineno++; 340 | this.yylloc.last_line++; 341 | } else { 342 | this.yylloc.last_column++; 343 | } 344 | if (this.options.ranges) { 345 | this.yylloc.range[1]++; 346 | } 347 | 348 | this._input = this._input.slice(1); 349 | return ch; 350 | }, 351 | 352 | // unshifts one char (or a string) into the input 353 | unput:function (ch) { 354 | var len = ch.length; 355 | var lines = ch.split(/(?:\r\n?|\n)/g); 356 | 357 | this._input = ch + this._input; 358 | this.yytext = this.yytext.substr(0, this.yytext.length - len); 359 | //this.yyleng -= len; 360 | this.offset -= len; 361 | var oldLines = this.match.split(/(?:\r\n?|\n)/g); 362 | this.match = this.match.substr(0, this.match.length - 1); 363 | this.matched = this.matched.substr(0, this.matched.length - 1); 364 | 365 | if (lines.length - 1) { 366 | this.yylineno -= lines.length - 1; 367 | } 368 | var r = this.yylloc.range; 369 | 370 | this.yylloc = { 371 | first_line: this.yylloc.first_line, 372 | last_line: this.yylineno + 1, 373 | first_column: this.yylloc.first_column, 374 | last_column: lines ? 375 | (lines.length === oldLines.length ? this.yylloc.first_column : 0) 376 | + oldLines[oldLines.length - lines.length].length - lines[0].length : 377 | this.yylloc.first_column - len 378 | }; 379 | 380 | if (this.options.ranges) { 381 | this.yylloc.range = [r[0], r[0] + this.yyleng - len]; 382 | } 383 | this.yyleng = this.yytext.length; 384 | return this; 385 | }, 386 | 387 | // When called from action, caches matched text and appends it on next action 388 | more:function () { 389 | this._more = true; 390 | return this; 391 | }, 392 | 393 | // When called from action, signals the lexer that this rule fails to match the input, so the next matching rule (regex) should be tested instead. 394 | reject:function () { 395 | if (this.options.backtrack_lexer) { 396 | this._backtrack = true; 397 | } else { 398 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true).\n' + this.showPosition(), { 399 | text: "", 400 | token: null, 401 | line: this.yylineno 402 | }); 403 | 404 | } 405 | return this; 406 | }, 407 | 408 | // retain first n characters of the match 409 | less:function (n) { 410 | this.unput(this.match.slice(n)); 411 | }, 412 | 413 | // displays already matched input, i.e. for error messages 414 | pastInput:function () { 415 | var past = this.matched.substr(0, this.matched.length - this.match.length); 416 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 417 | }, 418 | 419 | // displays upcoming input, i.e. for error messages 420 | upcomingInput:function () { 421 | var next = this.match; 422 | if (next.length < 20) { 423 | next += this._input.substr(0, 20-next.length); 424 | } 425 | return (next.substr(0,20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); 426 | }, 427 | 428 | // displays the character position where the lexing error occurred, i.e. for error messages 429 | showPosition:function () { 430 | var pre = this.pastInput(); 431 | var c = new Array(pre.length + 1).join("-"); 432 | return pre + this.upcomingInput() + "\n" + c + "^"; 433 | }, 434 | 435 | // test the lexed token: return FALSE when not a match, otherwise return token 436 | test_match:function(match, indexed_rule) { 437 | var token, 438 | lines, 439 | backup; 440 | 441 | if (this.options.backtrack_lexer) { 442 | // save context 443 | backup = { 444 | yylineno: this.yylineno, 445 | yylloc: { 446 | first_line: this.yylloc.first_line, 447 | last_line: this.last_line, 448 | first_column: this.yylloc.first_column, 449 | last_column: this.yylloc.last_column 450 | }, 451 | yytext: this.yytext, 452 | match: this.match, 453 | matches: this.matches, 454 | matched: this.matched, 455 | yyleng: this.yyleng, 456 | offset: this.offset, 457 | _more: this._more, 458 | _input: this._input, 459 | yy: this.yy, 460 | conditionStack: this.conditionStack.slice(0), 461 | done: this.done 462 | }; 463 | if (this.options.ranges) { 464 | backup.yylloc.range = this.yylloc.range.slice(0); 465 | } 466 | } 467 | 468 | lines = match[0].match(/(?:\r\n?|\n).*/g); 469 | if (lines) { 470 | this.yylineno += lines.length; 471 | } 472 | this.yylloc = { 473 | first_line: this.yylloc.last_line, 474 | last_line: this.yylineno + 1, 475 | first_column: this.yylloc.last_column, 476 | last_column: lines ? 477 | lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : 478 | this.yylloc.last_column + match[0].length 479 | }; 480 | this.yytext += match[0]; 481 | this.match += match[0]; 482 | this.matches = match; 483 | this.yyleng = this.yytext.length; 484 | if (this.options.ranges) { 485 | this.yylloc.range = [this.offset, this.offset += this.yyleng]; 486 | } 487 | this._more = false; 488 | this._backtrack = false; 489 | this._input = this._input.slice(match[0].length); 490 | this.matched += match[0]; 491 | token = this.performAction.call(this, this.yy, this, indexed_rule, this.conditionStack[this.conditionStack.length - 1]); 492 | if (this.done && this._input) { 493 | this.done = false; 494 | } 495 | if (token) { 496 | return token; 497 | } else if (this._backtrack) { 498 | // recover context 499 | for (var k in backup) { 500 | this[k] = backup[k]; 501 | } 502 | return false; // rule action called reject() implying the next rule should be tested instead. 503 | } 504 | return false; 505 | }, 506 | 507 | // return next match in input 508 | next:function () { 509 | if (this.done) { 510 | return this.EOF; 511 | } 512 | if (!this._input) { 513 | this.done = true; 514 | } 515 | 516 | var token, 517 | match, 518 | tempMatch, 519 | index; 520 | if (!this._more) { 521 | this.yytext = ''; 522 | this.match = ''; 523 | } 524 | var rules = this._currentRules(); 525 | for (var i = 0; i < rules.length; i++) { 526 | tempMatch = this._input.match(this.rules[rules[i]]); 527 | if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { 528 | match = tempMatch; 529 | index = i; 530 | if (this.options.backtrack_lexer) { 531 | token = this.test_match(tempMatch, rules[i]); 532 | if (token !== false) { 533 | return token; 534 | } else if (this._backtrack) { 535 | match = false; 536 | continue; // rule action called reject() implying a rule MISmatch. 537 | } else { 538 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 539 | return false; 540 | } 541 | } else if (!this.options.flex) { 542 | break; 543 | } 544 | } 545 | } 546 | if (match) { 547 | token = this.test_match(match, rules[index]); 548 | if (token !== false) { 549 | return token; 550 | } 551 | // else: this is a lexer rule which consumes input without producing a token (e.g. whitespace) 552 | return false; 553 | } 554 | if (this._input === "") { 555 | return this.EOF; 556 | } else { 557 | return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { 558 | text: "", 559 | token: null, 560 | line: this.yylineno 561 | }); 562 | } 563 | }, 564 | 565 | // return next match that has a token 566 | lex:function lex () { 567 | var r = this.next(); 568 | if (r) { 569 | return r; 570 | } else { 571 | return this.lex(); 572 | } 573 | }, 574 | 575 | // activates a new lexer condition state (pushes the new lexer condition state onto the condition stack) 576 | begin:function begin (condition) { 577 | this.conditionStack.push(condition); 578 | }, 579 | 580 | // pop the previously active lexer condition state off the condition stack 581 | popState:function popState () { 582 | var n = this.conditionStack.length - 1; 583 | if (n > 0) { 584 | return this.conditionStack.pop(); 585 | } else { 586 | return this.conditionStack[0]; 587 | } 588 | }, 589 | 590 | // produce the lexer rule set which is active for the currently active lexer condition state 591 | _currentRules:function _currentRules () { 592 | if (this.conditionStack.length && this.conditionStack[this.conditionStack.length - 1]) { 593 | return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; 594 | } else { 595 | return this.conditions["INITIAL"].rules; 596 | } 597 | }, 598 | 599 | // return the currently active lexer condition state; when an index argument is provided it produces the N-th previous condition state, if available 600 | topState:function topState (n) { 601 | n = this.conditionStack.length - 1 - Math.abs(n || 0); 602 | if (n >= 0) { 603 | return this.conditionStack[n]; 604 | } else { 605 | return "INITIAL"; 606 | } 607 | }, 608 | 609 | // alias for begin(condition) 610 | pushState:function pushState (condition) { 611 | this.begin(condition); 612 | }, 613 | 614 | // return the number of states currently on the stack 615 | stateStackSize:function stateStackSize() { 616 | return this.conditionStack.length; 617 | }, 618 | options: {"backtrack_lexer":true}, 619 | performAction: function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 620 | yy.initLexer(yy.lexer); 621 | 622 | var YYSTATE=YY_START; 623 | switch($avoiding_name_collisions) { 624 | case 0: 625 | yy.error(yy_.yylloc, 'TLDR008'); 626 | 627 | break; 628 | case 1: 629 | yy.error(yy_.yylloc, 'TLDR012'); 630 | var cleaned = this.match.replace(/\t/g, ' '); 631 | this.unput(cleaned); 632 | 633 | break; 634 | case 2: 635 | // this.setInput resets the state as well, so push that back in 636 | var currentConditionStack = this.conditionStack; 637 | var currentConditions = this.conditions; 638 | // Basically replace EOF with a final newline so lexing can continue 639 | this.setInput(this.match + '\n') 640 | this.conditionStack = currentConditionStack; 641 | this.currentConditions = currentConditions; 642 | yy.error(yy_.yylloc, 'TLDR009'); 643 | 644 | break; 645 | case 3: 646 | if (this.topState() !== 'INITIAL') { 647 | this.reject(); 648 | return; 649 | } 650 | this.pushState('title'); 651 | if (this.matches[1]) { 652 | yy.error(yy_.yylloc, 'TLDR001'); 653 | } 654 | if (this.matches[2] !== ' ') { 655 | yy.error(yy_.yylloc, 'TLDR002'); 656 | } 657 | return 9; 658 | 659 | break; 660 | case 4: 661 | if (this.topState() !== 'INITIAL') { 662 | this.reject(); 663 | return; 664 | } 665 | if (this.matches[2] !== ' ') { 666 | yy.error(yy_.yylloc, 'TLDR002'); 667 | } 668 | if (this.matches[1] == '>') { 669 | this.pushState('description'); 670 | return 13; 671 | } else { 672 | this.pushState('example_description'); 673 | return 23; 674 | } 675 | 676 | break; 677 | case 5: 678 | if (this.topState() !== 'description') { 679 | this.reject(); 680 | return; 681 | } 682 | if (this.matches[1] !== 'More information: ') { 683 | yy.error(yy_.yylloc, 'TLDR016'); 684 | } 685 | this.popState(); 686 | this.pushState('information_link'); 687 | return 15; 688 | 689 | break; 690 | case 6: 691 | if (this.topState() !== 'information_link') { 692 | this.reject(); 693 | return; 694 | } 695 | this.popState(); 696 | this.pushState('information_link_url'); 697 | return 16; 698 | 699 | break; 700 | case 7: 701 | if (this.topState() === "title") { 702 | yy_.yytext = this.matches[1]; 703 | if (this.matches[1].match(/([^\w+\[\]}!. -])|(\.$)/)) yy.error(yy_.yylloc, 'TLDR013'); 704 | this.checkTrailingWhitespace(this.matches[2], yy_.yylloc); 705 | this.checkNewline(this.matches[3], yy_.yylloc); 706 | this.popState(); 707 | return 10; 708 | } else if (this.topState() === 'information_link_url') { 709 | if (this.matches[1] != '.') yy.error(yy_.yylloc, 'TLDR004'); 710 | this.checkTrailingWhitespace(this.matches[2], yy_.yylloc); 711 | this.checkNewline(this.matches[3], yy_.yylloc); 712 | this.popState(); 713 | return 17; 714 | } else if (this.topState() === 'information_link') { 715 | this.checkTrailingWhitespace(this.matches[2], yy_.yylloc); 716 | this.checkNewline(this.matches[3], yy_.yylloc); 717 | this.popState(); 718 | return 18; 719 | } else { 720 | this.reject(); 721 | } 722 | 723 | break; 724 | case 8: 725 | if (this.topState() === 'description') { 726 | this.popState(); 727 | yy_.yytext = this.matches[1]; 728 | var exceptions = ['npm', 'pnpm']; 729 | if (!exceptions.includes(yy_.yytext.replace(/ .*/,'')) && yy_.yytext.match(/^[a-z]/)) { 730 | yy.error(yy_.yylloc, 'TLDR003'); 731 | } 732 | if (yy_.yytext.match(/(note|NOTE): /)) { 733 | yy.error(yy_.yylloc, 'TLDR020'); 734 | } 735 | var punctuation = this.matches[2]; 736 | if (punctuation !== '.') { 737 | yy.error(yy_.yylloc, 'TLDR004'); 738 | } 739 | if (punctuation.match(/[,;]/)) { 740 | console.warn(`Description ends in \'${punctuation}\'. Consider writing your sentence on one line.`); 741 | } 742 | this.checkTrailingWhitespace(this.matches[3], yy_.yylloc); 743 | this.checkNewline(this.matches[4], yy_.yylloc); 744 | return 14; 745 | } else { 746 | this.reject(); 747 | } 748 | 749 | break; 750 | case 9: 751 | if (this.topState() === 'example_description') { 752 | this.popState(); 753 | yy_.yytext = this.matches[1]; 754 | if (!yy_.yytext.match(/^[\p{Lu}\[]/u)) yy.error(yy_.yylloc, 'TLDR015'); 755 | if (this.matches[2] !== ':') yy.error(yy_.yylloc, 'TLDR005'); 756 | if (yy_.yytext.match(/(note|NOTE): /)) { 757 | yy.error(yy_.yylloc, 'TLDR020'); 758 | } 759 | // Try to ensure that verbs at the beginning of the description are in the infinitive tense 760 | // 1. Any word at the start of a sentence that ends with "ing" and is 6 or more characters long (e.g. executing, writing) is likely a verb in the gerund 761 | // 2. Any word at the start of a sentence that doesn't end with "us", "ss", or "ys" (e.g. superfluous, success, always) is likely a verb in the present tense 762 | if (yy_.yytext.match(/(^[A-Za-z]{3,}ing )|(^[A-Za-z]+[^usy]s )/)) { 763 | yy.error(yy_.yylloc, 'TLDR104'); 764 | } 765 | // Check if any sneaky spaces have been caught 766 | this.checkTrailingWhitespace(this.matches[3], yy_.yylloc); 767 | this.checkNewline(this.matches[3], yy_.yylloc); 768 | return 24; 769 | } else { 770 | this.reject(); 771 | } 772 | 773 | break; 774 | case 10: 775 | if (this.topState() === 'example_command') { 776 | this.popState(); 777 | this.checkNewline(this.matches[1], yy_.yylloc); 778 | return 26; 779 | } else { 780 | this.reject(); 781 | } 782 | 783 | break; 784 | case 11: 785 | this.pushState('example_command'); 786 | if (this.matches[1].match(/ /)) { 787 | yy.error(yy_.yylloc, 'TLDR021') 788 | } 789 | return 26; 790 | 791 | break; 792 | case 12: 793 | if (this.topState() === 'example_command') { 794 | yy_.yytext = this.matches[1]; 795 | if (this.matches[3]) { 796 | // Matched a newline where there certainly should not be one 797 | // This code is duplicated from two rules below.. should unify 798 | this.unput('`' + this.matches[2]); 799 | yy.error(yy_.yylloc, 'TLDR103') 800 | } 801 | return 29; 802 | } else this.reject(); 803 | 804 | break; 805 | case 13: 806 | if (this.topState() === 'example_command') { 807 | this.unput('{{'); 808 | yy_.yytext = this.matches[1]; 809 | return 28; 810 | } else this.reject(); 811 | 812 | break; 813 | case 14: 814 | if (this.topState() === 'example_command') { 815 | // Check if there are some trailing spaces 816 | if (this.matches[2].match(/ /)) { 817 | yy.error(yy_.yylloc, 'TLDR014'); 818 | } 819 | if (this.matches[1].endsWith(' ') && !this.matches[1].endsWith('\\ ')) { 820 | yy.error(yy_.yylloc, 'TLDR021'); 821 | } 822 | // Don't swallow the trailing backtick just yet 823 | if (this.matches[2].match(/\`/)) this.unput('`'); 824 | else { 825 | // If command doesn't end in a backtick, just add a backtick anyway 826 | // Also pop back the newline. Let's pretend we don't care what that is. 827 | this.unput('`\n'); 828 | yy.error(yy_.yylloc, 'TLDR103') 829 | } 830 | yy_.yytext = this.matches[1]; 831 | return 28; 832 | } else this.reject(); 833 | 834 | break; 835 | case 15: 836 | yy.error(yy_.yylloc, 'TLDR014'); 837 | 838 | break; 839 | case 16: 840 | // Either you've got more than a single \n or \r or more than \r\n 841 | var isdos = this.match.match(/\r\n/); 842 | if (isdos && this.match.length > 2 || !isdos && this.match.length > 1) { 843 | yy.error(yy_.yylloc, 'TLDR011'); 844 | } 845 | this.checkNewline(this.match, yy_.yylloc); 846 | return 5; 847 | 848 | break; 849 | case 17: 850 | yy_.yytext = this.matches[1]; 851 | return 8; 852 | 853 | break; 854 | } 855 | }, 856 | rules: [/^(?:\s+$)/,/^(?:.*?\t+.*)/,/^(?:[^\n]$)/,/^(?:(\s*)#(\s*))/,/^(?:([\>-])(\s*))/,/^(?:([Mm]ore\s+[Ii]nfo(?:rmation)?:?\s*))/,/^(?:(]*>))/,/^(?:(.+?)([ ]*)((?:\r\n)|\n|\r))/,/^(?:(.+?)([\.,;!\?]?)([ ]*)((?:\r\n)|\n|\r))/,/^(?:(.+?)([\.:,;]?)([ ]*)((?:\r\n)|\n|\r))/,/^(?:`((?:\r\n)|\n|\r))/,/^(?:`([ ]*))/,/^(?:\{\{([^\n\`\{\}]*)\}\}(((?:\r\n)|\n|\r)?))/,/^(?:([^\`\n]+?)\{\{)/,/^(?:([^\`\n]+?)(`[ ]*|[ ]*((?:\r\n)|\n|\r)))/,/^(?:[ ]+)/,/^(?:((?:\r\n)|\n|\r)+)/,/^(?:(.+?)[\.:]?((?:\r\n)|\n|\r))/], 857 | conditions: {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],"inclusive":true}} 858 | }); 859 | return lexer; 860 | })(); 861 | parser.lexer = lexer; 862 | function Parser () { 863 | this.yy = {}; 864 | } 865 | Parser.prototype = parser;parser.Parser = Parser; 866 | return new Parser; 867 | })(); 868 | 869 | 870 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 871 | exports.parser = tldrParser; 872 | exports.Parser = tldrParser.Parser; 873 | exports.parse = function () { return tldrParser.parse.apply(tldrParser, arguments); }; 874 | exports.main = function commonjsMain (args) { 875 | if (!args[1]) { 876 | console.log('Usage: '+args[0]+' FILE'); 877 | process.exit(1); 878 | } 879 | var source = require('fs').readFileSync(require('path').normalize(args[1]), "utf8"); 880 | return exports.parser.parse(source); 881 | }; 882 | if (typeof module !== 'undefined' && require.main === module) { 883 | exports.main(process.argv.slice(1)); 884 | } 885 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tldr-lint", 3 | "version": "0.0.18", 4 | "description": "A linting tool to validate tldr pages", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/tldr-pages/tldr-lint.git" 8 | }, 9 | "scripts": { 10 | "jison": "jison tldr.yy tldr.l -o lib/tldr-parser.js", 11 | "lint": "eslint lib specs", 12 | "prepare": "husky", 13 | "test": "jest specs", 14 | "watch": "concurrently 'npm run watch:jison' 'npm run watch:specs'", 15 | "watch:jison": "onchange '*.l' '*.yy' -- npm run jison", 16 | "watch:specs": "onchange 'specs/*.js' 'lib/*.js' '*.l' '*.yy' -- npm run test" 17 | }, 18 | "bin": { 19 | "tldr-lint": "lib/tldr-lint-cli.js", 20 | "tldrl": "lib/tldr-lint-cli.js" 21 | }, 22 | "keywords": [ 23 | "tldr", 24 | "pages", 25 | "lint", 26 | "validate", 27 | "format" 28 | ], 29 | "author": { 30 | "name": "Ruben Vereecken", 31 | "email": "rubenvereecken@gmail.com" 32 | }, 33 | "maintainers": [ 34 | { 35 | "name": "tldr-pages team" 36 | } 37 | ], 38 | "engines": { 39 | "node": ">=18" 40 | }, 41 | "license": "MIT", 42 | "dependencies": { 43 | "commander": "^14.0.0" 44 | }, 45 | "devDependencies": { 46 | "concurrently": "^9.1.2", 47 | "eslint": "^9.17.0", 48 | "eslint-config-eslint": "^11.0.0", 49 | "eslint-plugin-jest": "^28.10.0", 50 | "husky": "^9.1.7", 51 | "jest": "^29.7.0", 52 | "jison": "^0.4.18", 53 | "onchange": "^7.1.0" 54 | }, 55 | "funding": { 56 | "type": "liberapay", 57 | "url": "https://liberapay.com/tldr-pages" 58 | }, 59 | "files": [ 60 | "lib/" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /specs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest"], 3 | "env": { 4 | "jest/globals": true 5 | }, 6 | "extends": ["plugin:jest/recommended"] 7 | } 8 | -------------------------------------------------------------------------------- /specs/pages/failing/001.md: -------------------------------------------------------------------------------- 1 | 2 | # du 3 | 4 | > Estimate file space usage. 5 | -------------------------------------------------------------------------------- /specs/pages/failing/002.md: -------------------------------------------------------------------------------- 1 | #du 2 | 3 | >Estimate file space usage. 4 | 5 | -Where dat space: 6 | 7 | `du` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/003.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > estimate file space usage. 4 | 5 | - Where dat space: 6 | 7 | `du` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/004.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Secretly 4 | > This is really just 5 | > A really big line. 6 | > Even containing a space after period. 7 | -------------------------------------------------------------------------------- /specs/pages/failing/005.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | 5 | - Here goes an example ending in a period. 6 | 7 | `du` 8 | 9 | - And another, also wrong 10 | 11 | `du` 12 | -------------------------------------------------------------------------------- /specs/pages/failing/006.md: -------------------------------------------------------------------------------- 1 | # du 2 | > Estimate file space usage. 3 | -------------------------------------------------------------------------------- /specs/pages/failing/007.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | - Here goes an example: 5 | `du` 6 | -------------------------------------------------------------------------------- /specs/pages/failing/008.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | 5 | - Here goes an example: 6 | 7 | `du` 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /specs/pages/failing/009.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | 5 | - Here goes an example: 6 | 7 | `no newline after this line` -------------------------------------------------------------------------------- /specs/pages/failing/010.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > This file has dos line endings. 4 | 5 | - A lot of them: 6 | 7 | `about 7 I'd say` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/011.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | 4 | > Look at all that space. 5 | 6 | 7 | - Here goes an example: 8 | 9 | `blub` 10 | -------------------------------------------------------------------------------- /specs/pages/failing/012.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Look at all them tabs. 4 | -------------------------------------------------------------------------------- /specs/pages/failing/013.md: -------------------------------------------------------------------------------- 1 | # This is not a proper title. 2 | 3 | > Estimate file space usage. 4 | -------------------------------------------------------------------------------- /specs/pages/failing/014.md: -------------------------------------------------------------------------------- 1 | # nix-env 2 | 3 | > Manipulate or query Nix user environments. 4 | > More information: . 5 | 6 | - Show the status of available packages: 7 | 8 | `nix-env -qas` 9 | 10 | - Install package: 11 | 12 | `nix-env -i {{pkg_name}}` 13 | -------------------------------------------------------------------------------- /specs/pages/failing/015.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | 5 | - where dat space: 6 | 7 | `du` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/016.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program. 4 | > More info: . 5 | 6 | - Run demo: 7 | 8 | `demo` 9 | -------------------------------------------------------------------------------- /specs/pages/failing/017.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program. 4 | > More information: https://not.real.invalid 5 | 6 | - Run demo: 7 | 8 | `demo` 9 | -------------------------------------------------------------------------------- /specs/pages/failing/018.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program. 4 | > More information: . 5 | > More information: . 6 | > More information: . 7 | 8 | - Run demo: 9 | 10 | `demo` 11 | -------------------------------------------------------------------------------- /specs/pages/failing/019.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program. 4 | > More information: . 5 | 6 | - Example 1: 7 | 8 | `demo` 9 | 10 | - Example 2: 11 | 12 | `demo` 13 | 14 | - Example 3: 15 | 16 | `demo` 17 | 18 | - Example 4: 19 | 20 | `demo` 21 | 22 | - Example 5: 23 | 24 | `demo` 25 | 26 | - Example 6: 27 | 28 | `demo` 29 | 30 | - Example 7: 31 | 32 | `demo` 33 | 34 | - Example 8: 35 | 36 | `demo` 37 | 38 | - Example 9: 39 | 40 | `demo` 41 | -------------------------------------------------------------------------------- /specs/pages/failing/020.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program (note: this should error). 4 | > NOTE: this should not pass. 5 | > More information: . 6 | 7 | - Example 1 (note: this should error): 8 | 9 | `demo` 10 | -------------------------------------------------------------------------------- /specs/pages/failing/021.md: -------------------------------------------------------------------------------- 1 | # demo 2 | 3 | > Sample program. 4 | > More information: . 5 | 6 | - Example 1 should fail: 7 | 8 | ` demo` 9 | 10 | - Example 2 should fail: 11 | 12 | `demo ` 13 | 14 | - Example 3 should pass: 15 | 16 | `demo \ ` 17 | -------------------------------------------------------------------------------- /specs/pages/failing/101.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | JAR (Java Archive) is a package file format used to distribute application software or libraries on the Java platform. 4 | 5 | - Unzip *.jar/*.war file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/102.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/103.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar 8 | 9 | - Blub the blubbipity: 10 | 11 | `blub {{bluppity}} 12 | -------------------------------------------------------------------------------- /specs/pages/failing/104.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzips file to the current directory: 6 | 7 | `jar` 8 | 9 | - Ping someone: 10 | 11 | `ping` 12 | 13 | - Writing hello world: 14 | 15 | `echo` 16 | 17 | - Pass a value: 18 | 19 | `blub` 20 | 21 | - Always do something: 22 | 23 | `gimp` 24 | -------------------------------------------------------------------------------- /specs/pages/failing/105.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | 5 | - Here goes an example: 6 | 7 | `there are really a lot of ways` 8 | `such a long list` 9 | `a loooot` 10 | -------------------------------------------------------------------------------- /specs/pages/failing/106.md: -------------------------------------------------------------------------------- 1 | jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/107: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/108 .md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/109A.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/failing/110.md: -------------------------------------------------------------------------------- 1 | # tar 2 | 3 | > Archiving utility. 4 | > Often combined with a compression method, such as gzip or bzip. 5 | > More information: . 6 | 7 | - [c]reate an archive from [f]iles: 8 | 9 | `` 10 | -------------------------------------------------------------------------------- /specs/pages/failing/111.md: -------------------------------------------------------------------------------- 1 | # jar 2 | 3 | > JAR (Java Archive) is a package file format. 4 | 5 | - Unzip file to the current directory: 6 | 7 | `jar -xvf *.jar` 8 | -------------------------------------------------------------------------------- /specs/pages/passing/!.md: -------------------------------------------------------------------------------- 1 | # ! 2 | 3 | > Reuse and expand the shell history in `sh`, Bash, Zsh, `rbash` and `ksh`. 4 | > More information: . 5 | 6 | - Substitute with the previous command and run it with `sudo`: 7 | 8 | `sudo !!` 9 | 10 | - Substitute with a command based on its line number found with `history`: 11 | 12 | `!{{number}}` 13 | 14 | - Substitute with a command that was used a specified number of lines back: 15 | 16 | `!-{{number}}` 17 | 18 | - Substitute with the most recent command that starts with a string: 19 | 20 | `!{{string}}` 21 | 22 | - Substitute with the arguments of the latest command: 23 | 24 | `{{command}} !*` 25 | 26 | - Substitute with the last argument of the latest command: 27 | 28 | `{{command}} !$` 29 | 30 | - Substitute with the last command but without the last argument: 31 | 32 | `!:-` 33 | 34 | - Print last command that starts with a string without executing it: 35 | 36 | `!{{string}}:p` 37 | -------------------------------------------------------------------------------- /specs/pages/passing/bracket.md: -------------------------------------------------------------------------------- 1 | # tar 2 | 3 | > Archiving utility. 4 | > Often combined with a compression method, such as gzip or bzip. 5 | > More information: . 6 | 7 | - [c]reate an archive from [f]iles: 8 | 9 | `tar cf {{target.tar}} {{file1}} {{file2}} {{file3}}` 10 | -------------------------------------------------------------------------------- /specs/pages/passing/descriptions.md: -------------------------------------------------------------------------------- 1 | # du 2 | 3 | > Estimate file space usage. 4 | > This just goes on (Note: this is a note). 5 | > Because its so important. 6 | -------------------------------------------------------------------------------- /specs/pages/passing/lower-case.md: -------------------------------------------------------------------------------- 1 | # npm 2 | 3 | > npm is always written in lower case. 4 | > This just goes on. 5 | -------------------------------------------------------------------------------- /specs/pages/passing/special-characters.md: -------------------------------------------------------------------------------- 1 | # command 2 | 3 | > Test for special uppercase characters. 4 | > For example French has these uppercase letters `Ê`, `À`, `Ė`, `Ç`, etc. 5 | > More information: . 6 | 7 | - Éxecute une commande (exec a cmd in French): 8 | 9 | `command` 10 | -------------------------------------------------------------------------------- /specs/pages/passing/title++.md: -------------------------------------------------------------------------------- 1 | # title++ 2 | 3 | > Compiles C++ source files. 4 | > Part of GCC (GNU Compiler Collection). 5 | > More information: . 6 | 7 | - Compile a source code file into an executable binary: 8 | 9 | `g++ {{source.cpp}} -o {{output_executable}}` 10 | 11 | - Display (almost) all errors and warnings: 12 | 13 | `g++ {{source.cpp}} -Wall -o {{output_executable}}` 14 | 15 | - Choose a language standard to compile for(C++98/C++11/C++14/C++17): 16 | 17 | `g++ {{source.cpp}} -std={{language_standard}} -o {{output_executable}}` 18 | 19 | - Include libraries located at a different path than the source file: 20 | 21 | `g++ {{source.cpp}} -o {{output_executable}} -I{{header_path}} -L{{library_path}} -l{{library_name}}` 22 | -------------------------------------------------------------------------------- /specs/tldr-lint-helper.js: -------------------------------------------------------------------------------- 1 | const linter = require('../lib/tldr-lint.js'); 2 | const path = require('path'); 3 | 4 | const lintFile = function(file, ignoreErrors) { 5 | return linter.processFile(path.join(__dirname, file), false, false, ignoreErrors); 6 | }; 7 | 8 | const containsErrors = function(errors, expected) { 9 | if (errors.length === 0) return false; 10 | if (!(expected instanceof Array)) 11 | expected = Array.prototype.splice.call(arguments, 1); 12 | expected.forEach(function(expectedCode) { 13 | // If not some correspond to every expected, false 14 | if (!errors.some(function(error) { return error === expectedCode; })) 15 | return false; 16 | }); 17 | return true; 18 | }; 19 | 20 | const containsOnlyErrors = function(errors, expected) { 21 | if (!(expected instanceof Array)) { 22 | expected = Array.prototype.splice.call(arguments, 1); 23 | } 24 | expected.forEach(function(error) { 25 | if (!containsErrors(errors, error)) { 26 | console.error('Couldnt find error', error, 'in these errors'); 27 | console.error(errors); 28 | return false; 29 | } 30 | }); 31 | for (let i = 0; i < errors.length; i++) { 32 | const error = errors[i]; 33 | if (!expected.some(function(expectedCode) { 34 | return error.code === expectedCode; 35 | })) { 36 | return false; 37 | } 38 | } 39 | return true; 40 | }; 41 | 42 | module.exports = { 43 | lintFile, 44 | containsErrors, 45 | containsOnlyErrors, 46 | }; 47 | -------------------------------------------------------------------------------- /specs/tldr-lint.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const linter = require('../lib/tldr-lint.js'); 3 | const { lintFile, containsErrors, containsOnlyErrors } = require('./tldr-lint-helper'); 4 | 5 | describe('TLDR conventions', function() { 6 | it('TLDR001\t' + linter.ERRORS.TLDR001, function() { 7 | const errors = lintFile('pages/failing/001.md').errors; 8 | expect(containsOnlyErrors(errors, 'TLDR001')).toBeTruthy(); 9 | }); 10 | 11 | it('TLDR002\t' + linter.ERRORS.TLDR002, function() { 12 | const errors = lintFile('pages/failing/002.md').errors; 13 | expect(containsOnlyErrors(errors, 'TLDR002')).toBeTruthy(); 14 | // This error should occur in 3 different places 15 | expect(errors.length).toBe(3); 16 | }); 17 | 18 | it('TLDR003\t' + linter.ERRORS.TLDR003, function() { 19 | const errors = lintFile('pages/failing/003.md').errors; 20 | expect(containsOnlyErrors(errors, 'TLDR003')).toBeTruthy(); 21 | expect(errors.length).toBe(1); 22 | }); 23 | 24 | it('TLDR004\t' + linter.ERRORS.TLDR004, function() { 25 | let errors = lintFile('pages/failing/004.md').errors; 26 | expect(containsErrors(errors, ['TLDR004', 'TLDR014'])).toBeTruthy(); 27 | expect(errors.length).toBe(4); 28 | }); 29 | 30 | it('TLDR005\t' + linter.ERRORS.TLDR005, function() { 31 | let errors = lintFile('pages/failing/005.md').errors; 32 | expect(containsOnlyErrors(errors, 'TLDR005')).toBeTruthy(); 33 | expect(errors.length).toBe(2); 34 | }); 35 | 36 | it('TLDR006\t' + linter.ERRORS.TLDR006, function() { 37 | let errors = lintFile('pages/failing/006.md').errors; 38 | expect(containsOnlyErrors(errors, 'TLDR006')).toBeTruthy(); 39 | expect(errors.length).toBe(1); 40 | }); 41 | 42 | it('TLDR007\t' + linter.ERRORS.TLDR007, function() { 43 | let errors = lintFile('pages/failing/007.md').errors; 44 | expect(containsOnlyErrors(errors, 'TLDR007')).toBeTruthy(); 45 | expect(errors.length).toBe(2); 46 | }); 47 | 48 | it('TLDR008\t' + linter.ERRORS.TLDR008, function() { 49 | let errors = lintFile('pages/failing/008.md').errors; 50 | expect(containsOnlyErrors(errors, 'TLDR008')).toBeTruthy(); 51 | expect(errors.length).toBe(1); 52 | }); 53 | 54 | it('TLDR009\t' + linter.ERRORS.TLDR009, function() { 55 | let errors = lintFile('pages/failing/009.md').errors; 56 | expect(containsOnlyErrors(errors, 'TLDR009')).toBeTruthy(); 57 | expect(errors.length).toBe(1); 58 | }); 59 | 60 | it('TLDR011\t' + linter.ERRORS.TLDR011, function() { 61 | let errors = lintFile('pages/failing/011.md').errors; 62 | expect(containsOnlyErrors(errors, 'TLDR011')).toBeTruthy(); 63 | expect(errors.length).toBe(2); 64 | }); 65 | 66 | it('TLDR012\t' + linter.ERRORS.TLDR012, function() { 67 | let errors = lintFile('pages/failing/012.md').errors; 68 | expect(containsOnlyErrors(errors, 'TLDR012')).toBeTruthy(); 69 | expect(errors.length).toBe(2); 70 | }); 71 | 72 | it('TLDR013\t' + linter.ERRORS.TLDR013, function() { 73 | let errors = lintFile('pages/failing/013.md').errors; 74 | expect(containsOnlyErrors(errors, 'TLDR013')).toBeTruthy(); 75 | expect(errors.length).toBe(1); 76 | }); 77 | 78 | it('TLDR014\t' + linter.ERRORS.TLDR014, function() { 79 | let errors = lintFile('pages/failing/014.md').errors; 80 | expect(containsOnlyErrors(errors, 'TLDR014')).toBeTruthy(); 81 | expect(errors.length).toBe(5); 82 | }); 83 | 84 | it('TLDR015\t' + linter.ERRORS.TLDR015, function() { 85 | let errors = lintFile('pages/failing/015.md').errors; 86 | expect(containsOnlyErrors(errors, 'TLDR015')).toBeTruthy(); 87 | expect(errors.length).toBe(1); 88 | }); 89 | 90 | it('TLDR016\t' + linter.ERRORS.TLDR016, function() { 91 | let errors = lintFile('pages/failing/016.md').errors; 92 | expect(containsOnlyErrors(errors, 'TLDR016')).toBeTruthy(); 93 | expect(errors.length).toBe(1); 94 | }); 95 | 96 | it('TLDR017\t' + linter.ERRORS.TLDR017, function() { 97 | let errors = lintFile('pages/failing/017.md').errors; 98 | expect(containsOnlyErrors(errors, 'TLDR017')).toBeTruthy(); 99 | expect(errors.length).toBe(1); 100 | }); 101 | 102 | it('TLDR018\t' + linter.ERRORS.TLDR018, function() { 103 | let errors = lintFile('pages/failing/018.md').errors; 104 | expect(containsOnlyErrors(errors, 'TLDR018')).toBeTruthy(); 105 | expect(errors.length).toBe(2); 106 | }); 107 | 108 | it('TLDR019\t' + linter.ERRORS.TLDR019, function() { 109 | let errors = lintFile('pages/failing/019.md').errors; 110 | expect(containsOnlyErrors(errors, 'TLDR019')).toBeTruthy(); 111 | expect(errors.length).toBe(1); 112 | }); 113 | 114 | it('TLDR020\t' + linter.ERRORS.TLDR020, function() { 115 | let errors = lintFile('pages/failing/020.md').errors; 116 | expect(containsOnlyErrors(errors, 'TLDR020')).toBeTruthy(); 117 | expect(errors.length).toBe(3); 118 | }); 119 | 120 | it('TLDR021\t' + linter.ERRORS.TLDR021, function() { 121 | let errors = lintFile('pages/failing/021.md').errors; 122 | expect(containsOnlyErrors(errors, 'TLDR021')).toBeTruthy(); 123 | expect(errors.length).toBe(2); 124 | }); 125 | }); 126 | 127 | describe('Common TLDR formatting errors', function() { 128 | it('TLDR101\t' + linter.ERRORS.TLDR101, function() { 129 | let errors = lintFile('pages/failing/101.md').errors; 130 | expect(containsOnlyErrors(errors, 'TLDR101')).toBeTruthy(); 131 | expect(errors.length).toBe(1); 132 | }); 133 | 134 | it('TLDR102\t' + linter.ERRORS.TLDR102, function() { 135 | let errors = lintFile('pages/failing/102.md').errors; 136 | expect(containsOnlyErrors(errors, 'TLDR102')).toBeTruthy(); 137 | expect(errors.length).toBe(1); 138 | }); 139 | 140 | it('TLDR103\t' + linter.ERRORS.TLDR103, function() { 141 | let errors = lintFile('pages/failing/103.md').errors; 142 | expect(containsOnlyErrors(errors, 'TLDR103')).toBeTruthy(); 143 | expect(errors.length).toBe(2); 144 | }); 145 | 146 | it('TLDR104\t' + linter.ERRORS.TLDR104, function() { 147 | let errors = lintFile('pages/failing/104.md').errors; 148 | expect(containsOnlyErrors(errors, 'TLDR104')).toBeTruthy(); 149 | expect(errors.length).toBe(2); 150 | }); 151 | 152 | it('TLDR105\t' + linter.ERRORS.TLDR105, function() { 153 | let errors = lintFile('pages/failing/105.md').errors; 154 | expect(containsOnlyErrors(errors, 'TLDR105')).toBeTruthy(); 155 | expect(errors.length).toBe(2); 156 | }); 157 | 158 | it('TLDR106\t' + linter.ERRORS.TLDR106, function() { 159 | let errors = lintFile('pages/failing/106.md').errors; 160 | expect(containsOnlyErrors(errors, 'TLDR106')).toBeTruthy(); 161 | expect(errors.length).toBe(1); 162 | }); 163 | 164 | it('TLDR107\t' + linter.ERRORS.TLDR107, function() { 165 | let errors = lintFile('pages/failing/107').errors; 166 | expect(containsOnlyErrors(errors, 'TLDR107')).toBeTruthy(); 167 | expect(errors.length).toBe(1); 168 | }); 169 | 170 | it('TLDR108\t' + linter.ERRORS.TLDR108, function () { 171 | let errors = lintFile('pages/failing/108 .md').errors; 172 | expect(containsOnlyErrors(errors, 'TLDR108')).toBeTruthy(); 173 | expect(errors.length).toBe(1); 174 | }); 175 | 176 | it('TLDR109\t' + linter.ERRORS.TLDR109, function () { 177 | let errors = lintFile('pages/failing/109A.md').errors; 178 | expect(containsOnlyErrors(errors, 'TLDR109')).toBeTruthy(); 179 | expect(errors.length).toBe(1); 180 | }); 181 | 182 | it('TLDR110\t' + linter.ERRORS.TLDR110, function () { 183 | let errors = lintFile('pages/failing/110.md').errors; 184 | expect(containsOnlyErrors(errors, 'TLDR110')).toBeTruthy(); 185 | expect(errors.length).toBe(1); 186 | }); 187 | 188 | const invalidCharacters = ['<', '>', ':', '"', '/', '\\', '|', '?', '*']; 189 | invalidCharacters.forEach((char) => { 190 | it('TLDR111\t' + linter.ERRORS.TLDR111 + '\t - ${char}', function() { 191 | const basenameSpy = jest.spyOn(path, 'basename').mockImplementation((filePath) => { 192 | return `111${char}`; 193 | }); 194 | 195 | let errors = lintFile(`pages/failing/111.md`).errors; 196 | expect(containsOnlyErrors(errors, 'TLDR111')).toBeTruthy(); 197 | expect(errors.length).toBe(1); 198 | 199 | basenameSpy.mockRestore(); 200 | }); 201 | }); 202 | }); 203 | 204 | describe('TLDR pages that are simply correct', function() { 205 | it('Multiple description lines', function() { 206 | let errors = lintFile('pages/passing/descriptions.md').errors; 207 | expect(errors.length).toBe(0); 208 | }); 209 | 210 | it('Example starting with a bracket', function() { 211 | let errors = lintFile('pages/passing/bracket.md').errors; 212 | expect(errors.length).toBe(0); 213 | }); 214 | 215 | it('Example starting with an upper cased unicode character', function() { 216 | let errors = lintFile('pages/passing/special-characters.md').errors; 217 | expect(errors.length).toBe(0); 218 | }); 219 | 220 | it('Page filename and title includes + symbol', function() { 221 | let errors = lintFile('pages/passing/title++.md').errors; 222 | expect(errors.length).toBe(0); 223 | }); 224 | 225 | it('Page filename and title includes ! symbol', function() { 226 | let errors = lintFile('pages/passing/!.md').errors; 227 | expect(errors.length).toBe(0); 228 | }) 229 | 230 | it('Certain words are always written in lower case', function() { 231 | let errors = lintFile('pages/passing/lower-case.md').errors; 232 | expect(errors.length).toBe(0); 233 | }); 234 | }); 235 | 236 | describe('ignore errors', function() { 237 | it('ignore TLDR014', function() { 238 | let errors = lintFile('pages/failing/004.md', 'TLDR014').errors; 239 | expect(containsOnlyErrors(errors, 'TLDR004')).toBeTruthy(); 240 | expect(errors.length).toBe(2); 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /tldr.l: -------------------------------------------------------------------------------- 1 | %options backtrack_lexer 2 | %lex 3 | 4 | %{ 5 | yy.initLexer(yy.lexer); 6 | %} 7 | 8 | eol (?:\r\n)|\n|\r 9 | space [ \t] 10 | 11 | %% 12 | 13 | \s+<> 14 | %{ 15 | yy.error(yylloc, 'TLDR008'); 16 | %} 17 | 18 | .*?\t+.* 19 | %{ 20 | yy.error(yylloc, 'TLDR012'); 21 | var cleaned = this.match.replace(/\t/g, ' '); 22 | this.unput(cleaned); 23 | %} 24 | 25 | [^\n]<> 26 | %{ 27 | // this.setInput resets the state as well, so push that back in 28 | var currentConditionStack = this.conditionStack; 29 | var currentConditions = this.conditions; 30 | // Basically replace EOF with a final newline so lexing can continue 31 | this.setInput(this.match + '\n') 32 | this.conditionStack = currentConditionStack; 33 | this.currentConditions = currentConditions; 34 | yy.error(yylloc, 'TLDR009'); 35 | %} 36 | 37 | (\s*)\#(\s*) 38 | %{ 39 | if (this.topState() !== 'INITIAL') { 40 | this.reject(); 41 | return; 42 | } 43 | this.pushState('title'); 44 | if (this.matches[1]) { 45 | yy.error(yylloc, 'TLDR001'); 46 | } 47 | if (this.matches[2] !== ' ') { 48 | yy.error(yylloc, 'TLDR002'); 49 | } 50 | return 'HASH'; 51 | %} 52 | 53 | ([\>-])(\s*) 54 | %{ 55 | if (this.topState() !== 'INITIAL') { 56 | this.reject(); 57 | return; 58 | } 59 | if (this.matches[2] !== ' ') { 60 | yy.error(yylloc, 'TLDR002'); 61 | } 62 | if (this.matches[1] == '>') { 63 | this.pushState('description'); 64 | return 'GREATER_THAN'; 65 | } else { 66 | this.pushState('example_description'); 67 | return 'DASH'; 68 | } 69 | %} 70 | 71 | ([Mm]ore\s+[Ii]nfo(?:rmation)?\:?\s*) 72 | %{ 73 | if (this.topState() !== 'description') { 74 | this.reject(); 75 | return; 76 | } 77 | if (this.matches[1] !== 'More information: ') { 78 | yy.error(yylloc, 'TLDR016'); 79 | } 80 | this.popState(); 81 | this.pushState('information_link'); 82 | return 'INFORMATION_LINK'; 83 | %} 84 | 85 | (\]*\>) 86 | %{ 87 | if (this.topState() !== 'information_link') { 88 | this.reject(); 89 | return; 90 | } 91 | this.popState(); 92 | this.pushState('information_link_url'); 93 | return 'ANGLE_BRACKETED_URL'; 94 | %} 95 | 96 | // All regexes below are actually about the same, but it's better organized 97 | // this way around. 98 | (.+?)([ ]*){eol} 99 | %{ 100 | if (this.topState() === "title") { 101 | yytext = this.matches[1]; 102 | if (this.matches[1].match(/([^\w+\[\]}!. -])|(\.$)/)) yy.error(yylloc, 'TLDR013'); 103 | this.checkTrailingWhitespace(this.matches[2], yylloc); 104 | this.checkNewline(this.matches[3], yylloc); 105 | this.popState(); 106 | return 'TITLE'; 107 | } else if (this.topState() === 'information_link_url') { 108 | if (this.matches[1] != '.') yy.error(yylloc, 'TLDR004'); 109 | this.checkTrailingWhitespace(this.matches[2], yylloc); 110 | this.checkNewline(this.matches[3], yylloc); 111 | this.popState(); 112 | return 'END_INFORMATION_LINK_URL'; 113 | } else if (this.topState() === 'information_link') { 114 | this.checkTrailingWhitespace(this.matches[2], yylloc); 115 | this.checkNewline(this.matches[3], yylloc); 116 | this.popState(); 117 | return 'END_INFORMATION_LINK'; 118 | } else { 119 | this.reject(); 120 | } 121 | %} 122 | 123 | (.+?)([\.,;!\?]?)([ ]*){eol} 124 | %{ 125 | if (this.topState() === 'description') { 126 | this.popState(); 127 | yytext = this.matches[1]; 128 | var exceptions = ['npm', 'pnpm']; 129 | if (!exceptions.includes(yytext.replace(/ .*/,'')) && yytext.match(/^[a-z]/)) { 130 | yy.error(yylloc, 'TLDR003'); 131 | } 132 | if (yytext.match(/(note|NOTE): /)) { 133 | yy.error(yylloc, 'TLDR020'); 134 | } 135 | var punctuation = this.matches[2]; 136 | if (punctuation !== '.') { 137 | yy.error(yylloc, 'TLDR004'); 138 | } 139 | if (punctuation.match(/[,;]/)) { 140 | console.warn(`Description ends in \'${punctuation}\'. Consider writing your sentence on one line.`); 141 | } 142 | this.checkTrailingWhitespace(this.matches[3], yylloc); 143 | this.checkNewline(this.matches[4], yylloc); 144 | return 'DESCRIPTION_LINE'; 145 | } else { 146 | this.reject(); 147 | } 148 | %} 149 | 150 | 151 | (.+?)([\.:,;]?)([ ]*){eol} 152 | %{ 153 | if (this.topState() === 'example_description') { 154 | this.popState(); 155 | yytext = this.matches[1]; 156 | if (!yytext.match(/^[\p{Lu}\[]/u)) yy.error(yylloc, 'TLDR015'); 157 | if (this.matches[2] !== ':') yy.error(yylloc, 'TLDR005'); 158 | if (yytext.match(/(note|NOTE): /)) { 159 | yy.error(yylloc, 'TLDR020'); 160 | } 161 | // Try to ensure that verbs at the beginning of the description are in the infinitive tense 162 | // 1. Any word at the start of a sentence that ends with "ing" and is 6 or more characters long (e.g. executing, writing) is likely a verb in the gerund 163 | // 2. Any word at the start of a sentence that doesn't end with "us", "ss", or "ys" (e.g. superfluous, success, always) is likely a verb in the present tense 164 | if (yytext.match(/(^[A-Za-z]{3,}ing )|(^[A-Za-z]+[^usy]s )/)) { 165 | yy.error(yylloc, 'TLDR104'); 166 | } 167 | // Check if any sneaky spaces have been caught 168 | this.checkTrailingWhitespace(this.matches[3], yylloc); 169 | this.checkNewline(this.matches[3], yylloc); 170 | return 'EXAMPLE_DESCRIPTION'; 171 | } else { 172 | this.reject(); 173 | } 174 | %} 175 | 176 | \`{eol} 177 | %{ 178 | if (this.topState() === 'example_command') { 179 | this.popState(); 180 | this.checkNewline(this.matches[1], yylloc); 181 | return 'BACKTICK'; 182 | } else { 183 | this.reject(); 184 | } 185 | %} 186 | 187 | \`([ ]*) 188 | %{ 189 | this.pushState('example_command'); 190 | if (this.matches[1].match(/ /)) { 191 | yy.error(yylloc, 'TLDR021') 192 | } 193 | return 'BACKTICK'; 194 | %} 195 | 196 | \{\{([^\n\`\{\}]*)\}\}({eol}?) 197 | %{ 198 | if (this.topState() === 'example_command') { 199 | yytext = this.matches[1]; 200 | if (this.matches[3]) { 201 | // Matched a newline where there certainly should not be one 202 | // This code is duplicated from two rules below.. should unify 203 | this.unput('`' + this.matches[2]); 204 | yy.error(yylloc, 'TLDR103') 205 | } 206 | return 'COMMAND_TOKEN'; 207 | } else this.reject(); 208 | %} 209 | 210 | // Example commands text either runs up to two left braces (signaling a token) 211 | // Or up to a backtick, which means that's it for the command. 212 | ([^\`\n]+?)\{\{ 213 | %{ 214 | if (this.topState() === 'example_command') { 215 | this.unput('{{'); 216 | yytext = this.matches[1]; 217 | return 'COMMAND_TEXT'; 218 | } else this.reject(); 219 | %} 220 | 221 | ([^\`\n]+?)(\`[ ]*|[ ]*{eol}) 222 | %{ 223 | if (this.topState() === 'example_command') { 224 | // Check if there are some trailing spaces 225 | if (this.matches[2].match(/ /)) { 226 | yy.error(yylloc, 'TLDR014'); 227 | } 228 | if (this.matches[1].endsWith(' ') && !this.matches[1].endsWith('\\ ')) { 229 | yy.error(yylloc, 'TLDR021'); 230 | } 231 | // Don't swallow the trailing backtick just yet 232 | if (this.matches[2].match(/\`/)) this.unput('`'); 233 | else { 234 | // If command doesn't end in a backtick, just add a backtick anyway 235 | // Also pop back the newline. Let's pretend we don't care what that is. 236 | this.unput('`\n'); 237 | yy.error(yylloc, 'TLDR103') 238 | } 239 | yytext = this.matches[1]; 240 | return 'COMMAND_TEXT'; 241 | } else this.reject(); 242 | %} 243 | 244 | [ ]+ 245 | %{ 246 | yy.error(yylloc, 'TLDR014'); 247 | %} 248 | 249 | {eol}+ 250 | %{ 251 | // Either you've got more than a single \n or \r or more than \r\n 252 | var isdos = this.match.match(/\r\n/); 253 | if (isdos && this.match.length > 2 || !isdos && this.match.length > 1) { 254 | yy.error(yylloc, 'TLDR011'); 255 | } 256 | this.checkNewline(this.match, yylloc); 257 | return 'NEWLINE'; 258 | %} 259 | 260 | // This is a catchall that just slurps up a line if doesn't match 261 | // The compiler can then use it to create meaningful errors since it has 262 | // more context than the lexer does 263 | (.+?)[\.:]?{eol} 264 | %{ 265 | yytext = this.matches[1]; 266 | return 'TEXT'; 267 | %} 268 | -------------------------------------------------------------------------------- /tldr.yy: -------------------------------------------------------------------------------- 1 | %token HASH GREATER_THAN DASH BACKTICK 2 | %token NEWLINE 3 | %token DESCRIPTION_LINE 4 | %token EXAMPLE_DESCRIPTION 5 | %token COMMAND_TOKEN COMMAND_TEXT 6 | %token TEXT 7 | %token ANGLE_BRACKETED_URL INFORMATION_LINK END_INFORMATION_LINK_URL END_INFORMATION_LINK 8 | 9 | %start page 10 | 11 | %% 12 | 13 | page : title NEWLINE info examples 14 | | title info examples -> yy.error(@$, 'TLDR006') 15 | | title NEWLINE TEXT examples -> yy.error(@$, 'TLDR101') || yy.addDescription($TEXT); 16 | ; 17 | 18 | title : HASH TITLE -> yy.setTitle($TITLE) 19 | | TEXT -> yy.error(@TEXT, 'TLDR106') || yy.setTitle($TEXT) 20 | ; 21 | 22 | info : description 23 | | description information_link 24 | ; 25 | 26 | description : GREATER_THAN DESCRIPTION_LINE -> yy.addDescription($DESCRIPTION_LINE) 27 | | description GREATER_THAN DESCRIPTION_LINE -> yy.addDescription($DESCRIPTION_LINE) 28 | ; 29 | 30 | information_link : GREATER_THAN INFORMATION_LINK ANGLE_BRACKETED_URL END_INFORMATION_LINK_URL 31 | -> yy.addInformationLink($ANGLE_BRACKETED_URL) 32 | | GREATER_THAN INFORMATION_LINK END_INFORMATION_LINK 33 | -> yy.error(@$, 'TLDR017') || yy.addDescription($INFORMATION_LINK + $END_INFORMATION_LINK.trim()) 34 | | information_link GREATER_THAN INFORMATION_LINK ANGLE_BRACKETED_URL END_INFORMATION_LINK_URL 35 | -> yy.error(@$, 'TLDR018') 36 | ; 37 | 38 | examples : %empty 39 | | examples example 40 | ; 41 | 42 | example : maybe_newline example_description maybe_newline example_commands 43 | { 44 | yy.addExample($example_description, $example_commands); 45 | // Just use the description line's location, easy to find 46 | if (!$maybe_newline1) 47 | yy.error(@example_description, 'TLDR007'); 48 | if (!$maybe_newline2) 49 | yy.error(@example_commands, 'TLDR007'); 50 | } 51 | ; 52 | 53 | maybe_newline : %empty 54 | | NEWLINE 55 | ; 56 | 57 | example_description : DASH EXAMPLE_DESCRIPTION -> $EXAMPLE_DESCRIPTION 58 | | TEXT -> yy.error(@$, 'TLDR102') || $TEXT 59 | ; 60 | 61 | example_commands : example_command -> [$example_command] 62 | | example_commands example_command 63 | -> yy.error(@example_command, 'TLDR105') || $example_commands 64 | ; 65 | 66 | example_command : BACKTICK BACKTICK -> yy.error(@$, 'TLDR110') 67 | | BACKTICK example_command_inner BACKTICK -> $example_command_inner 68 | /* | BACKTICK example_command_inner -> yy.error(@$, 'TLDR103') || $example_command_inner */ 69 | ; 70 | 71 | example_command_inner : COMMAND_TEXT -> $COMMAND_TEXT 72 | | COMMAND_TOKEN -> $COMMAND_TOKEN 73 | | example_command_inner COMMAND_TEXT 74 | -> [].concat($example_command_inner, yy.createCommandText($COMMAND_TEXT)) 75 | | example_command_inner COMMAND_TOKEN 76 | -> [].concat($example_command_inner, yy.createToken($COMMAND_TOKEN)) 77 | ; 78 | --------------------------------------------------------------------------------