Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ jobs:
strategy:
matrix:
node-version:
- '22.1'
- '20.10'
- '18.18'
- '22.4'
- '20.16'
os:
- macos-latest
- ubuntu-latest
Expand Down
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
dist
node_modules
markdownlint-rules/emd002.js
markdownlint-rules/emd003.js
138 changes: 75 additions & 63 deletions bin/lint-markdown-api-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@

import { access, constants, readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';

import Ajv, { ValidateFunction } from 'ajv';
import type { fromHtml as FromHtmlFunction } from 'hast-util-from-html';
import type { HTML, Heading } from 'mdast';
import type { fromMarkdown as FromMarkdownFunction } from 'mdast-util-from-markdown';
import * as minimist from 'minimist';
import type { Literal, Node } from 'unist';
import type { visit as VisitFunction } from 'unist-util-visit';
import { fromHtml } from 'hast-util-from-html';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { visit } from 'unist-util-visit';
import { URI } from 'vscode-uri';
import { parseDocument, visit as yamlVisit } from 'yaml';

import { dynamicImport } from '../lib/helpers';
import { DocsWorkspace } from '../lib/markdown';
import type { HTML, Heading } from 'mdast';
import type { Literal, Node } from 'unist';

import { DocsWorkspace } from '../lib/markdown.js';

// "<any char>: <match group>"
const possibleStringRegex = /^[ \S]+?: *?(\S[ \S]+?)$/gm;
Expand All @@ -35,19 +36,19 @@ interface ApiHistory {

interface Options {
// Check if the API history block is preceded by a heading
checkPlacement: boolean;
checkPlacement?: boolean;
// Check if the 'breaking-changes-header' heading id's in the API history block exist in the breaking changes file at this filepath
breakingChangesFile: string;
breakingChangesFile?: string;
// Check if the API history block contains strings that might cause issues when parsing the YAML
checkStrings: boolean;
checkStrings?: boolean;
// Check if the API history block contains descriptions that aren't surrounded by double quotation marks
checkDescriptions: boolean;
checkDescriptions?: boolean;
// Check if the API history block contains comments
disallowComments: boolean;
disallowComments?: boolean;
// Array of glob patterns to ignore when processing files
ignoreGlobs: string[];
ignoreGlobs?: string[];
// Check if the API history block's YAML adheres to the JSON schema at this filepath
schema: string;
schema?: string;

// TODO: Implement this when GH_TOKEN isn't needed to fetch PR release versions anymore
// checkPullRequestLinks: boolean;
Expand All @@ -65,12 +66,6 @@ function isHTML(node: Node): node is HTML {
export async function findPossibleApiHistoryBlocks(
content: string,
): Promise<PossibleHistoryBlock[]> {
const { fromMarkdown } = (await dynamicImport('mdast-util-from-markdown')) as {
fromMarkdown: typeof FromMarkdownFunction;
};
const { visit } = (await dynamicImport('unist-util-visit')) as {
visit: typeof VisitFunction;
};
const tree = fromMarkdown(content);
const codeBlocks: PossibleHistoryBlock[] = [];

Expand Down Expand Up @@ -119,13 +114,6 @@ async function main(
let warningCounter = 0;

try {
const { fromHtml } = (await dynamicImport('hast-util-from-html')) as {
fromHtml: typeof FromHtmlFunction;
};
const { fromMarkdown } = (await dynamicImport('mdast-util-from-markdown')) as {
fromMarkdown: typeof FromMarkdownFunction;
};

const workspace = new DocsWorkspace(workspaceRoot, globs, ignoreGlobs);

let validateAgainstSchema: ValidateFunction<ApiHistory> | null = null;
Expand Down Expand Up @@ -407,47 +395,71 @@ async function main(
}

function parseCommandLine() {
const showUsage = (arg?: string): boolean => {
if (!arg || arg.startsWith('-')) {
console.log(
'Usage: lint-roller-markdown-api-history [--root <dir>] <globs>' +
' [-h|--help]' +
' [--check-placement] [--breaking-changes-file <path>] [--check-strings] [--check-descriptions] [--disallow-comments]' +
' [--schema <path>]' +
' [--ignore <globs>] [--ignore-path <path>]',
);
process.exit(1);
}

return true;
const showUsage = (): never => {
console.log(
'Usage: lint-roller-markdown-api-history [--root <dir>] <globs>' +
' [-h|--help]' +
' [--check-placement] [--breaking-changes-file <path>] [--check-strings] [--check-descriptions] [--disallow-comments]' +
' [--schema <path>]' +
' [--ignore <globs>] [--ignore-path <path>]',
);
process.exit(1);
};

const opts = minimist(process.argv.slice(2), {
boolean: [
'help',
'check-placement',
'check-strings',
'check-descriptions',
'disallow-comments',
],
string: ['root', 'ignore', 'ignore-path', 'schema', 'breaking-changes-file'],
unknown: showUsage,
default: {
'check-placement': true,
'check-strings': true,
'check-descriptions': true,
'disallow-comments': true,
},
});
try {
const opts = parseArgs({
allowNegative: true,
allowPositionals: true,
options: {
'check-placement': {
type: 'boolean',
default: true,
},
'check-strings': {
type: 'boolean',
default: true,
},
'check-descriptions': {
type: 'boolean',
default: true,
},
'disallow-comments': {
type: 'boolean',
default: true,
},
root: {
type: 'string',
},
ignore: {
type: 'string',
multiple: true,
},
'ignore-path': {
type: 'string',
},
schema: {
type: 'string',
},
'breaking-changes-file': {
type: 'string',
},
help: {
type: 'boolean',
},
},
});

if (opts.help || !opts._.length) showUsage();
if (opts.values.help || !opts.positionals.length) return showUsage();

return opts;
return opts;
} catch {
return showUsage();
}
}

async function init() {
try {
const opts = parseCommandLine();
const { values: opts, positionals } = parseCommandLine();

if (!opts.root) {
opts.root = '.';
Expand Down Expand Up @@ -477,7 +489,7 @@ async function init() {

const { historyBlockCounter, documentCounter, errorCounter, warningCounter } = await main(
resolve(process.cwd(), opts.root),
opts._,
positionals,
{
checkPlacement: opts['check-placement'],
breakingChangesFile: opts['breaking-changes-file'],
Expand All @@ -500,7 +512,7 @@ async function init() {
}
}

if (require.main === module) {
if (process.argv[1] === fileURLToPath(import.meta.url)) {
init().catch((error) => {
console.error(error);
process.exit(1);
Expand Down
64 changes: 42 additions & 22 deletions bin/lint-markdown-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { parseArgs } from 'node:util';

import {
createLanguageService,
Expand All @@ -10,11 +12,10 @@ import {
ILogger,
LogLevel,
} from '@dsanders11/vscode-markdown-languageservice';
import * as minimist from 'minimist';
import { CancellationTokenSource } from 'vscode-languageserver';
import { URI } from 'vscode-uri';

import { DocsWorkspace, MarkdownLinkComputer, MarkdownParser } from '../lib/markdown';
import { DocsWorkspace, MarkdownLinkComputer, MarkdownParser } from '../lib/markdown.js';

class NoOpLogger implements ILogger {
readonly level = LogLevel.Off;
Expand Down Expand Up @@ -144,31 +145,50 @@ async function main(
}

function parseCommandLine() {
const showUsage = (arg?: string): boolean => {
if (!arg || arg.startsWith('-')) {
console.log(
'Usage: lint-roller-markdown-links [--root <dir>] <globs> [-h|--help] [--fetch-external-links] ' +
'[--check-redirects] [--ignore <globs>]',
);
process.exit(1);
}

return true;
const showUsage = (): never => {
console.log(
'Usage: lint-roller-markdown-links [--root <dir>] <globs> [-h|--help] [--fetch-external-links] ' +
'[--check-redirects] [--ignore <globs>]',
);
process.exit(1);
};

const opts = minimist(process.argv.slice(2), {
boolean: ['help', 'fetch-external-links', 'check-redirects'],
string: ['root', 'ignore', 'ignore-path'],
unknown: showUsage,
});
try {
const opts = parseArgs({
allowPositionals: true,
options: {
'fetch-external-links': {
type: 'boolean',
},
'check-redirects': {
type: 'boolean',
},
root: {
type: 'string',
},
ignore: {
type: 'string',
multiple: true,
},
'ignore-path': {
type: 'string',
},
help: {
type: 'boolean',
},
},
});

if (opts.help || !opts._.length) showUsage();
if (opts.values.help || !opts.positionals.length) return showUsage();

return opts;
return opts;
} catch {
return showUsage();
}
}

if (require.main === module) {
const opts = parseCommandLine();
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const { values: opts, positionals } = parseCommandLine();

if (!opts.root) {
opts.root = '.';
Expand All @@ -188,7 +208,7 @@ if (require.main === module) {
}
}

main(path.resolve(process.cwd(), opts.root), opts._, {
main(path.resolve(process.cwd(), opts.root), positionals, {
fetchExternalLinks: opts['fetch-external-links'],
checkRedirects: opts['check-redirects'],
ignoreGlobs: opts.ignore,
Expand Down
Loading