├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .prettierignore ├── .gitattributes ├── .eslintignore ├── commitlint.config.js ├── src ├── custom.d.ts ├── types.ts ├── tags.ts ├── roles.ts ├── stringify.ts ├── utils.ts ├── index.ts └── descriptionFormatter.ts ├── .huskyrc.json ├── .prettierrc ├── jest.config.js ├── prettier-plugin-fake ├── package.json └── index.js ├── tests ├── files │ ├── order.jsx │ ├── create-ignorer.js │ ├── types.ts │ ├── tsdoc.ts │ ├── order-custom.jsx │ ├── typeScript.js │ ├── typeScript.ts │ └── prism-dependencies.js ├── __snapshots__ │ ├── remarks.test.ts.snap │ ├── dottedNames.test.ts.snap │ ├── files │ │ ├── order.jsx.shot │ │ ├── create-ignorer.js.shot │ │ ├── types.ts.shot │ │ ├── tsdoc.ts.shot │ │ ├── order-custom.jsx.shot │ │ ├── typeScript.ts.shot │ │ ├── typeScript.js.shot │ │ └── prism-dependencies.js.shot │ ├── jsdocParserDisable.test.ts.snap │ ├── modern.test.ts.snap │ ├── template.test.ts.snap │ ├── singleTag.test.ts.snap │ ├── objectProperty.test.ts.snap │ ├── tagGroup.test.ts.snap │ ├── react.test.ts.snap │ ├── default.test.ts.snap │ ├── compatibleWithPlugins.test.ts.snap │ ├── exampleTag.test.ts.snap │ └── typeScript.test.ts.snap ├── dottedNames.test.ts ├── remarks.test.ts ├── modern.test.ts ├── jsdocParserDisable.test.ts ├── template.test.ts ├── objectProperty.test.ts ├── react.test.ts ├── singleTag.test.ts ├── files.test.ts ├── tagGroup.test.ts ├── default.test.ts ├── exampleTag.test.ts ├── compatibleWithPlugins.test.ts └── typeScript.test.ts ├── .eslintrc.cjs ├── tsconfig.json ├── rollup.config.js ├── .github └── workflows │ └── test.yml ├── script.sh ├── .vscode └── launch.json ├── LICENSE ├── doc └── CUSTOM_TAGS_ORDER.md ├── .gitignore ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | prettier-plugin-fake/ 3 | lib/ 4 | tests/files 5 | .eslintrc.js -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | yarn commitlint --edit $1 -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # shellcheck source=./_/husky.sh 4 | . "$(dirname "$0")/_/husky.sh" 5 | 6 | yarn lint 7 | yarn test -------------------------------------------------------------------------------- /src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace jest { 2 | interface Matchers { 3 | toMatchSpecificSnapshot(filename: string): R; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "yarn lint && yarn test" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": false, 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "bracketSpacing": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('@jest/types').Config.InitialOptions} */ 4 | const config = { 5 | runner: 'jest-light-runner' 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prettier-plugin-fake/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier-plugin-fake", 3 | "version": "1.1.1", 4 | "private": true, 5 | "type": "module", 6 | "files": [ 7 | "index.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/files/order.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @callback ClassMapper 3 | * @param {string} className 4 | * @param {string} language 5 | * @returns {string} 6 | * 7 | * @callback ClassAdder 8 | * @param {ClassAdderEnvironment} env 9 | * @returns {undefined | string | string[]} 10 | * 11 | * @typedef ClassAdderEnvironment 12 | * @property {string} language 13 | * @property {string} type 14 | * @property {string} content 15 | */ -------------------------------------------------------------------------------- /tests/__snapshots__/remarks.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Description start with markdown 1`] = ` 4 | "/** 5 | * Just a simple test 6 | * 7 | * @remarks 8 | * - Remark 1 9 | * - Remark 2 10 | * - Remark 3 11 | */ 12 | " 13 | `; 14 | 15 | exports[`Description start with markdown 2`] = ` 16 | "/** 17 | * Just a simple test 18 | * 19 | * @remarks 20 | * - Remark 1 21 | * - Remark 2 22 | * - Remark 3 23 | */ 24 | " 25 | `; 26 | -------------------------------------------------------------------------------- /tests/__snapshots__/dottedNames.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`dotted names function param 1`] = ` 4 | "/** 5 | * @param {object} data 6 | * @param {string} data.userName 7 | * @param {string} data.password 8 | * 9 | * @typedef {object} LoginResponse 10 | * @property {string} token 11 | * @property {number} expires 12 | * @property {boolean} mustChangePassword 13 | * @returns {import("axios").AxiosResponse} 14 | */ 15 | function a() {} 16 | " 17 | `; 18 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/order.jsx.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: order.jsx 1`] = ` 4 | "/** 5 | * @callback ClassMapper 6 | * @param {string} className 7 | * @param {string} language 8 | * @returns {string} 9 | * 10 | * @callback ClassAdder 11 | * @param {ClassAdderEnvironment} env 12 | * @returns {undefined | string | string[]} 13 | * 14 | * @typedef ClassAdderEnvironment 15 | * @property {string} language 16 | * @property {string} type 17 | * @property {string} content 18 | */ 19 | " 20 | `; 21 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | commonjs: true, 6 | jest: true, 7 | }, 8 | parserOptions: { 9 | ecmaVersion: "next", 10 | }, 11 | parser: "@typescript-eslint/parser", 12 | plugins: ["@typescript-eslint"], 13 | extends: [ 14 | "prettier", 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | ], 18 | rules: { 19 | "@typescript-eslint/no-explicit-any": "off", 20 | "no-unused-vars": "off", 21 | "no-redeclare": "off", 22 | "no-const-assign": "off", 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": ["es2020"], 5 | "module": "es2020", 6 | "target": "es2020", 7 | "moduleResolution": "node", 8 | "removeComments": true, 9 | "sourceMap": true, 10 | "strict": true, 11 | "outDir": "./dist", 12 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["dist", "node_modules", "tests", "src/custom.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /tests/__snapshots__/jsdocParserDisable.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`disabled complex object typedef 1`] = ` 4 | "/** 5 | * The bread crumbs indicate the navigate path and trigger the active page. 6 | * @class 7 | * @typedef {object} props 8 | * @prop {any} navigation 9 | * @extends {PureComponent< props>} 10 | */ 11 | export class BreadCrumbs extends PureComponent {} 12 | " 13 | `; 14 | 15 | exports[`template for callback 1`] = ` 16 | "/** 17 | * @template T 18 | * @callback CallbackName 19 | * @param {GetStyles} getStyles 20 | * @returns {UseStyle} 21 | */ 22 | " 23 | `; 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 3 | import commonjs from "@rollup/plugin-commonjs"; 4 | import json from "@rollup/plugin-json"; 5 | 6 | // ignore all bare imports from node_modules 7 | // which are not relative and not absolute 8 | const external = (id) => 9 | id.startsWith(".") === false && 10 | path.isAbsolute(id) === false; 11 | 12 | export default { 13 | input: "./dist/index.js", 14 | output: { 15 | file: "dist/index.js", 16 | format: "esm", 17 | }, 18 | external, 19 | plugins: [ 20 | commonjs({}), 21 | nodeResolve({}), 22 | json({ 23 | preferConst: true, 24 | }), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | node: [16.x, 18.x, 20.x] 17 | prettier: ["3.0", "3.6"] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: ${{ matrix.node }} 25 | - name: Install yarn 26 | run: npm i -g yarn 27 | - name: Install project dependencies 28 | run: yarn install 29 | - name: Install specific prettier version 30 | run: yarn add prettier@${{ matrix.prettier }} && yarn list --depth=0 --pattern "prettier" 31 | - name: Run tests 32 | run: yarn test 33 | -------------------------------------------------------------------------------- /tests/dottedNames.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: ["prettier-plugin-jsdoc"], 7 | parser: "babel", 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("dotted names function param", async () => { 14 | const result = await subject(` 15 | /** 16 | * @param {object} data 17 | * @param {string} data.userName 18 | * @param {string} data.password 19 | * 20 | * @typedef {object} LoginResponse 21 | * @property {string} token 22 | * @property {number} expires 23 | * @property {boolean} mustChangePassword 24 | * 25 | * @returns {import('axios').AxiosResponse} 26 | */ 27 | function a(){} 28 | `); 29 | 30 | expect(result).toMatchSnapshot(); 31 | }); 32 | -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Parameter default value 4 | test=0 5 | 6 | while [ $# -gt 0 ]; do 7 | case "$1" in 8 | --test) 9 | test=1 10 | ;; 11 | # -a|-arg_1|--arg_1) 12 | # arg_1="$2" 13 | # ;; 14 | *) 15 | printf "***************************\n" 16 | printf "* Error: Invalid argument.*\n" 17 | printf "***************************\n" 18 | exit 1 19 | esac 20 | shift 21 | shift 22 | done 23 | 24 | npm run clean 25 | npm run lint 26 | tsc --project tsconfig.json 27 | 28 | 29 | if [ $test -ne 1 ]; then 30 | # bundleUmd 31 | rollup dist/index.js --file dist/index.umd.js --format umd --name sayHello 32 | # bundleUmdMin 33 | terser --ecma 6 --compress --mangle -o dist/index.umd.min.js -- dist/index.umd.js && gzip -9 -c dist/index.umd.min.js > dist/index.umd.min.js.gz 34 | 35 | # buildStats 36 | cd dist 37 | ls -lh index.umd* | tail -n +2 | awk '{print $5,$9}' 38 | fi -------------------------------------------------------------------------------- /tests/__snapshots__/modern.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`convert array to modern type 1`] = ` 4 | "/** 5 | * @typedef {import("react-native-reanimated").default.Adaptable} Adaptable 6 | * @param {Adaptable} animNode 7 | * @param {object} InterpolationConfig 8 | * @param {ReadonlyArray} InterpolationConfig.inputRange Like [0,1] 9 | * @param {string[]} InterpolationConfig.outputRange Like 10 | * ["#0000ff","#ff0000"] 11 | * @param {import("react-native-reanimated").default.Extrapolate} [InterpolationConfig.extrapolate] 12 | * @param {Foo[]} arg1 13 | * @param {((item: Foo) => Bar)[] | number[] | string[]} arg2 14 | * @param {((item: Foo) => Bar)[] | number[] | "Foo.<>"[]} arg3 15 | * @param {"Array.<(item: Foo.) => Bar.> | Array. | Array.<'Foo.<>'>"} arg4 16 | * @param {number[][][]} arg5 17 | * @param {{ foo: number[]; bar: string[] }} arg6 18 | */ 19 | function a() {} 20 | " 21 | `; 22 | -------------------------------------------------------------------------------- /tests/remarks.test.ts: -------------------------------------------------------------------------------- 1 | import { format } from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return format(code, { 6 | parser: "babel", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | ...options, 9 | } as AllOptions); 10 | } 11 | 12 | test("Description start with markdown", async () => { 13 | const result = await subject( 14 | ` 15 | /** 16 | * Just a simple test 17 | * 18 | * @remarks 19 | * - Remark 1 20 | * - Remark 2 21 | * - Remark 3 22 | */ 23 | `, 24 | { 25 | tsdoc: true, 26 | }, 27 | ); 28 | 29 | expect(result).toMatchSnapshot(); 30 | 31 | const result2 = await subject( 32 | ` 33 | /** 34 | * Just a simple test 35 | * 36 | * @remarks 37 | * - Remark 1 38 | * - Remark 2 39 | * - Remark 3 40 | */ 41 | `, 42 | { 43 | tsdoc: false, 44 | }, 45 | ); 46 | 47 | expect(result2).toMatchSnapshot(); 48 | }); 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug Jest tests", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeArgs": [ 12 | "--inspect", 13 | "./node_modules/jest/bin/jest.js", 14 | "--runInBand", 15 | "--watch" 16 | ], 17 | "cwd": "${workspaceRoot}", 18 | "protocol": "inspector", 19 | "console": "integratedTerminal" 20 | }, 21 | { 22 | "type": "pwa-chrome", 23 | "request": "launch", 24 | "name": "Launch Chrome against localhost", 25 | "url": "http://localhost:8080", 26 | "webRoot": "${workspaceFolder}" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /prettier-plugin-fake/index.js: -------------------------------------------------------------------------------- 1 | import { parsers as typescriptParsers } from "prettier/plugins/typescript"; 2 | 3 | /** 4 | * 5 | * @param {*} text 6 | * @param {import("prettier/index").Options} options 7 | * @returns 8 | */ 9 | const preprocess = (text, options) => { 10 | if ( 11 | options.plugins.find((plugin) => 12 | plugin.name?.includes("prettier-plugin-fake"), 13 | ) 14 | ) { 15 | return `//prettier-plugin-fake\n${text}`; 16 | } 17 | return text; 18 | }; 19 | 20 | export const parsers = { 21 | typescript: { 22 | ...typescriptParsers.typescript, 23 | preprocess: typescriptParsers.typescript.preprocess 24 | ? (text, options) => 25 | preprocess( 26 | typescriptParsers.typescript.preprocess(text, options), 27 | options, 28 | ) 29 | : preprocess, 30 | parse: (text, options) => { 31 | const ast = typescriptParsers.typescript.parse(text, options); 32 | if (ast.comments) { 33 | ast.comments[0].value = "PRETTIER-PLUGIN-FAKE"; 34 | } 35 | 36 | return ast; 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /tests/__snapshots__/template.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`extends 1`] = ` 4 | "/** 5 | * The bread crumbs indicate the navigate path and trigger the active page. 6 | * 7 | * @class 8 | * @extends {PureComponent} 9 | * @typedef {object} props 10 | * @property {any} navigation 11 | */ 12 | export class BreadCrumbs extends PureComponent {} 13 | " 14 | `; 15 | 16 | exports[`template for callback 1`] = ` 17 | "/** 18 | * @template T 19 | * @callback CallbackName 20 | * @param {GetStyles} getStyles 21 | * 22 | * @returns {UseStyle} 23 | */ 24 | " 25 | `; 26 | 27 | exports[`template for callback 2`] = ` 28 | "/** 29 | * @template T 30 | * @template H 31 | * @param {GetStyles} getStyles 32 | * 33 | * @returns {UseStyle} 34 | * 35 | * @callback CallbackName 36 | * @param {GetStyles} getStyles 37 | * 38 | * @returns {UseStyle} 39 | */ 40 | " 41 | `; 42 | 43 | exports[`typeParam for callback 1`] = ` 44 | "/** 45 | * @typeParam T 46 | * @callback CallbackName 47 | * @param {GetStyles} getStyles 48 | * 49 | * @returns {UseStyle} 50 | */ 51 | " 52 | `; 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Hossein Mohammadi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/__snapshots__/singleTag.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Comment Line Strategy keep multi 1`] = ` 4 | "/** 5 | * @type {import("eslint").Linter.Config} should Be multiline 6 | */ 7 | const config = { 8 | // ... 9 | }; 10 | " 11 | `; 12 | 13 | exports[`Comment Line Strategy keep single 1`] = ` 14 | "/** @type {import("eslint").Linter.Config} should Be single line */ 15 | const config = { 16 | // ... 17 | }; 18 | " 19 | `; 20 | 21 | exports[`Comment Line Strategy multiline 1`] = ` 22 | "/** 23 | * @type {import("eslint").Linter.Config} should Be multiline 24 | */ 25 | const config = { 26 | // ... 27 | }; 28 | " 29 | `; 30 | 31 | exports[`Comment Line Strategy singleLine 1`] = ` 32 | "/** @type {import("eslint").Linter.Config} should Be single */ 33 | const config = { 34 | // ... 35 | }; 36 | " 37 | `; 38 | 39 | exports[`single tag 1`] = ` 40 | "/** @param {string} param0 Description */ 41 | function fun(param0) {} 42 | 43 | export const SubDomain = { 44 | /** @returns {import("axios").AxiosResponse} */ 45 | async subDomain(subDomainAddress) {}, 46 | }; 47 | " 48 | `; 49 | -------------------------------------------------------------------------------- /tests/files/create-ignorer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const ignore = require("ignore"); 5 | const getFileContentOrNull = require("../utils/get-file-content-or-null"); 6 | 7 | /** 8 | * @param {string?} ignorePath 9 | * @param {boolean?} withNodeModules 10 | */ 11 | async function createIgnorer(ignorePath, withNodeModules) { 12 | const ignoreContent = ignorePath 13 | ? await getFileContentOrNull(path.resolve(ignorePath)) 14 | : null; 15 | 16 | return _createIgnorer(ignoreContent, withNodeModules); 17 | } 18 | 19 | /** 20 | * @param {string?} ignorePath 21 | * @param {boolean?} withNodeModules 22 | */ 23 | createIgnorer.sync = function (ignorePath, withNodeModules) { 24 | const ignoreContent = !ignorePath 25 | ? null 26 | : getFileContentOrNull.sync(path.resolve(ignorePath)); 27 | return _createIgnorer(ignoreContent, withNodeModules); 28 | }; 29 | 30 | /** 31 | * @param {null | string} ignoreContent 32 | * @param {boolean?} withNodeModules 33 | */ 34 | function _createIgnorer(ignoreContent, withNodeModules) { 35 | const ignorer = ignore().add(ignoreContent || ""); 36 | if (!withNodeModules) { 37 | ignorer.add("node_modules"); 38 | } 39 | return ignorer; 40 | } 41 | 42 | module.exports = createIgnorer; 43 | -------------------------------------------------------------------------------- /tests/__snapshots__/objectProperty.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`object property 1`] = ` 4 | "/** 5 | * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. 6 | * 7 | * This source code is licensed under the license found in the LICENSE file in 8 | * the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * Copyright (c) 2015-present, Facebook, Inc. All rights reserved. 13 | * 14 | * This source code is licensed under the license found in the LICENSE file in 15 | * the root directory of this source tree. 16 | */ 17 | 18 | const obj = { 19 | /** 20 | * @param {object} [filters] 21 | * @param {string} [filters.searchInput] 22 | * @param {boolean} [filters.isActive] 23 | * @param {boolean} [filters.isPerson] 24 | * @param {import("../types").IdentityStatus} [filters.identityStatuses] 25 | * @param {string} [filters.lastActivityFrom] YYYY-MM-DD 26 | * @param {string} [filters.lastActivityTo] 27 | * @param {string} [filters.registeredFrom] 28 | * @param {string} [filters.registeredTo] 29 | * @param {number} [filters.skip] 30 | * @param {number} [filters.take] 31 | * @param {string} [filters.orderBy] 32 | * @param {boolean} [filters.orderDesc] 33 | * @returns {import("axios").AxiosResponse< 34 | * import("../types").ResellerUserIntroduced[] 35 | * >} 36 | */ 37 | a(filters) {}, 38 | }; 39 | " 40 | `; 41 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/create-ignorer.js.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: create-ignorer.js 1`] = ` 4 | "\\"use strict\\"; 5 | 6 | const path = require(\\"path\\"); 7 | const ignore = require(\\"ignore\\"); 8 | const getFileContentOrNull = require(\\"../utils/get-file-content-or-null\\"); 9 | 10 | /** 11 | * @param {string | null} ignorePath 12 | * @param {boolean | null} withNodeModules 13 | */ 14 | async function createIgnorer(ignorePath, withNodeModules) { 15 | const ignoreContent = ignorePath 16 | ? await getFileContentOrNull(path.resolve(ignorePath)) 17 | : null; 18 | 19 | return _createIgnorer(ignoreContent, withNodeModules); 20 | } 21 | 22 | /** 23 | * @param {string | null} ignorePath 24 | * @param {boolean | null} withNodeModules 25 | */ 26 | createIgnorer.sync = function (ignorePath, withNodeModules) { 27 | const ignoreContent = !ignorePath 28 | ? null 29 | : getFileContentOrNull.sync(path.resolve(ignorePath)); 30 | return _createIgnorer(ignoreContent, withNodeModules); 31 | }; 32 | 33 | /** 34 | * @param {null | string} ignoreContent 35 | * @param {boolean | null} withNodeModules 36 | */ 37 | function _createIgnorer(ignoreContent, withNodeModules) { 38 | const ignorer = ignore().add(ignoreContent || \\"\\"); 39 | if (!withNodeModules) { 40 | ignorer.add(\\"node_modules\\"); 41 | } 42 | return ignorer; 43 | } 44 | 45 | module.exports = createIgnorer; 46 | " 47 | `; 48 | -------------------------------------------------------------------------------- /tests/modern.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: ["prettier-plugin-jsdoc"], 7 | parser: "typescript", 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("convert array to modern type", async () => { 14 | const result = await subject(` 15 | /** 16 | * @typedef {import("react-native-reanimated").default.Adaptable} Adaptable 17 | * @param {Adaptable} animNode 18 | * @param {object} InterpolationConfig 19 | * @param {ReadonlyArray} InterpolationConfig.inputRange Like [0,1] 20 | * @param {Array} InterpolationConfig.outputRange Like ["#0000ff","#ff0000"] 21 | * @param {import("react-native-reanimated").default.Extrapolate} [InterpolationConfig.extrapolate] 22 | * @param {Array>} arg1 23 | * @param {Array<(item: Foo) => Bar> | Array | Array} arg2 24 | * @param {Array.<(item: Foo.) => Bar.> | Array. | Array.<'Foo.<>'>} arg3 25 | * @param {"Array.<(item: Foo.) => Bar.> | Array. | Array.<'Foo.<>'>"} arg4 26 | * @param {Array>>} arg5 27 | * @param {{ foo: Array; bar: Array }} arg6 28 | * 29 | */ 30 | function a(){} 31 | `); 32 | 33 | expect(result).toMatchSnapshot(); 34 | }); 35 | -------------------------------------------------------------------------------- /tests/__snapshots__/tagGroup.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Inconsistant formatting 1`] = ` 4 | "/** 5 | * Aliquip ex proident tempor eiusmod aliquip amet. Labore commodo nulla 6 | * tempor consequat exercitation incididunt non. Duis laboris reprehenderit 7 | * proident proident. 8 | * 9 | * @example 10 | * const foo = 0; 11 | * 12 | * @param id A test id. 13 | * 14 | * @throws Minim sit ad commodo ut dolore magna magna minim consequat. Ex 15 | * consequat esse incididunt qui voluptate id voluptate quis ex et. 16 | * Ullamco cillum nisi amet fugiat. 17 | * 18 | * @see {@link http://acme.com} 19 | */ 20 | " 21 | `; 22 | 23 | exports[`Tag group 1`] = ` 24 | "/** 25 | * Aliquip ex proident tempor eiusmod aliquip amet. Labore commodo nulla 26 | * tempor consequat exercitation incididunt non. Duis laboris reprehenderit 27 | * proident proident. 28 | * 29 | * @example 30 | * const foo = 0; 31 | * const a = ""; 32 | * const b = ""; 33 | * 34 | * @param id A test id. 35 | * 36 | * @returns Minim sit a. 37 | * 38 | * @throws Minim sit ad commodo ut dolore magna magna minim consequat. Ex 39 | * consequat esse incididunt qui voluptate id voluptate quis ex et. Ullamco 40 | * cillum nisi amet fugiat. 41 | * 42 | * @see {@link http://acme.com} 43 | */ 44 | " 45 | `; 46 | 47 | exports[`space after unknownTag 1`] = ` 48 | "/** 49 | * A description. 50 | * 51 | * @unknownTag A note. 52 | * 53 | * @see http://acme.com 54 | */ 55 | " 56 | `; 57 | -------------------------------------------------------------------------------- /doc/CUSTOM_TAGS_ORDER.md: -------------------------------------------------------------------------------- 1 | # jsdocTagsOrder 2 | 3 | Use this param for customizing tags order. To change tags order, Change the weight of them, for example the `default` tag has 22 weight for put that below of `returns` (42) we could change weight of it to 42.1 4 | 5 | ```json 6 | { 7 | "jsdocTagsOrder": "{\"default\":42.1}" 8 | } 9 | ``` 10 | 11 | jsdocTagsOrder accepting json string 12 | 13 | ### plugin builtin order 14 | 15 | ``` 16 | remarks: 1, 17 | privateRemarks: 2, 18 | providesModule: 3, 19 | module: 4, 20 | license: 5, 21 | flow: 6, 22 | async: 7, 23 | private: 8, 24 | ignore: 9, 25 | memberof: 10, 26 | version: 11, 27 | file: 12, 28 | author: 13, 29 | deprecated: 14, 30 | since: 15, 31 | category: 16, 32 | description: 17, 33 | example: 18, 34 | abstract: 19, 35 | augments: 20, 36 | constant: 21, 37 | default: 22, 38 | defaultValue: 23, 39 | external: 24, 40 | overload: 25, 41 | fires: 26, 42 | template: 27, 43 | typeParam: 28, 44 | function: 29, 45 | namespace: 30, 46 | borrows: 31, 47 | class: 32, 48 | extends: 33, 49 | member: 34, 50 | typedef: 35, 51 | type: 36, 52 | satisfies: 37, 53 | property: 38, 54 | callback: 39, 55 | param: 40, 56 | yields: 41, 57 | returns: 42, 58 | throws: 43, 59 | 60 | other: 44, // any other tags which is not listed here 61 | 62 | see: 45, 63 | todo: 46, 64 | 65 | ``` 66 | 67 | ### example 68 | 69 | ```json 70 | { 71 | "jsdocTagsOrder": "{\"param\":28.1, \"return\":28.2, \"example\":70}" 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /tests/files/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Array<(element: HTMLElement) => boolean>} 3 | */ 4 | var filters = []; 5 | 6 | /** 7 | * Returns a slice of the first array that doesn't contain the leading and trailing elements the both arrays have in 8 | * common. 9 | * 10 | * Examples: 11 | * 12 | * trimCommon([1,2,3,4], [1,3,2,4]) => [2,3] 13 | * trimCommon([1,2,3,4], [1,2,3,4]) => [] 14 | * trimCommon([1,2,0,0,3,4], [1,2,3,4]) => [0,0] 15 | * trimCommon([1,2,3,4], [1,2,0,0,3,4]) => [] 16 | * 17 | * 18 | * 19 | * trimCommon([1,2,3,4],[1,3,2,4]) 20 | * trimCommon([1,2,3,4 ], [1,2,3,4]) 21 | * trimCommon([1,2,0,0, 3,4], [1,2,3,4]) 22 | * trimCommon([1,2,3,4],[1,2,0,0,3,4]) 23 | * 24 | * @param {readonly T[]} a1 25 | * @param {readonly T[]} a2 26 | * @returns {T[]} 27 | * @template T 28 | */ 29 | 30 | 31 | /** 32 | * Returns a slice of the first array that doesn't contain the leading and trailing elements the both arrays have in 33 | * common. 34 | * 35 | * Examples: 36 | * 37 | * trimCommon([1,2,3,4], [1,3,2,4]) => [2,3] 38 | * trimCommon([1,2,3,4], [1,2,3,4]) => [] 39 | * trimCommon([1,2,0,0,3,4], [1,2,3,4]) => [0,0] 40 | * trimCommon([1,2,3,4], [1,2,0,0,3,4]) => [] 41 | * 42 | * trimCommon([1,2,3,4],[1,3,2,4]) 43 | * trimCommon([1,2,3,4 ], [1,2,3,4]) 44 | * trimCommon([1,2,0,0, 3,4], [1,2,3,4]) 45 | * trimCommon([1,2,3,4],[1,2,0,0,3,4]) 46 | * 47 | * @param {readonly T[]} a1 48 | * @param {readonly T[]} a2 49 | * @returns {T[]} 50 | * @template T 51 | */ -------------------------------------------------------------------------------- /tests/jsdocParserDisable.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: [], 7 | parser: "babel", 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("template for callback", async () => { 14 | const result = await subject(` 15 | /** 16 | * @template T 17 | * @callback CallbackName 18 | * @param {GetStyles} getStyles 19 | * @returns {UseStyle} 20 | */ 21 | `); 22 | 23 | expect(result).toMatchSnapshot(); 24 | }); 25 | 26 | test("disabled complex object typedef ", async () => { 27 | const result = await subject(` 28 | /** 29 | * The bread crumbs indicate the navigate path and trigger the active page. 30 | * @class 31 | * @typedef {object} props 32 | * @prop {any} navigation 33 | * @extends {PureComponent< props>} 34 | */ 35 | export class BreadCrumbs extends PureComponent {} 36 | `); 37 | 38 | expect(result).toMatchSnapshot(); 39 | 40 | const result2 = await subject( 41 | ` 42 | /** 43 | * The bread crumbs indicate the navigate path and trigger the active page. 44 | * @class 45 | * @typedef {object} props 46 | * @prop {any} navigation 47 | * @extends {PureComponent< props>} 48 | */ 49 | export class BreadCrumbs extends PureComponent {} 50 | `, 51 | { 52 | plugins: [], 53 | }, 54 | ); 55 | 56 | expect(result).toBe(result2); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/types.ts.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: types.ts 1`] = ` 4 | "/** @type {((element: HTMLElement) => boolean)[]} */ 5 | var filters = []; 6 | 7 | /** 8 | * Returns a slice of the first array that doesn't contain the leading and 9 | * trailing elements the both arrays have in common. 10 | * 11 | * Examples: 12 | * 13 | * trimCommon([1,2,3,4], [1,3,2,4]) => [2,3] 14 | * trimCommon([1,2,3,4], [1,2,3,4]) => [] 15 | * trimCommon([1,2,0,0,3,4], [1,2,3,4]) => [0,0] 16 | * trimCommon([1,2,3,4], [1,2,0,0,3,4]) => [] 17 | * 18 | * 19 | * 20 | * trimCommon([1,2,3,4],[1,3,2,4]) 21 | * trimCommon([1,2,3,4 ], [1,2,3,4]) 22 | * trimCommon([1,2,0,0, 3,4], [1,2,3,4]) 23 | * trimCommon([1,2,3,4],[1,2,0,0,3,4]) 24 | * 25 | * @template T 26 | * @param {readonly T[]} a1 27 | * @param {readonly T[]} a2 28 | * @returns {T[]} 29 | */ 30 | 31 | /** 32 | * Returns a slice of the first array that doesn't contain the leading and 33 | * trailing elements the both arrays have in common. 34 | * 35 | * Examples: 36 | * 37 | * trimCommon([1,2,3,4], [1,3,2,4]) => [2,3] 38 | * trimCommon([1,2,3,4], [1,2,3,4]) => [] 39 | * trimCommon([1,2,0,0,3,4], [1,2,3,4]) => [0,0] 40 | * trimCommon([1,2,3,4], [1,2,0,0,3,4]) => [] 41 | * 42 | * trimCommon([1,2,3,4],[1,3,2,4]) 43 | * trimCommon([1,2,3,4 ], [1,2,3,4]) 44 | * trimCommon([1,2,0,0, 3,4], [1,2,3,4]) 45 | * trimCommon([1,2,3,4],[1,2,0,0,3,4]) 46 | * 47 | * @template T 48 | * @param {readonly T[]} a1 49 | * @param {readonly T[]} a2 50 | * @returns {T[]} 51 | */ 52 | " 53 | `; 54 | -------------------------------------------------------------------------------- /tests/template.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: ["prettier-plugin-jsdoc"], 7 | jsdocSpaces: 1, 8 | parser: "babel-flow", 9 | jsdocSeparateReturnsFromParam: true, 10 | ...options, 11 | } as AllOptions); 12 | } 13 | 14 | test("template for callback", async () => { 15 | const result = await subject(` 16 | /** 17 | * @template T 18 | * @callback CallbackName 19 | * @param {GetStyles} getStyles 20 | * @returns {UseStyle} 21 | */ 22 | `); 23 | 24 | expect(result).toMatchSnapshot(); 25 | 26 | const result2 = await subject(` 27 | /** 28 | * @template T 29 | * @param {GetStyles} getStyles 30 | * @returns {UseStyle} 31 | * @template H 32 | * @callback CallbackName 33 | * @param {GetStyles} getStyles 34 | * @returns {UseStyle} 35 | */ 36 | `); 37 | 38 | expect(result2).toMatchSnapshot(); 39 | }); 40 | 41 | test("extends", async () => { 42 | const result = await subject(` 43 | /** 44 | * The bread crumbs indicate the navigate path and trigger the active page. 45 | * @class 46 | * @typedef {object} props 47 | * @prop {any} navigation 48 | * @extends {PureComponent} 49 | */ 50 | export class BreadCrumbs extends PureComponent {} 51 | `); 52 | 53 | expect(result).toMatchSnapshot(); 54 | }); 55 | 56 | test("typeParam for callback", async () => { 57 | const result = await subject(` 58 | /** 59 | * @typeParam T 60 | * @callback CallbackName 61 | * @param {GetStyles} getStyles 62 | * @returns {UseStyle} 63 | */ 64 | `); 65 | 66 | expect(result).toMatchSnapshot(); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/__snapshots__/react.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`JS code should be formatted as usuall 1`] = ` 4 | "import React, { memo } from "react"; 5 | import { Text, View, StyleSheet } from "react-native"; 6 | import * as d3Scale from "d3-scale"; 7 | import * as array from "d3-array"; 8 | import Svg, { G, Text as SVGText } from "react-native-svg"; 9 | import { useLayout, useInlineStyle } from "./hooks"; 10 | 11 | /** 12 | * @typedef {object} XAxisProps 13 | * @property {number} [spacingOuter] Spacing between the labels. Only 14 | * applicable if \`scale=d3Scale.scaleBand\` and should then be equal to 15 | * \`spacingOuter\` prop on the actual BarChart 16 | * 17 | * Default is \`0.05\` 18 | * @property {number} [spacingInner] Spacing between the labels. Only 19 | * applicable if \`scale=d3Scale.scaleBand\` and should then be equal to 20 | * \`spacingInner\` prop on the actual BarChart 21 | * 22 | * Default is \`0.05\` 23 | * @property {d3Scale.scaleLinear} [scale] Should be the same as passed into 24 | * the charts \`xScale\` Default is \`d3Scale.scaleLinear\` 25 | * @property {() => any} [xAccessor] Default is \`({index}) => index\` 26 | * @property {number} [max] 27 | * @property {number} [min] 28 | */ 29 | 30 | /** @type {React.FC} */ 31 | const XAxis = memo( 32 | ({ 33 | contentInset: { left = 0, right = 0 } = {}, 34 | style, 35 | data, 36 | numberOfTicks, 37 | children, 38 | min, 39 | max, 40 | spacingInner = 0.05, 41 | spacingOuter = 0.05, 42 | xAccessor = ({ index }) => index, 43 | scale = d3Scale.scaleLinear, 44 | formatLabel = (value) => value, 45 | ...svg 46 | }) => {}, 47 | ); 48 | " 49 | `; 50 | -------------------------------------------------------------------------------- /tests/objectProperty.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | parser: "babel-flow", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("object property", async () => { 14 | const result = await subject(` 15 | 16 | /** 17 | * Copyright (c) 2015-present, Facebook, Inc. 18 | * All rights reserved. 19 | * 20 | * This source code is licensed under the license found in the LICENSE file in 21 | * the root directory of this source tree. 22 | */ 23 | 24 | /** 25 | * Copyright (c) 2015-present, Facebook, Inc. 26 | * All rights reserved. 27 | * 28 | * This source code is licensed under the license found in the LICENSE file in 29 | * the root directory of this source tree. 30 | */ 31 | 32 | const obj = { 33 | /** 34 | * @param {object} [filters] 35 | * @param {string} [filters.searchInput] 36 | * @param {boolean} [filters.isActive] 37 | * @param {boolean} [filters.isPerson] 38 | * @param {import('../types').IdentityStatus} [filters.identityStatuses] 39 | * @param {string} [filters.lastActivityFrom] YYYY-MM-DD 40 | * @param {string} [filters.lastActivityTo] 41 | * @param {string} [filters.registeredFrom] 42 | * @param {string} [filters.registeredTo] 43 | * @param {number} [filters.skip] 44 | * @param {number} [filters.take] 45 | * @param {string} [filters.orderBy] 46 | * @param {boolean} [filters.orderDesc] 47 | * @returns {import('axios').AxiosResponse< 48 | import('../types').ResellerUserIntroduced[] 49 | >} 50 | */ 51 | a(filters) {}, 52 | }; 53 | `); 54 | 55 | expect(result).toMatchSnapshot(); 56 | }); 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist/ 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_STORE 107 | 108 | .test-cli-temp -------------------------------------------------------------------------------- /tests/react.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: ["prettier-plugin-jsdoc"], 7 | jsdocSpaces: 1, 8 | parser: "babel", 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("JS code should be formatted as usuall", async () => { 14 | const result = await subject(` 15 | import React, { memo } from "react"; 16 | import { Text, View, StyleSheet } from "react-native"; 17 | import * as d3Scale from "d3-scale"; 18 | import * as array from "d3-array"; 19 | import Svg, { G, Text as SVGText } from "react-native-svg"; 20 | import { useLayout, useInlineStyle } from "./hooks"; 21 | 22 | /** 23 | * @typedef {object} XAxisProps 24 | * @property {number} [spacingOuter] 25 | * Spacing between the labels. Only applicable if 26 | * \`scale=d3Scale.scaleBand\` and should then be equal to \`spacingOuter\` prop on the 27 | * actual BarChart 28 | * 29 | * Default is \`0.05\` 30 | * @property {number} [spacingInner] Spacing between the labels. Only applicable if 31 | * \`scale=d3Scale.scaleBand\` and should then be equal to \`spacingInner\` prop on the 32 | * actual BarChart 33 | * 34 | * Default is \`0.05\` 35 | * @property {d3Scale.scaleLinear} [scale] Should be the same as passed into the charts \`xScale\` 36 | * Default is \`d3Scale.scaleLinear\` 37 | * 38 | * @property {()=>any} [xAccessor] Default is \`({index}) => index\` 39 | * @property {number} [max] 40 | * @property {number} [min] 41 | */ 42 | 43 | /** 44 | * @type {React.FC} 45 | */ 46 | const XAxis = memo( 47 | ({ 48 | contentInset: { left = 0, right = 0 } = {}, 49 | style, 50 | data, 51 | numberOfTicks, 52 | children, 53 | min, 54 | max, 55 | spacingInner = 0.05, 56 | spacingOuter = 0.05, 57 | xAccessor = ({ index }) => index, 58 | scale = d3Scale.scaleLinear, 59 | formatLabel = value => value, 60 | ...svg 61 | })=>{}) 62 | `); 63 | expect(result).toMatchSnapshot(); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/singleTag.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | plugins: ["prettier-plugin-jsdoc"], 7 | jsdocSpaces: 1, 8 | parser: "babel", 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("single tag", async () => { 14 | const result = await subject(` 15 | /** 16 | * @param { string } param0 description 17 | */ 18 | function fun(param0){} 19 | 20 | export const SubDomain = { 21 | /** 22 | * @returns {import('axios').AxiosResponse} 23 | */ 24 | async subDomain(subDomainAddress) { 25 | }, 26 | }; 27 | 28 | `); 29 | 30 | expect(result).toMatchSnapshot(); 31 | }); 32 | 33 | describe("Comment Line Strategy", () => { 34 | test("keep single", async () => { 35 | const result = await subject( 36 | ` 37 | /** @type {import('eslint').Linter.Config} should be single line */ 38 | const config = { 39 | // ... 40 | }; 41 | `, 42 | { 43 | jsdocCommentLineStrategy: "keep", 44 | }, 45 | ); 46 | expect(result).toMatchSnapshot(); 47 | }); 48 | 49 | test("keep multi", async () => { 50 | const result1 = await subject( 51 | ` 52 | /** 53 | * @type {import('eslint').Linter.Config} should be multiline 54 | */ 55 | const config = { 56 | // ... 57 | }; 58 | `, 59 | { 60 | jsdocCommentLineStrategy: "keep", 61 | }, 62 | ); 63 | 64 | expect(result1).toMatchSnapshot(); 65 | }); 66 | test("singleLine ", async () => { 67 | const result2 = await subject( 68 | ` 69 | /** 70 | * @type {import('eslint').Linter.Config} should be single 71 | */ 72 | const config = { 73 | // ... 74 | }; 75 | `, 76 | { 77 | jsdocCommentLineStrategy: "singleLine", 78 | }, 79 | ); 80 | expect(result2).toMatchSnapshot(); 81 | }); 82 | test("multiline ", async () => { 83 | const result3 = await subject( 84 | ` 85 | /** @type {import('eslint').Linter.Config} should be multiline */ 86 | const config = { 87 | // ... 88 | }; 89 | `, 90 | { 91 | jsdocCommentLineStrategy: "multiline", 92 | }, 93 | ); 94 | expect(result3).toMatchSnapshot(); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /tests/files/tsdoc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @example 3 | * Adding one adds one 4 | * ```typescript 5 | * import plusOne from 'plus-one' 6 | * import expect from 'test-lib' 7 | * 8 | * const actual=plusOne(3); 9 | * expect(actual).toEqual(4); 10 | * ``` 11 | */ 12 | export function plusOne(input: number) { 13 | return input + 1; 14 | } 15 | 16 | 17 | 18 | /** 19 | * Parses a JSON file. 20 | * 21 | * @param path - Full path to the file. 22 | * @returns An object containing the JSON data. 23 | * 24 | * @example Parsing a basic JSON file 25 | * 26 | * # Contents of `file.json` 27 | * ```json 28 | * { 29 | * "exampleItem":"text" 30 | * } 31 | * ``` 32 | * 33 | * # Usage 34 | * ```ts 35 | * const result = parseFile("file.json"); 36 | * ``` 37 | * 38 | * # Result 39 | * ```ts 40 | * { 41 | * exampleItem:'text', 42 | * } 43 | * ``` 44 | */ 45 | 46 | /** 47 | * Adds two numbers together. 48 | * @example 49 | * Here's a simple example: 50 | * ```js 51 | * // Prints "2": 52 | * console.log(add(1,1)); 53 | * ``` 54 | * @example 55 | * Here's an example with negative numbers: 56 | * ``` 57 | * // Prints "0": 58 | * console.log(add(1,-1)); 59 | * ``` 60 | */ 61 | export function add(x: number, y: number): number { 62 | return x* y 63 | } 64 | 65 | /** 66 | * This is a summary for foo. 67 | * 68 | * foo is the name of the function. 69 | * 70 | * @remarks 71 | * This is some additional info 72 | * 73 | * 1. point 1 74 | * 2. point 2 75 | * 3. point 3 76 | * 77 | * @example 78 | * ```ts 79 | * foo(2, 5, 9) 80 | * ``` 81 | */ 82 | function foo(num1, num2, num3) { 83 | // 84 | } 85 | 86 | /** 87 | * @providesModule Foo 88 | * @remarks 89 | * This source code is licensed under the license found in the LICENSE file in 90 | * the root directory of this source tree. 91 | * @privateRemarks This source code is licensed under the license found in the LICENSE file in 92 | * the root directory of this source tree. 93 | * @PrivaTeremaRks description 94 | * @providesmodule bar 95 | * 96 | * @flow 97 | */ 98 | 99 | 100 | interface DialogProps { 101 | /** 102 | * Whether the dialog should disable the main content while open 103 | * 104 | * @defaultValue true 105 | */ 106 | modal?: boolean; 107 | } 108 | -------------------------------------------------------------------------------- /tests/files.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { readFileSync } from "node:fs"; 3 | import { resolve } from "node:path"; 4 | import { AllOptions } from "../src/types"; 5 | 6 | import "jest-specific-snapshot"; 7 | 8 | function subjectFiles(relativePath: string, options: Partial = {}) { 9 | const filepath = resolve(process.cwd(), "tests", relativePath); 10 | 11 | try { 12 | const code = readFileSync(filepath).toString(); 13 | 14 | return prettier.format(code, { 15 | plugins: ["prettier-plugin-jsdoc"], 16 | jsdocSpaces: 1, 17 | trailingComma: "all", 18 | filepath, 19 | ...options, 20 | } as AllOptions); 21 | } catch (error) { 22 | console.error(error); 23 | } 24 | } 25 | 26 | const PrismOptions = { 27 | arrowParens: "avoid", 28 | printWidth: 120, 29 | quoteProps: "preserve", 30 | semi: true, 31 | singleQuote: true, 32 | tabWidth: 4, 33 | trailingComma: "none", 34 | useTabs: true, 35 | jsdocKeepUnParseAbleExampleIndent: true, 36 | } as const; 37 | 38 | /** 39 | * @type {TestFile[]} 40 | * 41 | * @typedef TestFile 42 | * @property {string} name 43 | * @property {import("prettier").Options} [options] 44 | */ 45 | const files: { 46 | name: string; 47 | options?: Partial; 48 | }[] = [ 49 | { name: "typeScript.js" }, 50 | { name: "typeScript.js" }, 51 | { name: "typeScript.ts" }, 52 | { name: "types.ts" }, 53 | { name: "order.jsx" }, 54 | { name: "create-ignorer.js" }, 55 | { 56 | name: "prism-core.js", 57 | options: PrismOptions, 58 | }, 59 | { 60 | name: "prism-dependencies.js", 61 | options: { 62 | jsdocSeparateTagGroups: true, 63 | ...PrismOptions, 64 | }, 65 | }, 66 | { 67 | name: "tsdoc.ts", 68 | options: { 69 | tsdoc: true, 70 | }, 71 | }, 72 | { 73 | name: "order-custom.jsx", 74 | options: { 75 | jsdocTagsOrder: '{"example":43, "typedef":0, "returns": 46}' as any, 76 | // { 77 | // example: 70, 78 | // } as any, 79 | }, 80 | }, 81 | ]; 82 | 83 | for (let i = 0; i < files.length; i++) { 84 | const { name, options } = files[i]; 85 | test(`File: ${name}`, async () => { 86 | const result = await subjectFiles("./files/" + name, options); 87 | (expect(result) as any).toMatchSpecificSnapshot( 88 | `./__snapshots__/files/${name}.shot`, 89 | ); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ParserOptions } from "prettier"; 2 | 3 | export interface JsdocOptions { 4 | jsdocSpaces: number; 5 | jsdocPrintWidth?: number; 6 | jsdocDescriptionWithDot: boolean; 7 | jsdocDescriptionTag: boolean; 8 | jsdocVerticalAlignment: boolean; 9 | jsdocKeepUnParseAbleExampleIndent: boolean; 10 | /** 11 | * @deprecated use jsdocCommentLineStrategy instead 12 | * @default true 13 | */ 14 | jsdocSingleLineComment: boolean; 15 | /** @default "singleLine" */ 16 | jsdocCommentLineStrategy: "singleLine" | "multiline" | "keep"; 17 | jsdocSeparateReturnsFromParam: boolean; 18 | jsdocSeparateTagGroups: boolean; 19 | jsdocAddDefaultToDescription: boolean; 20 | jsdocCapitalizeDescription: boolean; 21 | jsdocPreferCodeFences: boolean; 22 | tsdoc: boolean; 23 | jsdocLineWrappingStyle: "greedy" | "balance"; 24 | jsdocTagsOrder?: Record; 25 | jsdocFormatImports: boolean; 26 | jsdocNamedImportPadding: boolean; 27 | jsdocMergeImports: boolean; 28 | jsdocNamedImportLineSplitting: boolean; 29 | jsdocEmptyCommentStrategy: "remove" | "keep"; 30 | jsdocBracketSpacing: boolean; 31 | } 32 | 33 | export interface AllOptions extends ParserOptions, JsdocOptions {} 34 | 35 | type LocationDetails = { line: number; column: number }; 36 | type Location = { start: LocationDetails; end: LocationDetails }; 37 | 38 | export type PrettierComment = { 39 | type: "CommentBlock" | "Block"; 40 | value: string; 41 | start: number; 42 | end: number; 43 | loc: Location; 44 | }; 45 | 46 | export type Token = { 47 | type: 48 | | "CommentBlock" 49 | | "Block" 50 | | { 51 | label: string; // "function" | "name"; 52 | keyword?: string; 53 | beforeExpr: boolean; 54 | startsExpr: boolean; 55 | rightAssociative: boolean; 56 | isLoop: boolean; 57 | isAssign: boolean; 58 | prefix: boolean; 59 | postfix: boolean; 60 | binop: null; 61 | }; 62 | value: string; 63 | start: number; 64 | end: number; 65 | loc: Location; 66 | }; 67 | 68 | export type AST = { 69 | start: number; 70 | end: number; 71 | loc: Location; 72 | errors: []; 73 | program: { 74 | type: "Program"; 75 | start: number; 76 | end: number; 77 | loc: []; 78 | sourceType: "module"; 79 | interpreter: null; 80 | body: []; 81 | directives: []; 82 | }; 83 | comments: PrettierComment[]; 84 | tokens: Token[]; 85 | }; 86 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/tsdoc.ts.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: tsdoc.ts 1`] = ` 4 | "/** 5 | * @example Adding one adds one 6 | * 7 | * \`\`\`typescript 8 | * import plusOne from \\"plus-one\\"; 9 | * import expect from \\"test-lib\\"; 10 | * 11 | * const actual = plusOne(3); 12 | * expect(actual).toEqual(4); 13 | * \`\`\` 14 | */ 15 | export function plusOne(input: number) { 16 | return input + 1; 17 | } 18 | 19 | /** 20 | * Parses a JSON file. 21 | * 22 | * @example Parsing a basic JSON file 23 | * 24 | * # Contents of \`file.json\` 25 | * 26 | * \`\`\`json 27 | * { 28 | * \\"exampleItem\\": \\"text\\" 29 | * } 30 | * \`\`\` 31 | * 32 | * # Usage 33 | * 34 | * \`\`\`ts 35 | * const result = parseFile(\\"file.json\\"); 36 | * \`\`\` 37 | * 38 | * # Result 39 | * 40 | * \`\`\`ts 41 | * { 42 | * \\"exampleItem\\": \\"text\\" 43 | * } 44 | * \`\`\` 45 | * 46 | * @param path - Full path to the file. 47 | * @returns An object containing the JSON data. 48 | */ 49 | 50 | /** 51 | * Adds two numbers together. 52 | * 53 | * @example Here's a simple example: 54 | * 55 | * \`\`\`js 56 | * // Prints \\"2\\": 57 | * console.log(add(1, 1)); 58 | * \`\`\` 59 | * 60 | * @example Here's an example with negative numbers: 61 | * 62 | * // Prints \\"0\\": 63 | * console.log(add(1, -1)); 64 | */ 65 | export function add(x: number, y: number): number { 66 | return x * y; 67 | } 68 | 69 | /** 70 | * This is a summary for foo. 71 | * 72 | * Foo is the name of the function. 73 | * 74 | * @remarks 75 | * This is some additional info 76 | * 77 | * 1. Point 1 78 | * 2. Point 2 79 | * 3. Point 3 80 | * 81 | * @example 82 | * 83 | * \`\`\`ts 84 | * foo(2, 5, 9); 85 | * \`\`\` 86 | */ 87 | function foo(num1, num2, num3) { 88 | // 89 | } 90 | 91 | /** 92 | * @remarks 93 | * This source code is licensed under the license found in the LICENSE file in 94 | * the root directory of this source tree. 95 | * @privateRemarks 96 | * This source code is licensed under the license found in the LICENSE file in 97 | * the root directory of this source tree. 98 | * @privateRemarks 99 | * Description 100 | * @providesModule Foo 101 | * @providesModule bar 102 | * @flow 103 | */ 104 | 105 | interface DialogProps { 106 | /** 107 | * Whether the dialog should disable the main content while open 108 | * 109 | * @defaultValue true 110 | */ 111 | modal?: boolean; 112 | } 113 | " 114 | `; 115 | -------------------------------------------------------------------------------- /tests/__snapshots__/default.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`@default filled array 1`] = ` 4 | "/** 5 | * The summary 6 | * 7 | * @default [1, "two", { three: true }, ["four"]] 8 | */ 9 | " 10 | `; 11 | 12 | exports[`@default filled object 1`] = ` 13 | "/** 14 | * The summary 15 | * 16 | * @default { object: "value", nestingTest: { obj: "nested" } } 17 | */ 18 | " 19 | `; 20 | 21 | exports[`@defaultValue filled array 1`] = ` 22 | "/** 23 | * The summary 24 | * 25 | * @defaultValue [1, "two", { three: true }, ["four"]] 26 | */ 27 | " 28 | `; 29 | 30 | exports[`@defaultValue filled object 1`] = ` 31 | "/** 32 | * The summary 33 | * 34 | * @defaultValue { object: "value", nestingTest: { obj: "nested" } } 35 | */ 36 | " 37 | `; 38 | 39 | exports[`Can't convert double quote @default if a single quote character is in the string 1`] = ` 40 | "/** 41 | * The summary 42 | * 43 | * @default "This isn't bad" 44 | */ 45 | " 46 | `; 47 | 48 | exports[`Multi line codegen 1`] = ` 49 | "/** @default codegen */ 50 | " 51 | `; 52 | 53 | exports[`Single line codegen 1`] = ` 54 | "/** @default codegen */ 55 | " 56 | `; 57 | 58 | exports[`code in default 1`] = ` 59 | "/** 60 | * The path to the config file or directory contains the config file. 61 | * 62 | * @default process.cwd() 63 | */ 64 | " 65 | `; 66 | 67 | exports[`convert double quote @default to single quote 1`] = ` 68 | "/** 69 | * The summary 70 | * 71 | * @default 'value' 72 | */ 73 | " 74 | `; 75 | 76 | exports[`convert single quote @default to double quote 1`] = ` 77 | "/** 78 | * The summary 79 | * 80 | * @default "value" 81 | */ 82 | " 83 | `; 84 | 85 | exports[`default empty array 1`] = ` 86 | "/** 87 | * The summary 88 | * 89 | * @default [ ] 90 | */ 91 | " 92 | `; 93 | 94 | exports[`default empty object 1`] = ` 95 | "/** 96 | * The summary 97 | * 98 | * @default {} 99 | */ 100 | " 101 | `; 102 | 103 | exports[`default string with description 1`] = ` 104 | "/** 105 | * The summary 106 | * 107 | * @default "type" description 108 | */ 109 | " 110 | `; 111 | 112 | exports[`double default one 1`] = ` 113 | "/** 114 | * The summary 115 | * 116 | * @default "something" 117 | * @default {} 118 | */ 119 | " 120 | `; 121 | 122 | exports[`double default two 1`] = ` 123 | "/** 124 | * The summary 125 | * 126 | * @default {} 127 | * @default "something" 128 | */ 129 | " 130 | `; 131 | 132 | exports[`empty default tag 1`] = ` 133 | "/** 134 | * The summary 135 | * 136 | * @default 137 | */ 138 | " 139 | `; 140 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier-plugin-jsdoc", 3 | "version": "1.8.0", 4 | "description": "A Prettier plugin to format JSDoc comments.", 5 | "private": false, 6 | "type": "module", 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "default": "./dist/index.js" 11 | } 12 | }, 13 | "browser": "dist/index.umd.min.js", 14 | "unpkg": "dist/index.umd.min.js", 15 | "types": "dist/index.d.ts", 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "prepare": "yarn build", 21 | "lint": "eslint --ext '.ts' ./src", 22 | "test": "yarn build --test && NODE_OPTIONS=\"--loader ts-node/esm\" jest", 23 | "release": "standard-version && yarn publish && git push --follow-tags origin master", 24 | "prettierAll": "prettier --write \"**/*.ts\"", 25 | "clean": "rm -fr dist", 26 | "build": "chmod +x ./script.sh && ./script.sh" 27 | }, 28 | "keywords": [ 29 | "prettier", 30 | "plugin", 31 | "jsdoc", 32 | "comment" 33 | ], 34 | "author": "Hossein mohammadi (hosseinm.developer@gmail.com)", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/hosseinmd/prettier-plugin-jsdoc/issues" 38 | }, 39 | "homepage": "https://github.com/hosseinmd/prettier-plugin-jsdoc#readme", 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/hosseinmd/prettier-plugin-jsdoc.git" 43 | }, 44 | "devDependencies": { 45 | "prettier-plugin-tailwindcss": "^0.7.1", 46 | "@commitlint/config-conventional": "^14.1.0", 47 | "@rollup/plugin-commonjs": "^21.0.3", 48 | "@rollup/plugin-json": "^4.1.0", 49 | "@rollup/plugin-node-resolve": "^13.1.3", 50 | "@types/jest": "^29.5.4", 51 | "@types/mdast": "^4.0.1", 52 | "@typescript-eslint/eslint-plugin": "^6.6.0", 53 | "@typescript-eslint/parser": "^6.6.0", 54 | "commitlint": "^14.1.0", 55 | "eslint": "^8.49.0", 56 | "eslint-config-prettier": "^9.0.0", 57 | "eslint-plugin-prettier": "^5.0.0", 58 | "husky": "^7.0.4", 59 | "jest": "^29.6.4", 60 | "jest-light-runner": "^0.5.0", 61 | "jest-specific-snapshot": "^5.0.0", 62 | "prettier": "^3.6.1", 63 | "rollup": "^2.70.1", 64 | "standard-version": "^9.3.2", 65 | "terser": "^5.12.1", 66 | "ts-node": "^10.9.1", 67 | "typescript": "^5.2.2" 68 | }, 69 | "peerDependencies": { 70 | "prettier": "^3.0.0" 71 | }, 72 | "dependencies": { 73 | "binary-searching": "^2.0.5", 74 | "comment-parser": "^1.4.0", 75 | "mdast-util-from-markdown": "^2.0.0" 76 | }, 77 | "engines": { 78 | "node": ">=14.13.1 || >=16.0.0" 79 | }, 80 | "packageManager": "yarn@1.22.22" 81 | } 82 | -------------------------------------------------------------------------------- /src/tags.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from "comment-parser"; 2 | 3 | const ABSTRACT = "abstract"; 4 | const ASYNC = "async"; 5 | const AUGMENTS = "augments"; 6 | const AUTHOR = "author"; 7 | const BORROWS = "borrows"; 8 | const CALLBACK = "callback"; 9 | const CATEGORY = "category"; 10 | const CLASS = "class"; 11 | const CONSTANT = "constant"; 12 | const DEFAULT = "default"; 13 | const DEFAULT_VALUE = "defaultValue"; 14 | const DEPRECATED = "deprecated"; 15 | const DESCRIPTION = "description"; 16 | const EXAMPLE = "example"; 17 | const EXTENDS = "extends"; 18 | const EXTERNAL = "external"; 19 | const FILE = "file"; 20 | const FIRES = "fires"; 21 | const FLOW = "flow"; 22 | const FUNCTION = "function"; 23 | const IGNORE = "ignore"; 24 | const IMPORT = "import"; 25 | const LICENSE = "license"; 26 | const MEMBER = "member"; 27 | const MEMBEROF = "memberof"; 28 | const MODULE = "module"; 29 | const NAMESPACE = "namespace"; 30 | const OVERLOAD = "overload"; 31 | const OVERRIDE = "override"; 32 | const PARAM = "param"; 33 | const PRIVATE = "private"; 34 | const PRIVATE_REMARKS = "privateRemarks"; 35 | const PROPERTY = "property"; 36 | const PROVIDES_MODULE = "providesModule"; 37 | const REMARKS = "remarks"; 38 | const RETURNS = "returns"; 39 | const SEE = "see"; 40 | const SINCE = "since"; 41 | const TEMPLATE = "template"; 42 | const THIS = "this"; 43 | const THROWS = "throws"; 44 | const TODO = "todo"; 45 | const TYPE = "type"; 46 | const TYPE_PARAM = "typeParam"; 47 | const TYPEDEF = "typedef"; 48 | const SATISFIES = "satisfies"; 49 | const VERSION = "version"; 50 | const YIELDS = "yields"; 51 | 52 | const SPACE_TAG_DATA: Spec = { 53 | tag: "this_is_for_space", 54 | name: "", 55 | optional: false, 56 | type: "", 57 | description: "", 58 | source: [], 59 | problems: [], 60 | }; 61 | 62 | export { 63 | ABSTRACT, 64 | ASYNC, 65 | AUGMENTS, 66 | AUTHOR, 67 | BORROWS, 68 | CALLBACK, 69 | CATEGORY, 70 | CLASS, 71 | CONSTANT, 72 | DEFAULT, 73 | DEFAULT_VALUE, 74 | DEPRECATED, 75 | DESCRIPTION, 76 | EXAMPLE, 77 | EXTENDS, 78 | EXTERNAL, 79 | FILE, 80 | FIRES, 81 | FLOW, 82 | FUNCTION, 83 | IGNORE, 84 | IMPORT, 85 | LICENSE, 86 | MEMBER, 87 | MEMBEROF, 88 | MODULE, 89 | NAMESPACE, 90 | OVERLOAD, 91 | OVERRIDE, 92 | PARAM, 93 | PRIVATE_REMARKS, 94 | PRIVATE, 95 | PROPERTY, 96 | PROVIDES_MODULE, 97 | REMARKS, 98 | RETURNS, 99 | SEE, 100 | SINCE, 101 | TEMPLATE, 102 | THIS, 103 | THROWS, 104 | TODO, 105 | TYPE, 106 | TYPE_PARAM, 107 | TYPEDEF, 108 | SATISFIES, 109 | VERSION, 110 | YIELDS, 111 | SPACE_TAG_DATA, 112 | }; 113 | -------------------------------------------------------------------------------- /tests/files/order-custom.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} className 3 | * @param {string} language 4 | * @see http://github.com/hosseinmd/prettier-plugin-jsdoc 5 | * @param {ClassAdderEnvironment} env 6 | * @returns {undefined | string | string[]} 7 | * @example 8 | * let foo = "foo" 9 | * @property {string} language 10 | * @property {string} type 11 | * @property {string} content 12 | * 13 | */ 14 | 15 | 16 | 17 | /** 18 | * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and 19 | * a way to efficiently load them in synchronously and asynchronous contexts (`load`). 20 | * 21 | * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding 22 | * components will have to reloaded. 23 | * 24 | * The ids in `load` and `loaded` may be in any order and can contain duplicates. 25 | * 26 | * @param {Components} components 27 | * @param {string[]} load 28 | * @param {string[]} [loaded=[]] A list of already loaded components. 29 | * 30 | * If a component is in this list, then all of its requirements will also be assumed to be in the list. 31 | * @returns {Loader} 32 | * 33 | * @typedef Loader 34 | * @property {() => string[]} getIds A function to get all ids of the components to load. 35 | * 36 | * The returned ids will be duplicate-free, alias-free and in load order. 37 | * @property {LoadFunction} load A functional interface to load components. 38 | * 39 | * @typedef { (loadComponent: (id: string) => T, chainer?: LoadChainer) => T} LoadFunction 40 | * A functional interface to load components. 41 | * 42 | * The `loadComponent` function will be called for every component in the order in which they have to be loaded. 43 | * 44 | * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as 45 | * `Promise#then` and `Promise.all`. 46 | * 47 | * @example 48 | * load(id => { loadComponent(id); }); // returns undefined 49 | * 50 | * await load( 51 | * id => loadComponentAsync(id), // returns a Promise for each id 52 | * { 53 | * series: async (before, after) => { 54 | * await before; 55 | * await after(); 56 | * }, 57 | * parallel: async (values) => { 58 | * await Promise.all(values); 59 | * } 60 | * } 61 | * ); 62 | */ 63 | 64 | 65 | /** 66 | * function example description that was wrapped by hand 67 | * so it have more then one line and don't end with a dot 68 | * REPEATED TWO TIMES BECAUSE IT WAS EASIER to copy 69 | * function example description that was wrapped by hand 70 | * so it have more then one line. 71 | * @return {Boolean} Description for @returns with s 72 | * @param {String|Number} text - some text description that is very long and needs to be wrapped 73 | * @param {String} [defaultValue="defaultTest"] TODO 74 | * @arg {Number|Null} [optionalNumber] 75 | * @private 76 | *@memberof test 77 | @async 78 | * @examples 79 | * var one = 5 80 | * var two = 10 81 | * 82 | * if(one > 2) { two += one } 83 | * @undefiendTag${" "} 84 | * @undefiendTag {number} name des 85 | */ 86 | const testFunction = (text, defaultValue, optionalNumber) => true 87 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/order-custom.jsx.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: order-custom.jsx 1`] = ` 4 | "/** 5 | * @property {string} language 6 | * @property {string} type 7 | * @property {string} content 8 | * @param {string} className 9 | * @param {string} language 10 | * @param {ClassAdderEnvironment} env 11 | * @example 12 | * let foo = \\"foo\\" 13 | * 14 | * @see http://github.com/hosseinmd/prettier-plugin-jsdoc 15 | * @returns {undefined | string | string[]} 16 | */ 17 | 18 | /** 19 | * Returns an object which provides methods to get the ids of the components 20 | * which have to be loaded (\`getIds\`) and a way to efficiently load them in 21 | * synchronously and asynchronous contexts (\`load\`). 22 | * 23 | * The set of ids to be loaded is a superset of \`load\`. If some of these ids 24 | * are in \`loaded\`, the corresponding components will have to reloaded. 25 | * 26 | * The ids in \`load\` and \`loaded\` may be in any order and can contain 27 | * duplicates. 28 | * 29 | * @param {Components} components 30 | * @param {string[]} load 31 | * @param {string[]} [loaded=[]] A list of already loaded components. 32 | * 33 | * If a component is in this list, then all of its requirements will also be 34 | * assumed to be in the list. Default is \`[]\` 35 | * @returns {Loader} 36 | * 37 | * @typedef Loader 38 | * @property {() => string[]} getIds A function to get all ids of the 39 | * components to load. 40 | * 41 | * The returned ids will be duplicate-free, alias-free and in load order. 42 | * @property {LoadFunction} load A functional interface to load components. 43 | * 44 | * @typedef {( 45 | * loadComponent: (id: string) => T, 46 | * chainer?: LoadChainer, 47 | * ) => T} LoadFunction 48 | * A functional interface to load components. 49 | * 50 | * The \`loadComponent\` function will be called for every component in the 51 | * order in which they have to be loaded. 52 | * 53 | * The \`chainer\` is useful for asynchronous loading and its \`series\` and 54 | * \`parallel\` functions can be thought of as \`Promise#then\` and 55 | * \`Promise.all\`. 56 | * @example 57 | * load(id => { loadComponent(id); }); // returns undefined 58 | * 59 | * await load( 60 | * id => loadComponentAsync(id), // returns a Promise for each id 61 | * { 62 | * series: async (before, after) => { 63 | * await before; 64 | * await after(); 65 | * }, 66 | * parallel: async (values) => { 67 | * await Promise.all(values); 68 | * } 69 | * } 70 | * ); 71 | */ 72 | 73 | /** 74 | * Function example description that was wrapped by hand so it have more then 75 | * one line and don't end with a dot REPEATED TWO TIMES BECAUSE IT WAS EASIER to 76 | * copy function example description that was wrapped by hand so it have more 77 | * then one line. 78 | * 79 | * @async 80 | * @private 81 | * @memberof test 82 | * @param {String | Number} text - Some text description that is very long and 83 | * needs to be wrapped 84 | * @param {String} [defaultValue=\\"defaultTest\\"] TODO. Default is \`\\"defaultTest\\"\` 85 | * @param {Number | Null} [optionalNumber] 86 | * @example 87 | * var one = 5 88 | * var two = 10 89 | * 90 | * if(one > 2) { two += one } 91 | * 92 | * @undefiendTag\${\\" \\"} 93 | * @undefiendTag {number} name des 94 | * @returns {Boolean} Description for @returns with s 95 | */ 96 | const testFunction = (text, defaultValue, optionalNumber) => true; 97 | " 98 | `; 99 | -------------------------------------------------------------------------------- /tests/tagGroup.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | parser: "babel", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | ...options, 9 | } as AllOptions); 10 | } 11 | 12 | test("Tag group", async () => { 13 | const result = await subject( 14 | ` 15 | /** 16 | * Aliquip ex proident tempor eiusmod aliquip amet. Labore commodo nulla tempor 17 | * consequat exercitation incididunt non. Duis laboris reprehenderit proident 18 | * proident. 19 | * @see {@link http://acme.com} 20 | * @example 21 | * const foo = 0; 22 | * const a = ""; 23 | * const b = ""; 24 | * 25 | * @param id A test id. 26 | * @throws Minim sit ad commodo ut dolore magna magna minim consequat. Ex 27 | * consequat esse incididunt qui voluptate id voluptate quis ex et. Ullamco 28 | * cillum nisi amet fugiat. 29 | * @return Minim sit a. 30 | */ 31 | 32 | `, 33 | { 34 | jsdocSeparateTagGroups: true, 35 | }, 36 | ); 37 | 38 | expect(result).toMatchSnapshot(); 39 | }); 40 | 41 | test("space after unknownTag", async () => { 42 | function _subject(str: string) { 43 | return subject(str, { 44 | arrowParens: "always", 45 | bracketSameLine: false, 46 | bracketSpacing: true, 47 | embeddedLanguageFormatting: "auto", 48 | endOfLine: "lf", 49 | htmlWhitespaceSensitivity: "css", 50 | insertPragma: false, 51 | jsxSingleQuote: true, 52 | printWidth: 180, 53 | proseWrap: "preserve", 54 | quoteProps: "preserve", 55 | requirePragma: false, 56 | semi: true, 57 | singleAttributePerLine: false, 58 | singleQuote: true, 59 | tabWidth: 4, 60 | trailingComma: "all", 61 | useTabs: true, 62 | vueIndentScriptAndStyle: true, 63 | 64 | jsdocAddDefaultToDescription: false, 65 | jsdocCapitalizeDescription: true, 66 | jsdocDescriptionTag: false, 67 | jsdocDescriptionWithDot: true, 68 | jsdocKeepUnParseAbleExampleIndent: false, 69 | jsdocLineWrappingStyle: "greedy", 70 | jsdocPreferCodeFences: false, 71 | jsdocPrintWidth: 120, 72 | jsdocSeparateReturnsFromParam: false, 73 | jsdocSeparateTagGroups: true, 74 | jsdocCommentLineStrategy: "multiline", 75 | jsdocSpaces: 1, 76 | jsdocVerticalAlignment: true, 77 | tsdoc: false, 78 | }); 79 | } 80 | const result = await _subject(` 81 | /** 82 | * 83 | * A description. 84 | * 85 | * @unknownTag A note. 86 | * 87 | * @see http://acme.com 88 | */ 89 | `); 90 | 91 | expect(await _subject(await _subject(result))).toMatchSnapshot(); 92 | }); 93 | 94 | test("Inconsistant formatting", async () => { 95 | const result = await subject( 96 | ` 97 | /** 98 | * Aliquip ex proident tempor eiusmod aliquip amet. Labore commodo nulla tempor 99 | * consequat exercitation incididunt non. Duis laboris reprehenderit proident 100 | * proident. 101 | * 102 | * @example 103 | * const foo = 0; 104 | * 105 | * 106 | * @param id A test id. 107 | * 108 | * @throws Minim sit ad commodo ut dolore magna magna minim consequat. Ex 109 | * consequat esse incididunt qui voluptate id voluptate quis ex et. Ullamco 110 | * cillum nisi amet fugiat. 111 | * @see {@link http://acme.com} 112 | */ 113 | `, 114 | { 115 | jsdocSeparateTagGroups: true, 116 | }, 117 | ); 118 | 119 | expect(result).toMatchSnapshot(); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/files/typeScript.js: -------------------------------------------------------------------------------- 1 | /** 2 | @typedef { 3 | { 4 | "userId": string, 5 | "title": string, 6 | "profileImageLink": string, 7 | "identityStatus": "None", 8 | "isBusinessUser": boolean, 9 | "isResellerUser": boolean, 10 | "isSubUser": boolean, 11 | "shareCode": number, 12 | "referredBy": string, 13 | "businessName": string, 14 | "businessUserId": string, 15 | "nationalCode": string, 16 | "state": string, 17 | "city": string, 18 | "address": string, 19 | "phoneNumber": string 20 | } 21 | } User 22 | */ 23 | export let User; 24 | 25 | /** 26 | @typedef { 27 | { 28 | "domainId": 0, 29 | persianName: string, 30 | "englishName": string, // comment 31 | "resellerUserId": string, 32 | "isActive": true, 33 | "logoFileUniqueId": string, 34 | "logoFileName": string, 35 | "logoFileUrl": string, 36 | "domainPersianName": string, 37 | "domainEnglishName": string, 38 | "resellerUserDisplayName": string, 39 | "about": string 40 | } 41 | } SubDomain 42 | */ 43 | 44 | /** 45 | @typedef { 46 | () => a.b 47 | } SubDomain 48 | */ 49 | /** 50 | @typedef { 51 | { 52 | "userId": { 53 | title: string, 54 | "profileImageLink": *, 55 | "identityStatus": "None", 56 | "isBusinessUser": "isResellerUser"|"isBoolean"| "isSubUser" | "isNot", 57 | "shareCode": number, 58 | "referredBy": any, 59 | }, 60 | id:number 61 | } 62 | } User 63 | */ 64 | 65 | class test { 66 | /** 67 | * Replaces text in a string, using a regular expression or search string. 68 | * @param {string | RegExp} searchValue A string to search for. 69 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A string containing the text to replace for every successful match of searchValue in this string. 70 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A string containing the text to replace for every successful match of searchValue in this string. 71 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 72 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 73 | * @returns {StarkStringType & NativeString} 74 | */ 75 | replace(searchValue, replaceValue) { 76 | class test { 77 | /** 78 | * Replaces text in a string, using a regular expression or search string. 79 | * @param {string | RegExp} searchValue A string to search for. 80 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A string containing the text to replace for every successful match of 81 | * searchValue in this string. 82 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 83 | A string containing the text to replace for every successful match of searchValue 84 | * in this string. 85 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 86 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 87 | * A_big_string_for_test string containing the text to replace for every successful 88 | * match of searchValue in this string. 89 | * @returns {StarkStringType & NativeString} 90 | */ 91 | testFunction() {} 92 | } 93 | 94 | this._value = this._value.replace(searchValue, replaceValue); 95 | return this; 96 | } 97 | } 98 | /** 99 | * @typedef {import("Foo")} Foo 100 | */ 101 | -------------------------------------------------------------------------------- /tests/default.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | parser: "babel", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("default string with description", async () => { 14 | const result = await subject(` 15 | /** 16 | * The summary 17 | * 18 | * @default "type" description 19 | */ 20 | `); 21 | 22 | expect(result).toMatchSnapshot(); 23 | }); 24 | 25 | test("convert double quote @default to single quote", async () => { 26 | const result = await subject( 27 | ` 28 | /** 29 | * The summary 30 | * 31 | * @default "value" 32 | */ 33 | `, 34 | { singleQuote: true }, 35 | ); 36 | 37 | expect(result).toMatchSnapshot(); 38 | }); 39 | 40 | test("convert single quote @default to double quote", async () => { 41 | const result = await subject( 42 | ` 43 | /** 44 | * The summary 45 | * 46 | * @default 'value' 47 | */ 48 | `, 49 | { singleQuote: false }, 50 | ); 51 | 52 | expect(result).toMatchSnapshot(); 53 | }); 54 | 55 | test("Can't convert double quote @default if a single quote character is in the string", async () => { 56 | const result = await subject( 57 | ` 58 | /** 59 | * The summary 60 | * 61 | * @default "This isn't bad" 62 | */ 63 | `, 64 | { singleQuote: true }, 65 | ); 66 | 67 | expect(result).toMatchSnapshot(); 68 | }); 69 | 70 | test("default empty array", async () => { 71 | const input = ` 72 | /** 73 | * The summary 74 | * 75 | * @default [] 76 | */ 77 | `; 78 | const result = await subject(input); 79 | 80 | expect(result).toMatchSnapshot(); 81 | }); 82 | 83 | test("default empty object", async () => { 84 | const input = ` 85 | /** 86 | * The summary 87 | * 88 | * @default {} 89 | */ 90 | `; 91 | const result = await subject(input); 92 | 93 | expect(result).toMatchSnapshot(); 94 | }); 95 | 96 | test("empty default tag", async () => { 97 | const input = ` 98 | /** 99 | * The summary 100 | * 101 | * @default 102 | */ 103 | `; 104 | const result = await subject(input); 105 | 106 | expect(result).toMatchSnapshot(); 107 | }); 108 | 109 | ["default", "defaultValue"].forEach((tag: string) => { 110 | test(`@${tag} filled array`, async () => { 111 | const input = ` 112 | /** 113 | * The summary 114 | * 115 | * @${tag} [1,'two',{three:true},['four']] 116 | */ 117 | `; 118 | const result = await subject(input); 119 | 120 | expect(result).toMatchSnapshot(); 121 | }); 122 | 123 | test(`@${tag} filled object`, async () => { 124 | const input = ` 125 | /** 126 | * The summary 127 | * 128 | * @${tag} {object:'value',nestingTest:{obj:'nested'}} 129 | */ 130 | `; 131 | const result = await subject(input); 132 | 133 | expect(result).toMatchSnapshot(); 134 | }); 135 | }); 136 | 137 | test("double default one", async () => { 138 | const input = ` 139 | /** 140 | * The summary 141 | * 142 | * @default "something" 143 | * @default {} 144 | */ 145 | `; 146 | const result = await subject(input); 147 | 148 | expect(result).toMatchSnapshot(); 149 | }); 150 | 151 | test("double default two", async () => { 152 | const input = ` 153 | /** 154 | * The summary 155 | * 156 | * @default {} 157 | * @default "something" 158 | */ 159 | `; 160 | const result = await subject(input); 161 | 162 | expect(result).toMatchSnapshot(); 163 | }); 164 | 165 | test("Single line codegen", async () => { 166 | const result = await subject( 167 | ` 168 | /** @default codegen */ 169 | `, 170 | ); 171 | 172 | expect(result).toMatchSnapshot(); 173 | }); 174 | 175 | test("Multi line codegen", async () => { 176 | const result = await subject( 177 | ` 178 | /** 179 | * @default codegen 180 | */ 181 | `, 182 | ); 183 | 184 | expect(result).toMatchSnapshot(); 185 | }); 186 | 187 | test("code in default", async () => { 188 | const result = await subject( 189 | `/** 190 | * The path to the config file or directory contains the config file. 191 | * 192 | * @default process.cwd() 193 | */ 194 | `, 195 | ); 196 | 197 | expect(result).toMatchSnapshot(); 198 | }); 199 | -------------------------------------------------------------------------------- /tests/files/typeScript.ts: -------------------------------------------------------------------------------- 1 | //@ts-nocheck 2 | /** 3 | @typedef { 4 | { 5 | "userId": string, 6 | "title": string, 7 | "profileImageLink": string, 8 | "identityStatus": "None", 9 | "isBusinessUser": boolean, 10 | "isResellerUser": boolean, 11 | "isSubUser": boolean, 12 | "shareCode": number, 13 | "referredBy": string, 14 | "businessName": string, 15 | "businessUserId": string, 16 | "nationalCode": string, 17 | "state": string, 18 | "city": string, 19 | "address": string, 20 | "phoneNumber": string 21 | } 22 | } User 23 | */ 24 | export let User: string 25 | 26 | /** 27 | @typedef { 28 | { 29 | "domainId": 0, 30 | persianName: string, 31 | "englishName": string, // comment 32 | "resellerUserId": string, 33 | "isActive": true, 34 | "logoFileUniqueId": string, 35 | "logoFileName": string, 36 | "logoFileUrl": string, 37 | "domainPersianName": string, 38 | "domainEnglishName": string, 39 | "resellerUserDisplayName": string, 40 | "about": string 41 | } 42 | } SubDomain 43 | */ 44 | 45 | /** 46 | @typedef { 47 | () => a.b 48 | } SubDomain 49 | */ 50 | /** 51 | @typedef { 52 | { 53 | "userId": { 54 | title: string, 55 | "profileImageLink": *, 56 | "identityStatus": "None", 57 | "isBusinessUser": "isResellerUser"|"isBoolean"| "isSubUser" | "isNot", 58 | "shareCode": number, 59 | "referredBy": any, 60 | }, 61 | id:number 62 | } 63 | } User 64 | */ 65 | 66 | class test { 67 | /** 68 | * Replaces text in a string, using a regular expression or search string. 69 | * @param {string | RegExp} searchValue A string to search for. 70 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A string containing the text to replace for every successful match of searchValue in this string. 71 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A string containing the text to replace for every successful match of searchValue in this string. 72 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 73 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 74 | * @returns {StarkStringType & NativeString} 75 | */ 76 | replace(searchValue: any, replaceValue: any) { 77 | class test { 78 | /** 79 | * Replaces text in a string, using a regular expression or search string. 80 | * @param {string | RegExp} searchValue A string to search for. 81 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A string containing the text to replace for every successful match of 82 | * searchValue in this string. 83 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 84 | A string containing the text to replace for every successful match of searchValue 85 | * in this string. 86 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 87 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 88 | * A_big_string_for_test string containing the text to replace for every successful 89 | * match of searchValue in this string. 90 | * @returns {StarkStringType & NativeString} 91 | */ 92 | testFunction() {} 93 | } 94 | 95 | this._value = this._value.replace(searchValue, replaceValue); 96 | return test; 97 | } 98 | } 99 | export interface FetchCallbackResponseArray { 100 | resource: Resource; 101 | /** 102 | * @deprecated Resolve clear with condition in your fetch api this function will be remove 103 | */ 104 | refetch: (...arg: V[]) => void; 105 | /** 106 | * @deprecated Resolve clear with condition in your fetch api this function will be remove 107 | */ 108 | clear: () => void; 109 | } 110 | /** 111 | * @typedef {import("Foo")} Foo 112 | */ 113 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/typeScript.ts.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: typeScript.ts 1`] = ` 4 | "//@ts-nocheck 5 | /** 6 | * @typedef {{ 7 | * userId: string; 8 | * title: string; 9 | * profileImageLink: string; 10 | * identityStatus: \\"None\\"; 11 | * isBusinessUser: boolean; 12 | * isResellerUser: boolean; 13 | * isSubUser: boolean; 14 | * shareCode: number; 15 | * referredBy: string; 16 | * businessName: string; 17 | * businessUserId: string; 18 | * nationalCode: string; 19 | * state: string; 20 | * city: string; 21 | * address: string; 22 | * phoneNumber: string; 23 | * }} User 24 | */ 25 | export let User: string; 26 | 27 | /** 28 | * @typedef {{ 29 | * domainId: 0; 30 | * persianName: string; 31 | * englishName: string; // comment 32 | * resellerUserId: string; 33 | * isActive: true; 34 | * logoFileUniqueId: string; 35 | * logoFileName: string; 36 | * logoFileUrl: string; 37 | * domainPersianName: string; 38 | * domainEnglishName: string; 39 | * resellerUserDisplayName: string; 40 | * about: string; 41 | * }} SubDomain 42 | */ 43 | 44 | /** @typedef {() => a.b} SubDomain */ 45 | /** 46 | * @typedef {{ 47 | * userId: { 48 | * title: string; 49 | * profileImageLink: any; 50 | * identityStatus: \\"None\\"; 51 | * isBusinessUser: \\"isResellerUser\\" | \\"isBoolean\\" | \\"isSubUser\\" | \\"isNot\\"; 52 | * shareCode: number; 53 | * referredBy: any; 54 | * }; 55 | * id: number; 56 | * }} User 57 | */ 58 | 59 | class test { 60 | /** 61 | * Replaces text in a string, using a regular expression or search string. 62 | * 63 | * @param {string | RegExp} searchValue A string to search for. 64 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 65 | * A string containing the text to replace for every successful match of 66 | * searchValue in this string. 67 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 68 | * A string containing the text to replace for every successful match of 69 | * searchValue in this string. 70 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 71 | * A_big_string_for_test string containing the text to replace for every 72 | * successful match of searchValue in this string. 73 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 74 | * A_big_string_for_test string containing the text to replace for every 75 | * successful match of searchValue in this string. 76 | * @returns {StarkStringType & NativeString} 77 | */ 78 | replace(searchValue: any, replaceValue: any) { 79 | class test { 80 | /** 81 | * Replaces text in a string, using a regular expression or search string. 82 | * 83 | * @param {string | RegExp} searchValue A string to search for. 84 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 85 | * A string containing the text to replace for every successful match of 86 | * searchValue in this string. 87 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 88 | * A string containing the text to replace for every successful match of 89 | * searchValue in this string. 90 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 91 | * A_big_string_for_test string containing the text to replace for every 92 | * successful match of searchValue in this string. 93 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 94 | * A_big_string_for_test string containing the text to replace for every 95 | * successful match of searchValue in this string. 96 | * @returns {StarkStringType & NativeString} 97 | */ 98 | testFunction() {} 99 | } 100 | 101 | this._value = this._value.replace(searchValue, replaceValue); 102 | return test; 103 | } 104 | } 105 | export interface FetchCallbackResponseArray { 106 | resource: Resource; 107 | /** 108 | * @deprecated Resolve clear with condition in your fetch api this function 109 | * will be remove 110 | */ 111 | refetch: (...arg: V[]) => void; 112 | /** 113 | * @deprecated Resolve clear with condition in your fetch api this function 114 | * will be remove 115 | */ 116 | clear: () => void; 117 | } 118 | /** @typedef {import(\\"Foo\\")} Foo */ 119 | " 120 | `; 121 | -------------------------------------------------------------------------------- /src/roles.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ABSTRACT, 3 | ASYNC, 4 | AUGMENTS, 5 | AUTHOR, 6 | BORROWS, 7 | CALLBACK, 8 | CATEGORY, 9 | CLASS, 10 | CONSTANT, 11 | DEFAULT, 12 | DEFAULT_VALUE, 13 | DEPRECATED, 14 | DESCRIPTION, 15 | EXAMPLE, 16 | EXTENDS, 17 | EXTERNAL, 18 | FILE, 19 | FIRES, 20 | FLOW, 21 | FUNCTION, 22 | IGNORE, 23 | IMPORT, 24 | LICENSE, 25 | MEMBER, 26 | MEMBEROF, 27 | MODULE, 28 | NAMESPACE, 29 | OVERLOAD, 30 | OVERRIDE, 31 | PARAM, 32 | PRIVATE, 33 | PRIVATE_REMARKS, 34 | PROPERTY, 35 | PROVIDES_MODULE, 36 | REMARKS, 37 | RETURNS, 38 | SEE, 39 | SINCE, 40 | TEMPLATE, 41 | THIS, 42 | THROWS, 43 | TODO, 44 | TYPE, 45 | SATISFIES, 46 | TYPE_PARAM, 47 | TYPEDEF, 48 | VERSION, 49 | YIELDS, 50 | } from "./tags.js"; 51 | 52 | const TAGS_SYNONYMS = { 53 | // One TAG TYPE can have different titles called SYNONYMS. We want 54 | // to avoid different titles in the same tag so here is map with 55 | // synonyms as keys and tag type as value that we want to have in 56 | // final jsDoc. 57 | arg: PARAM, 58 | argument: PARAM, 59 | const: CONSTANT, 60 | constructor: CLASS, 61 | desc: DESCRIPTION, 62 | emits: FIRES, 63 | examples: EXAMPLE, 64 | exception: THROWS, 65 | fileoverview: FILE, 66 | func: FUNCTION, 67 | host: EXTERNAL, 68 | method: FUNCTION, 69 | overview: FILE, 70 | params: PARAM, 71 | prop: PROPERTY, 72 | return: RETURNS, 73 | var: MEMBER, 74 | virtual: ABSTRACT, 75 | yield: YIELDS, 76 | hidden: IGNORE, 77 | }; 78 | 79 | const TAGS_DEFAULT = [DEFAULT, DEFAULT_VALUE]; 80 | 81 | const TAGS_NAMELESS = [ 82 | BORROWS, 83 | CATEGORY, 84 | DEPRECATED, 85 | DESCRIPTION, 86 | EXAMPLE, 87 | EXTENDS, 88 | LICENSE, 89 | IMPORT, 90 | MODULE, 91 | NAMESPACE, 92 | OVERLOAD, 93 | OVERRIDE, 94 | PRIVATE_REMARKS, 95 | REMARKS, 96 | RETURNS, 97 | SINCE, 98 | THIS, 99 | THROWS, 100 | TODO, 101 | YIELDS, 102 | FILE, 103 | ...TAGS_DEFAULT, 104 | ]; 105 | 106 | const TAGS_TYPELESS = [ 107 | BORROWS, 108 | BORROWS, 109 | DEPRECATED, 110 | DESCRIPTION, 111 | EXAMPLE, 112 | IGNORE, 113 | IMPORT, 114 | LICENSE, 115 | MODULE, 116 | NAMESPACE, 117 | OVERLOAD, 118 | OVERRIDE, 119 | PRIVATE_REMARKS, 120 | REMARKS, 121 | SINCE, 122 | TODO, 123 | FILE, 124 | ]; 125 | 126 | const TAGS_PEV_FORMATE_DESCRIPTION = [ 127 | /** @todo should be formate like jsdoc standard saw https://jsdoc.app/tags-borrows.html */ 128 | BORROWS, 129 | ...TAGS_DEFAULT, 130 | IMPORT, 131 | MEMBEROF, 132 | MODULE, 133 | SEE, 134 | ]; 135 | 136 | const TAGS_DESCRIPTION_NEEDED = [ 137 | BORROWS, 138 | CATEGORY, 139 | DESCRIPTION, 140 | EXAMPLE, 141 | IMPORT, 142 | PRIVATE_REMARKS, 143 | REMARKS, 144 | SINCE, 145 | TODO, 146 | ]; 147 | 148 | const TAGS_TYPE_NEEDED = [ 149 | EXTENDS, 150 | PARAM, 151 | PROPERTY, 152 | RETURNS, 153 | THIS, 154 | THROWS, 155 | TYPE, 156 | SATISFIES, 157 | TYPEDEF, 158 | YIELDS, 159 | ]; 160 | 161 | const TAGS_VERTICALLY_ALIGN_ABLE = [ 162 | EXTENDS, 163 | PARAM, 164 | PROPERTY, 165 | RETURNS, 166 | THIS, 167 | THROWS, 168 | TYPE, 169 | SATISFIES, 170 | TYPEDEF, 171 | YIELDS, 172 | ]; 173 | 174 | const TAGS_GROUP_HEAD = [CALLBACK, TYPEDEF]; 175 | const TAGS_GROUP_CONDITION = [ 176 | ...TAGS_GROUP_HEAD, 177 | TYPE, 178 | PROPERTY, 179 | PARAM, 180 | RETURNS, 181 | THIS, 182 | YIELDS, 183 | THROWS, 184 | ]; 185 | 186 | const TAGS_ORDER = { 187 | [IMPORT]: 0, 188 | [REMARKS]: 1, 189 | [PRIVATE_REMARKS]: 2, 190 | [PROVIDES_MODULE]: 3, 191 | [MODULE]: 4, 192 | [LICENSE]: 5, 193 | [FLOW]: 6, 194 | [ASYNC]: 7, 195 | [PRIVATE]: 8, 196 | [IGNORE]: 9, 197 | [MEMBEROF]: 10, 198 | [VERSION]: 11, 199 | [FILE]: 12, 200 | [AUTHOR]: 13, 201 | [DEPRECATED]: 14, 202 | [SINCE]: 15, 203 | [CATEGORY]: 16, 204 | [DESCRIPTION]: 17, 205 | [EXAMPLE]: 18, 206 | [ABSTRACT]: 19, 207 | [AUGMENTS]: 20, 208 | [CONSTANT]: 21, 209 | [DEFAULT]: 22, 210 | [DEFAULT_VALUE]: 23, 211 | [EXTERNAL]: 24, 212 | [OVERLOAD]: 25, 213 | [FIRES]: 26, 214 | [TEMPLATE]: 27, 215 | [TYPE_PARAM]: 28, 216 | [FUNCTION]: 29, 217 | [NAMESPACE]: 30, 218 | [BORROWS]: 31, 219 | [CLASS]: 32, 220 | [EXTENDS]: 33, 221 | [MEMBER]: 34, 222 | [TYPEDEF]: 35, 223 | [TYPE]: 36, 224 | [SATISFIES]: 37, 225 | [PROPERTY]: 38, 226 | [CALLBACK]: 39, 227 | [THIS]: 39.5, 228 | [PARAM]: 40, 229 | [YIELDS]: 41, 230 | [RETURNS]: 42, 231 | [THROWS]: 43, 232 | other: 44, 233 | [SEE]: 45, 234 | [TODO]: 46, 235 | }; 236 | 237 | export { 238 | TAGS_PEV_FORMATE_DESCRIPTION, 239 | TAGS_DESCRIPTION_NEEDED, 240 | TAGS_NAMELESS, 241 | TAGS_GROUP_HEAD, 242 | TAGS_GROUP_CONDITION, 243 | TAGS_ORDER, 244 | TAGS_SYNONYMS, 245 | TAGS_TYPE_NEEDED, 246 | TAGS_TYPELESS, 247 | TAGS_VERTICALLY_ALIGN_ABLE, 248 | TAGS_DEFAULT, 249 | }; 250 | -------------------------------------------------------------------------------- /tests/__snapshots__/compatibleWithPlugins.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CLI Compatibility Should format via CLI with --write flag 1`] = ` 4 | "/** 5 | * @param {number} [arg1=123] The width 6 | * 7 | * Default is \`123\` 8 | * @returns {void} 9 | */ 10 | function myFunc(arg1) {} 11 | " 12 | `; 13 | 14 | exports[`CLI Compatibility Should format with tailwindcss plugin via CLI without infinite recursion 1`] = ` 15 | "/** 16 | * @param {String | Number} text - Some text description 17 | * @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\` 18 | * @returns {Boolean} Description for returns 19 | */ 20 | const testFunction = (text, defaultValue) => true 21 | " 22 | `; 23 | 24 | exports[`CLI Compatibility Should work with plugins in different orders via CLI 1`] = ` 25 | "/** 26 | * @param {string} name 27 | 28 | 29 | 30 | 31 | * @returns {Promise} 32 | */ 33 | async function example(name: string): Promise {} 34 | " 35 | `; 36 | 37 | exports[`CLI Compatibility Should work with plugins in different orders via CLI 2`] = ` 38 | "/** 39 | * @param {string} name 40 | * @returns {Promise} 41 | */ 42 | async function example(name: string): Promise {} 43 | " 44 | `; 45 | 46 | exports[`Should compatible with tailwindcss 1`] = ` 47 | "/** 48 | * @param {String} [arg1="defaultTest"] Foo. Default is \`"defaultTest"\` 49 | * @param {number} [arg2=123] The width of the rectangle. Default is \`123\` 50 | * @param {number} [arg3=123] Default is \`123\` 51 | * @param {number} [arg4=Foo.bar.baz] Default is \`Foo.bar.baz\` 52 | * @param {number | string} [arg5=123] Something. Default is \`123\` 53 | */ 54 | " 55 | `; 56 | 57 | exports[`Should convert to single line if necessary 1`] = ` 58 | "//prettier-plugin-fake 59 | /** Single line description */ 60 | " 61 | `; 62 | 63 | exports[`Should convert to single line if necessary 2`] = ` 64 | "//prettier-plugin-fake 65 | /** Single line description */ 66 | " 67 | `; 68 | 69 | exports[`Should convert to single line if necessary 3`] = ` 70 | "//prettier-plugin-fake 71 | /** 72 | * Single line description 73 | * 74 | * @returns {Boolean} Always true 75 | */ 76 | " 77 | `; 78 | 79 | exports[`Should format jsDoc default values 1`] = ` 80 | "//prettier-plugin-fake 81 | 82 | /** 83 | * @param {String} [arg1="defaultTest"] Foo. Default is \`"defaultTest"\` 84 | * @param {number} [arg2=123] The width of the rectangle. Default is \`123\` 85 | * @param {number} [arg3=123] Default is \`123\` 86 | * @param {number} [arg4=Foo.bar.baz] Default is \`Foo.bar.baz\` 87 | * @param {number | string} [arg5=123] Something. Default is \`123\` 88 | */ 89 | " 90 | `; 91 | 92 | exports[`Should format jsDoc default values 2`] = ` 93 | "//prettier-plugin-fake 94 | //prettier-plugin-fake 95 | 96 | /** 97 | * @param {String} [arg1="defaultTest"] Foo. Default is \`"defaultTest"\` 98 | * @param {number} [arg2=123] The width of the rectangle. Default is \`123\` 99 | * @param {number} [arg3=123] Default is \`123\` 100 | * @param {number} [arg4=Foo.bar.baz] Default is \`Foo.bar.baz\` 101 | * @param {number | string} [arg5=123] Something. Default is \`123\` 102 | */ 103 | " 104 | `; 105 | 106 | exports[`Should format regular jsDoc 1`] = ` 107 | "//prettier-plugin-fake 108 | 109 | import b from "b"; 110 | import { k } from "k"; 111 | import a from "a"; 112 | 113 | /** 114 | * Function example description that was wrapped by hand so it have more then 115 | * one line and don't end with a dot REPEATED TWO TIMES BECAUSE IT WAS EASIER to 116 | * copy function example description that was wrapped by hand so it have more 117 | * then one line. 118 | * 119 | * @async 120 | * @private 121 | * @memberof test 122 | * @example 123 | * //prettier-plugin-fake 124 | * 125 | * var one = 5; 126 | * var two = 10; 127 | * 128 | * if (one > 2) { 129 | * two += one; 130 | * } 131 | * 132 | * @param {String | Number} text - Some text description that is very long and 133 | * needs to be wrapped 134 | * @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\` 135 | * @param {Number | Null} [optionalNumber] 136 | * @returns {Boolean} Description for @returns with s 137 | * @undefiendTag 138 | * @undefiendTag {number} name des 139 | */ 140 | const testFunction = (text, defaultValue, optionalNumber) => true; 141 | " 142 | `; 143 | 144 | exports[`Should format regular jsDoc 2`] = ` 145 | "//prettier-plugin-fake 146 | //prettier-plugin-fake 147 | 148 | import b from "b"; 149 | import { k } from "k"; 150 | import a from "a"; 151 | 152 | /** 153 | * Function example description that was wrapped by hand so it have more then 154 | * one line and don't end with a dot REPEATED TWO TIMES BECAUSE IT WAS EASIER to 155 | * copy function example description that was wrapped by hand so it have more 156 | * then one line. 157 | * 158 | * @async 159 | * @private 160 | * @memberof test 161 | * @example 162 | * //prettier-plugin-fake 163 | * 164 | * //prettier-plugin-fake 165 | * 166 | * var one = 5; 167 | * var two = 10; 168 | * 169 | * if (one > 2) { 170 | * two += one; 171 | * } 172 | * 173 | * @param {String | Number} text - Some text description that is very long and 174 | * needs to be wrapped 175 | * @param {String} [defaultValue="defaultTest"] TODO. Default is \`"defaultTest"\` 176 | * @param {Number | Null} [optionalNumber] 177 | * @returns {Boolean} Description for @returns with s 178 | * @undefiendTag 179 | * @undefiendTag {number} name des 180 | */ 181 | const testFunction = (text, defaultValue, optionalNumber) => true; 182 | " 183 | `; 184 | -------------------------------------------------------------------------------- /tests/__snapshots__/exampleTag.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Example javascript code 1`] = ` 4 | "/** 5 | * @example 6 | * var one = 5; 7 | * var two = 10; 8 | * 9 | * const resolveDescription = formatDescription( 10 | * tag, 11 | * description, 12 | * tagString, 13 | * a, 14 | * ); 15 | * 16 | * if (one > 2) { 17 | * two += one; 18 | * } 19 | * 20 | * @undefiendTag 21 | * @undefiendTag {number} name des 22 | */ 23 | const testFunction = (text, defaultValue, optionalNumber) => true; 24 | " 25 | `; 26 | 27 | exports[`Example javascript code 2`] = ` 28 | "/** 29 | * @example 30 | * var one = 5; 31 | * var two = 10; 32 | * 33 | * const resolveDescription = formatDescription( 34 | * tag, 35 | * description, 36 | * tagString, 37 | * a, 38 | * ); 39 | * 40 | * if (one > 2) { 41 | * two += one; 42 | * } 43 | * 44 | * @undefiendTag 45 | * @undefiendTag {number} name des 46 | */ 47 | const testFunction = (text, defaultValue, optionalNumber) => true; 48 | " 49 | `; 50 | 51 | exports[`Example start by xml tag 1`] = ` 52 | "/** 53 | * @example TradingViewChart 54 | * export default Something; 55 | */ 56 | " 57 | `; 58 | 59 | exports[`Example start by xml tag 2`] = ` 60 | "/** 61 | * @example TradingViewChart 62 | * function Something() { 63 | * return TradingViewChart; 64 | * } 65 | * export default Something; 66 | */ 67 | " 68 | `; 69 | 70 | exports[`empty example 1`] = ` 71 | "/** Single line description */ 72 | " 73 | `; 74 | 75 | exports[`empty example 2`] = ` 76 | "/** 77 | * Single line description 78 | * 79 | * @returns {Boolean} Always true 80 | */ 81 | " 82 | `; 83 | 84 | exports[`example json 1`] = ` 85 | "/** 86 | * Then this man came to realize the truth: Besides six pence, there is the 87 | * moon. Besides bread and butter, there is the bug. And... Besides women, 88 | * there is the code. 89 | * 90 | * @example 91 | * { 92 | * "0%": "#afc163", 93 | * "25%": "#66FF00", 94 | * "50%": "#00CC00", // ====> linear-gradient(to right, #afc163 0%, #66FF00 25%, 95 | * "75%": "#009900", // #00CC00 50%, #009900 75%, #ffffff 100%) 96 | * "100%": "#ffffff" 97 | * } 98 | */ 99 | " 100 | `; 101 | 102 | exports[`example unParseAble 1`] = ` 103 | "/** 104 | * @example 105 | * Prism.languages['css-with-colors'] = Prism.languages.extend('css', { 106 | * // Prism.languages.css already has a 'comment' token, so this token will overwrite CSS' 'comment' token 107 | * // at its original position 108 | * 'comment': { ... }, 109 | * // CSS doesn't have a 'color' token, so this token will be appended 110 | * 'color': /(?:red|green|blue)/ 111 | * }); 112 | */ 113 | " 114 | `; 115 | 116 | exports[`example with @this tag preserves indentation 1`] = ` 117 | "/** 118 | * @example 119 | * import { createServer } from "node:http"; 120 | * import extendedResponse from "extended-response"; 121 | * 122 | * async function* messages() { 123 | * for (let i = 1; i <= 5; i++) { 124 | * yield \`Message \${i}\\r\\n\`; 125 | * await new Promise((resolve) => setTimeout(resolve, 1000)); 126 | * } 127 | * } 128 | * 129 | * const server = createServer(async (req, res) => { 130 | * if ( 131 | * req.method === "GET" && 132 | * new URL(req.url || "").pathname === "/stream5" 133 | * ) { 134 | * await extendedResponse.call(res, { 135 | * messages, 136 | * }); 137 | * } 138 | * }); 139 | * 140 | * server.listen(port, () => { 141 | * console.log(\`Server running on http://localhost:\${port}\`); 142 | * }); 143 | * 144 | * @this {ServerResponse} A Node.js HTTP 145 | * {@link https://nodejs.org/api/http.html#class-httpserverresponse|ServerResponse} 146 | * instance. 147 | * @param {object} params Extended Response Parameters 148 | * @param {AsyncIterable} params.messages Async messages to be sent 149 | * response stream will be closed 150 | * @returns {Promise} 151 | */ 152 | " 153 | `; 154 | 155 | exports[`example with @this tag preserves indentation 2`] = ` 156 | "/** 157 | * @example 158 | * import { createServer } from "node:http"; 159 | * import extendedResponse from "extended-response"; 160 | * 161 | * async function* messages() { 162 | * for (let i = 1; i <= 5; i++) { 163 | * yield \`Message \${i}\\r\\n\`; 164 | * await new Promise((resolve) => setTimeout(resolve, 1000)); 165 | * } 166 | * } 167 | * 168 | * const server = createServer(async (req, res) => { 169 | * if ( 170 | * req.method === "GET" && 171 | * new URL(req.url || "").pathname === "/stream5" 172 | * ) { 173 | * await extendedResponse.call(res, { 174 | * messages, 175 | * }); 176 | * } 177 | * }); 178 | * 179 | * server.listen(port, () => { 180 | * console.log(\`Server running on http://localhost:\${port}\`); 181 | * }); 182 | * 183 | * @this {ServerResponse} A Node.js HTTP 184 | * {@link https://nodejs.org/api/http.html#class-httpserverresponse|ServerResponse} 185 | * instance. 186 | * @param {object} params Extended Response Parameters 187 | * @param {AsyncIterable} params.messages Async messages to be sent 188 | * response stream will be closed 189 | * @returns {Promise} 190 | */ 191 | " 192 | `; 193 | 194 | exports[`examples Json 1`] = ` 195 | "/** 196 | * @example 197 | * { "testArr": [1, 2] } 198 | */ 199 | " 200 | `; 201 | 202 | exports[`examples Json 2`] = ` 203 | "/** 204 | * @example 205 | * [ 206 | * { 207 | * foo: 1, 208 | * foo: 2, 209 | * foo: 9, 210 | * }, 211 | * { 212 | * bar: 1, 213 | * bar: 5, 214 | * }, 215 | * ]; 216 | */ 217 | " 218 | `; 219 | -------------------------------------------------------------------------------- /src/stringify.ts: -------------------------------------------------------------------------------- 1 | import { Spec } from "comment-parser"; 2 | import { 3 | formatDescription, 4 | descriptionEndLine, 5 | } from "./descriptionFormatter.js"; 6 | import { 7 | DESCRIPTION, 8 | EXAMPLE, 9 | PRIVATE_REMARKS, 10 | REMARKS, 11 | SPACE_TAG_DATA, 12 | } from "./tags.js"; 13 | import { 14 | TAGS_ORDER, 15 | TAGS_PEV_FORMATE_DESCRIPTION, 16 | TAGS_VERTICALLY_ALIGN_ABLE, 17 | } from "./roles.js"; 18 | import { AllOptions } from "./types.js"; 19 | import { formatCode, isDefaultTag } from "./utils.js"; 20 | 21 | const stringify = async ( 22 | { name, description, type, tag }: Spec, 23 | tagIndex: number, 24 | finalTagsArray: Spec[], 25 | options: AllOptions, 26 | maxTagTitleLength: number, 27 | maxTagTypeNameLength: number, 28 | maxTagNameLength: number, 29 | ): Promise => { 30 | let tagString = "\n"; 31 | 32 | if (tag === SPACE_TAG_DATA.tag) { 33 | return tagString; 34 | } 35 | 36 | const { 37 | printWidth, 38 | jsdocSpaces, 39 | jsdocVerticalAlignment, 40 | jsdocDescriptionTag, 41 | tsdoc, 42 | useTabs, 43 | tabWidth, 44 | jsdocSeparateTagGroups, 45 | jsdocBracketSpacing, 46 | } = options; 47 | const gap = " ".repeat(jsdocSpaces); 48 | 49 | let tagTitleGapAdj = 0; 50 | let tagTypeGapAdj = 0; 51 | let tagNameGapAdj = 0; 52 | let descGapAdj = 0; 53 | 54 | if (jsdocVerticalAlignment && TAGS_VERTICALLY_ALIGN_ABLE.includes(tag)) { 55 | if (tag) tagTitleGapAdj += maxTagTitleLength - tag.length; 56 | else if (maxTagTitleLength) descGapAdj += maxTagTitleLength + gap.length; 57 | 58 | if (type) tagTypeGapAdj += maxTagTypeNameLength - type.length; 59 | else if (maxTagTypeNameLength) 60 | descGapAdj += maxTagTypeNameLength + gap.length; 61 | 62 | if (name) tagNameGapAdj += maxTagNameLength - name.length; 63 | else if (maxTagNameLength) descGapAdj = maxTagNameLength + gap.length; 64 | } 65 | 66 | const useTagTitle = tag !== DESCRIPTION || jsdocDescriptionTag; 67 | 68 | if (useTagTitle) { 69 | tagString += `@${tag}${" ".repeat(tagTitleGapAdj || 0)}`; 70 | } 71 | if (type) { 72 | const getUpdatedType = () => { 73 | const wrapType = (innerType: string) => { 74 | return jsdocBracketSpacing ? `{ ${innerType} }` : `{${innerType}}`; 75 | }; 76 | 77 | if (!isDefaultTag(tag)) { 78 | return wrapType(type); 79 | } 80 | 81 | // The space is to improve readability in non-monospace fonts 82 | if (type === "[]") return "[ ]"; 83 | if (type === "{}") return jsdocBracketSpacing ? "{ }" : "{}"; 84 | 85 | const isAnObject = (value: string): boolean => 86 | /^{.*[A-z0-9_]+ ?:.*}$/.test(value); 87 | const fixObjectCommas = (objWithBrokenCommas: string): string => 88 | objWithBrokenCommas.replace(/; ([A-z0-9_])/g, ", $1"); 89 | 90 | if (isAnObject(type)) { 91 | return fixObjectCommas(type); 92 | } 93 | 94 | return type; 95 | }; 96 | const updatedType = getUpdatedType(); 97 | tagString += gap + updatedType + " ".repeat(tagTypeGapAdj); 98 | } 99 | if (name) tagString += `${gap}${name}${" ".repeat(tagNameGapAdj)}`; 100 | 101 | // Try to use prettier on @example tag description 102 | if (tag === EXAMPLE && !tsdoc) { 103 | const exampleCaption = description.match(/([\s\S]*?)<\/caption>/i); 104 | 105 | if (exampleCaption) { 106 | description = description.replace(exampleCaption[0], ""); 107 | tagString = `${tagString} ${exampleCaption[0]}`; 108 | } 109 | 110 | const beginningSpace = useTabs ? "\t" : " ".repeat(tabWidth); 111 | const formattedExample = await formatCode( 112 | description, 113 | beginningSpace, 114 | options, 115 | ); 116 | 117 | tagString += formattedExample 118 | .replace( 119 | new RegExp( 120 | `^\\n${beginningSpace 121 | .replace(/[\t]/g, "[\\t]") 122 | .replace(/[^S\r\n]/g, "[^S\\r\\n]")}\\n`, 123 | ), 124 | "", 125 | ) 126 | .trimEnd(); 127 | } // Add description (complicated because of text wrap) 128 | else if (description) { 129 | let descriptionString = ""; 130 | if (useTagTitle) tagString += gap + " ".repeat(descGapAdj); 131 | if ( 132 | TAGS_PEV_FORMATE_DESCRIPTION.includes(tag) || 133 | TAGS_ORDER[tag as keyof typeof TAGS_ORDER] === undefined 134 | ) { 135 | // Avoid wrapping 136 | descriptionString = description; 137 | } else { 138 | const [, firstWord] = /^\s*(\S+)/.exec(description) || ["", ""]; 139 | 140 | // Wrap tag description 141 | const beginningSpace = 142 | tag === DESCRIPTION || 143 | ([EXAMPLE, REMARKS, PRIVATE_REMARKS].includes(tag) && tsdoc) 144 | ? "" 145 | : " "; // google style guide space 146 | 147 | if ( 148 | (tag !== DESCRIPTION && 149 | tagString.length + firstWord.length > printWidth) || 150 | // tsdoc tags 151 | [REMARKS, PRIVATE_REMARKS].includes(tag) 152 | ) { 153 | // the tag is already longer than we are allowed to, so let's start at a new line 154 | descriptionString = 155 | `\n${beginningSpace}` + 156 | (await formatDescription(tag, description, options, { 157 | beginningSpace, 158 | })); 159 | } else { 160 | // append the description to the tag 161 | descriptionString = await formatDescription(tag, description, options, { 162 | // 1 is `\n` which added to tagString 163 | tagStringLength: tagString.length - 1, 164 | beginningSpace, 165 | }); 166 | } 167 | } 168 | 169 | if (jsdocSeparateTagGroups) { 170 | descriptionString = descriptionString.trimEnd(); 171 | } 172 | 173 | tagString += descriptionString.startsWith("\n") 174 | ? descriptionString.replace(/^\n[\s]+\n/g, "\n") 175 | : descriptionString.trimStart(); 176 | } 177 | 178 | // Add empty line after some tags if there is something below 179 | tagString += descriptionEndLine({ 180 | tag, 181 | isEndTag: tagIndex === finalTagsArray.length - 1, 182 | }); 183 | 184 | return tagString; 185 | }; 186 | 187 | export { stringify }; 188 | -------------------------------------------------------------------------------- /tests/exampleTag.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | parser: "babel", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("Example javascript code", async () => { 14 | const result = await subject(` 15 | /** 16 | * @examples 17 | * var one = 5 18 | * var two = 10 19 | 20 | const resolveDescription = formatDescription(tag, description, tagString, a); 21 | * 22 | * if(one > 2) { two += one 23 | 24 | } 25 | 26 | * @undefiendTag 27 | * @undefiendTag {number} name des 28 | */ 29 | const testFunction = (text, defaultValue, optionalNumber) => true 30 | `); 31 | 32 | expect(result).toMatchSnapshot(); 33 | expect(await subject(result)).toMatchSnapshot(); 34 | }); 35 | 36 | test("empty example", async () => { 37 | const Result2 = await subject(`/** 38 | * single line description 39 | * @example 40 | */`); 41 | 42 | const Result3 = await subject(`/** 43 | * single line description 44 | * @return {Boolean} Always true 45 | * @example 46 | */`); 47 | 48 | expect(Result2).toMatchSnapshot(); 49 | expect(Result3).toMatchSnapshot(); 50 | }); 51 | 52 | test("examples Json", async () => { 53 | const options = { 54 | jsdocKeepUnParseAbleExampleIndent: true, 55 | }; 56 | const Result1 = await subject( 57 | `/** 58 | * @example 59 | * {testArr: [ 60 | * 1, 61 | * 2, 62 | * ] 63 | * } 64 | */`, 65 | options, 66 | ); 67 | 68 | const Result2 = await subject( 69 | `/** 70 | * @example 71 | * 72 | * [{ 73 | * foo: 1, 74 | * foo: 2, 75 | * foo: 9, 76 | * }, { 77 | * bar: 1, 78 | * bar: 5 79 | * }] 80 | */`, 81 | options, 82 | ); 83 | expect(Result1).toMatchSnapshot(); 84 | expect(Result2).toMatchSnapshot(); 85 | }); 86 | 87 | test("Example start by xml tag", async () => { 88 | const result = await subject(` 89 | /** 90 | * @example TradingViewChart; 91 | * 92 | * export default Something 93 | */ 94 | `); 95 | 96 | expect(result).toMatchSnapshot(); 97 | 98 | const result1 = await subject(` 99 | /** 100 | * @example TradingViewChart 101 | * 102 | * function Something(){ 103 | * return TradingViewChart 104 | * } 105 | * export default Something 106 | */ 107 | `); 108 | 109 | expect(result1).toMatchSnapshot(); 110 | }); 111 | 112 | test("example json ", async () => { 113 | const result = await subject(` 114 | /** 115 | * @example { 116 | * '0%': '#afc163', 117 | * '25%': '#66FF00', 118 | * '50%': '#00CC00', // ====> linear-gradient(to right, #afc163 0%, #66FF00 25%, 119 | * '75%': '#009900', // #00CC00 50%, #009900 75%, #ffffff 100%) 120 | * '100%': '#ffffff' 121 | * } 122 | * @description 123 | * Then this man came to realize the truth: 124 | * Besides six pence, there is the moon. 125 | * Besides bread and butter, there is the bug. 126 | * And... 127 | * Besides women, there is the code. 128 | */ 129 | `); 130 | 131 | expect(result).toMatchSnapshot(); 132 | }); 133 | 134 | test("example should be same after few time format ", async () => { 135 | const result = await subject(` 136 | /** 137 | * @example with selector 138 | * const $ = ccashio.test(\` 139 | *
140 | *

