From 08fc3a21b172038517c83572cf786048cf458ed0 Mon Sep 17 00:00:00 2001 From: David Anson Date: Wed, 25 Mar 2026 21:04:58 -0700 Subject: [PATCH] Add configPointer input, examples for package.json/pyproject.toml. --- .github/dictionary.txt | 2 + .github/workflows/test.yml | 30 ++++ README.md | 29 ++++ action.yml | 4 + config/package.json | 9 + config/pyproject.toml | 5 + dist/index.mjs | 314 ++++++++++++++++++++++++++++++++++- eslint.config.mjs | 3 +- markdownlint-cli2-action.mjs | 8 +- 9 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 config/package.json create mode 100644 config/pyproject.toml diff --git a/.github/dictionary.txt b/.github/dictionary.txt index e5c0979..4e954a1 100644 --- a/.github/dictionary.txt +++ b/.github/dictionary.txt @@ -1,5 +1,7 @@ CommonMark config +configPointer +JSON LLMs markdownlint-cli2-action npm diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c8a9f59..572a720 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -96,6 +96,36 @@ jobs: id: test - run: exit 1 if: steps.test.outcome != 'failure' + package-json: + name: package.json (test/errors.md, 2 errors) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: rm .markdownlint.json + - uses: ./ + with: + config: 'config/package.json' + configPointer: '/markdownlint-cli2' + globs: 'test/*.md' + continue-on-error: true + id: test + - run: exit 1 + if: steps.test.outcome != 'failure' + pyproject-toml: + name: pyproject.toml (test/errors.md, 2 errors) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - run: rm .markdownlint.json + - uses: ./ + with: + config: 'config/pyproject.toml' + configPointer: '/tool/markdownlint-cli2' + globs: 'test/*.md' + continue-on-error: true + id: test + - run: exit 1 + if: steps.test.outcome != 'failure' fix: name: fix (test/errors.md, 0 errors) runs-on: ubuntu-latest diff --git a/README.md b/README.md index 1cdb44f..7729a28 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,14 @@ Path of a file to use for the base configuration object (defaults to none) Equivalent to using the `--config` [command-line option][command-line] and passing the specified configuration file. +### configPointer (optional) + +[JSON Pointer][json-pointer] to a configuration object within the `--config` +file (defaults to none) + +Equivalent to using the `--configPointer` [command-line option][command-line] +and passing the specified JSON Pointer. + ### fix (optional) Whether to fix supported issues automatically (any truthy value enables) @@ -100,6 +108,26 @@ To specify a custom configuration file: globs: '**/*.md' ``` +To specify an embedded object in `package.json`: + +```yaml +- uses: DavidAnson/markdownlint-cli2-action@v22 + with: + config: 'package.json' + configPointer: '/markdownlint-cli2' + globs: '**/*.md' +``` + +To specify an embedded object in `pyproject.toml`: + +```yaml +- uses: DavidAnson/markdownlint-cli2-action@v22 + with: + config: 'pyproject.toml' + configPointer: '/tool/markdownlint-cli2' + globs: '**/*.md' +``` + To prevent linting issues from failing the workflow run: ```yaml @@ -119,6 +147,7 @@ and/or gradually introducing linting rules to a new repository). [commonmark]: https://commonmark.org/ [example-yml]: .github/workflows/example.yml [glob-syntax]: https://github.com/DavidAnson/markdownlint-cli2#use +[json-pointer]: https://datatracker.ietf.org/doc/html/rfc6901 [markdown]: https://wikipedia.org/wiki/Markdown [markdownlint]: https://github.com/DavidAnson/markdownlint [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 diff --git a/action.yml b/action.yml index c27f5cc..026e5ac 100644 --- a/action.yml +++ b/action.yml @@ -9,6 +9,10 @@ inputs: description: Path of a file to use for the base configuration object (defaults to none) default: '' required: false + configPointer: + description: JSON Pointer to a configuration object within the --config file (defaults to none) + default: '' + required: false fix: description: Whether to fix supported issues automatically (any truthy value enables) default: '' diff --git a/config/package.json b/config/package.json new file mode 100644 index 0000000..5caf763 --- /dev/null +++ b/config/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + + "markdownlint-cli2": { + "config": { + "single-trailing-newline": false + } + } +} diff --git a/config/pyproject.toml b/config/pyproject.toml new file mode 100644 index 0000000..33265f7 --- /dev/null +++ b/config/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "my-project" + +[tool.markdownlint-cli2.config] +single-trailing-newline = false diff --git a/dist/index.mjs b/dist/index.mjs index eaa45b5..29d8426 100644 --- a/dist/index.mjs +++ b/dist/index.mjs @@ -7192,6 +7192,8 @@ const path = __nccwpck_require__(6928); const WIN_SLASH = '\\\\/'; const WIN_NO_SLASH = `[^${WIN_SLASH}]`; +const DEFAULT_MAX_EXTGLOB_RECURSION = 0; + /** * Posix glob regex */ @@ -7255,6 +7257,7 @@ const WINDOWS_CHARS = { */ const POSIX_REGEX_SOURCE = { + __proto__: null, alnum: 'a-zA-Z0-9', alpha: 'a-zA-Z', ascii: '\\x00-\\x7F', @@ -7272,6 +7275,7 @@ const POSIX_REGEX_SOURCE = { }; module.exports = { + DEFAULT_MAX_EXTGLOB_RECURSION, MAX_LENGTH: 1024 * 64, POSIX_REGEX_SOURCE, @@ -7285,6 +7289,7 @@ module.exports = { // Replace globs with equivalent patterns to reduce parsing time. REPLACEMENTS: { + __proto__: null, '***': '*', '**/**': '**', '**/**/**': '**' @@ -7419,6 +7424,277 @@ const syntaxError = (type, char) => { return `Missing ${type}: "${char}" - use "\\\\${char}" to match literal characters`; }; +const splitTopLevel = input => { + const parts = []; + let bracket = 0; + let paren = 0; + let quote = 0; + let value = ''; + let escaped = false; + + for (const ch of input) { + if (escaped === true) { + value += ch; + escaped = false; + continue; + } + + if (ch === '\\') { + value += ch; + escaped = true; + continue; + } + + if (ch === '"') { + quote = quote === 1 ? 0 : 1; + value += ch; + continue; + } + + if (quote === 0) { + if (ch === '[') { + bracket++; + } else if (ch === ']' && bracket > 0) { + bracket--; + } else if (bracket === 0) { + if (ch === '(') { + paren++; + } else if (ch === ')' && paren > 0) { + paren--; + } else if (ch === '|' && paren === 0) { + parts.push(value); + value = ''; + continue; + } + } + } + + value += ch; + } + + parts.push(value); + return parts; +}; + +const isPlainBranch = branch => { + let escaped = false; + + for (const ch of branch) { + if (escaped === true) { + escaped = false; + continue; + } + + if (ch === '\\') { + escaped = true; + continue; + } + + if (/[?*+@!()[\]{}]/.test(ch)) { + return false; + } + } + + return true; +}; + +const normalizeSimpleBranch = branch => { + let value = branch.trim(); + let changed = true; + + while (changed === true) { + changed = false; + + if (/^@\([^\\()[\]{}|]+\)$/.test(value)) { + value = value.slice(2, -1); + changed = true; + } + } + + if (!isPlainBranch(value)) { + return; + } + + return value.replace(/\\(.)/g, '$1'); +}; + +const hasRepeatedCharPrefixOverlap = branches => { + const values = branches.map(normalizeSimpleBranch).filter(Boolean); + + for (let i = 0; i < values.length; i++) { + for (let j = i + 1; j < values.length; j++) { + const a = values[i]; + const b = values[j]; + const char = a[0]; + + if (!char || a !== char.repeat(a.length) || b !== char.repeat(b.length)) { + continue; + } + + if (a === b || a.startsWith(b) || b.startsWith(a)) { + return true; + } + } + } + + return false; +}; + +const parseRepeatedExtglob = (pattern, requireEnd = true) => { + if ((pattern[0] !== '+' && pattern[0] !== '*') || pattern[1] !== '(') { + return; + } + + let bracket = 0; + let paren = 0; + let quote = 0; + let escaped = false; + + for (let i = 1; i < pattern.length; i++) { + const ch = pattern[i]; + + if (escaped === true) { + escaped = false; + continue; + } + + if (ch === '\\') { + escaped = true; + continue; + } + + if (ch === '"') { + quote = quote === 1 ? 0 : 1; + continue; + } + + if (quote === 1) { + continue; + } + + if (ch === '[') { + bracket++; + continue; + } + + if (ch === ']' && bracket > 0) { + bracket--; + continue; + } + + if (bracket > 0) { + continue; + } + + if (ch === '(') { + paren++; + continue; + } + + if (ch === ')') { + paren--; + + if (paren === 0) { + if (requireEnd === true && i !== pattern.length - 1) { + return; + } + + return { + type: pattern[0], + body: pattern.slice(2, i), + end: i + }; + } + } + } +}; + +const getStarExtglobSequenceOutput = pattern => { + let index = 0; + const chars = []; + + while (index < pattern.length) { + const match = parseRepeatedExtglob(pattern.slice(index), false); + + if (!match || match.type !== '*') { + return; + } + + const branches = splitTopLevel(match.body).map(branch => branch.trim()); + if (branches.length !== 1) { + return; + } + + const branch = normalizeSimpleBranch(branches[0]); + if (!branch || branch.length !== 1) { + return; + } + + chars.push(branch); + index += match.end + 1; + } + + if (chars.length < 1) { + return; + } + + const source = chars.length === 1 + ? utils.escapeRegex(chars[0]) + : `[${chars.map(ch => utils.escapeRegex(ch)).join('')}]`; + + return `${source}*`; +}; + +const repeatedExtglobRecursion = pattern => { + let depth = 0; + let value = pattern.trim(); + let match = parseRepeatedExtglob(value); + + while (match) { + depth++; + value = match.body.trim(); + match = parseRepeatedExtglob(value); + } + + return depth; +}; + +const analyzeRepeatedExtglob = (body, options) => { + if (options.maxExtglobRecursion === false) { + return { risky: false }; + } + + const max = + typeof options.maxExtglobRecursion === 'number' + ? options.maxExtglobRecursion + : constants.DEFAULT_MAX_EXTGLOB_RECURSION; + + const branches = splitTopLevel(body).map(branch => branch.trim()); + + if (branches.length > 1) { + if ( + branches.some(branch => branch === '') || + branches.some(branch => /^[*?]+$/.test(branch)) || + hasRepeatedCharPrefixOverlap(branches) + ) { + return { risky: true }; + } + } + + for (const branch of branches) { + const safeOutput = getStarExtglobSequenceOutput(branch); + if (safeOutput) { + return { risky: true, safeOutput }; + } + + if (repeatedExtglobRecursion(branch) > max) { + return { risky: true }; + } + } + + return { risky: false }; +}; + /** * Parse the given input string. * @param {String} input @@ -7600,6 +7876,8 @@ const parse = (input, options) => { token.prev = prev; token.parens = state.parens; token.output = state.output; + token.startIndex = state.index; + token.tokensIndex = tokens.length; const output = (opts.capture ? '(' : '') + token.open; increment('parens'); @@ -7609,6 +7887,34 @@ const parse = (input, options) => { }; const extglobClose = token => { + const literal = input.slice(token.startIndex, state.index + 1); + const body = input.slice(token.startIndex + 2, state.index); + const analysis = analyzeRepeatedExtglob(body, opts); + + if ((token.type === 'plus' || token.type === 'star') && analysis.risky) { + const safeOutput = analysis.safeOutput + ? (token.output ? '' : ONE_CHAR) + (opts.capture ? `(${analysis.safeOutput})` : analysis.safeOutput) + : undefined; + const open = tokens[token.tokensIndex]; + + open.type = 'text'; + open.value = literal; + open.output = safeOutput || utils.escapeRegex(literal); + + for (let i = token.tokensIndex + 1; i < tokens.length; i++) { + tokens[i].value = ''; + tokens[i].output = ''; + delete tokens[i].suffix; + } + + state.output = token.output + open.output; + state.backtrack = true; + + push({ type: 'paren', extglob: true, value, output: '' }); + decrement('parens'); + return; + } + let output = token.close + (opts.capture ? ')' : ''); let rest; @@ -80009,7 +80315,7 @@ const markdownlint_cli2_main = async (/** @type {Parameters} */ params) => { const logMessage = info; -const outputFormatter = (options) => { +const outputFormatter = (/** @type {any} */ options) => { const { results } = options; for (const lintError of results) { const { @@ -80030,6 +80336,7 @@ const outputFormatter = (options) => { const information = ruleInformation ? ` ${ruleInformation}` : ""; const message = `${fileName}${line}${column} ${name} ${ruleDescription}${detail}${context}${information}`; + /** @type {import("@actions/core").AnnotationProperties} */ const annotation = { "title": ruleDescription, "file": fileName, @@ -80058,6 +80365,10 @@ const config = getInput("config"); if (config) { argv.push("--config", config); } +const configPointer = getInput("configPointer"); +if (configPointer) { + argv.push("--configPointer", configPointer); +} const fix = Boolean(getInput("fix")); if (fix) { argv.push("--fix"); @@ -80070,6 +80381,7 @@ const parameters = { "outputFormatters": [ [ outputFormatter ] ] } }; +// @ts-ignore markdownlint_cli2_main(parameters).then( (code) => code && setFailed(`Failed with exit code: ${code}`), (error) => setFailed(`Failed due to error: ${error}`) diff --git a/eslint.config.mjs b/eslint.config.mjs index fb7bce2..04d15ee 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -16,13 +16,14 @@ export default [ "quotes": "double", "semi": true }), - eslintPluginUnicorn.configs["flat/all"], + eslintPluginUnicorn.configs.all, { "linterOptions": { "reportUnusedDisableDirectives": true }, "rules": { "max-statements": "off", + "no-inline-comments": "off", "no-magic-numbers": "off", "no-ternary": "off", "one-var": "off", diff --git a/markdownlint-cli2-action.mjs b/markdownlint-cli2-action.mjs index 5dacada..438b949 100644 --- a/markdownlint-cli2-action.mjs +++ b/markdownlint-cli2-action.mjs @@ -4,7 +4,7 @@ import * as core from "@actions/core"; import { main as markdownlintCli2 } from "markdownlint-cli2"; const logMessage = core.info; -const outputFormatter = (options) => { +const outputFormatter = (/** @type {any} */ options) => { const { results } = options; for (const lintError of results) { const { @@ -25,6 +25,7 @@ const outputFormatter = (options) => { const information = ruleInformation ? ` ${ruleInformation}` : ""; const message = `${fileName}${line}${column} ${name} ${ruleDescription}${detail}${context}${information}`; + /** @type {import("@actions/core").AnnotationProperties} */ const annotation = { "title": ruleDescription, "file": fileName, @@ -53,6 +54,10 @@ const config = core.getInput("config"); if (config) { argv.push("--config", config); } +const configPointer = core.getInput("configPointer"); +if (configPointer) { + argv.push("--configPointer", configPointer); +} const fix = Boolean(core.getInput("fix")); if (fix) { argv.push("--fix"); @@ -65,6 +70,7 @@ const parameters = { "outputFormatters": [ [ outputFormatter ] ] } }; +// @ts-ignore markdownlintCli2(parameters).then( (code) => code && core.setFailed(`Failed with exit code: ${code}`), (error) => core.setFailed(`Failed due to error: ${error}`)