diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bd9de83 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +--- +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: npm + directory: / + schedule: + interval: weekly diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml new file mode 100644 index 0000000..4c0f98c --- /dev/null +++ b/.github/linters/.markdown-lint.yml @@ -0,0 +1,29 @@ +########################### +## Markdown Linter rules ## +########################### + +# Linter rules doc: +# - https://github.com/DavidAnson/markdownlint + +############### +# Rules by id # +############### +MD004: false # Unordered list style +MD007: + indent: 2 # Unordered list indentation +MD013: + line_length: 3000 # Line length +MD025: + front_matter_title: "" # Allow a body H1 alongside the front matter title +MD026: + punctuation: ".,;:!。,;:" # List of not allowed +MD029: false # Ordered list item prefix +MD033: false # Allow inline HTML +MD036: false # Emphasis used instead of a heading +MD041: false # First line in file should be a top level heading, PULL_REQUEST_TEMPLATE.md is an exception +MD060: false # Table column style — content mixes compact and aligned tables + +################# +# Rules by tags # +################# +blank_lines: false # Error on blank lines diff --git a/.github/scripts/validate-markdown.mjs b/.github/scripts/validate-markdown.mjs new file mode 100644 index 0000000..e90904b --- /dev/null +++ b/.github/scripts/validate-markdown.mjs @@ -0,0 +1,88 @@ +import { spawnSync } from 'node:child_process'; +import { lstatSync, readFileSync, realpathSync } from 'node:fs'; +import { isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { lint, readConfig } from 'markdownlint-cli2/markdownlint/promise'; +import yamlParse from 'markdownlint-cli2/parsers/yaml'; + +/** Read repository file metadata without invoking a shell. */ +function readGitOutput(directory, args) { + const result = spawnSync('git', ['-C', directory, ...args], { encoding: 'utf8' }); + if (result.error) { + throw result.error; + } + if (result.status !== 0) { + throw new Error(`Git file discovery failed in ${JSON.stringify(directory)}: ${JSON.stringify(result.stderr.trim())}`); + } + return result.stdout; +} + +/** Select existing tracked and nonignored Markdown, retaining Git symlink metadata. */ +function findMarkdownFiles(directory) { + const deleted = new Set(readGitOutput(directory, ['ls-files', '--deleted', '-z']).split('\0')); + const files = readGitOutput(directory, [ + 'ls-files', '--cached', '--others', '--exclude-standard', '--deduplicate', '-z', + ]).split('\0') + .filter((name) => !deleted.has(name)) + .filter((name) => /\.(?:md|markdown)$/i.test(name) && !name.split('/').includes('node_modules')) + .map((name) => join(directory, ...name.split('/'))) + .sort(); + // Git symlinks can be ordinary path-text files in Windows checkouts. + const symlinks = new Set(readGitOutput(directory, ['ls-files', '--stage', '-z']) + .split('\0') + .filter((record) => record.startsWith('120000 ')) + .map((record) => join(directory, ...record.slice(record.indexOf('\t') + 1).split('/')))); + return { files, symlinks }; +} + +// Optional first argument selects an isolated root; the shared rules stay fixed. +const directory = resolve(process.argv[2] ?? process.cwd()); +if (!lstatSync(directory).isDirectory()) { + throw new Error(`Validation root is not a directory: ${JSON.stringify(directory)}`); +} +const repositoryRoot = readGitOutput(directory, ['rev-parse', '--show-toplevel']).replace(/\r?\n$/, ''); +const resolvedRepositoryRoot = realpathSync(directory); +if (resolvedRepositoryRoot !== realpathSync(repositoryRoot)) { + throw new Error(`Validation must run from the repository root: ${JSON.stringify(repositoryRoot)}`); +} +const configurationPath = fileURLToPath(new URL('../linters/.markdown-lint.yml', import.meta.url)); +const configuration = await readConfig(configurationPath, [yamlParse]); +const { files, symlinks } = findMarkdownFiles(directory); +if (files.length === 0) { + throw new Error(`No Markdown files were checked in ${JSON.stringify(directory)}. Check the validation root.`); +} +const contributionSource = join(directory, '.github', 'CONTRIBUTING.md'); +if (!files.includes(contributionSource)) { + throw new Error(`Required community file is missing from the validation input: ${JSON.stringify(contributionSource)}. Restore the file or explicitly change the contract.`); +} +for (const file of files) { + const metadata = lstatSync(file); + if (metadata.isSymbolicLink() || symlinks.has(file)) { + throw new Error(`Markdown symlinks are not allowed: ${JSON.stringify(file)}. Use a regular file instead.`); + } + if (!metadata.isFile()) { + throw new Error(`Markdown input must be a regular file: ${JSON.stringify(file)}`); + } + const resolvedLocation = relative(resolvedRepositoryRoot, realpathSync(file)); + if (resolvedLocation === '..' || resolvedLocation.startsWith(`..${sep}`) || isAbsolute(resolvedLocation)) { + throw new Error(`Markdown input resolves outside the repository root: ${JSON.stringify(file)}. Use a repository-local source.`); + } + if (readFileSync(file, 'utf8').trim().length === 0) { + throw new Error(`Empty Markdown file: ${JSON.stringify(file)}. Restore its content before validation.`); + } +} + +const results = await lint({ files, config: configuration }); +const checkedFiles = Object.keys(results); +if (checkedFiles.length !== files.length) { + throw new Error(`Markdown lint checked ${checkedFiles.length} of ${files.length} selected files.`); +} +for (const file of checkedFiles) { + console.log(JSON.stringify(relative(directory, file))); +} +const hasErrors = Object.values(results).some((errors) => errors.length > 0); +if (hasErrors) { + console.error(JSON.stringify(results, null, 2)); +} +console.log(`Checked ${checkedFiles.length} Markdown files; result: ${hasErrors ? 'failed' : 'passed'}.`); +process.exitCode = hasErrors ? 1 : 0; diff --git a/.github/tests/validate-markdown.test.mjs b/.github/tests/validate-markdown.test.mjs new file mode 100644 index 0000000..75239ea --- /dev/null +++ b/.github/tests/validate-markdown.test.mjs @@ -0,0 +1,376 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { existsSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; +import yamlParse from 'markdownlint-cli2/parsers/yaml'; + +const script = fileURLToPath(new URL('../scripts/validate-markdown.mjs', import.meta.url)); +const gitignore = readFileSync(new URL('../../.gitignore', import.meta.url), 'utf8'); +const communityFiles = { + '.github/CONTRIBUTING.md': '# Contributing\n', +}; + +/** Isolate fixture Git state without changing the real repository environment. */ +function fixtureEnvironment(directory) { + return { + ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^GIT_/i.test(name))), + GIT_CONFIG_NOSYSTEM: '1', + GIT_CONFIG_GLOBAL: join(directory, '.git', 'fixture-global-config'), + }; +} + +/** Keep anonymous fixture Git operations isolated from personal hooks. */ +function fixtureGit(directory, args, input) { + const result = spawnSync('git', [ + '-c', `core.hooksPath=${join(directory, '.git', 'fixture-hooks')}`, + '-C', directory, ...args, + ], { encoding: 'utf8', input, env: fixtureEnvironment(directory) }); + assert.equal(result.status, 0, result.stderr); + return result.stdout; +} + +/** Create a disposable Git root with the same authored-input contract. */ +function createFixture(context, files) { + const directory = mkdtempSync(join(tmpdir(), 'community-markdown-')); + context.after(() => rmSync(directory, { recursive: true, force: true })); + mkdirSync(join(directory, '.git')); + writeFileSync(join(directory, '.git', 'fixture-global-config'), ''); + fixtureGit(directory, ['init', '--quiet', '--template=']); + fixtureGit(directory, ['config', 'core.excludesfile', join(directory, '.git', 'fixture-excludes')]); + for (const [name, content] of Object.entries({ + '.gitignore': gitignore, + '.git/fixture-excludes': '', + ...files, + })) { + const path = join(directory, name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content); + } + return directory; +} + +/** Run the real validator against disposable, anonymous Markdown files. */ +function runValidator(directory) { + return spawnSync(process.execPath, [script, directory], { + encoding: 'utf8', + env: fixtureEnvironment(directory), + }); +} + +/** Validate a disposable root using the same contract as repository CI. */ +function validateFixture(context, files) { + const directory = createFixture(context, files); + return runValidator(directory); +} + +test('validates central sources without any PR-template file', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'README.md': '# Community defaults\n', + '.github/CONTRIBUTING.md': '# Contributing\n\nFollow the shared workflow.\n', + }); + assert.equal(existsSync(join(directory, '.github', 'pull_request_template.md')), false); + const result = runValidator(directory); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 2 Markdown files; result: passed/); + assert.match(result.stdout, /\.github[/\\]+CONTRIBUTING\.md/); + assert.match(result.stdout, /README\.md/); +}); + +test('accepts an unstaged deletion of optional tracked Markdown', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'guide.md': '# Optional guide\n', + }); + fixtureGit(directory, ['add', '--', '.github/CONTRIBUTING.md', 'guide.md']); + rmSync(join(directory, 'guide.md')); + const result = runValidator(directory); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 1 Markdown files; result: passed/); +}); + +test('still rejects an unstaged deletion of the required contribution source', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'README.md': '# Community defaults\n', + }); + fixtureGit(directory, ['add', '--', '.github/CONTRIBUTING.md', 'README.md']); + rmSync(join(directory, '.github', 'CONTRIBUTING.md')); + const result = runValidator(directory); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Required community file/); +}); + +test('rejects an empty scan when all tracked Markdown was deleted', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'guide.md': '# Optional guide\n', + }); + fixtureGit(directory, ['add', '--', '.github/CONTRIBUTING.md', 'guide.md']); + rmSync(join(directory, '.github', 'CONTRIBUTING.md')); + rmSync(join(directory, 'guide.md')); + const result = runValidator(directory); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /No Markdown files were checked/); +}); + +for (const name of ['guide.md', '.github/CONTRIBUTING.md']) { + test(`rejects malformed Markdown in ${name}`, (context) => { + const result = validateFixture(context, { + ...communityFiles, + 'README.md': '# Community defaults\n', + [name]: '# Community file\n\n### Skipped heading level\n', + }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /MD001/); + assert.match(result.stderr, /heading-increment/); + }); +} + +test('rejects duplicate document titles', (context) => { + const result = validateFixture(context, { + ...communityFiles, + '.github/CONTRIBUTING.md': '# Contributing\n\n# Another title\n', + }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /MD025/); +}); + +test('validates ordinary authored Markdown with supporting HTML blocks', (context) => { + const result = validateFixture(context, { + ...communityFiles, + 'guide.md': + '# Usage guide\n\nReaders follow the documented instructions.\n\n
\nSupporting details\n\nAdditional context.\n\n
\n', + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 2 Markdown files; result: passed/); +}); + +test('rejects a scan with no Markdown files', (context) => { + const result = validateFixture(context, { 'unrelated.txt': 'Not Markdown.\n' }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /No Markdown files were checked/); + const path = result.stderr.match(/No Markdown files were checked in (".*")\. Check the validation root/); + assert.ok(path, result.stderr); + assert.equal(typeof JSON.parse(path[1]), 'string'); +}); + +test('rejects the blank central contribution source even when other files are valid', (context) => { + const result = validateFixture(context, { + ...communityFiles, + 'README.md': '# Community defaults\n', + '.github/CONTRIBUTING.md': ' \n\t\n', + }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Empty Markdown file/); + assert.match(result.stderr, /CONTRIBUTING\.md/); + const path = result.stderr.match(/Empty Markdown file: (".*")\. Restore its content/); + assert.ok(path, result.stderr); + assert.ok(JSON.parse(path[1]).endsWith('CONTRIBUTING.md')); +}); + +test('ignores dependency and Git metadata trees, not hidden community files', (context) => { + const result = validateFixture(context, { + ...communityFiles, + '.github/CONTRIBUTING.md': '# Contributing\n', + 'node_modules/dependency/README.md': '# Dependency\n\n### Invalid heading\n', + '.git/README.md': '# Git metadata\n\n### Invalid heading\n', + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 1 Markdown files; result: passed/); +}); + +test('ignores untracked editor history using the repository ignore rules', (context) => { + const result = validateFixture(context, { + ...communityFiles, + '.history/README_old.md': '# Old snapshot\n\n### Invalid heading\n', + '.idea/notes.md': '# Editor notes\n\n### Invalid heading\n', + '.vscode/notes.md': '# Editor notes\n\n### Invalid heading\n', + }); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 1 Markdown files; result: passed/); +}); + +test('still validates deliberately tracked Markdown under an ignored directory', (context) => { + const directory = createFixture(context, { + ...communityFiles, + '.history/tracked.md': '# Authored Markdown\n\n### Invalid heading\n', + }); + fixtureGit(directory, ['add', '--force', '--', '.history/tracked.md']); + const result = runValidator(directory); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /MD001/); +}); + +test('validates the .markdown extension instead of silently skipping it', (context) => { + const result = validateFixture(context, { + ...communityFiles, + 'README.markdown': '# Markdown document\n\n### Invalid heading\n', + }); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /MD001/); +}); + +for (const name of Object.keys(communityFiles)) { + test(`rejects missing required ${name} even when other Markdown remains`, (context) => { + const files = { ...communityFiles, 'README.md': '# Community defaults\n' }; + delete files[name]; + const result = validateFixture(context, files); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Required community file/); + assert.ok(result.stderr.includes(name.split('/').at(-1))); + }); +} + +test('rejects Git symlink entries even when Windows materializes them as regular path text', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'guide.md': 'target.txt\n', + 'target.txt': '# Invalid target\n\n### Skipped heading level\n', + }); + const object = fixtureGit(directory, ['hash-object', '-w', '--stdin'], 'target.txt\n').trim(); + fixtureGit(directory, ['update-index', '--add', '--cacheinfo', `120000,${object},guide.md`]); + const result = runValidator(directory); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /symlink|symbolic link/i); + assert.match(result.stderr, /guide\.md/); +}); + +test('rejects a non-Git root rather than silently changing the input contract', (context) => { + const directory = mkdtempSync(join(tmpdir(), 'community-markdown-')); + context.after(() => rmSync(directory, { recursive: true, force: true })); + for (const [name, content] of Object.entries(communityFiles)) { + const path = join(directory, name); + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, content); + } + const result = runValidator(directory); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Git file discovery failed/); +}); + +test('rejects a repository subdirectory instead of validating only a partial checkout', (context) => { + const directory = createFixture(context, { + ...communityFiles, + 'nested/.github/CONTRIBUTING.md': '# Nested contribution fixture\n', + }); + const result = runValidator(join(directory, 'nested')); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /repository root/); +}); + +test('rejects Markdown reached through a parent link outside the repository', (context) => { + const directory = createFixture(context, communityFiles); + fixtureGit(directory, ['add', '--', '.github']); + const outside = mkdtempSync(join(tmpdir(), 'anonymous-outside-sources-')); + context.after(() => rmSync(outside, { recursive: true, force: true })); + for (const [name, content] of Object.entries(communityFiles)) { + writeFileSync(join(outside, name.split('/').at(-1)), content); + } + renameSync(join(directory, '.github'), join(directory, 'original-sources')); + symlinkSync(outside, join(directory, '.github'), process.platform === 'win32' ? 'junction' : 'dir'); + const result = runValidator(directory); + assert.notEqual(result.status, 0, result.stdout); + assert.match(result.stderr, /outside the repository root/); +}); + +test('preserves Markdown parent aliases whose targets stay inside the repository', (context) => { + const directory = createFixture(context, communityFiles); + fixtureGit(directory, ['add', '--', '.github']); + const target = join(directory, 'central-sources'); + renameSync(join(directory, '.github'), target); + symlinkSync(target, join(directory, '.github'), process.platform === 'win32' ? 'junction' : 'dir'); + const result = runValidator(directory); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Checked 2 Markdown files; result: passed/); +}); + +test('fixture Git and validator subprocesses cannot use the caller index', (context) => { + const caller = mkdtempSync(join(tmpdir(), 'anonymous-caller-')); + context.after(() => rmSync(caller, { recursive: true, force: true })); + const environment = { + ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^GIT_/i.test(name))), + GIT_CONFIG_NOSYSTEM: '1', + GIT_CONFIG_GLOBAL: join(caller, 'empty-global-config'), + }; + writeFileSync(environment.GIT_CONFIG_GLOBAL, ''); + for (const args of [['init', '--quiet', '--template='], ['add', '--', 'sentinel.txt']]) { + writeFileSync(join(caller, 'sentinel.txt'), 'Anonymous caller data.\n'); + const result = spawnSync('git', [ + '-c', `core.hooksPath=${join(caller, '.git', 'fixture-hooks')}`, + '-C', caller, ...args, + ], { encoding: 'utf8', env: environment }); + assert.equal(result.status, 0, result.stderr); + } + const index = join(caller, '.git', 'index'); + const originalIndex = readFileSync(index); + const previousIndex = process.env.GIT_INDEX_FILE; + try { + process.env.GIT_INDEX_FILE = index; + const directory = createFixture(context, { ...communityFiles, 'guide.md': 'target.txt\n' }); + const object = fixtureGit(directory, ['hash-object', '-w', '--stdin'], 'target.txt\n').trim(); + fixtureGit(directory, ['update-index', '--add', '--cacheinfo', `120000,${object},guide.md`]); + const result = runValidator(directory); + assert.deepEqual(readFileSync(index), originalIndex, 'Disposable fixtures must not change the caller index.'); + assert.equal(result.status, 1, result.stderr); + assert.match(result.stderr, /symlink/i); + } finally { + if (previousIndex === undefined) { + delete process.env.GIT_INDEX_FILE; + } else { + process.env.GIT_INDEX_FILE = previousIndex; + } + } +}); + +test('disposable Git initialization copies no user or default Git templates', (context) => { + const template = mkdtempSync(join(tmpdir(), 'anonymous-git-template-')); + context.after(() => rmSync(template, { recursive: true, force: true })); + mkdirSync(join(template, 'info')); + writeFileSync(join(template, 'info', 'exclude'), '*.md\n'); + const previousTemplate = process.env.GIT_TEMPLATE_DIR; + try { + process.env.GIT_TEMPLATE_DIR = template; + const directory = createFixture(context, communityFiles); + const result = runValidator(directory); + assert.equal(result.status, 0, result.stderr); + assert.equal(existsSync(join(directory, '.git', 'info', 'exclude')), false); + assert.match(result.stdout, /Checked 1 Markdown files; result: passed/); + } finally { + if (previousTemplate === undefined) { + delete process.env.GIT_TEMPLATE_DIR; + } else { + process.env.GIT_TEMPLATE_DIR = previousTemplate; + } + } +}); + +test('rejects a nonexistent root instead of reporting a successful scan', (context) => { + const directory = mkdtempSync(join(tmpdir(), 'community-markdown-')); + context.after(() => rmSync(directory, { recursive: true, force: true })); + const result = runValidator(join(directory, 'missing')); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /ENOENT|not a directory/); +}); + +for (const expectedHead of ['not-a-pr-head', '']) { + test(`candidate identity rejects ${expectedHead ? 'mismatched' : 'absent'} PR head metadata`, () => { + const workflow = yamlParse(readFileSync( + new URL('../workflows/Validate-Community-Files.yml', import.meta.url), 'utf8')); + const step = workflow.jobs.validate.steps.find((candidate) => + typeof candidate.run === 'string' && candidate.run.includes('git rev-parse HEAD')); + assert.ok(step, 'The workflow must verify the checked-out candidate.'); + const result = spawnSync('pwsh', ['-NoProfile', '-NonInteractive', '-Command', step.run], { + cwd: fileURLToPath(new URL('../../', import.meta.url)), + encoding: 'utf8', + env: { ...process.env, PR_HEAD_SHA: expectedHead }, + }); + assert.ifError(result.error); + assert.notEqual(result.status, 0, result.stdout); + assert.match(result.stderr, /Checked-out commit does not match the PR head/); + assert.equal(step.env?.PR_HEAD_SHA, '${{ github.event.pull_request.head.sha }}'); + }); +} diff --git a/.github/workflows/Validate-Community-Files.yml b/.github/workflows/Validate-Community-Files.yml new file mode 100644 index 0000000..510eca0 --- /dev/null +++ b/.github/workflows/Validate-Community-Files.yml @@ -0,0 +1,61 @@ +--- +name: Community file validation + +'on': + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + - edited + - ready_for_review + +permissions: {} + +concurrency: + group: community-file-validation-${{ github.event.pull_request.number }} + cancel-in-progress: true + +defaults: + run: + shell: pwsh + +jobs: + validate: + name: Community file validation + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Check out the actual PR head + # yamllint disable-line rule:line-length + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + persist-credentials: false + + - name: Set up the declared Node.js LTS + # yamllint disable-line rule:line-length + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version-file: .node-version + + - name: Verify the checked-out PR head + env: + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + $ErrorActionPreference = 'Stop' + $actualHead = git rev-parse HEAD + if ($LASTEXITCODE -ne 0 -or $actualHead -ne $env:PR_HEAD_SHA) { + throw 'Checked-out commit does not match the PR head.' + } + Write-Output -InputObject $actualHead + + - name: Install locked validation tooling + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Validate fixtures and community Markdown + run: npm run validate diff --git a/.gitignore b/.gitignore index 0d5ccaf..4aeb3ae 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ Thumbs.db *.code-workspace *.swp *~ + +# Local validation dependencies +node_modules/ diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..1dda03d --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +26.13.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7f54b7b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +--- +repos: + - repo: local + hooks: + - id: community-file-validation + name: Community file validation + entry: npm run validate + language: system + pass_filenames: false + always_run: true diff --git a/README.md b/README.md index 0e884f9..68725e6 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,69 @@ `PSModule/.github` is GitHub's special public organization-defaults repository. GitHub uses its community files when a PSModule repository does not define its own corresponding file. + +Ordinary PSModule repositories inherit these defaults. Keep a local community +file only when repository-specific content is needed; inheritance does not +require module-local template or `CONTRIBUTING.md` copies. + +## Validate central sources + +This validator protects the current source files in `PSModule/.github` only. +Its remaining required contribution source is not an ordinary-repository baseline. +PR-template files are not required, generated, or distributed by validation; +their intentional absence is valid. + +Use Git, PowerShell 7 (`pwsh`), and the Node.js LTS version in `.node-version`. +Install the locked tooling and run the same validation command used by the PR check: + +```powershell +npm ci --ignore-scripts +npm run validate +``` + +The local pre-commit hook invokes `npm run validate` too. After installing +the pre-commit runner, use `pre-commit install` to enable it in your checkout +and `pre-commit run --all-files` to check all files before committing. + +`npm test` runs isolated valid and malformed Git fixtures. `npm run lint:markdown` +checks tracked `.md` and `.markdown` files and untracked files not ignored by Git, +including hidden community directories, with MSX's [shared Markdown rules][shared-markdown-rules]. +Ignored editor history, dependency trees, and Git metadata do not affect the +scan. Deliberately tracked Markdown under an ignored directory is still checked; +`node_modules` stays excluded. +Git-reported deletions of optional Markdown are excluded even before staging. +Deleting the required contribution source or every Markdown input still fails. + +Fixture subprocesses discard inherited Git repository variables and use empty +global configuration with template copying disabled. This keeps a pre-commit +hook's real index and contributor configuration outside the disposable roots; +the subsequent real-repository lint retains its caller environment. + +In this special defaults repository, `.github/CONTRIBUTING.md` remains a +required regular, nonempty source. Missing or renamed contribution sources, +Markdown symlinks, empty scans, blank files, lint findings, and tool errors fail. +Symlinks are rejected rather than followed, including Git links materialized as +path-text files on Windows. Isolated test roots contain ordinary authored +Markdown and the contribution source without copying a PR template. + +Resolved Markdown inputs must stay inside this central repository. Parent +directory aliases to outside targets fail before their content is read. + +The rule file is an unchanged snapshot of the linked merged revision. Refresh +it from its owning source rather than adding repository-specific rule overrides. +The check measures Markdown syntax/style, not PR-authoring completeness, source +correspondence, release decisions, or template distribution. Repositories with +local community files keep their own files. + +Normal PR events targeting `main`, including drafts, run the named +`Community file validation` job against the actual head. The checkout must match +the PR-head metadata; absent or mismatched identity fails instead of accepting +a default checkout. GitHub does not start +`pull_request` workflows for PRs with merge conflicts; resolve the conflicts +and obtain a fresh current-head result before readiness. There is no path or +draft filter, manual-dispatch substitute, elevated trigger, write token, or +publisher. Making this result mandatory requires separately authorized branch governance to +require that exact check name and its GitHub Actions integration. Do not rename +the check independently of its protection rule. + +[shared-markdown-rules]: https://github.com/MSXOrg/docs/blob/36da9154aa55d2052345fb2f28b812b29daf8bc9/.github/linters/.markdown-lint.yml diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7503695 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1419 @@ +{ + "name": "psmodule-community-validation", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "psmodule-community-validation", + "devDependencies": { + "markdownlint-cli2": "0.24.0" + }, + "engines": { + "node": ">=26.13.0 <27" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.5.0.tgz", + "integrity": "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/entities": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz", + "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "16.2.4", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.4.tgz", + "integrity": "sha512-c8B/VNLmxRcmqqenRA9t+9IyOjf9+V6lTxPaUJLqOCONdQkWZ0ETYgX0qbtJqPsgCNusT9MZ5Jeidw8Eb9tn2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "micromatch": "^4.0.8", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/linkify-it": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", + "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^3.0.0" + } + }, + "node_modules/markdown-it": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.3.tgz", + "integrity": "sha512-+Az7Ojz3iRGu8KYMvJ6xVhvrxP3eE+vEtJX75Ow88XBpcQNV/0lGX5ZynIIBftREo4DC2jsveAMDaeltmm8QvQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^3.0.3", + "entities": "^8.1.0", + "linkify-it": "^6.1.0", + "mdurl": "^2.1.0", + "punycode.js": "^2.3.1", + "uc.micro": "^3.0.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.4.tgz", + "integrity": "sha512-fwWK3Zq22l4XJJjx5mcBHGikIk+hJqO+32dPm0Jur7+rtG5x1scOjSfdT9ZKeWHPpsya/l0HHK4tDImBkTrtvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "PSF-2.0" + }, + "node_modules/markdownlint": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.42.0.tgz", + "integrity": "sha512-kzsCV/rPWRFn3jUhcB919YF4Qgeh0g9tcogoXFadGSo6Zxj6662YOqWXv0jDn8nv5KCCxDfMKJZv6N4HkNkvTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark": "4.0.3", + "micromark-core-commonmark": "2.0.4", + "micromark-extension-directive": "4.0.0", + "micromark-extension-gfm-autolink-literal": "2.1.0", + "micromark-extension-gfm-footnote": "2.1.0", + "micromark-extension-gfm-table": "2.1.2", + "micromark-extension-math": "3.1.0", + "micromark-util-types": "2.0.3", + "re2js": "2.8.6", + "string-width": "8.3.0" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.24.0.tgz", + "integrity": "sha512-c5hU2MDoTlS230MLzdNlkIyxImXq5acxGKz4nYz4aDhH/2BgFtpsA7iK9/OoTHMgZ4FuR0wedTQA31K6Lp9I/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "16.2.4", + "js-yaml": "5.4.3", + "jsonc-parser": "3.3.1", + "jsonpointer": "5.0.1", + "markdown-it": "15.0.3", + "markdownlint": "0.42.0", + "markdownlint-cli2-formatter-default": "0.0.6", + "micromatch": "4.0.8", + "smol-toml": "1.9.1" + }, + "bin": { + "markdownlint-cli2": "markdownlint-cli2-bin.mjs" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + } + }, + "node_modules/markdownlint-cli2-formatter-default": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/markdownlint-cli2-formatter-default/-/markdownlint-cli2-formatter-default-0.0.6.tgz", + "integrity": "sha512-VVDGKsq9sgzu378swJ0fcHfSicUnMxnL8gnLm/Q4J/xsNJ4e5bA6lvAz7PCzIl0/No0lHyaWdqVD2jotxOSFMQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/DavidAnson" + }, + "peerDependencies": { + "markdownlint-cli2": ">=0.0.4" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/re2js": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.6.tgz", + "integrity": "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smol-toml": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/uc.micro": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", + "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.1.tgz", + "integrity": "sha512-lzlXPoVB0Uy/FvTaUgX39RbixYiSKoc3JSoSf01+0c2B6FR3qdYIPN7Qjo/AV7n6sqLCk0509cT3LRj1oSZ1+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1eb2858 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "psmodule-community-validation", + "private": true, + "type": "module", + "engines": { + "node": ">=26.13.0 <27" + }, + "scripts": { + "test": "node --test .github/tests/validate-markdown.test.mjs", + "lint:markdown": "node .github/scripts/validate-markdown.mjs", + "validate": "npm test && npm run lint:markdown" + }, + "devDependencies": { + "markdownlint-cli2": "0.24.0" + } +}