Hello

141 | *

World

142 | *
143 | * \`); 144 | */ 145 | `); 146 | 147 | const result2 = await subject(result); 148 | const result3 = await subject(result2); 149 | 150 | expect(result).toEqual(result2); 151 | expect(result).toEqual(result3); 152 | }); 153 | 154 | test("example unParseAble", async () => { 155 | const result = await subject( 156 | `/** 157 | * @example 158 | * Prism.languages['css-with-colors'] = Prism.languages.extend('css', { 159 | * // Prism.languages.css already has a 'comment' token, so this token will overwrite CSS' 'comment' token 160 | * // at its original position 161 | * 'comment': { ... }, 162 | * // CSS doesn't have a 'color' token, so this token will be appended 163 | * 'color': /\b(?:red|green|blue)\b/ 164 | * }); 165 | */`, 166 | { 167 | jsdocKeepUnParseAbleExampleIndent: true, 168 | }, 169 | ); 170 | 171 | expect(result).toMatchSnapshot(); 172 | const result2 = await subject(result, { 173 | jsdocKeepUnParseAbleExampleIndent: true, 174 | }); 175 | 176 | expect(result2).toEqual(result); 177 | expect(await subject(result)).not.toEqual(result); 178 | }); 179 | 180 | test("example with @this tag preserves indentation", async () => { 181 | const code = `/** 182 | * @example 183 | * import { createServer } from "node:http"; 184 | * import extendedResponse from "extended-response"; 185 | * 186 | * async function* messages() { 187 | * for (let i = 1; i <= 5; i++) { 188 | * yield \`Message \${i}\\r\\n\`; 189 | * await new Promise((resolve) => setTimeout(resolve, 1000)); 190 | * } 191 | * } 192 | * 193 | * const server = createServer(async (req, res) => { 194 | * if ( 195 | * req.method === "GET" && 196 | * new URL(req.url || "").pathname === "/stream5" 197 | * ) { 198 | * await extendedResponse.call(res, { 199 | * messages, 200 | * }); 201 | * } 202 | * }); 203 | * 204 | * server.listen(port, () => { 205 | * console.log(\`Server running on http://localhost:\${port}\`); 206 | * }); 207 | * 208 | * @param {object} params Extended Response Parameters 209 | * @param {AsyncIterable} params.messages Async messages to be sent 210 | * response stream will be closed 211 | * @returns {Promise} 212 | * 213 | * @this {ServerResponse} 214 | * A Node.js HTTP 215 | * {@link https://nodejs.org/api/http.html#class-httpserverresponse|ServerResponse} 216 | * instance. 217 | */`; 218 | 219 | const result = await subject(code, { 220 | jsdocTagsOrder: '{"this":39.9}' as any, 221 | jsdocKeepUnParseAbleExampleIndent: true, 222 | }); 223 | 224 | expect(result).toMatchSnapshot(); 225 | 226 | // Format again to ensure it's stable 227 | const result2 = await subject(result, { 228 | jsdocTagsOrder: '{"this":39.9}' as any, 229 | jsdocKeepUnParseAbleExampleIndent: true, 230 | }); 231 | 232 | // The example should preserve its indentation 233 | expect(result2).toMatchSnapshot(); 234 | expect(result2).toContain(" import { createServer }"); 235 | expect(result2).toContain(" for (let i = 1; i <= 5; i++)"); 236 | }); 237 | -------------------------------------------------------------------------------- /tests/__snapshots__/typeScript.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Default export 1`] = ` 4 | "/** @typedef {import("Foo")} Foo */ 5 | " 6 | `; 7 | 8 | exports[`JS code should be formatted as usuall 1`] = ` 9 | "/** 10 | * @typedef {{ 11 | * userId: string; 12 | * title: string; 13 | * profileImageLink: string; 14 | * identityStatus: "None"; 15 | * isBusinessUser: boolean; 16 | * isResellerUser: boolean; 17 | * isSubUser: boolean; 18 | * shareCode: number; 19 | * referredBy: string; 20 | * businessName: string; 21 | * businessUserId: string; 22 | * nationalCode: string; 23 | * state: string; 24 | * city: string; 25 | * address: string; 26 | * phoneNumber: string; 27 | * }} User 28 | */ 29 | export let User; 30 | 31 | /** 32 | * @typedef {{ 33 | * domainId: 0; 34 | * persianName: string; 35 | * englishName: string; // comment 36 | * resellerUserId: string; 37 | * isActive: true; 38 | * logoFileUniqueId: string; 39 | * logoFileName: string; 40 | * logoFileUrl: string; 41 | * domainPersianName: string; 42 | * domainEnglishName: string; 43 | * resellerUserDisplayName: string; 44 | * about: string; 45 | * }} SubDomain 46 | */ 47 | 48 | /** @typedef {() => a.b} SubDomain */ 49 | " 50 | `; 51 | 52 | exports[`Long type Union types 1`] = ` 53 | "/** 54 | * Gets a configuration object assembled from environment variables and .env 55 | * configuration files. 56 | * 57 | * @memberof Config 58 | * @function getEnvConfig 59 | * @returns {Config.SomeConfiguration 60 | * | Config.SomeOtherConfiguration 61 | * | Config.AnotherConfiguration 62 | * | Config.YetAnotherConfiguration} 63 | * The environment configuration 64 | */ 65 | export default () => configurator.config; 66 | " 67 | `; 68 | 69 | exports[`Union types 1`] = ` 70 | "/** 71 | * @typedef {{ foo: string } 72 | * | { bar: string; manyMoreLongArguments: object } 73 | * | { baz: string }} Foo 74 | */ 75 | " 76 | `; 77 | 78 | exports[`description in interface 1`] = ` 79 | "export interface FetchCallbackResponseArray { 80 | resource: Resource; 81 | /** 82 | * @deprecated Resolve clear with condition in your fetch api this function 83 | * will be remove 84 | */ 85 | refetch: (...arg: V[]) => void; 86 | /** 87 | * @deprecated Resolve clear with condition in your fetch api this function 88 | * will be remove 89 | */ 90 | clear: () => void; 91 | } 92 | " 93 | `; 94 | 95 | exports[`hoisted object 1`] = ` 96 | "/** 97 | * @typedef {{ 98 | * userId: { 99 | * title: string; 100 | * profileImageLink: any; 101 | * identityStatus: "None"; 102 | * isBusinessUser: "isResellerUser" | "isBoolean" | "isSubUser" | "isNot"; 103 | * shareCode: number; 104 | * referredBy: any; 105 | * }; 106 | * id: number; 107 | * }} User 108 | */ 109 | " 110 | `; 111 | 112 | exports[`max width challenge 1`] = ` 113 | "class test { 114 | /** 115 | * Replaces text in a string, using a regular expression or search string. 116 | * 117 | * @param {string | RegExp} searchValue A string to search for. 118 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 119 | * A string containing the text to replace for every successful match of 120 | * searchValue in this string. 121 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 122 | * A string containing the text to replace for every successful match of 123 | * searchValue in this string. 124 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 125 | * A_big_string_for_test string containing the text to replace for every 126 | * successful match of searchValue in this string. 127 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 128 | * A_big_string_for_test string containing the text to replace for every 129 | * successful match of searchValue in this string. 130 | * @returns {StarkStringType & NativeString} 131 | */ 132 | replace(searchValue, replaceValue) { 133 | class test { 134 | /** 135 | * Replaces text in a string, using a regular expression or search string. 136 | * 137 | * @param {string | RegExp} searchValue A string to search for. 138 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 139 | * A string containing the text to replace for every successful match of 140 | * searchValue in this string. 141 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 142 | * A string containing the text to replace for every successful match of 143 | * searchValue in this string. 144 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 145 | * A_big_string_for_test string containing the text to replace for every 146 | * successful match of searchValue in this string. 147 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 148 | * A_big_string_for_test string containing the text to replace for every 149 | * successful match of searchValue in this string. 150 | * @returns {StarkStringType & NativeString} 151 | */ 152 | testFunction() {} 153 | } 154 | 155 | this._value = this._value.replace(searchValue, replaceValue); 156 | return this; 157 | } 158 | } 159 | " 160 | `; 161 | 162 | exports[`type imports 1`] = ` 163 | "/** 164 | * @import {A} from "modulea" 165 | * @import BMain, { 166 | * B as B1, 167 | * B2, 168 | * B3, 169 | * B4 170 | * } from "moduleb" 171 | * @typedef {Object} Foo 172 | */ 173 | /** 174 | * @import C from "modulec" 175 | * @import BDefault, {B5} from "./moduleb" 176 | */ 177 | " 178 | `; 179 | 180 | exports[`type imports with import merging turned off 1`] = ` 181 | "/** 182 | * @import {A} from "modulea" 183 | * @import BM, { 184 | * B as B1, 185 | * B2, 186 | * B4 187 | * } from "moduleb" 188 | * @import BMain, {B3} from "moduleb" 189 | * @typedef {Object} Foo 190 | */ 191 | /** 192 | * @import C from "modulec" 193 | * @import BDefault, {B5} from "./moduleb" 194 | */ 195 | " 196 | `; 197 | 198 | exports[`type imports with named import line splitting turned off 1`] = ` 199 | "/** 200 | * @import {A} from "modulea" 201 | * @import BMain, {B as B1, B2, B3, B4} from "moduleb" 202 | * @typedef {Object} Foo 203 | */ 204 | /** 205 | * @import C from "modulec" 206 | * @import BDefault, {B5} from "./moduleb" 207 | */ 208 | " 209 | `; 210 | 211 | exports[`type imports with named import padding 1`] = ` 212 | "/** 213 | * @import { A } from "modulea" 214 | * @import BMain, { 215 | * B as B1, 216 | * B2, 217 | * B3, 218 | * B4 219 | * } from "moduleb" 220 | * @typedef {Object} Foo 221 | */ 222 | /** 223 | * @import C from "modulec" 224 | * @import BDefault, { B5 } from "./moduleb" 225 | */ 226 | " 227 | `; 228 | 229 | exports[`type imports with no import formatting 1`] = ` 230 | "/** 231 | * @import BM, { B as B1, 232 | * B2 , B4 } from 'moduleb' 233 | * @import BMain, {B3 } from "moduleb" 234 | * @import {A} from 'modulea' 235 | * @typedef {Object} Foo 236 | */ 237 | /** 238 | * @import BDefault, { B5 } from './moduleb' 239 | * @import C from "modulec" 240 | */ 241 | " 242 | `; 243 | -------------------------------------------------------------------------------- /tests/compatibleWithPlugins.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | import { execSync } from "child_process"; 4 | import { mkdirSync, writeFileSync, readFileSync, rmSync } from "fs"; 5 | import { join } from "path"; 6 | 7 | function subject(code: string, options: Partial = {}) { 8 | return prettier.format(code, { 9 | parser: "typescript", 10 | plugins: ["./prettier-plugin-fake/index.js", "prettier-plugin-jsdoc"], 11 | jsdocSpaces: 1, 12 | ...options, 13 | } as AllOptions); 14 | } 15 | 16 | function subjectTailwindcss(code: string, options: Partial = {}) { 17 | return prettier.format(code, { 18 | parser: "typescript", 19 | plugins: ["prettier-plugin-tailwindcss", "prettier-plugin-jsdoc"], 20 | jsdocSpaces: 1, 21 | ...options, 22 | } as AllOptions); 23 | } 24 | 25 | test("Should format regular jsDoc", async () => { 26 | const code = ` 27 | import b from "b" 28 | import {k} from "k" 29 | import a from "a" 30 | 31 | /** 32 | * function example description that was wrapped by hand 33 | * so it have more then one line and don't end with a dot 34 | * REPEATED TWO TIMES BECAUSE IT WAS EASIER to copy 35 | * function example description that was wrapped by hand 36 | * so it have more then one line. 37 | * @return {Boolean} Description for @returns with s 38 | * @param {String|Number} text - some text description that is very long and needs to be wrapped 39 | * @param {String} [defaultValue="defaultTest"] TODO 40 | * @arg {Number|Null} [optionalNumber] 41 | * @private 42 | *@memberof test 43 | @async 44 | * @examples 45 | * var one = 5 46 | * var two = 10 47 | * 48 | * if(one > 2) { two += one } 49 | * @undefiendTag${" "} 50 | * @undefiendTag {number} name des 51 | */ 52 | const testFunction = (text, defaultValue, optionalNumber) => true 53 | `; 54 | const result = await subject(code); 55 | 56 | expect(result).toMatchSnapshot(); 57 | expect(await subject(result)).toMatchSnapshot(); 58 | }); 59 | 60 | test("Should format jsDoc default values", async () => { 61 | const result = await subject(` 62 | /** 63 | * @param {String} [arg1="defaultTest"] foo 64 | * @param {number} [arg2=123] the width of the rectangle 65 | * @param {number} [arg3= 123 ] 66 | * @param {number} [arg4= Foo.bar.baz ] 67 | * @param {number|string} [arg5=123] Something. Default is \`"wrong"\` 68 | */ 69 | `); 70 | 71 | expect(result).toMatchSnapshot(); 72 | expect(await subject(result)).toMatchSnapshot(); 73 | }); 74 | 75 | test("Should convert to single line if necessary", async () => { 76 | const Result1 = await subject(`/** single line description*/`); 77 | const Result2 = await subject(`/** 78 | * single line description 79 | * @example 80 | */`); 81 | 82 | const Result3 = await subject(`/** 83 | * single line description 84 | * @return {Boolean} Always true 85 | * @example 86 | */`); 87 | 88 | expect(Result1).toMatchSnapshot(); 89 | expect(Result2).toMatchSnapshot(); 90 | expect(Result3).toMatchSnapshot(); 91 | }); 92 | 93 | test("Should compatible with tailwindcss", async () => { 94 | const code = ` 95 | /** 96 | * @param {String} [arg1="defaultTest"] foo 97 | * @param {number} [arg2=123] the width of the rectangle 98 | * @param {number} [arg3= 123 ] 99 | * @param {number} [arg4= Foo.bar.baz ] 100 | * @param {number|string} [arg5=123] Something. Default is \`"wrong"\` 101 | */ 102 | `; 103 | const result = await subjectTailwindcss(code); 104 | 105 | expect(result).toMatchSnapshot(); 106 | }); 107 | 108 | describe("CLI Compatibility", () => { 109 | // CLI-based tests 110 | const testDir = join(process.cwd(), ".test-cli-temp"); 111 | 112 | beforeAll(() => { 113 | try { 114 | mkdirSync(testDir, { recursive: true }); 115 | } catch (err) { 116 | // Directory might already exist 117 | } 118 | }); 119 | 120 | afterAll(() => { 121 | try { 122 | rmSync(testDir, { recursive: true, force: true }); 123 | } catch (err) { 124 | // Ignore cleanup errors 125 | } 126 | }); 127 | 128 | function runCommand(command: string): string { 129 | return execSync(`cd ${testDir} && ${command}`, { 130 | timeout: 10000, 131 | encoding: "utf8", 132 | cwd: process.cwd(), 133 | }); 134 | } 135 | 136 | test("Should format with tailwindcss plugin via CLI without infinite recursion", () => { 137 | const testFile = join(testDir, "test-cli-tailwind.js"); 138 | const configFile = join(testDir, ".prettierrc.json"); 139 | 140 | const code = `/** 141 | * @param {String|Number} text - some text description 142 | 143 | 144 | 145 | * @param {String} [defaultValue="defaultTest"] TODO 146 | * @returns {Boolean} Description for returns 147 | */ 148 | const testFunction = (text, defaultValue) => true; 149 | `; 150 | 151 | const prettierConfig = { 152 | semi: false, 153 | tabWidth: 2, 154 | printWidth: 100, 155 | singleQuote: true, 156 | plugins: ["prettier-plugin-tailwindcss", "prettier-plugin-jsdoc"], 157 | }; 158 | 159 | writeFileSync(testFile, code); 160 | writeFileSync(configFile, JSON.stringify(prettierConfig, null, 2)); 161 | 162 | const output = runCommand( 163 | `npx prettier --config ".prettierrc.json" "test-cli-tailwind.js"`, 164 | ); 165 | 166 | expect(output).toBeDefined(); 167 | expect(output).toMatchSnapshot(); 168 | }); 169 | 170 | test("Should format via CLI with --write flag", () => { 171 | const testFile = join(testDir, "test-cli-write.js"); 172 | const configFile = join(testDir, ".prettierrc.json"); 173 | 174 | const code = `/** 175 | * @param {number} [arg1=123] the width 176 | 177 | 178 | 179 | * @returns {void} 180 | */ 181 | function myFunc(arg1) {} 182 | `; 183 | 184 | const prettierConfig = { 185 | semi: false, 186 | plugins: ["prettier-plugin-tailwindcss", "prettier-plugin-jsdoc"], 187 | }; 188 | 189 | writeFileSync(testFile, code); 190 | writeFileSync(configFile, JSON.stringify(prettierConfig, null, 2)); 191 | 192 | runCommand( 193 | `npx prettier --config ".prettierrc.json" --write "test-cli-write.js"`, 194 | ); 195 | 196 | const formatted = readFileSync(testFile, "utf8"); 197 | expect(formatted).toMatchSnapshot(); 198 | }); 199 | 200 | test("Should work with plugins in different orders via CLI", () => { 201 | const testFile = join(testDir, "test-cli-order.ts"); 202 | const configFile = join(testDir, ".prettierrc.json"); 203 | 204 | const code = `/** 205 | * @param {string} name 206 | 207 | 208 | 209 | 210 | * @returns {Promise} 211 | */ 212 | async function example(name: string): Promise {} 213 | `; 214 | 215 | const configs = [ 216 | ["prettier-plugin-jsdoc", "prettier-plugin-tailwindcss"], 217 | ["prettier-plugin-tailwindcss", "prettier-plugin-jsdoc"], 218 | ]; 219 | 220 | configs.forEach((plugins) => { 221 | const prettierConfig = { 222 | parser: "typescript", 223 | plugins, 224 | }; 225 | 226 | writeFileSync(testFile, code); 227 | writeFileSync(configFile, JSON.stringify(prettierConfig, null, 2)); 228 | 229 | const output = runCommand( 230 | `npx prettier --config ".prettierrc.json" "test-cli-order.ts"`, 231 | ); 232 | 233 | expect(output).toBeDefined(); 234 | expect(output).toMatchSnapshot(); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/typeScript.js.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: typeScript.js 1`] = ` 4 | "/** 5 | * @typedef {{ 6 | * userId: string; 7 | * title: string; 8 | * profileImageLink: string; 9 | * identityStatus: \\"None\\"; 10 | * isBusinessUser: boolean; 11 | * isResellerUser: boolean; 12 | * isSubUser: boolean; 13 | * shareCode: number; 14 | * referredBy: string; 15 | * businessName: string; 16 | * businessUserId: string; 17 | * nationalCode: string; 18 | * state: string; 19 | * city: string; 20 | * address: string; 21 | * phoneNumber: string; 22 | * }} User 23 | */ 24 | export let User; 25 | 26 | /** 27 | * @typedef {{ 28 | * domainId: 0; 29 | * persianName: string; 30 | * englishName: string; // comment 31 | * resellerUserId: string; 32 | * isActive: true; 33 | * logoFileUniqueId: string; 34 | * logoFileName: string; 35 | * logoFileUrl: string; 36 | * domainPersianName: string; 37 | * domainEnglishName: string; 38 | * resellerUserDisplayName: string; 39 | * about: string; 40 | * }} SubDomain 41 | */ 42 | 43 | /** @typedef {() => a.b} SubDomain */ 44 | /** 45 | * @typedef {{ 46 | * userId: { 47 | * title: string; 48 | * profileImageLink: any; 49 | * identityStatus: \\"None\\"; 50 | * isBusinessUser: \\"isResellerUser\\" | \\"isBoolean\\" | \\"isSubUser\\" | \\"isNot\\"; 51 | * shareCode: number; 52 | * referredBy: any; 53 | * }; 54 | * id: number; 55 | * }} User 56 | */ 57 | 58 | class test { 59 | /** 60 | * Replaces text in a string, using a regular expression or search string. 61 | * 62 | * @param {string | RegExp} searchValue A string to search for. 63 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 64 | * A string containing the text to replace for every successful match of 65 | * searchValue in this string. 66 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 67 | * A string containing the text to replace for every successful match of 68 | * searchValue in this string. 69 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 70 | * A_big_string_for_test string containing the text to replace for every 71 | * successful match of searchValue in this string. 72 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 73 | * A_big_string_for_test string containing the text to replace for every 74 | * successful match of searchValue in this string. 75 | * @returns {StarkStringType & NativeString} 76 | */ 77 | replace(searchValue, replaceValue) { 78 | class test { 79 | /** 80 | * Replaces text in a string, using a regular expression or search string. 81 | * 82 | * @param {string | RegExp} searchValue A string to search for. 83 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 84 | * A string containing the text to replace for every successful match of 85 | * searchValue in this string. 86 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 87 | * A string containing the text to replace for every successful match of 88 | * searchValue in this string. 89 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 90 | * A_big_string_for_test string containing the text to replace for every 91 | * successful match of searchValue in this string. 92 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 93 | * A_big_string_for_test string containing the text to replace for every 94 | * successful match of searchValue in this string. 95 | * @returns {StarkStringType & NativeString} 96 | */ 97 | testFunction() {} 98 | } 99 | 100 | this._value = this._value.replace(searchValue, replaceValue); 101 | return this; 102 | } 103 | } 104 | /** @typedef {import(\\"Foo\\")} Foo */ 105 | " 106 | `; 107 | 108 | exports[`File: typeScript.js 2`] = ` 109 | "/** 110 | * @typedef {{ 111 | * userId: string; 112 | * title: string; 113 | * profileImageLink: string; 114 | * identityStatus: \\"None\\"; 115 | * isBusinessUser: boolean; 116 | * isResellerUser: boolean; 117 | * isSubUser: boolean; 118 | * shareCode: number; 119 | * referredBy: string; 120 | * businessName: string; 121 | * businessUserId: string; 122 | * nationalCode: string; 123 | * state: string; 124 | * city: string; 125 | * address: string; 126 | * phoneNumber: string; 127 | * }} User 128 | */ 129 | export let User; 130 | 131 | /** 132 | * @typedef {{ 133 | * domainId: 0; 134 | * persianName: string; 135 | * englishName: string; // comment 136 | * resellerUserId: string; 137 | * isActive: true; 138 | * logoFileUniqueId: string; 139 | * logoFileName: string; 140 | * logoFileUrl: string; 141 | * domainPersianName: string; 142 | * domainEnglishName: string; 143 | * resellerUserDisplayName: string; 144 | * about: string; 145 | * }} SubDomain 146 | */ 147 | 148 | /** @typedef {() => a.b} SubDomain */ 149 | /** 150 | * @typedef {{ 151 | * userId: { 152 | * title: string; 153 | * profileImageLink: any; 154 | * identityStatus: \\"None\\"; 155 | * isBusinessUser: \\"isResellerUser\\" | \\"isBoolean\\" | \\"isSubUser\\" | \\"isNot\\"; 156 | * shareCode: number; 157 | * referredBy: any; 158 | * }; 159 | * id: number; 160 | * }} User 161 | */ 162 | 163 | class test { 164 | /** 165 | * Replaces text in a string, using a regular expression or search string. 166 | * 167 | * @param {string | RegExp} searchValue A string to search for. 168 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 169 | * A string containing the text to replace for every successful match of 170 | * searchValue in this string. 171 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 172 | * A string containing the text to replace for every successful match of 173 | * searchValue in this string. 174 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 175 | * A_big_string_for_test string containing the text to replace for every 176 | * successful match of searchValue in this string. 177 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 178 | * A_big_string_for_test string containing the text to replace for every 179 | * successful match of searchValue in this string. 180 | * @returns {StarkStringType & NativeString} 181 | */ 182 | replace(searchValue, replaceValue) { 183 | class test { 184 | /** 185 | * Replaces text in a string, using a regular expression or search string. 186 | * 187 | * @param {string | RegExp} searchValue A string to search for. 188 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 189 | * A string containing the text to replace for every successful match of 190 | * searchValue in this string. 191 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 192 | * A string containing the text to replace for every successful match of 193 | * searchValue in this string. 194 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 195 | * A_big_string_for_test string containing the text to replace for every 196 | * successful match of searchValue in this string. 197 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 198 | * A_big_string_for_test string containing the text to replace for every 199 | * successful match of searchValue in this string. 200 | * @returns {StarkStringType & NativeString} 201 | */ 202 | testFunction() {} 203 | } 204 | 205 | this._value = this._value.replace(searchValue, replaceValue); 206 | return this; 207 | } 208 | } 209 | /** @typedef {import(\\"Foo\\")} Foo */ 210 | " 211 | `; 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/prettier-plugin-jsdoc.png)](https://nodei.co/npm/prettier-plugin-jsdoc/) 2 | 3 | [![Installation size](https://packagephobia.now.sh/badge?p=prettier-plugin-jsdoc)](https://packagephobia.now.sh/result?p=prettier-plugin-jsdoc) 4 | 5 | # prettier-plugin-jsdoc 6 | 7 | Prettier plugin for formatting comment blocks and converting to a standard. 8 | Match with Visual Studio Code and other IDEs that support JSDoc and comments as Markdown. 9 | 10 | Many good examples of how this plugin works are in the [`/tests`](/tests) directory. 11 | Compare tests and their [snapshots](/tests//__snapshots__). 12 | 13 | Configured with best practices of JSDoc style guides. 14 | 15 | ## Contents 16 | 17 | - [Installation](#installation) 18 | - [Configuration](#configuration) 19 | - [Ignore](#ignore) 20 | - [Examples](#examples) 21 | - [Options](#options) 22 | - [Supported Prettier versions](#supported-prettier-versions) 23 | - [Contributing](#contributing) 24 | - [Links](#links) 25 | - [Acknowledge](#acknowledge) 26 | 27 | ## Installation 28 | 29 | 1. [Install](https://prettier.io/docs/en/install.html) and [configure](https://prettier.io/docs/en/configuration) Prettier 30 | 2. Install `prettier-plugin-jsdoc`: 31 | 32 | ```sh 33 | npm install prettier-plugin-jsdoc --save-dev 34 | ``` 35 | 36 | ```sh 37 | yarn add prettier-plugin-jsdoc --dev 38 | ``` 39 | 40 | ## Configuration 41 | 42 | Add `prettier-plugin-jsdoc` to your `plugins` list. 43 | 44 | **Important:** When using multiple plugins, add `prettier-plugin-jsdoc` to the **end** of the plugins list. 45 | 46 | `.prettierrc`: 47 | 48 | ```json 49 | { 50 | "plugins": ["prettier-plugin-jsdoc"] 51 | } 52 | ``` 53 | 54 | With other plugins: 55 | 56 | ```json 57 | { 58 | "plugins": [..., "prettier-plugin-jsdoc"] 59 | } 60 | ``` 61 | 62 | `prettier.config.js`: 63 | 64 | ```js 65 | export default { 66 | plugins: ["prettier-plugin-jsdoc"], 67 | }; 68 | ``` 69 | 70 | If you want to ignore some types of files, use `overrides` with an empty `plugins`: 71 | 72 | ```json 73 | { 74 | "plugins": ["prettier-plugin-jsdoc"], 75 | "overrides": [ 76 | { 77 | "files": "*.tsx", 78 | "options": { 79 | "plugins": [] 80 | } 81 | } 82 | ] 83 | } 84 | ``` 85 | 86 | ## Ignore 87 | 88 | To prevent Prettier from formatting, use `/* */` or `//` instead of /\*\* \*/, or see [Ignoring Code](https://prettier.io/docs/en/ignore#javascript). 89 | 90 | ## Examples 91 | 92 | #### Single line 93 | 94 | ```js 95 | /** 96 | * @param { string } param0 description 97 | */ 98 | function fun(param0) {} 99 | ``` 100 | 101 | Formats to: 102 | 103 | ```js 104 | /** @param {string} param0 Description */ 105 | function fun(param0) {} 106 | ``` 107 | 108 | #### React component 109 | 110 | ```js 111 | /** 112 | * @type {React.FC<{ message:string} >} 113 | */ 114 | const Component = memo(({ message }) => { 115 | return

{message}

; 116 | }); 117 | ``` 118 | 119 | Formats to: 120 | 121 | ```js 122 | /** @type {React.FC<{message: string}>} */ 123 | const Component = memo(({ message }) => { 124 | return

{message}

; 125 | }); 126 | ``` 127 | 128 | #### TypeScript objects 129 | 130 | ```js 131 | /** 132 | @typedef { 133 | { 134 | "userId": { 135 | "profileImageLink": *, 136 | "isBusinessUser": "isResellerUser"|"isBoolean"| "isSubUser" | "isNot", 137 | "shareCode": number, 138 | "referredBy": any, 139 | }, 140 | id:number 141 | } 142 | } User 143 | */ 144 | ``` 145 | 146 | Format to: 147 | 148 | ```js 149 | /** 150 | * @typedef {{ 151 | * userId: { 152 | * profileImageLink: any; 153 | * isBusinessUser: "isResellerUser" | "isBoolean" | "isSubUser" | "isNot"; 154 | * shareCode: number; 155 | * referredBy: any; 156 | * }; 157 | * id: number; 158 | * }} User 159 | */ 160 | ``` 161 | 162 | #### Example 163 | 164 | Add code to `@examples` tag. 165 | 166 | ```js 167 | /** 168 | * @examples 169 | * var one= 5 170 | * var two=10 171 | * 172 | * if(one > 2) { two += one } 173 | */ 174 | ``` 175 | 176 | Formats to: 177 | 178 | ```js 179 | /** 180 | * @example 181 | * var one = 5; 182 | * var two = 10; 183 | * 184 | * if (one > 2) { 185 | * two += one; 186 | * } 187 | */ 188 | ``` 189 | 190 | #### Description 191 | 192 | `@description` is formatted as Markdown so that you can use any features of Markdown on that. 193 | Like code tags (` ```js `), header tags like `# Header`, or other Markdown features. 194 | 195 | ## Options 196 | 197 | | Key | Type | Default | Description | 198 | | :---------------------------------- | :-------------------------------- | :----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 199 | | `jsdocSpaces` | Number | 1 | 200 | | `jsdocDescriptionWithDot` | Boolean | false | 201 | | `jsdocDescriptionTag` | Boolean | false | 202 | | `jsdocVerticalAlignment` | Boolean | false | 203 | | `jsdocKeepUnParseAbleExampleIndent` | Boolean | false | 204 | | `jsdocCommentLineStrategy` | ("singleLine","multiline","keep") | "singleLine" | 205 | | `jsdocCapitalizeDescription` | Boolean | true | 206 | | `jsdocSeparateReturnsFromParam` | Boolean | false | Adds a space between last `@param` and `@returns` | 207 | | `jsdocSeparateTagGroups` | Boolean | false | Adds a space between tag groups | 208 | | `jsdocPreferCodeFences` | Boolean | false | Always fence code blocks (surround them by triple backticks) | 209 | | `jsdocEmptyCommentStrategy` | ("remove","keep") | "remove" | How to handle empty JSDoc comment blocks | 210 | | `jsdocBracketSpacing` | Boolean | false | Whether to add spaces inside JSDoc type brackets. `{string}` (false) vs `{ string }` (true) | 211 | | `tsdoc` | Boolean | false | See [TSDoc](#tsdoc) | 212 | | `jsdocPrintWidth` | Number | undefined | If you don't set the value to `jsdocPrintWidth`, `printWidth` will be used as `jsdocPrintWidth` | 213 | | `jsdocLineWrappingStyle` | String | "greedy" | "greedy": lines wrap as soon as they reach `printWidth`. "balance": preserve existing line breaks if lines are shorter than `printWidth`, otherwise use greedy wrapping | 214 | | `jsdocTagsOrder` | String (object) | undefined | See [Custom Tags Order](doc/CUSTOM_TAGS_ORDER.md) | 215 | 216 | ### TSDoc 217 | 218 | We hope to support the whole [TSDoc](https://tsdoc.org/). 219 | If we missed something, please [create an issue](https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/new). 220 | 221 | To enable, add: 222 | 223 | ```json 224 | { 225 | "tsdoc": true 226 | } 227 | ``` 228 | 229 | ## Supported Prettier versions 230 | 231 | | Plugin version | Prettier version | 232 | | -------------- | ---------------- | 233 | | 1.0.0+ | 3.0.0+ | 234 | | 0.4.2 | 2.x+ | 235 | 236 | ## Contributing 237 | 238 | 1. Fork and clone the repository 239 | 2. [Install Yarn](https://yarnpkg.com/getting-started/install) 240 | 3. Install project dependencies: 241 | 242 | ```sh 243 | yarn install 244 | ``` 245 | 246 | 4. Make changes and make sure that tests pass: 247 | ```js 248 | yarn run test 249 | ``` 250 | 5. Update or add tests to your changes if needed 251 | 6. Create PR 252 | 253 | ## Links 254 | 255 | - [Prettier](https://prettier.io) 256 | - [JSDoc](https://jsdoc.app) 257 | 258 | ## Acknowledge 259 | 260 | This project extended from the @gum3n worked project on GitLab. 261 | -------------------------------------------------------------------------------- /tests/typeScript.test.ts: -------------------------------------------------------------------------------- 1 | import * as prettier from "prettier"; 2 | import { AllOptions } from "../src/types"; 3 | 4 | function subject(code: string, options: Partial = {}) { 5 | return prettier.format(code, { 6 | parser: "typescript", 7 | plugins: ["prettier-plugin-jsdoc"], 8 | jsdocSpaces: 1, 9 | ...options, 10 | } as AllOptions); 11 | } 12 | 13 | test("JS code should be formatted as usuall", async () => { 14 | const result = await subject(` 15 | /** 16 | @typedef { 17 | { 18 | "userId": string, 19 | "title": string, 20 | "profileImageLink": string, 21 | "identityStatus": "None", 22 | "isBusinessUser": boolean, 23 | "isResellerUser": boolean, 24 | "isSubUser": boolean, 25 | "shareCode": number, 26 | "referredBy": string, 27 | "businessName": string, 28 | "businessUserId": string, 29 | "nationalCode": string, 30 | "state": string, 31 | "city": string, 32 | "address": string, 33 | "phoneNumber": string 34 | } 35 | } User 36 | */ 37 | export let User 38 | 39 | /** 40 | @typedef { 41 | { 42 | "domainId": 0, 43 | persianName: string, 44 | "englishName": string, // comment 45 | "resellerUserId": string, 46 | "isActive": true, 47 | "logoFileUniqueId": string, 48 | "logoFileName": string, 49 | "logoFileUrl": string, 50 | "domainPersianName": string, 51 | "domainEnglishName": string, 52 | "resellerUserDisplayName": string, 53 | "about": string 54 | } 55 | } SubDomain 56 | */ 57 | 58 | /** 59 | @typedef { 60 | () => a.b 61 | } SubDomain 62 | */ 63 | `); 64 | 65 | expect(result).toMatchSnapshot(); 66 | }); 67 | 68 | test("hoisted object", async () => { 69 | const result = await subject(` 70 | /** 71 | @typedef { 72 | { 73 | "userId": { 74 | title: string, 75 | "profileImageLink": *, 76 | "identityStatus": "None", 77 | "isBusinessUser": "isResellerUser"|"isBoolean"| "isSubUser" | "isNot", 78 | "shareCode": number, 79 | "referredBy": any, 80 | }, 81 | id:number 82 | } 83 | } User 84 | */ 85 | 86 | `); 87 | 88 | expect(result).toMatchSnapshot(); 89 | }); 90 | 91 | test("max width challenge", async () => { 92 | const result = await subject( 93 | ` 94 | class test { 95 | /** 96 | * Replaces text in a string, using a regular expression or search string. 97 | * @param {string | RegExp} searchValue A string to search for. 98 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A string containing the text to replace for every successful match of searchValue in this string. 99 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A string containing the text to replace for every successful match of searchValue in this string. 100 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 101 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test A_big_string_for_test string containing the text to replace for every successful match of searchValue in this string. 102 | * @returns {StarkStringType & NativeString} 103 | */ 104 | replace(searchValue, replaceValue) { 105 | class test{ 106 | /** 107 | * Replaces text in a string, using a regular expression or search string. 108 | * 109 | * @param {string | RegExp} searchValue A string to search for. 110 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 111 | * A string containing the text to replace for every successful match of 112 | * searchValue in this string. 113 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 114 | * A string containing the text to replace for every successful match of searchValue 115 | * in this string. 116 | * @param {string | (substring: string, ...args: any[]) => string} replaceValue 117 | * A_big_string_for_test string containing the text to replace for every 118 | * successful match of searchValue in this string. 119 | * @param {string | (substring: string, ...args: any[]) => string} A_big_string_for_test 120 | * A_big_string_for_test string containing the text to replace for every successful 121 | * match of searchValue in this string. 122 | * @returns {StarkStringType & NativeString} 123 | */ 124 | testFunction(){ 125 | 126 | } 127 | } 128 | 129 | this._value = this._value.replace(searchValue, replaceValue); 130 | return this; 131 | } 132 | } 133 | `, 134 | ); 135 | 136 | expect(result).toMatchSnapshot(); 137 | }); 138 | 139 | test("description in interface", async () => { 140 | const result = await subject( 141 | ` 142 | export interface FetchCallbackResponseArray { 143 | resource: Resource; 144 | /** 145 | * @deprecated Resolve clear with condition in your fetch api this function will be remove 146 | */ 147 | refetch: (...arg: V[]) => void; 148 | /** 149 | * @deprecated Resolve clear with condition in your fetch api this function will be remove 150 | */ 151 | clear: () => void; 152 | } 153 | `, 154 | ); 155 | 156 | expect(await subject(await subject(result))).toMatchSnapshot(); 157 | }); 158 | 159 | test("Default export", async () => { 160 | const result = await subject(` 161 | /** 162 | * @typedef {import("Foo")} Foo 163 | */ 164 | `); 165 | 166 | expect(result).toMatchSnapshot(); 167 | }); 168 | 169 | test("Union types", async () => { 170 | const result = await subject(` 171 | /** 172 | * @typedef {{ foo: string } | { bar: string; manyMoreLongArguments: object } | { baz: string }} Foo 173 | */ 174 | `); 175 | 176 | expect(result).toMatchSnapshot(); 177 | }); 178 | 179 | test("Long type Union types", async () => { 180 | const result = await subject(` 181 | /** 182 | * Gets a configuration object assembled from environment variables and .env configuration files. 183 | * 184 | * @memberof Config 185 | * @function getEnvConfig 186 | * @returns {Config.SomeConfiguration | Config.SomeOtherConfiguration | Config.AnotherConfiguration | Config.YetAnotherConfiguration } The environment configuration 187 | */ 188 | export default () => configurator.config; 189 | `); 190 | 191 | expect(result).toMatchSnapshot(); 192 | }); 193 | 194 | test("type imports", async () => { 195 | const result = await subject( 196 | ` 197 | /** 198 | * @import BM, { B as B1, 199 | * B2 , B4 } from 'moduleb' 200 | * @typedef {Object} Foo 201 | * @import BMain, {B3 } from "moduleb" 202 | * @import {A} from 'modulea' 203 | */ 204 | /** 205 | * @import BDefault, { B5 } from './moduleb' 206 | * @import C from "modulec" 207 | */ 208 | `, 209 | ); 210 | 211 | expect(result).toMatchSnapshot(); 212 | }); 213 | 214 | test("type imports with named import padding", async () => { 215 | const result = await subject( 216 | ` 217 | /** 218 | * @import BM, { B as B1, 219 | * B2 , B4 } from 'moduleb' 220 | * @typedef {Object} Foo 221 | * @import BMain, {B3 } from "moduleb" 222 | * @import {A} from 'modulea' 223 | */ 224 | /** 225 | * @import BDefault, { B5 } from './moduleb' 226 | * @import C from "modulec" 227 | */ 228 | `, 229 | { jsdocNamedImportPadding: true }, 230 | ); 231 | 232 | expect(result).toMatchSnapshot(); 233 | }); 234 | 235 | test("type imports with import merging turned off", async () => { 236 | const result = await subject( 237 | ` 238 | /** 239 | * @import BM, { B as B1, 240 | * B2 , B4 } from 'moduleb' 241 | * @typedef {Object} Foo 242 | * @import BMain, {B3 } from "moduleb" 243 | * @import {A} from 'modulea' 244 | */ 245 | /** 246 | * @import BDefault, { B5 } from './moduleb' 247 | * @import C from "modulec" 248 | */ 249 | `, 250 | { jsdocMergeImports: false }, 251 | ); 252 | 253 | expect(result).toMatchSnapshot(); 254 | }); 255 | 256 | test("type imports with named import line splitting turned off", async () => { 257 | const result = await subject( 258 | ` 259 | /** 260 | * @import BM, { B as B1, 261 | * B2 , B4 } from 'moduleb' 262 | * @typedef {Object} Foo 263 | * @import BMain, {B3 } from "moduleb" 264 | * @import {A} from 'modulea' 265 | */ 266 | /** 267 | * @import BDefault, { B5 } from './moduleb' 268 | * @import C from "modulec" 269 | */ 270 | `, 271 | { jsdocNamedImportLineSplitting: false }, 272 | ); 273 | 274 | expect(result).toMatchSnapshot(); 275 | }); 276 | 277 | test("type imports with no import formatting", async () => { 278 | const result = await subject( 279 | ` 280 | /** 281 | * @import BM, { B as B1, 282 | * B2 , B4 } from 'moduleb' 283 | * @typedef {Object} Foo 284 | * @import BMain, {B3 } from "moduleb" 285 | * @import {A} from 'modulea' 286 | */ 287 | /** 288 | * @import BDefault, { B5 } from './moduleb' 289 | * @import C from "modulec" 290 | */ 291 | `, 292 | { jsdocFormatImports: false }, 293 | ); 294 | 295 | expect(result).toMatchSnapshot(); 296 | }); 297 | 298 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { format, Options, ParserOptions, Plugin } from "prettier"; 2 | import { AllOptions, Token } from "./types.js"; 3 | import BSearch from "binary-searching"; 4 | import { TAGS_DEFAULT } from "./roles.js"; 5 | 6 | function convertToModernType(oldType: string): string { 7 | return withoutStrings(oldType, (type) => { 8 | type = type.trim(); 9 | 10 | // JSDoc supports generics of the form `Foo.` 11 | type = type.replace(/\.` to `Foo[]` 24 | let changed = true; 25 | while (changed) { 26 | changed = false; 27 | type = type.replace( 28 | /(^|[^$\w\xA0-\uFFFF])Array\s*<((?:[^<>=]|=>|=(?!>)|<(?:[^<>=]|=>|=(?!>))+>)+)>/g, 29 | (_, prefix, inner) => { 30 | changed = true; 31 | return `${prefix}(${inner})[]`; 32 | }, 33 | ); 34 | } 35 | 36 | return type; 37 | }); 38 | } 39 | 40 | /** 41 | * Given a valid TS type expression, this will replace all string literals in 42 | * the type with unique identifiers. The modified type expression will be passed 43 | * to the given map function. The unique identifiers in the output if the map 44 | * function will then be replaced with the original string literals. 45 | * 46 | * This allows the map function to do type transformations without worrying 47 | * about string literals. 48 | * 49 | * @param type 50 | * @param mapFn 51 | */ 52 | function withoutStrings(type: string, mapFn: (type: string) => string): string { 53 | const strings: string[] = []; 54 | let modifiedType = type.replace( 55 | // copied from Prism's C-like language that is used to tokenize JS strings 56 | // https://github.com/PrismJS/prism/blob/266cc7002e54dae674817ab06a02c2c15ed64a6f/components/prism-clike.js#L15 57 | /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/g, 58 | (m) => { 59 | strings.push(m); 60 | // the pattern of the unique identifiers 61 | // let's hope that nobody uses an identifier like this in real code 62 | return `String$${strings.length - 1}$`; 63 | }, 64 | ); 65 | 66 | if (modifiedType.includes("`")) { 67 | // We are current unable to correct handle template literal types. 68 | return type; 69 | } 70 | 71 | modifiedType = mapFn(modifiedType); 72 | 73 | return modifiedType.replace(/String\$(\d+)\$/g, (_, index) => strings[index]); 74 | } 75 | 76 | async function formatType(type: string, options?: Options): Promise { 77 | try { 78 | const TYPE_START = "type name = "; 79 | 80 | let pretty = type; 81 | 82 | // Rest parameter types start with "...". This is supported by TS and JSDoc 83 | // but it's implemented in a weird way in TS. TS will only acknowledge the 84 | // "..." if the function parameter is a rest parameter. In this case, TS 85 | // will interpret `...T` as `T[]`. But we can't just convert "..." to arrays 86 | // because of @callback types. In @callback types `...T` and `T[]` are not 87 | // equivalent, so we have to support "..." as is. 88 | // 89 | // This formatting itself is rather simple. If `...T` is detected, it will 90 | // be replaced with `T[]` and formatted. At the end, the outer array will 91 | // be removed and "..." will be added again. 92 | // 93 | // As a consequence, union types will get an additional pair of parentheses 94 | // (e.g. `...A|B` -> `...(A|B)`). This is technically unnecessary but it 95 | // makes the operator precedence very clear. 96 | // 97 | // https://www.typescriptlang.org/docs/handbook/functions.html#rest-parameters 98 | let rest = false; 99 | if (pretty.startsWith("...")) { 100 | rest = true; 101 | pretty = `(${pretty.slice(3)})[]`; 102 | } 103 | 104 | pretty = await format(`${TYPE_START}${pretty}`, { 105 | ...options, 106 | parser: "typescript", 107 | plugins: [], 108 | filepath: "file.ts", 109 | }); 110 | pretty = pretty.slice(TYPE_START.length); 111 | 112 | pretty = pretty 113 | .replace(/^\s*/g, "") 114 | .replace(/[;\n]*$/g, "") 115 | .replace(/^\|/g, "") 116 | .trim(); 117 | 118 | if (rest) { 119 | pretty = "..." + pretty.replace(/\[\s*\]$/, ""); 120 | } 121 | 122 | return pretty; 123 | } catch (error) { 124 | // console.log("jsdoc-parser", error); 125 | return type; 126 | } 127 | } 128 | 129 | function addStarsToTheBeginningOfTheLines( 130 | originalComment: string, 131 | comment: string, 132 | options: AllOptions, 133 | ): string { 134 | if ( 135 | (options.jsdocCommentLineStrategy === "singleLine" && 136 | numberOfAStringInString(comment.trim(), "\n") === 0) || 137 | (options.jsdocCommentLineStrategy === "keep" && 138 | numberOfAStringInString(originalComment, "\n") === 0) 139 | ) { 140 | return `* ${comment.trim()} `; 141 | } 142 | 143 | return `*${comment.replace(/(\n(?!$))/g, "\n * ")}\n `; 144 | } 145 | 146 | function numberOfAStringInString(string: string, search: string | RegExp) { 147 | return (string.match(new RegExp(search, "g")) || []).length; 148 | } 149 | 150 | // capitalize if needed 151 | function capitalizer(str: string): string { 152 | if (!str) { 153 | return str; 154 | } 155 | 156 | if (str.match(/^https?:\/\//i)) { 157 | return str; 158 | } 159 | 160 | if (str.startsWith("- ")) { 161 | return str.slice(0, 2) + capitalizer(str.slice(2)); 162 | } 163 | 164 | return str[0].toUpperCase() + str.slice(1); 165 | } 166 | 167 | /** 168 | * Detects the line ends of the given text. 169 | * 170 | * If multiple line ends are used, the most common one will be returned. 171 | * 172 | * If the given text is a single line, "lf" will be returned. 173 | * 174 | * @param text 175 | */ 176 | function detectEndOfLine(text: string): "cr" | "crlf" | "lf" { 177 | const counter = { 178 | "\r": 0, 179 | "\r\n": 0, 180 | "\n": 0, 181 | }; 182 | 183 | const lineEndPattern = /\r\n?|\n/g; 184 | let m; 185 | while ((m = lineEndPattern.exec(text))) { 186 | counter[m[0] as keyof typeof counter]++; 187 | } 188 | 189 | const cr = counter["\r"]; 190 | const crlf = counter["\r\n"]; 191 | const lf = counter["\n"]; 192 | const max = Math.max(cr, crlf, lf); 193 | 194 | if (lf === max) { 195 | return "lf"; 196 | } else if (crlf === max) { 197 | return "crlf"; 198 | } else { 199 | return "cr"; 200 | } 201 | } 202 | 203 | /** 204 | * Returns the index of a token within the given token array. 205 | * 206 | * This method uses binary search using the token location. 207 | * 208 | * @param tokens 209 | * @param token 210 | */ 211 | function findTokenIndex(tokens: Token[], token: Token): number { 212 | if (!Array.isArray(tokens) || tokens.length === 0) { 213 | return -1; 214 | } 215 | return BSearch.eq(tokens, token, (a, b) => { 216 | if (a.loc.start.line === b.loc.start.line) { 217 | return a.loc.start.column - b.loc.start.column; 218 | } else { 219 | return a.loc.start.line - b.loc.start.line; 220 | } 221 | }); 222 | } 223 | 224 | async function formatCode( 225 | result: string, 226 | beginningSpace: string, 227 | options: AllOptions, 228 | ): Promise { 229 | const { printWidth, jsdocKeepUnParseAbleExampleIndent } = options; 230 | 231 | if ( 232 | result 233 | .split("\n") 234 | .slice(1) 235 | .every((v) => !v.trim() || v.startsWith(beginningSpace)) 236 | ) { 237 | result = result.replace( 238 | new RegExp(`\n${beginningSpace.replace(/[\t]/g, "[\\t]")}`, "g"), 239 | "\n", 240 | ); 241 | } 242 | 243 | try { 244 | let formattedExample = ""; 245 | const examplePrintWith = printWidth - 4; 246 | 247 | // If example is a json 248 | if (result.trim().startsWith("{")) { 249 | formattedExample = await format(result || "", { 250 | ...options, 251 | parser: "json", 252 | printWidth: examplePrintWith, 253 | }); 254 | } else { 255 | formattedExample = await format(result || "", { 256 | ...options, 257 | printWidth: examplePrintWith, 258 | }); 259 | } 260 | 261 | result = formattedExample.replace(/(^|\n)/g, `\n${beginningSpace}`); // Add spaces to start of lines 262 | } catch (err) { 263 | result = `\n${result 264 | .split("\n") 265 | .map( 266 | (l) => 267 | `${beginningSpace}${ 268 | jsdocKeepUnParseAbleExampleIndent ? l : l.trim() 269 | }`, 270 | ) 271 | .join("\n")}\n`; 272 | 273 | result = result.replace(/^\n[\s]+\n/g, "\n"); 274 | } 275 | 276 | return result; 277 | } 278 | 279 | const findPluginByParser = (parserName: string, options: ParserOptions) => { 280 | const tsPlugin = options.plugins.find((plugin) => { 281 | return ( 282 | typeof plugin === "object" && 283 | plugin !== null && 284 | !(plugin instanceof URL) && 285 | (plugin as any).name && 286 | (plugin as any).name !== "prettier-plugin-jsdoc" && 287 | (plugin as any).parsers && 288 | !("jsdoc-parser" in (plugin as any).parsers) && 289 | // eslint-disable-next-line no-prototype-builtins 290 | (plugin as any).parsers.hasOwnProperty(parserName) 291 | ); 292 | }) as Plugin | undefined; 293 | 294 | return !tsPlugin ? undefined : tsPlugin.parsers?.[parserName]; 295 | }; 296 | 297 | const isDefaultTag = (tag: string): boolean => TAGS_DEFAULT.includes(tag); 298 | 299 | export { 300 | convertToModernType, 301 | formatType, 302 | addStarsToTheBeginningOfTheLines, 303 | capitalizer, 304 | detectEndOfLine, 305 | findTokenIndex, 306 | formatCode, 307 | findPluginByParser, 308 | isDefaultTag, 309 | }; 310 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { getParser } from "./parser.js"; 2 | import parserBabel from "prettier/plugins/babel"; 3 | import parserFlow from "prettier/plugins/flow"; 4 | import parserTypescript from "prettier/plugins/typescript"; 5 | import prettier, { ChoiceSupportOption, SupportOption } from "prettier"; 6 | import { JsdocOptions } from "./types.js"; 7 | import { findPluginByParser } from "./utils.js"; 8 | 9 | const options = { 10 | jsdocSpaces: { 11 | name: "jsdocSpaces", 12 | type: "int", 13 | category: "jsdoc", 14 | default: 1, 15 | description: "How many spaces will be used to separate tag elements.", 16 | }, 17 | jsdocDescriptionWithDot: { 18 | name: "jsdocDescriptionWithDot", 19 | type: "boolean", 20 | category: "jsdoc", 21 | default: false, 22 | description: "Should dot be inserted at the end of description", 23 | }, 24 | jsdocDescriptionTag: { 25 | name: "jsdocDescriptionTag", 26 | type: "boolean", 27 | category: "jsdoc", 28 | default: false, 29 | description: "Should description tag be used", 30 | }, 31 | jsdocVerticalAlignment: { 32 | name: "jsdocVerticalAlignment", 33 | type: "boolean", 34 | category: "jsdoc", 35 | default: false, 36 | description: "Should tags, types, names and description be aligned", 37 | }, 38 | jsdocKeepUnParseAbleExampleIndent: { 39 | name: "jsdocKeepUnParseAbleExampleIndent", 40 | type: "boolean", 41 | category: "jsdoc", 42 | default: false, 43 | description: 44 | "Should unParseAble example (pseudo code or no js code) keep its indentation", 45 | }, 46 | jsdocSingleLineComment: { 47 | name: "jsdocSingleLineComment", 48 | type: "boolean", 49 | category: "jsdoc", 50 | deprecated: "use jsdocCommentLineStrategy instead will be remove on v2", 51 | default: true, 52 | description: "Should compact single line comment", 53 | }, 54 | jsdocCommentLineStrategy: { 55 | name: "jsdocCommentLineStrategy", 56 | type: "choice", 57 | choices: [ 58 | { 59 | value: "singleLine", 60 | description: `Should compact single line comment, if possible`, 61 | }, 62 | { 63 | value: "multiline", 64 | description: `Should compact multi line comment`, 65 | }, 66 | { 67 | value: "keep", 68 | description: `Should keep original line comment`, 69 | }, 70 | ] as ChoiceSupportOption["choices"], 71 | category: "jsdoc", 72 | default: "singleLine", 73 | description: "How comments line should be", 74 | }, 75 | jsdocSeparateReturnsFromParam: { 76 | name: "jsdocSeparateReturnsFromParam", 77 | type: "boolean", 78 | category: "jsdoc", 79 | default: false, 80 | description: "Add an space between last @param and @returns", 81 | }, 82 | jsdocSeparateTagGroups: { 83 | name: "jsdocSeparateTagGroups", 84 | type: "boolean", 85 | category: "jsdoc", 86 | default: false, 87 | description: "Add an space between tag groups", 88 | }, 89 | jsdocCapitalizeDescription: { 90 | name: "jsdocCapitalizeDescription", 91 | type: "boolean", 92 | category: "jsdoc", 93 | default: true, 94 | description: "Should capitalize first letter of description", 95 | }, 96 | tsdoc: { 97 | name: "tsdoc", 98 | type: "boolean", 99 | category: "jsdoc", 100 | default: false, 101 | description: "Should format as tsdoc", 102 | }, 103 | jsdocPrintWidth: { 104 | name: "jsdocPrintWidth", 105 | type: "int", 106 | category: "jsdoc", 107 | default: undefined, 108 | description: 109 | "If You don't set value to jsdocPrintWidth, the printWidth will be use as jsdocPrintWidth.", 110 | }, 111 | jsdocAddDefaultToDescription: { 112 | name: "jsdocAddDefaultToDescription", 113 | type: "boolean", 114 | category: "jsdoc", 115 | default: true, 116 | description: "Add Default value of a param to end description", 117 | }, 118 | jsdocPreferCodeFences: { 119 | name: "jsdocPreferCodeFences", 120 | type: "boolean", 121 | category: "jsdoc", 122 | default: false, 123 | description: `Prefer to render code blocks using "fences" (triple backticks). If not set, blocks without a language tag will be rendered with a four space indentation.`, 124 | }, 125 | jsdocLineWrappingStyle: { 126 | name: "jsdocLineWrappingStyle", 127 | type: "choice", 128 | choices: [ 129 | { 130 | value: "greedy", 131 | description: `Lines wrap as soon as they reach the print width`, 132 | }, 133 | { 134 | value: "balance", 135 | description: `Preserve existing line breaks if lines are shorter than print width, otherwise use greedy wrapping`, 136 | }, 137 | ] as ChoiceSupportOption["choices"], 138 | category: "jsdoc", 139 | default: "greedy", 140 | description: `Strategy for wrapping lines for the given print width. More options may be added in the future.`, 141 | }, 142 | jsdocTagsOrder: { 143 | name: "jsdocTagsOrder", 144 | type: "string", 145 | category: "jsdoc", 146 | default: undefined, 147 | description: "How many spaces will be used to separate tag elements.", 148 | }, 149 | jsdocMergeImports: { 150 | name: "jsdocMergeImports", 151 | type: "boolean", 152 | category: "jsdoc", 153 | default: true, 154 | description: 155 | "Merge all imports tags in the same block from the same source into one tag", 156 | }, 157 | jsdocNamedImportPadding: { 158 | name: "jsdocNamedImportPadding", 159 | type: "boolean", 160 | category: "jsdoc", 161 | default: false, 162 | description: "Whether or not to pad brackets for single line named imports", 163 | }, 164 | jsdocNamedImportLineSplitting: { 165 | name: "jsdocNamedImportLineSplitting", 166 | type: "boolean", 167 | category: "jsdoc", 168 | default: true, 169 | description: 170 | "Split import tags with multiple named imports into multiple lines", 171 | }, 172 | jsdocFormatImports: { 173 | name: "jsdocFormatImports", 174 | type: "boolean", 175 | category: "jsdoc", 176 | default: true, 177 | description: "Format import tags", 178 | }, 179 | jsdocEmptyCommentStrategy: { 180 | name: "jsdocEmptyCommentStrategy", 181 | type: "choice", 182 | choices: [ 183 | { 184 | value: "remove", 185 | description: "Remove empty JSDoc comment blocks", 186 | }, 187 | { 188 | value: "keep", 189 | description: "Keep empty JSDoc comment blocks", 190 | }, 191 | ] as ChoiceSupportOption["choices"], 192 | category: "jsdoc", 193 | default: "remove", 194 | description: "How to handle empty JSDoc comment blocks", 195 | }, 196 | jsdocBracketSpacing: { 197 | name: "jsdocBracketSpacing", 198 | type: "boolean", 199 | category: "jsdoc", 200 | default: false, 201 | description: "Whether to add spaces inside JSDoc type brackets.", 202 | }, 203 | } as const satisfies Record; 204 | 205 | const defaultOptions: JsdocOptions = { 206 | jsdocSpaces: options.jsdocSpaces.default, 207 | jsdocPrintWidth: options.jsdocPrintWidth.default, 208 | jsdocDescriptionWithDot: options.jsdocDescriptionWithDot.default, 209 | jsdocDescriptionTag: options.jsdocDescriptionTag.default, 210 | jsdocVerticalAlignment: options.jsdocVerticalAlignment.default, 211 | jsdocKeepUnParseAbleExampleIndent: 212 | options.jsdocKeepUnParseAbleExampleIndent.default, 213 | jsdocSingleLineComment: options.jsdocSingleLineComment.default, 214 | jsdocCommentLineStrategy: options.jsdocCommentLineStrategy.default, 215 | jsdocSeparateReturnsFromParam: options.jsdocSeparateReturnsFromParam.default, 216 | jsdocSeparateTagGroups: options.jsdocSeparateTagGroups.default, 217 | jsdocCapitalizeDescription: options.jsdocCapitalizeDescription.default, 218 | jsdocAddDefaultToDescription: options.jsdocAddDefaultToDescription.default, 219 | jsdocPreferCodeFences: options.jsdocPreferCodeFences.default, 220 | tsdoc: options.tsdoc.default, 221 | jsdocLineWrappingStyle: options.jsdocLineWrappingStyle.default, 222 | jsdocTagsOrder: options.jsdocTagsOrder.default, 223 | jsdocFormatImports: options.jsdocFormatImports.default, 224 | jsdocNamedImportPadding: options.jsdocNamedImportPadding.default, 225 | jsdocMergeImports: options.jsdocMergeImports.default, 226 | jsdocNamedImportLineSplitting: options.jsdocNamedImportLineSplitting.default, 227 | jsdocEmptyCommentStrategy: options.jsdocEmptyCommentStrategy.default, 228 | jsdocBracketSpacing: options.jsdocBracketSpacing.default, 229 | }; 230 | 231 | const parsers = { 232 | // JS - Babel 233 | get babel() { 234 | const parser = parserBabel.parsers.babel; 235 | return mergeParsers(parser, "babel"); 236 | }, 237 | get "babel-flow"() { 238 | const parser = parserBabel.parsers["babel-flow"]; 239 | return mergeParsers(parser, "babel-flow"); 240 | }, 241 | get "babel-ts"() { 242 | const parser = parserBabel.parsers["babel-ts"]; 243 | return mergeParsers(parser, "babel-ts"); 244 | }, 245 | // JS - Flow 246 | get flow() { 247 | const parser = parserFlow.parsers.flow; 248 | return mergeParsers(parser, "flow"); 249 | }, 250 | // JS - TypeScript 251 | get typescript(): prettier.Parser { 252 | const parser = parserTypescript.parsers.typescript; 253 | 254 | return mergeParsers(parser, "typescript"); 255 | // require("./parser-typescript").parsers.typescript; 256 | }, 257 | get "jsdoc-parser"() { 258 | // Backward compatible, don't use this in new version since 1.0.0 259 | const parser = parserBabel.parsers["babel-ts"]; 260 | 261 | return mergeParsers(parser, "babel-ts"); 262 | }, 263 | }; 264 | 265 | function mergeParsers(originalParser: prettier.Parser, parserName: string) { 266 | const jsDocParse = getParser(originalParser.parse, parserName) as any; 267 | let hasPreprocessed = false; 268 | 269 | const jsDocPreprocess = (text: string, options: prettier.ParserOptions) => { 270 | normalizeOptions(options as any); 271 | 272 | // Prevent infinite recursion by checking if we've already preprocessed 273 | if (hasPreprocessed) { 274 | return text; 275 | } 276 | 277 | hasPreprocessed = true; 278 | try { 279 | const tsPluginParser = findPluginByParser(parserName, options); 280 | 281 | if (!tsPluginParser) { 282 | return originalParser.preprocess 283 | ? originalParser.preprocess(text, options) 284 | : text; 285 | } 286 | 287 | const preprocess = 288 | tsPluginParser?.preprocess || originalParser.preprocess; 289 | return preprocess ? preprocess(text, options) : text; 290 | } finally { 291 | hasPreprocessed = false; 292 | } 293 | }; 294 | 295 | const parser = { 296 | ...originalParser, 297 | preprocess: jsDocPreprocess, 298 | parse: jsDocParse, 299 | }; 300 | 301 | return parser; 302 | } 303 | 304 | const name = "prettier-plugin-jsdoc"; 305 | export { name, options, parsers, defaultOptions }; 306 | export type Options = Partial; 307 | 308 | function normalizeOptions(options: prettier.ParserOptions & JsdocOptions) { 309 | if (options.jsdocTagsOrder) { 310 | options.jsdocTagsOrder = JSON.parse(options.jsdocTagsOrder as any); 311 | } 312 | 313 | if (options.jsdocCommentLineStrategy) { 314 | return; 315 | } 316 | if (options.jsdocSingleLineComment) { 317 | options.jsdocCommentLineStrategy = "singleLine"; 318 | } else { 319 | options.jsdocCommentLineStrategy = "multiline"; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /tests/files/prism-dependencies.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * @typedef {Object} Components 5 | * @typedef {Object} ComponentCategory 6 | * 7 | * @typedef ComponentEntry 8 | * @property {string} [title] The title of the component. 9 | * @property {string} [owner] The GitHub user name of the owner. 10 | * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded. 11 | * @property {string | string[]} [alias] An optional list of aliases for the id of the component. 12 | * @property {Object} [aliasTitles] An optional map from an alias to its title. 13 | * 14 | * Aliases which are not in this map will the get title of the component. 15 | * @property {string | string[]} [optional] 16 | * @property {string | string[]} [require] 17 | * @property {string | string[]} [modify] 18 | */ 19 | 20 | var getLoader = (function () { 21 | 22 | /** 23 | * A function which does absolutely nothing. 24 | * 25 | * @type {any} 26 | */ 27 | var noop = function () { }; 28 | 29 | /** 30 | * Invokes the given callback for all elements of the given value. 31 | * 32 | * If the given value is an array, the callback will be invokes for all elements. If the given value is `null` or 33 | * `undefined`, the callback will not be invoked. In all other cases, the callback will be invoked with the given 34 | * value as parameter. 35 | * 36 | * @param {null | undefined | T | T[]} value 37 | * @param {(value: T, index: number) => void} callbackFn 38 | * @returns {void} 39 | * @template T 40 | */ 41 | function forEach(value, callbackFn) { 42 | if (Array.isArray(value)) { 43 | value.forEach(callbackFn); 44 | } else if (value != null) { 45 | callbackFn(value, 0); 46 | } 47 | } 48 | 49 | /** 50 | * Returns a new set for the given string array. 51 | * 52 | * @param {string[]} array 53 | * @returns {StringSet} 54 | * 55 | * @typedef {Object} StringSet 56 | */ 57 | function toSet(array) { 58 | /** @type {StringSet} */ 59 | var set = {}; 60 | for (var i = 0, l = array.length; i < l; i++) { 61 | set[array[i]] = true; 62 | } 63 | return set; 64 | } 65 | 66 | /** 67 | * Creates a map of every components id to its entry. 68 | * 69 | * @param {Components} components 70 | * @returns {EntryMap} 71 | * 72 | * @typedef {{ readonly [id: string]: Readonly | undefined }} EntryMap 73 | */ 74 | function createEntryMap(components) { 75 | /** @type {Object>} */ 76 | var map = {}; 77 | 78 | for (var categoryName in components) { 79 | var category = components[categoryName]; 80 | for (var id in category) { 81 | if (id != 'meta') { 82 | /** @type {ComponentEntry | string} */ 83 | var entry = category[id]; 84 | map[id] = typeof entry == 'string' ? { title: entry } : entry; 85 | } 86 | } 87 | } 88 | 89 | return map; 90 | } 91 | 92 | /** 93 | * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies. 94 | * 95 | * @param {EntryMap} entryMap 96 | * @returns {DependencyResolver} 97 | * 98 | * @typedef {(id: string) => StringSet} DependencyResolver 99 | */ 100 | function createDependencyResolver(entryMap) { 101 | /** @type {Object} */ 102 | var map = {}; 103 | var _stackArray = []; 104 | 105 | /** 106 | * Adds the dependencies of the given component to the dependency map. 107 | * 108 | * @param {string} id 109 | * @param {string[]} stack 110 | */ 111 | function addToMap(id, stack) { 112 | if (id in map) { 113 | return; 114 | } 115 | 116 | stack.push(id); 117 | 118 | // check for circular dependencies 119 | var firstIndex = stack.indexOf(id); 120 | if (firstIndex < stack.length - 1) { 121 | throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> ')); 122 | } 123 | 124 | /** @type {StringSet} */ 125 | var dependencies = {}; 126 | 127 | var entry = entryMap[id]; 128 | if (entry) { 129 | /** 130 | * This will add the direct dependency and all of its transitive dependencies to the set of 131 | * dependencies of `entry`. 132 | * 133 | * @param {string} depId 134 | * @returns {void} 135 | */ 136 | function handleDirectDependency(depId) { 137 | if (!(depId in entryMap)) { 138 | throw new Error(id + ' depends on an unknown component ' + depId); 139 | } 140 | if (depId in dependencies) { 141 | // if the given dependency is already in the set of deps, then so are its transitive deps 142 | return; 143 | } 144 | 145 | addToMap(depId, stack); 146 | dependencies[depId] = true; 147 | for (var transitiveDepId in map[depId]) { 148 | dependencies[transitiveDepId] = true; 149 | } 150 | } 151 | 152 | forEach(entry.require, handleDirectDependency); 153 | forEach(entry.optional, handleDirectDependency); 154 | forEach(entry.modify, handleDirectDependency); 155 | } 156 | 157 | map[id] = dependencies; 158 | 159 | stack.pop(); 160 | } 161 | 162 | return function (id) { 163 | var deps = map[id]; 164 | if (!deps) { 165 | addToMap(id, _stackArray); 166 | deps = map[id]; 167 | } 168 | return deps; 169 | }; 170 | } 171 | 172 | /** 173 | * Returns a function which resolves the aliases of its given id of alias. 174 | * 175 | * @param {EntryMap} entryMap 176 | * @returns {(idOrAlias: string) => string} 177 | */ 178 | function createAliasResolver(entryMap) { 179 | /** @type {Object | undefined} */ 180 | var map; 181 | 182 | return function (idOrAlias) { 183 | if (idOrAlias in entryMap) { 184 | return idOrAlias; 185 | } else { 186 | // only create the alias map if necessary 187 | if (!map) { 188 | map = {}; 189 | 190 | for (var id in entryMap) { 191 | var entry = entryMap[id]; 192 | forEach(entry && entry.alias, function (alias) { 193 | if (alias in map) { 194 | throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]); 195 | } 196 | if (alias in entryMap) { 197 | throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.'); 198 | } 199 | map[alias] = id; 200 | }); 201 | } 202 | } 203 | return map[idOrAlias] || idOrAlias; 204 | } 205 | }; 206 | } 207 | 208 | /** 209 | * @typedef LoadChainer 210 | * @property {(before: T, after: () => T) => T} series 211 | * @property {(values: T[]) => T} parallel 212 | * @template T 213 | */ 214 | 215 | /** 216 | * Creates an implicit DAG from the given components and dependencies and call the given `loadComponent` for each 217 | * component in topological order. 218 | * 219 | * @param {DependencyResolver} dependencyResolver 220 | * @param {StringSet} ids 221 | * @param {(id: string) => T} loadComponent 222 | * @param {LoadChainer} [chainer] 223 | * @returns {T} 224 | * @template T 225 | */ 226 | function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) { 227 | const series = chainer ? chainer.series : undefined; 228 | const parallel = chainer ? chainer.parallel : noop; 229 | 230 | /** @type {Object} */ 231 | var cache = {}; 232 | 233 | /** 234 | * A set of ids of nodes which are not depended upon by any other node in the graph. 235 | * @type {StringSet} 236 | */ 237 | var ends = {}; 238 | 239 | /** 240 | * Loads the given component and its dependencies or returns the cached value. 241 | * 242 | * @param {string} id 243 | * @returns {T} 244 | */ 245 | function handleId(id) { 246 | if (id in cache) { 247 | return cache[id]; 248 | } 249 | 250 | // assume that it's an end 251 | // if it isn't, it will be removed later 252 | ends[id] = true; 253 | 254 | // all dependencies of the component in the given ids 255 | var dependsOn = []; 256 | for (var depId in dependencyResolver(id)) { 257 | if (depId in ids) { 258 | dependsOn.push(depId); 259 | } 260 | } 261 | 262 | /** 263 | * The value to be returned. 264 | * @type {T} 265 | */ 266 | var value; 267 | 268 | if (dependsOn.length === 0) { 269 | value = loadComponent(id); 270 | } else { 271 | var depsValue = parallel(dependsOn.map(function (depId) { 272 | var value = handleId(depId); 273 | // none of the dependencies can be ends 274 | delete ends[depId]; 275 | return value; 276 | })); 277 | if (series) { 278 | // the chainer will be responsibly for calling the function calling loadComponent 279 | value = series(depsValue, function () { return loadComponent(id); }); 280 | } else { 281 | // we don't have a chainer, so we call loadComponent ourselves 282 | loadComponent(id); 283 | } 284 | } 285 | 286 | // cache and return 287 | return cache[id] = value; 288 | } 289 | 290 | for (var id in ids) { 291 | handleId(id); 292 | } 293 | 294 | /** @type {T[]} */ 295 | var endValues = []; 296 | for (var endId in ends) { 297 | endValues.push(cache[endId]); 298 | } 299 | return parallel(endValues); 300 | } 301 | 302 | /** 303 | * Returns whether the given object has any keys. 304 | * 305 | * @param {object} obj 306 | */ 307 | function hasKeys(obj) { 308 | for (var key in obj) { 309 | return true; 310 | } 311 | return false; 312 | } 313 | 314 | /** 315 | * Returns an object which provides methods to get the ids of the components which have to be loaded (`getIds`) and 316 | * a way to efficiently load them in synchronously and asynchronous contexts (`load`). 317 | * 318 | * The set of ids to be loaded is a superset of `load`. If some of these ids are in `loaded`, the corresponding 319 | * components will have to reloaded. 320 | * 321 | * The ids in `load` and `loaded` may be in any order and can contain duplicates. 322 | * 323 | * @param {Components} components 324 | * @param {string[]} load 325 | * @param {string[]} [loaded=[]] A list of already loaded components. 326 | * 327 | * If a component is in this list, then all of its requirements will also be assumed to be in the list. 328 | * @returns {Loader} 329 | * 330 | * @typedef Loader 331 | * @property {() => string[]} getIds A function to get all ids of the components to load. 332 | * 333 | * The returned ids will be duplicate-free, alias-free and in load order. 334 | * @property {LoadFunction} load A functional interface to load components. 335 | * 336 | * @typedef { (loadComponent: (id: string) => T, chainer?: LoadChainer) => T} LoadFunction 337 | * A functional interface to load components. 338 | * 339 | * The `loadComponent` function will be called for every component in the order in which they have to be loaded. 340 | * 341 | * The `chainer` is useful for asynchronous loading and its `series` and `parallel` functions can be thought of as 342 | * `Promise#then` and `Promise.all`. 343 | * 344 | * @example 345 | * load(id => { loadComponent(id); }); // returns undefined 346 | * 347 | * await load( 348 | * id => loadComponentAsync(id), // returns a Promise for each id 349 | * { 350 | * series: async (before, after) => { 351 | * await before; 352 | * await after(); 353 | * }, 354 | * parallel: async (values) => { 355 | * await Promise.all(values); 356 | * } 357 | * } 358 | * ); 359 | */ 360 | function getLoader(components, load, loaded) { 361 | var entryMap = createEntryMap(components); 362 | var resolveAlias = createAliasResolver(entryMap); 363 | 364 | load = load.map(resolveAlias); 365 | loaded = (loaded || []).map(resolveAlias); 366 | 367 | var loadSet = toSet(load); 368 | var loadedSet = toSet(loaded); 369 | 370 | // add requirements 371 | 372 | load.forEach(addRequirements); 373 | function addRequirements(id) { 374 | var entry = entryMap[id]; 375 | forEach(entry && entry.require, function (reqId) { 376 | if (!(reqId in loadedSet)) { 377 | loadSet[reqId] = true; 378 | addRequirements(reqId); 379 | } 380 | }); 381 | } 382 | 383 | // add components to reload 384 | 385 | // A component x in `loaded` has to be reloaded if 386 | // 1) a component in `load` modifies x. 387 | // 2) x depends on a component in `load`. 388 | // The above two condition have to be applied until nothing changes anymore. 389 | 390 | var dependencyResolver = createDependencyResolver(entryMap); 391 | 392 | /** @type {StringSet} */ 393 | var loadAdditions = loadSet; 394 | /** @type {StringSet} */ 395 | var newIds; 396 | while (hasKeys(loadAdditions)) { 397 | newIds = {}; 398 | 399 | // condition 1) 400 | for (var loadId in loadAdditions) { 401 | var entry = entryMap[loadId]; 402 | forEach(entry && entry.modify, function (modId) { 403 | if (modId in loadedSet) { 404 | newIds[modId] = true; 405 | } 406 | }); 407 | } 408 | 409 | // condition 2) 410 | for (var loadedId in loadedSet) { 411 | if (!(loadedId in loadSet)) { 412 | for (var depId in dependencyResolver(loadedId)) { 413 | if (depId in loadSet) { 414 | newIds[loadedId] = true; 415 | break; 416 | } 417 | } 418 | } 419 | } 420 | 421 | loadAdditions = newIds; 422 | for (var newId in loadAdditions) { 423 | loadSet[newId] = true; 424 | } 425 | } 426 | 427 | /** @type {Loader} */ 428 | var loader = { 429 | getIds: function () { 430 | var ids = []; 431 | loader.load(function (id) { 432 | ids.push(id); 433 | }); 434 | return ids; 435 | }, 436 | load: function (loadComponent, chainer) { 437 | return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer); 438 | } 439 | }; 440 | 441 | return loader; 442 | } 443 | 444 | return getLoader; 445 | 446 | }()); 447 | 448 | if (typeof module !== 'undefined') { 449 | module.exports = getLoader; 450 | } 451 | -------------------------------------------------------------------------------- /tests/__snapshots__/files/prism-dependencies.js.shot: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`File: prism-dependencies.js 1`] = ` 4 | "'use strict'; 5 | 6 | /** 7 | * @typedef {Object} Components 8 | * 9 | * @typedef {Object} ComponentCategory 10 | * 11 | * @typedef ComponentEntry 12 | * 13 | * @property {string} [title] The title of the component. 14 | * @property {string} [owner] The GitHub user name of the owner. 15 | * @property {boolean} [noCSS=false] Whether the component doesn't have style sheets which should also be loaded. 16 | * Default is \`false\` 17 | * @property {string | string[]} [alias] An optional list of aliases for the id of the component. 18 | * @property {Object} [aliasTitles] An optional map from an alias to its title. 19 | * 20 | * Aliases which are not in this map will the get title of the component. 21 | * @property {string | string[]} [optional] 22 | * @property {string | string[]} [require] 23 | * @property {string | string[]} [modify] 24 | */ 25 | 26 | var getLoader = (function () { 27 | /** 28 | * A function which does absolutely nothing. 29 | * 30 | * @type {any} 31 | */ 32 | var noop = function () {}; 33 | 34 | /** 35 | * Invokes the given callback for all elements of the given value. 36 | * 37 | * If the given value is an array, the callback will be invokes for all elements. If the given value is \`null\` or 38 | * \`undefined\`, the callback will not be invoked. In all other cases, the callback will be invoked with the given 39 | * value as parameter. 40 | * 41 | * @template T 42 | * 43 | * @param {null | undefined | T | T[]} value 44 | * @param {(value: T, index: number) => void} callbackFn 45 | * 46 | * @returns {void} 47 | */ 48 | function forEach(value, callbackFn) { 49 | if (Array.isArray(value)) { 50 | value.forEach(callbackFn); 51 | } else if (value != null) { 52 | callbackFn(value, 0); 53 | } 54 | } 55 | 56 | /** 57 | * Returns a new set for the given string array. 58 | * 59 | * @param {string[]} array 60 | * 61 | * @returns {StringSet} 62 | * 63 | * @typedef {Object} StringSet 64 | */ 65 | function toSet(array) { 66 | /** @type {StringSet} */ 67 | var set = {}; 68 | for (var i = 0, l = array.length; i < l; i++) { 69 | set[array[i]] = true; 70 | } 71 | return set; 72 | } 73 | 74 | /** 75 | * Creates a map of every components id to its entry. 76 | * 77 | * @param {Components} components 78 | * 79 | * @returns {EntryMap} 80 | * 81 | * @typedef {{ readonly [id: string]: Readonly | undefined }} EntryMap 82 | */ 83 | function createEntryMap(components) { 84 | /** @type {Object>} */ 85 | var map = {}; 86 | 87 | for (var categoryName in components) { 88 | var category = components[categoryName]; 89 | for (var id in category) { 90 | if (id != 'meta') { 91 | /** @type {ComponentEntry | string} */ 92 | var entry = category[id]; 93 | map[id] = typeof entry == 'string' ? { title: entry } : entry; 94 | } 95 | } 96 | } 97 | 98 | return map; 99 | } 100 | 101 | /** 102 | * Creates a full dependencies map which includes all types of dependencies and their transitive dependencies. 103 | * 104 | * @param {EntryMap} entryMap 105 | * 106 | * @returns {DependencyResolver} 107 | * 108 | * @typedef {(id: string) => StringSet} DependencyResolver 109 | */ 110 | function createDependencyResolver(entryMap) { 111 | /** @type {Object} */ 112 | var map = {}; 113 | var _stackArray = []; 114 | 115 | /** 116 | * Adds the dependencies of the given component to the dependency map. 117 | * 118 | * @param {string} id 119 | * @param {string[]} stack 120 | */ 121 | function addToMap(id, stack) { 122 | if (id in map) { 123 | return; 124 | } 125 | 126 | stack.push(id); 127 | 128 | // check for circular dependencies 129 | var firstIndex = stack.indexOf(id); 130 | if (firstIndex < stack.length - 1) { 131 | throw new Error('Circular dependency: ' + stack.slice(firstIndex).join(' -> ')); 132 | } 133 | 134 | /** @type {StringSet} */ 135 | var dependencies = {}; 136 | 137 | var entry = entryMap[id]; 138 | if (entry) { 139 | /** 140 | * This will add the direct dependency and all of its transitive dependencies to the set of dependencies 141 | * of \`entry\`. 142 | * 143 | * @param {string} depId 144 | * 145 | * @returns {void} 146 | */ 147 | function handleDirectDependency(depId) { 148 | if (!(depId in entryMap)) { 149 | throw new Error(id + ' depends on an unknown component ' + depId); 150 | } 151 | if (depId in dependencies) { 152 | // if the given dependency is already in the set of deps, then so are its transitive deps 153 | return; 154 | } 155 | 156 | addToMap(depId, stack); 157 | dependencies[depId] = true; 158 | for (var transitiveDepId in map[depId]) { 159 | dependencies[transitiveDepId] = true; 160 | } 161 | } 162 | 163 | forEach(entry.require, handleDirectDependency); 164 | forEach(entry.optional, handleDirectDependency); 165 | forEach(entry.modify, handleDirectDependency); 166 | } 167 | 168 | map[id] = dependencies; 169 | 170 | stack.pop(); 171 | } 172 | 173 | return function (id) { 174 | var deps = map[id]; 175 | if (!deps) { 176 | addToMap(id, _stackArray); 177 | deps = map[id]; 178 | } 179 | return deps; 180 | }; 181 | } 182 | 183 | /** 184 | * Returns a function which resolves the aliases of its given id of alias. 185 | * 186 | * @param {EntryMap} entryMap 187 | * 188 | * @returns {(idOrAlias: string) => string} 189 | */ 190 | function createAliasResolver(entryMap) { 191 | /** @type {Object | undefined} */ 192 | var map; 193 | 194 | return function (idOrAlias) { 195 | if (idOrAlias in entryMap) { 196 | return idOrAlias; 197 | } else { 198 | // only create the alias map if necessary 199 | if (!map) { 200 | map = {}; 201 | 202 | for (var id in entryMap) { 203 | var entry = entryMap[id]; 204 | forEach(entry && entry.alias, function (alias) { 205 | if (alias in map) { 206 | throw new Error(alias + ' cannot be alias for both ' + id + ' and ' + map[alias]); 207 | } 208 | if (alias in entryMap) { 209 | throw new Error(alias + ' cannot be alias of ' + id + ' because it is a component.'); 210 | } 211 | map[alias] = id; 212 | }); 213 | } 214 | } 215 | return map[idOrAlias] || idOrAlias; 216 | } 217 | }; 218 | } 219 | 220 | /** 221 | * @template T 222 | * 223 | * @typedef LoadChainer 224 | * 225 | * @property {(before: T, after: () => T) => T} series 226 | * @property {(values: T[]) => T} parallel 227 | */ 228 | 229 | /** 230 | * Creates an implicit DAG from the given components and dependencies and call the given \`loadComponent\` for each 231 | * component in topological order. 232 | * 233 | * @template T 234 | * 235 | * @param {DependencyResolver} dependencyResolver 236 | * @param {StringSet} ids 237 | * @param {(id: string) => T} loadComponent 238 | * @param {LoadChainer} [chainer] 239 | * 240 | * @returns {T} 241 | */ 242 | function loadComponentsInOrder(dependencyResolver, ids, loadComponent, chainer) { 243 | const series = chainer ? chainer.series : undefined; 244 | const parallel = chainer ? chainer.parallel : noop; 245 | 246 | /** @type {Object} */ 247 | var cache = {}; 248 | 249 | /** 250 | * A set of ids of nodes which are not depended upon by any other node in the graph. 251 | * 252 | * @type {StringSet} 253 | */ 254 | var ends = {}; 255 | 256 | /** 257 | * Loads the given component and its dependencies or returns the cached value. 258 | * 259 | * @param {string} id 260 | * 261 | * @returns {T} 262 | */ 263 | function handleId(id) { 264 | if (id in cache) { 265 | return cache[id]; 266 | } 267 | 268 | // assume that it's an end 269 | // if it isn't, it will be removed later 270 | ends[id] = true; 271 | 272 | // all dependencies of the component in the given ids 273 | var dependsOn = []; 274 | for (var depId in dependencyResolver(id)) { 275 | if (depId in ids) { 276 | dependsOn.push(depId); 277 | } 278 | } 279 | 280 | /** 281 | * The value to be returned. 282 | * 283 | * @type {T} 284 | */ 285 | var value; 286 | 287 | if (dependsOn.length === 0) { 288 | value = loadComponent(id); 289 | } else { 290 | var depsValue = parallel( 291 | dependsOn.map(function (depId) { 292 | var value = handleId(depId); 293 | // none of the dependencies can be ends 294 | delete ends[depId]; 295 | return value; 296 | }) 297 | ); 298 | if (series) { 299 | // the chainer will be responsibly for calling the function calling loadComponent 300 | value = series(depsValue, function () { 301 | return loadComponent(id); 302 | }); 303 | } else { 304 | // we don't have a chainer, so we call loadComponent ourselves 305 | loadComponent(id); 306 | } 307 | } 308 | 309 | // cache and return 310 | return (cache[id] = value); 311 | } 312 | 313 | for (var id in ids) { 314 | handleId(id); 315 | } 316 | 317 | /** @type {T[]} */ 318 | var endValues = []; 319 | for (var endId in ends) { 320 | endValues.push(cache[endId]); 321 | } 322 | return parallel(endValues); 323 | } 324 | 325 | /** 326 | * Returns whether the given object has any keys. 327 | * 328 | * @param {object} obj 329 | */ 330 | function hasKeys(obj) { 331 | for (var key in obj) { 332 | return true; 333 | } 334 | return false; 335 | } 336 | 337 | /** 338 | * Returns an object which provides methods to get the ids of the components which have to be loaded (\`getIds\`) and 339 | * a way to efficiently load them in synchronously and asynchronous contexts (\`load\`). 340 | * 341 | * The set of ids to be loaded is a superset of \`load\`. If some of these ids are in \`loaded\`, the corresponding 342 | * components will have to reloaded. 343 | * 344 | * The ids in \`load\` and \`loaded\` may be in any order and can contain duplicates. 345 | * 346 | * @example 347 | * load(id => { 348 | * loadComponent(id); 349 | * }); // returns undefined 350 | * 351 | * await load( 352 | * id => loadComponentAsync(id), // returns a Promise for each id 353 | * { 354 | * series: async (before, after) => { 355 | * await before; 356 | * await after(); 357 | * }, 358 | * parallel: async values => { 359 | * await Promise.all(values); 360 | * } 361 | * } 362 | * ); 363 | * 364 | * @param {Components} components 365 | * @param {string[]} load 366 | * @param {string[]} [loaded=[]] A list of already loaded components. 367 | * 368 | * If a component is in this list, then all of its requirements will also be assumed to be in the list. Default is 369 | * \`[]\` 370 | * 371 | * @returns {Loader} 372 | * 373 | * 374 | * 375 | * @typedef Loader 376 | * 377 | * @property {() => string[]} getIds A function to get all ids of the components to load. 378 | * 379 | * The returned ids will be duplicate-free, alias-free and in load order. 380 | * @property {LoadFunction} load A functional interface to load components. 381 | * 382 | * 383 | * 384 | * @typedef {(loadComponent: (id: string) => T, chainer?: LoadChainer) => T} LoadFunction A functional 385 | * interface to load components. 386 | * 387 | * The \`loadComponent\` function will be called for every component in the order in which they have to be loaded. 388 | * 389 | * The \`chainer\` is useful for asynchronous loading and its \`series\` and \`parallel\` functions can be thought of as 390 | * \`Promise#then\` and \`Promise.all\`. 391 | */ 392 | function getLoader(components, load, loaded) { 393 | var entryMap = createEntryMap(components); 394 | var resolveAlias = createAliasResolver(entryMap); 395 | 396 | load = load.map(resolveAlias); 397 | loaded = (loaded || []).map(resolveAlias); 398 | 399 | var loadSet = toSet(load); 400 | var loadedSet = toSet(loaded); 401 | 402 | // add requirements 403 | 404 | load.forEach(addRequirements); 405 | function addRequirements(id) { 406 | var entry = entryMap[id]; 407 | forEach(entry && entry.require, function (reqId) { 408 | if (!(reqId in loadedSet)) { 409 | loadSet[reqId] = true; 410 | addRequirements(reqId); 411 | } 412 | }); 413 | } 414 | 415 | // add components to reload 416 | 417 | // A component x in \`loaded\` has to be reloaded if 418 | // 1) a component in \`load\` modifies x. 419 | // 2) x depends on a component in \`load\`. 420 | // The above two condition have to be applied until nothing changes anymore. 421 | 422 | var dependencyResolver = createDependencyResolver(entryMap); 423 | 424 | /** @type {StringSet} */ 425 | var loadAdditions = loadSet; 426 | /** @type {StringSet} */ 427 | var newIds; 428 | while (hasKeys(loadAdditions)) { 429 | newIds = {}; 430 | 431 | // condition 1) 432 | for (var loadId in loadAdditions) { 433 | var entry = entryMap[loadId]; 434 | forEach(entry && entry.modify, function (modId) { 435 | if (modId in loadedSet) { 436 | newIds[modId] = true; 437 | } 438 | }); 439 | } 440 | 441 | // condition 2) 442 | for (var loadedId in loadedSet) { 443 | if (!(loadedId in loadSet)) { 444 | for (var depId in dependencyResolver(loadedId)) { 445 | if (depId in loadSet) { 446 | newIds[loadedId] = true; 447 | break; 448 | } 449 | } 450 | } 451 | } 452 | 453 | loadAdditions = newIds; 454 | for (var newId in loadAdditions) { 455 | loadSet[newId] = true; 456 | } 457 | } 458 | 459 | /** @type {Loader} */ 460 | var loader = { 461 | getIds: function () { 462 | var ids = []; 463 | loader.load(function (id) { 464 | ids.push(id); 465 | }); 466 | return ids; 467 | }, 468 | load: function (loadComponent, chainer) { 469 | return loadComponentsInOrder(dependencyResolver, loadSet, loadComponent, chainer); 470 | } 471 | }; 472 | 473 | return loader; 474 | } 475 | 476 | return getLoader; 477 | })(); 478 | 479 | if (typeof module !== 'undefined') { 480 | module.exports = getLoader; 481 | } 482 | " 483 | `; 484 | -------------------------------------------------------------------------------- /src/descriptionFormatter.ts: -------------------------------------------------------------------------------- 1 | import { format, BuiltInParserName } from "prettier"; 2 | import { DESCRIPTION, EXAMPLE, TODO } from "./tags.js"; 3 | import { AllOptions } from "./types.js"; 4 | import { capitalizer, formatCode } from "./utils.js"; 5 | import { Root, Content, Link, Image, Text, List } from "mdast"; 6 | import { TAGS_PEV_FORMATE_DESCRIPTION } from "./roles.js"; 7 | import { fromMarkdown } from "mdast-util-from-markdown"; 8 | 9 | const TABLE = "2@^5!~#sdE!_TABLE"; 10 | 11 | interface DescriptionEndLineParams { 12 | tag: string; 13 | isEndTag: boolean; 14 | } 15 | 16 | const parserSynonyms = (lang: string): BuiltInParserName[] => { 17 | switch (lang) { 18 | case "js": 19 | case "javascript": 20 | case "jsx": 21 | return ["babel", "babel-flow", "vue"]; 22 | case "ts": 23 | case "typescript": 24 | case "tsx": 25 | return ["typescript", "babel-ts", "angular"]; 26 | case "json": 27 | case "css": 28 | return ["css"]; 29 | case "less": 30 | return ["less"]; 31 | case "scss": 32 | return ["scss"]; 33 | case "html": 34 | return ["html"]; 35 | case "yaml": 36 | return ["yaml"]; 37 | default: 38 | return ["babel"]; 39 | } 40 | }; 41 | 42 | function descriptionEndLine({ 43 | tag, 44 | isEndTag, 45 | }: DescriptionEndLineParams): string { 46 | if ([DESCRIPTION, EXAMPLE, TODO].includes(tag) && !isEndTag) { 47 | return "\n"; 48 | } 49 | 50 | return ""; 51 | } 52 | 53 | interface FormatOptions { 54 | tagStringLength?: number; 55 | beginningSpace: string; 56 | } 57 | 58 | /** 59 | * Trim, make single line with capitalized text. Insert dot if flag for it is 60 | * set to true and last character is a word character 61 | * 62 | * @private 63 | */ 64 | async function formatDescription( 65 | tag: string, 66 | text: string, 67 | options: AllOptions, 68 | formatOptions: FormatOptions, 69 | ): Promise { 70 | if (!text) return text; 71 | 72 | const { printWidth } = options; 73 | const { tagStringLength = 0, beginningSpace } = formatOptions; 74 | const originalText = text; // Save original text for nowrap mode 75 | 76 | /** 77 | * change list with dash to dot for example: 78 | * 1- a thing 79 | * 80 | * to 81 | * 82 | * 1. a thing 83 | */ 84 | text = text.replace(/^(\d+)[-][\s|]+/g, "$1. "); // Start 85 | text = text.replace(/\n+(\s*\d+)[-][\s]+/g, "\n$1. "); 86 | 87 | const fencedCodeBlocks = text.matchAll(/```\S*?\n[\s\S]+?```/gm); 88 | const indentedCodeBlocks = text.matchAll( 89 | /^\r?\n^(?:(?:(?:[ ]{4}|\t).*(?:\r?\n|$))+)/gm, 90 | ); 91 | const allCodeBlocks = [...fencedCodeBlocks, ...indentedCodeBlocks]; 92 | const tables: string[] = []; 93 | text = text.replace( 94 | /((\n|^)\|[\s\S]*?)((\n[^|])|$)/g, 95 | (code, _1, _2, _3, _, offs: number) => { 96 | // If this potential table is inside a code block, don't touch it 97 | for (const block of allCodeBlocks) { 98 | if ( 99 | block.index !== undefined && 100 | block.index <= offs + 1 && 101 | offs + code.length + 1 <= block.index + block[0].length 102 | ) { 103 | return code; 104 | } 105 | } 106 | 107 | code = _3 ? code.slice(0, -1) : code; 108 | 109 | tables.push(code); 110 | return `\n\n${TABLE}\n\n${_3 ? _3.slice(1) : ""}`; 111 | }, 112 | ); 113 | 114 | if ( 115 | options.jsdocCapitalizeDescription && 116 | !TAGS_PEV_FORMATE_DESCRIPTION.includes(tag) 117 | ) { 118 | text = capitalizer(text); 119 | } 120 | 121 | text = `${tagStringLength ? `${"!".repeat(tagStringLength - 1)}?` : ""}${ 122 | text.startsWith("```") ? "\n" : "" 123 | }${text}`; 124 | 125 | let tableIndex = 0; 126 | 127 | text = text.replace( 128 | new RegExp("\\n" + `[\u0020]{${beginningSpace.length}}`, "g"), 129 | "\n", 130 | ); 131 | 132 | const rootAst = fromMarkdown(text); 133 | 134 | async function stringifyASTWithoutChildren( 135 | mdAst: Content | Root, 136 | intention: string, 137 | parent: Content | Root | null, 138 | ) { 139 | if (mdAst.type === "inlineCode") { 140 | return `\`${mdAst.value}\``; 141 | } 142 | 143 | if (mdAst.type === "code") { 144 | let result = mdAst.value || ""; 145 | let _intention = intention; 146 | 147 | if (result) { 148 | // Remove two space from lines, maybe added previous format 149 | if (mdAst.lang) { 150 | const supportParsers = parserSynonyms(mdAst.lang.toLowerCase()); 151 | const parser = supportParsers?.includes(options.parser as any) 152 | ? options.parser 153 | : supportParsers?.[0] || mdAst.lang; 154 | 155 | result = await formatCode(result, intention, { 156 | ...options, 157 | parser, 158 | jsdocKeepUnParseAbleExampleIndent: true, 159 | }); 160 | } else if (options.jsdocPreferCodeFences || false) { 161 | result = await formatCode(result, _intention, { 162 | ...options, 163 | jsdocKeepUnParseAbleExampleIndent: true, 164 | }); 165 | } else { 166 | _intention = intention + " ".repeat(4); 167 | 168 | result = await formatCode(result, _intention, { 169 | ...options, 170 | jsdocKeepUnParseAbleExampleIndent: true, 171 | }); 172 | } 173 | } 174 | const addFence = options.jsdocPreferCodeFences || !!mdAst.lang; 175 | result = addFence ? result : result.trimEnd(); 176 | return result 177 | ? addFence 178 | ? `\n\n${_intention}\`\`\`${mdAst.lang || ""}${result}\`\`\`` 179 | : `\n${result}` 180 | : ""; 181 | } 182 | 183 | if ((mdAst as Text).value === TABLE) { 184 | if (parent) { 185 | (parent as any).costumeType = TABLE; 186 | } 187 | 188 | if (tables.length > 0) { 189 | let result = tables?.[tableIndex] || ""; 190 | tableIndex++; 191 | if (result) { 192 | result = ( 193 | await format(result, { 194 | ...options, 195 | parser: "markdown", 196 | }) 197 | ).trim(); 198 | } 199 | return `${ 200 | result 201 | ? `\n\n${intention}${result.split("\n").join(`\n${intention}`)}` 202 | : (mdAst as Text).value 203 | }`; 204 | } 205 | } 206 | 207 | if (mdAst.type === "break") { 208 | return `\\\n`; 209 | } 210 | 211 | return ((mdAst as Text).value || 212 | (mdAst as Link).title || 213 | (mdAst as Image).alt || 214 | "") as string; 215 | } 216 | 217 | async function stringyfy( 218 | mdAst: Content | Root, 219 | intention: string, 220 | parent: Content | Root | null, 221 | ): Promise { 222 | if (!Array.isArray((mdAst as Root).children)) { 223 | return stringifyASTWithoutChildren(mdAst, intention, parent); 224 | } 225 | 226 | return ( 227 | await Promise.all( 228 | ((mdAst as Root).children as Content[]).map(async (ast, index) => { 229 | switch (ast.type) { 230 | case "listItem": { 231 | let _listCount = `\n${intention}- `; 232 | // .replace(/((?!(^))\n)/g, "\n" + _intention); 233 | if (typeof (mdAst as List).start === "number") { 234 | const count = index + (((mdAst as List).start as number) ?? 1); 235 | _listCount = `\n${intention}${count}. `; 236 | } 237 | 238 | const _intention = intention + " ".repeat(_listCount.length - 1); 239 | 240 | const result = (await stringyfy(ast, _intention, mdAst)).trim(); 241 | 242 | return `${_listCount}${result}`; 243 | } 244 | 245 | case "list": { 246 | let end = ""; 247 | /** 248 | * Add empty line after list if that is end of description 249 | * issue: {@link https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/98} 250 | */ 251 | if ( 252 | tag !== DESCRIPTION && 253 | mdAst.type === "root" && 254 | index === mdAst.children.length - 1 255 | ) { 256 | end = "\n"; 257 | } 258 | return `\n${await stringyfy(ast, intention, mdAst)}${end}`; 259 | } 260 | 261 | case "paragraph": { 262 | const paragraph = await stringyfy(ast, intention, parent); 263 | if ((ast as any).costumeType === TABLE) { 264 | return paragraph; 265 | } 266 | 267 | return `\n\n${paragraph 268 | /** 269 | * Break by backslash\ 270 | * issue: https://github.com/hosseinmd/prettier-plugin-jsdoc/issues/102 271 | */ 272 | .split("\\\n") 273 | .map((_paragraph) => { 274 | const links: string[] = []; 275 | // Find jsdoc links and remove spaces 276 | _paragraph = _paragraph.replace( 277 | /{@(link|linkcode|linkplain)[\s](([^{}])*)}/g, 278 | (_, tag: string, link: string) => { 279 | links.push(link); 280 | 281 | return `{@${tag}${"_".repeat(link.length)}}`; 282 | }, 283 | ); 284 | 285 | // In balance mode, check if we should preserve original line breaks 286 | // Helper: Apply capitalization to first character if needed 287 | const applyCapitalization = (text: string): string => { 288 | const shouldCapitalize = 289 | options.jsdocCapitalizeDescription && 290 | !TAGS_PEV_FORMATE_DESCRIPTION.includes(tag); 291 | return shouldCapitalize ? capitalizer(text) : text; 292 | }; 293 | 294 | // Helper: Add trailing dot if needed 295 | const applyTrailingDot = (text: string): string => { 296 | return options.jsdocDescriptionWithDot 297 | ? text.replace(/([\w\p{L}])$/u, "$1.") 298 | : text; 299 | }; 300 | 301 | // Helper: Format text with standard transformations 302 | const applyStandardFormatting = (text: string): string => { 303 | return applyTrailingDot(applyCapitalization(text)); 304 | }; 305 | 306 | // Helper: Join lines with proper indentation 307 | const joinWithIndentation = (lines: string[]): string => { 308 | const indentedLines = lines.map( 309 | (line) => `${intention}${line}`, 310 | ); 311 | return indentedLines.join("\n"); 312 | }; 313 | 314 | // Helper: Apply greedy wrapping 315 | const applyGreedyWrapping = (text: string): string => { 316 | const singleLine = text.replace(/\s+/g, " "); 317 | const formatted = applyStandardFormatting(singleLine); 318 | return breakDescriptionToLines( 319 | formatted, 320 | printWidth, 321 | intention, 322 | ); 323 | }; 324 | 325 | // Main logic: Determine wrapping strategy 326 | const isBalanceMode = 327 | options.jsdocLineWrappingStyle === "balance"; 328 | const originalHasLineBreaks = originalText.includes("\n"); 329 | const shouldTryBalanceMode = 330 | isBalanceMode && originalHasLineBreaks; 331 | 332 | let result: string; 333 | 334 | if (shouldTryBalanceMode) { 335 | const originalLines = originalText 336 | .split("\n") 337 | .map((line) => line.trim()) 338 | .filter((line) => line.length > 0); 339 | 340 | const effectiveMaxWidth = 341 | tag === DESCRIPTION && tagStringLength > 0 342 | ? printWidth - tagStringLength 343 | : printWidth - intention.length; 344 | 345 | const allLinesFit = originalLines.every( 346 | (line) => line.length <= effectiveMaxWidth, 347 | ); 348 | const hasMultipleLines = originalLines.length > 1; 349 | 350 | if (allLinesFit && hasMultipleLines) { 351 | // Preserve original line breaks 352 | const formattedLines = originalLines.map( 353 | (line, index) => { 354 | const isFirstLine = index === 0; 355 | const isLastLine = index === originalLines.length - 1; 356 | 357 | let formatted = line; 358 | if (isFirstLine) { 359 | formatted = applyCapitalization(formatted); 360 | } 361 | if (isLastLine) { 362 | formatted = applyTrailingDot(formatted); 363 | } 364 | return formatted; 365 | }, 366 | ); 367 | 368 | result = joinWithIndentation(formattedLines); 369 | } else { 370 | // Fall back to greedy wrapping 371 | result = applyGreedyWrapping(_paragraph); 372 | } 373 | } else { 374 | // Default greedy wrapping mode 375 | result = applyGreedyWrapping(_paragraph); 376 | } 377 | 378 | // Replace links 379 | result = result.replace( 380 | /{@(link|linkcode|linkplain)([_]+)}/g, 381 | (original: string, tag: string, underline: string) => { 382 | const link = links[0]; 383 | 384 | if (link.length === underline.length) { 385 | links.shift(); 386 | return `{@${tag} ${link}}`; 387 | } 388 | 389 | return original; 390 | }, 391 | ); 392 | 393 | return result; 394 | }) 395 | .join("\\\n")}`; 396 | } 397 | 398 | case "strong": { 399 | return `**${await stringyfy(ast, intention, mdAst)}**`; 400 | } 401 | 402 | case "emphasis": { 403 | return `_${await stringyfy(ast, intention, mdAst)}_`; 404 | } 405 | 406 | case "heading": { 407 | return `\n\n${intention}${"#".repeat( 408 | ast.depth, 409 | )} ${await stringyfy(ast, intention, mdAst)}`; 410 | } 411 | 412 | case "link": 413 | case "image": { 414 | return `[${await stringyfy(ast, intention, mdAst)}](${ast.url})`; 415 | } 416 | 417 | case "linkReference": { 418 | return `[${await stringyfy(ast, intention, mdAst)}][${ 419 | ast.label 420 | }]`; 421 | } 422 | case "definition": { 423 | return `\n\n[${ast.label}]: ${ast.url}`; 424 | } 425 | 426 | case "blockquote": { 427 | const paragraph = await stringyfy(ast, "", mdAst); 428 | return `\n\n> ${paragraph 429 | .trim() 430 | .replace(/(\n+)/g, `$1${intention}> `)}`; 431 | } 432 | } 433 | return stringyfy(ast, intention, mdAst); 434 | }), 435 | ) 436 | ).join(""); 437 | } 438 | 439 | let result = await stringyfy(rootAst, beginningSpace, null); 440 | 441 | result = result.replace(/^[\s\n]+/g, ""); 442 | result = result.replace(/^([!]+\?)/g, ""); 443 | 444 | return result; 445 | } 446 | 447 | function breakDescriptionToLines( 448 | desContent: string, 449 | maxWidth: number, 450 | beginningSpace: string, 451 | ): string { 452 | let str = desContent.trim(); 453 | 454 | if (!str) { 455 | return str; 456 | } 457 | 458 | let result = ""; 459 | while (str.length > maxWidth) { 460 | let sliceIndex = str.lastIndexOf( 461 | " ", 462 | str.startsWith("\n") ? maxWidth + 1 : maxWidth, 463 | ); 464 | /** 465 | * When a str is a long word lastIndexOf will gives 4 every time loop 466 | * running unlimited time 467 | */ 468 | if (sliceIndex <= beginningSpace.length) 469 | sliceIndex = str.indexOf(" ", beginningSpace.length + 1); 470 | 471 | if (sliceIndex === -1) sliceIndex = str.length; 472 | 473 | result += str.substring(0, sliceIndex); 474 | str = str.substring(sliceIndex + 1); 475 | if (str) { 476 | str = `${beginningSpace}${str}`; 477 | str = `\n${str}`; 478 | } 479 | } 480 | 481 | result += str; 482 | 483 | return `${beginningSpace}${result}`; 484 | } 485 | 486 | export { descriptionEndLine, FormatOptions, formatDescription }; 487 | --------------------------------------------------------------------------------