diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..cfe5424 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,39 @@ +const rules = require('@silverstripe/eslint-config/.eslintrc'); + +rules.plugins = ['markdown']; +rules.overrides = [ + { + files: ['**/*.md'], + processor: 'markdown/markdown' + }, + { + files: ['**/*.md/*.js'], + parserOptions: { + ecmaFeatures: { + impliedStrict: true + } + }, + settings: { + react: { + version: '16' + } + }, + rules: { + // These rules are not appropriate for linting markdown code blocks + 'lines-around-comment': 'off', + 'import/no-unresolved': 'off', + 'import/extensions': 'off', + 'react/jsx-no-undef': 'off', + 'no-undef': 'off', + 'no-unused-expressions': 'off', + 'no-unused-vars': 'off', + 'brace-style': 'off', // it's useful to have comments before the else block + // These rules are disabled because they are difficult to adhere to right now + 'jsx-a11y/label-has-associated-control': 'off', + 'react/prefer-stateless-function': 'off', + } + } +]; +rules.ignorePatterns = ['**/node_modules/**', '**/.eslintrc.js']; + +module.exports = rules; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f237ac6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules/ +/vendor/ +composer.lock +yarn.lock diff --git a/.markdownlint-cli2.mjs b/.markdownlint-cli2.mjs new file mode 100644 index 0000000..7b9004f --- /dev/null +++ b/.markdownlint-cli2.mjs @@ -0,0 +1,14 @@ + +import markdownlint from 'markdownlint'; +import enhancedProperNames from 'markdownlint-rule-enhanced-proper-names/src/enhanced-proper-names.js'; +import titleCaseStyle from 'markdownlint-rule-title-case-style'; +import { load } from 'js-yaml'; + +export default { + ignores: ['**/node_modules/**'], + customRules: [ + enhancedProperNames, + titleCaseStyle, + ], + config: markdownlint.readConfigSync('./.markdownlint.yml', [ load ]), +}; diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..0978879 --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,155 @@ +# Enable all rules with default settings as a baseline +default: true + +# MD041: Ignore the frontmatter (metadata) title when checking for H1s +first-line-h1: + front_matter_title: '' + +# MD025: Ignore the frontmatter (metadata) title when checking for H1s +single-h1: + front_matter_title: '' + +# MD003: Enforce ATX style headings +heading-style: + style: 'atx' + +# MD049: Use asterisks for italics +emphasis-style: + style: 'asterisk' + +# MD050: Use asterisks for bold +strong-style: + style: 'asterisk' + +# MD004: Use hyphens for unordered lists +ul-style: + style: 'dash' + +# MD029: Always use 1. for ordered lists +ol-prefix: + style: 'one' + +# MD013: Disable line-length rule for now as it touches too many lines of doc +line-length: false +# line_length: 120 + +# MD010: Use two spaces for each tab (default was 1) +no-hard-tabs: + spaces_per_tab: 2 + +# MD031: Don't require empty lines after code blocks in lists +blanks-around-fences: + list_items: false + +# MD035: Enforce a style for horizontal rules. +# Hyphens would be confusing since we use those for frontmatter (metadata) +hr-style: + style: '___' + +# MD046: Don't allow indented codeblocks +code-block-style: + style: 'fenced' + +# MD048: Use backticks for codeblocks +code-fence-style: + style: 'backtick' + +# MD040: Explicitly only allow some languages for code blocks +# This helps with consistency (e.g. avoid having both yml and yaml) +fenced-code-language: + language_only: true + allowed_languages: + - 'bash' # use this instead of shell or env + - 'css' + - 'diff' + - 'graphql' + - 'html' + - 'js' + - 'json' + - 'php' + - 'scss' + - 'ss' + - 'sql' + - 'text' + - 'xml' + - 'yml' + +# MD044: Disable in favour of the enhanced version which ignores custom anchors for headings +# markdownlint-rule-enhanced-proper-names: Enforces capitalisation for specific names +proper-names: off +enhanced-proper-names: + code_blocks: false + heading_id: false + names: + - 'API' + - 'type/api-break' # the GitHub label + - 'CI' + - 'CMS' + - '/cms' # e.g. "silverstripe/cms" + - '-cms' # e.g. "silverstripe/recipe-cms" + - 'CSS' + - 'GitHub' + - 'GraphQL' + - '/graphql' # e.g. "silverstripe/graphql" + - 'HTTP' + - 'JavaScript' + - 'JS' + - '.js' # e.g. "Node.js" + - 'jQuery' + - 'ORM' + - 'PHP' + - 'php-' # e.g. "php-intl extension" + - 'SCSS' + - 'Silverstripe' + - 'silverstripe/' # e.g. "silverstripe/framework" + - 'silverstripe-' # e.g. "silverstripe-vendormodule" + - '@silverstripe.org' + - 'TinyMCE' + - 'UI' + - 'URL' + - 'YAML' + +# markdownlint-rule-title-case-style: Use sentence-style headings +title-case-style: + case: 'sentence' + # commas in the following list are intentional and necessary since the plugin makes no distinction + # between words and punctuation + ignore: + - 'Apache' + - 'APIs' + - 'Composer' + - 'CTE' + - 'GitHub' + - 'GraphQL' + - 'Huntr' + - 'JavaScript' + - 'I' + - 'InnoDB' + - 'Git' + - 'jQuery' + - 'jQuery,' + - 'Lighttpd' + - 'MyISAM' + - 'MySQL' + - 'Nginx' + - 'Nginx,' + - 'PHPUnit' + - 'RFCs' + - 'Silverstripe' + - 'TinyMCE' + - 'Transifex' + - 'URLs' + - 'WebP' + +# MD033: Allow specific HTML tags +no-inline-html: + allowed_elements: + # br is necessary for new lines in tables + - 'br' + # accordians are okay + - 'details' + - 'summary' + # description lists are okay + - 'dl' + - 'dd' + - 'dt' diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..3c03207 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18 diff --git a/README.md b/README.md index 9c884b7..631a499 100644 --- a/README.md +++ b/README.md @@ -1 +1,80 @@ -# Documentation lint \ No newline at end of file +# Documentation lint + +An abstraction around various documentation linters to make linting markdown documentation easy. + +> [!WARNING] +> This repository is intended for use by commercially supported Silverstripe CMS modules. Its linting rules are opinionated and may include rules which are specific to the documentation style and syntax used by those modules. + +## Installation + +Add this package as a dev dependency + +```bash +composer require --dev silverstripe/documentation-lint +``` + +## Setup + +### Required software + +You need `nvm` (or `node` installed with the right version - see `.nvmrc`) and `yarn` installed. + +> [!WARNING] +> If you're using a shell other than bash (e.g. zsh) you'll need to set your node version before running the linting script, since your `nvm` installation is probably not set up in a way that it can be used in bash. + +You also need `getopt` (enhanced) installed - which you probably do but the script will let you know if you don't. + +### Setup in the repository + +You must add a `.ss-docs-lint` file to the root of the repository which has markdown documentation you want linted. This file must contain a relative path to the directory which holds your documentation, and that path must end with a `/`. + +For example: +```text +docs/en/ +``` + +> [!WARNING] +> Only the first line of the file will be used. + +## Usage + +Simply run the command. By default it will assume you're running it from the directory that holds the `.ss-docs-lint` file and that you want to lint markdown, php, and javascript within `.md` files. + +```bash +vendor/bin/ss-docs-lint +``` + +To lint for a specific module (e.g. if you have multiple modules installed) you can pass the relative path to the folder containing the `.ss-docs-lint` file. + +```bash +vendor/bin/ss-docs-lint vendor/silverstripe/developer-docs +``` + +If you want to specifically only lint one of markdown syntax, PHP codeblocks, or JavaScript codeblocks, use the [appropriate flag(s)](#flags). + +```bash +vendor/bin/ss-docs-lint -jp +# or +vendor/bin/ss-docs-lint --with-js --with-php +``` + +To automatically fix any problems that can be automatically fixed, pass the `--fix` flag. + +```bash +vendor/bin/ss-docs-lint -f +# or +vendor/bin/ss-docs-lint --fix +``` + +### Flags + +The following flags can be used with the `ss-docs-lint` script: + +|long name|short name|description| +|---|---|---| +|`--fix`|`-f`|Fix any automatically fixable problems| +|`--with-md`|`-m`|Lint markdown syntax| +|`--with-js`|`-j`|Lint JavaScript code blocks| +|`--with-php`|`-p`|Lint PHP code blocks| + +If any language flag is passed, only the languages that are passed will be linted (or fixed if `--fix` is passed). If no language flags are passed, all languages will be linted. diff --git a/bin/ss-docs-lint b/bin/ss-docs-lint new file mode 100755 index 0000000..bb42aa5 --- /dev/null +++ b/bin/ss-docs-lint @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +SCRIPT_DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) +DOCS_LINT_DIR=$(dirname $SCRIPT_DIR) +ORIG_DIR=$(pwd) +cd $DOCS_LINT_DIR + +# Check we have the necessary binaries installed +which nvm >/dev/null 2>&1 +if [[ $? == 0 ]]; then + # Switch to correct node version + nvm use +else + # If nvm isn't installed, node has to be installed and already be the correct version + which node >/dev/null 2>&1 + if [[ $? == 0 ]]; then + NVMRC="${DOCS_LINT_DIR}/.nvmrc" + NODE_VERSION="$(head -n 1 $NVMRC)" + CURRENT_NODE_VERSION="$(node -v)" + NODE_VERSION_REGEX="/^v?${NODE_VERSION}(\.[0-9]+\.[0-9]+)?$/" + if [[ $CURRENT_NODE_VERSION =~ $NODE_VERSION_REGEX ]]; then + echo "nvm must be installed or node must be the correct version (currently ${CURRENT_NODE_VERSION}, needs to be ${NODE_VERSION})" + exit 1 + fi + else + echo "nvm and node must be installed" + exit 1 + fi +fi +which yarn >/dev/null 2>&1 +if [[ $? != 0 ]]; then + echo 'yarn must be installed for linting to work' + exit 1 +fi +which getopt >/dev/null 2>&1 +if [[ $? != 0 ]]; then + echo 'getopt (enhanced) must be installed to parse CLI flags' + exit 1 +fi + +# Modern linux should have the enhanced version by default but we should check just in case. +# Old getopt will exit with 0, new getopt will exit with 4 +getopt --test +if [[ $? != 4 ]]; then + echo 'getopt (enhanced) must be installed to parse CLI flags. You have the non-enhanced version.' + exit 1 +fi + +# Parse flags +DO_FIX=0 +WITH_MD=0 +WITH_PHP=0 +WITH_JS=0 +OPTS=$(getopt -o 'fmpj' --long 'fix,with-md,with-php,with-js' -n 'ss-docs-lint' -- "$@") +eval set -- "$OPTS" +# getopt adds a "--" after all of the options it has found, +# so "while true" is safe because we will always hit that break. +while true; do + case "$1" in + '-f'|'--fix') + DO_FIX=1 + shift 1 + continue + ;; + '-m'|'--with-md') + WITH_MD=1 + shift 1 + continue + ;; + '-p'|'--with-php') + WITH_PHP=1 + shift 1 + continue + ;; + '-j'|'--with-js') + WITH_JS=1 + shift 1 + continue + ;; + # Anything after "--" on its own can be ignored - it will all be args from here on + '--') + shift + break + ;; + # This should never be reached (getopt doesn't give us args in this loop) - but throw an error just in case + *) + echo "Internal error! got '$1'" >&2 + exit 1 + ;; + esac +done + +# lint everything if we're not told what to lint +if [[ $WITH_MD == 0 && $WITH_PHP == 0 && $WITH_JS == 0 ]]; then + WITH_MD=1 + WITH_PHP=1 + WITH_JS=1 +fi + +# If there's any arguments, the first arg should be the module directory +MODULE_DIR=$1 +if [[ -z $MODULE_DIR ]]; then + MODULE_DIR=$ORIG_DIR +fi + +# Get and validate the config file path +CONFIG_FILE="${MODULE_DIR}/.ss-docs-lint" +if ! [[ -f $CONFIG_FILE ]]; then + echo "No '.ss-docs-lint' file found at ${MODULE_DIR}" + exit 1 +fi + +# Get and validate the documentation directory +DOCS_DIR="${MODULE_DIR}/$(head -n 1 $CONFIG_FILE)" +if ! [[ $DOCS_DIR == *'/' ]]; then + echo "${DOCS_DIR} must end with / in the .ss-docs-lint file" + exit 1 +elif ! [[ -d $DOCS_DIR ]]; then + echo "${DOCS_DIR} is not a directory" + exit 1 +fi + +PHP_LINT="${COMPOSER_RUNTIME_BIN_DIR}/mdphpcs" + +# change to the docs directory, or else yarn may stop us from linting the files we want to lint +cd $DOCS_DIR + +# Prepare a cleanup function +cleanup () { + rm package.json >/dev/null 2>&1 + rm yarn.lock >/dev/null 2>&1 + rm yarn-error.log >/dev/null 2>&1 + rm .markdownlint-cli2.mjs >/dev/null 2>&1 + rm .markdownlint.yml >/dev/null 2>&1 + rm phpcs.xml >/dev/null 2>&1 + rm .eslintrc.js >/dev/null 2>&1 + rm -r node_modules/ >/dev/null 2>&1 +} + +# If linting markdown or js we need to install the yarn dependencies. +# These have to be in the directory that holds the docs, or a parent directory of it, +# otherwise the linting tools will refuse to lint for us. +if [[ $WITH_MD == 1 || $WITH_JS == 1 ]]; then + cp "${DOCS_LINT_DIR}/package.json" package.json + yarn install + YARN_EXIT=$? + if [[ $YARN_EXIT != 0 ]]; then + echo "Error installing npm dependencies" + cleanup + exit $YARN_EXIT + fi +fi + +EXIT_CODE=0 +if [[ $DO_FIX == 1 ]]; then + FLAGS='--fix' +fi + +# Lint markdown +if [[ $WITH_MD == 1 ]]; then + cp "${DOCS_LINT_DIR}/.markdownlint-cli2.mjs" .markdownlint-cli2.mjs + cp "${DOCS_LINT_DIR}/.markdownlint.yml" .markdownlint.yml + echo "linting markdown in docs" + yarn markdownlint-cli2 "${DOCS_DIR}**/*.md" $FLAGS + if [[ $? != 0 ]]; then + EXIT_CODE=1 + fi +fi + +# Lint PHP in docs +if [[ $WITH_PHP == 1 ]]; then + cp "${DOCS_LINT_DIR}/phpcs.xml" phpcs.xml + echo "linting php in docs" + $PHP_LINT $DOCS_DIR -p --colors --ignore=*/node_modules/* $FLAGS + if [[ $? != 0 ]]; then + EXIT_CODE=1 + fi +fi + +# Lint js in docs +if [[ $WITH_JS == 1 ]]; then + cp "${DOCS_LINT_DIR}/.eslintrc.js" .eslintrc.js + echo "linting javascript in docs" + yarn eslint $DOCS_DIR $FLAGS + if [[ $? != 0 ]]; then + EXIT_CODE=1 + fi +fi + +cleanup + +if [[ $DO_FIX == 1 ]]; then + echo 'Fixed all auto-fixable problems' +elif [[ $EXIT_CODE == 0 ]]; then + echo 'Linting passed successfully' +fi +exit $EXIT_CODE diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f398cca --- /dev/null +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "silverstripe/documentation-lint", + "description": "An abstraction around various documentation linters to make linting documentation easy for commercially supported Silverstripe CMS modules.", + "homepage": "https://silverstripe.org", + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Silverstripe", + "homepage": "https://silverstripe.com" + } + ], + "require": { + "silverstripe/markdown-php-codesniffer": "^1", + "slevomat/coding-standard": "^8.14" + }, + "bin": [ + "bin/ss-docs-lint" + ], + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..815652a --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "documentation-lint", + "license": "BSD-3-Clause", + "dependencies": { + "@silverstripe/eslint-config": "^1.1.0", + "eslint": "^8.52.0", + "eslint-plugin-markdown": "^3.0.1", + "js-yaml": "^4.1.0", + "markdownlint": "^0.31.1", + "markdownlint-cli2": "^0.10.0", + "markdownlint-rule-title-case-style": "^0.4.3", + "markdownlint-rule-enhanced-proper-names": "^0.0.1" + } +} diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..980ebb8 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,102 @@ + + + A sensible set of rules for producing clear documentation of PHP code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +