@markuplint/create-rule 4.0.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/bin/create-rule.mjs +9 -0
- package/lib/cli.d.ts +1 -0
- package/lib/cli.js +79 -0
- package/lib/create-rule-helper-error.d.ts +3 -0
- package/lib/create-rule-helper-error.js +6 -0
- package/lib/create-rule-helper.d.ts +2 -0
- package/lib/create-rule-helper.js +16 -0
- package/lib/create-rule-package.d.ts +2 -0
- package/lib/create-rule-package.js +18 -0
- package/lib/create-rule-to-core.d.ts +3 -0
- package/lib/create-rule-to-core.js +36 -0
- package/lib/create-rule-to-project.d.ts +2 -0
- package/lib/create-rule-to-project.js +16 -0
- package/lib/fs-exists.d.ts +1 -0
- package/lib/fs-exists.js +10 -0
- package/lib/glob.d.ts +1 -0
- package/lib/glob.js +6 -0
- package/lib/install-scaffold.d.ts +4 -0
- package/lib/install-scaffold.js +61 -0
- package/lib/is-markuplint-repo.d.ts +1 -0
- package/lib/is-markuplint-repo.js +5 -0
- package/lib/read-package-json.d.ts +1 -0
- package/lib/read-package-json.js +13 -0
- package/lib/search-core-repository.d.ts +1 -0
- package/lib/search-core-repository.js +17 -0
- package/lib/transfer.d.ts +8 -0
- package/lib/transfer.js +112 -0
- package/lib/types.d.ts +30 -0
- package/lib/types.js +1 -0
- package/package.json +40 -0
- package/scaffold/.eslintrc +5 -0
- package/scaffold/core/README.ja.md +26 -0
- package/scaffold/core/README.md +22 -0
- package/scaffold/core/index.spec.ts +18 -0
- package/scaffold/core/index.ts +31 -0
- package/scaffold/core/schema.json +47 -0
- package/scaffold/package/README.md +60 -0
- package/scaffold/package/src/index.ts +15 -0
- package/scaffold/package/src/rules/__ruleName__.ts +94 -0
- package/scaffold/package/tsconfig.json +20 -0
- package/scaffold/project/index.ts +15 -0
- package/scaffold/project/rules/__ruleName__.spec.ts +32 -0
- package/scaffold/project/rules/__ruleName__.ts +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017-2024 Yusuke Hirao
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/lib/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createRule(): Promise<void>;
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { input, installModule, select, confirm, font, header } from '@markuplint/cli-utils';
|
|
3
|
+
import { createRuleHelper } from './create-rule-helper.js';
|
|
4
|
+
import { isMarkuplintRepo } from './is-markuplint-repo.js';
|
|
5
|
+
const icons = {
|
|
6
|
+
README: '📝',
|
|
7
|
+
index: '📜',
|
|
8
|
+
schema: '⚙️ ',
|
|
9
|
+
package: '🎁',
|
|
10
|
+
tsconfig: '💎',
|
|
11
|
+
};
|
|
12
|
+
export async function createRule() {
|
|
13
|
+
process.stdout.write(header('Create a rule'));
|
|
14
|
+
process.stdout.write('\n');
|
|
15
|
+
process.stdout.write('\n');
|
|
16
|
+
const firstChoices = [
|
|
17
|
+
{ name: 'Add the rule to this project', value: 'ADD_TO_PROJECT' },
|
|
18
|
+
{ name: 'Create the rule and publish it as a package', value: 'PUBLISH_AS_PACKAGE' },
|
|
19
|
+
];
|
|
20
|
+
if (await isMarkuplintRepo()) {
|
|
21
|
+
firstChoices.push({ name: 'Contribute the new rule to markuplint core rules', value: 'CONTRIBUTE_TO_CORE' });
|
|
22
|
+
}
|
|
23
|
+
const purpose = await select({
|
|
24
|
+
message: 'What purpose do you create the rule for?',
|
|
25
|
+
choices: firstChoices,
|
|
26
|
+
});
|
|
27
|
+
const dirQuestion = purpose === 'ADD_TO_PROJECT' ? 'What is the directory name?' : 'What is the plugin name?';
|
|
28
|
+
const pluginName = purpose === 'CONTRIBUTE_TO_CORE' ? '' : await input(dirQuestion, /^[a-z][\da-z]*(?:-[a-z][\da-z]*)*$/i);
|
|
29
|
+
const ruleName = await input('What is the rule name?', /^[a-z][\da-z]*(?:-[a-z][\da-z]*)*$/i);
|
|
30
|
+
const core = purpose === 'CONTRIBUTE_TO_CORE'
|
|
31
|
+
? {
|
|
32
|
+
description: await input('Description:'),
|
|
33
|
+
category: await select({
|
|
34
|
+
message: 'Category:',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'Conformance checking', value: 'validation' },
|
|
37
|
+
{ name: 'Accessibility', value: 'a11y' },
|
|
38
|
+
{ name: 'Naming Convention', value: 'naming-convention' },
|
|
39
|
+
{ name: 'Maintainability', value: 'maintainability' },
|
|
40
|
+
{ name: 'Style', value: 'style' },
|
|
41
|
+
],
|
|
42
|
+
}),
|
|
43
|
+
severity: await select({
|
|
44
|
+
message: 'Severity:',
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: 'error', value: 'error' },
|
|
47
|
+
{ name: 'warning', value: 'warning' },
|
|
48
|
+
],
|
|
49
|
+
}),
|
|
50
|
+
}
|
|
51
|
+
: undefined;
|
|
52
|
+
const lang = purpose === 'CONTRIBUTE_TO_CORE'
|
|
53
|
+
? 'TYPESCRIPT'
|
|
54
|
+
: await select({
|
|
55
|
+
message: 'Which language will you implement?',
|
|
56
|
+
choices: [
|
|
57
|
+
{ name: 'TypeScript', value: 'TYPESCRIPT' },
|
|
58
|
+
{ name: 'JavaScript', value: 'JAVASCRIPT' },
|
|
59
|
+
],
|
|
60
|
+
});
|
|
61
|
+
const needTest = purpose === 'CONTRIBUTE_TO_CORE' ? true : await confirm('Do you need the test?', { initial: true });
|
|
62
|
+
const result = await createRuleHelper({ purpose, pluginName, ruleName, lang, needTest, core });
|
|
63
|
+
for (const file of result.files) {
|
|
64
|
+
output(pluginName || 'core', file.test ? '🖍 ' : icons[file.name] ?? '🛡 ', file.fileName, resolve(file.destDir, file.fileName + file.ext));
|
|
65
|
+
}
|
|
66
|
+
if (result.dependencies.length > 0) {
|
|
67
|
+
await installModule(result.dependencies);
|
|
68
|
+
}
|
|
69
|
+
if (result.devDependencies.length > 0) {
|
|
70
|
+
await installModule(result.devDependencies, true);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function output(name, icon, title, path) {
|
|
74
|
+
const _marker = font.xterm(39)('✔') + ' ';
|
|
75
|
+
const _title = (icon, title) => `${icon} ` + font.bold(`${name}/${title}`);
|
|
76
|
+
const _file = (path) => ' ' + font.cyanBright(path);
|
|
77
|
+
process.stdout.write(_marker + _title(icon, title) + _file(path));
|
|
78
|
+
process.stdout.write('\n');
|
|
79
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRulePackage } from './create-rule-package.js';
|
|
2
|
+
import { createRuleToCore } from './create-rule-to-core.js';
|
|
3
|
+
import { createRuleToProject } from './create-rule-to-project.js';
|
|
4
|
+
export async function createRuleHelper(params) {
|
|
5
|
+
switch (params.purpose) {
|
|
6
|
+
case 'ADD_TO_PROJECT': {
|
|
7
|
+
return await createRuleToProject(params);
|
|
8
|
+
}
|
|
9
|
+
case 'PUBLISH_AS_PACKAGE': {
|
|
10
|
+
return await createRulePackage(params);
|
|
11
|
+
}
|
|
12
|
+
case 'CONTRIBUTE_TO_CORE': {
|
|
13
|
+
return await createRuleToCore(params);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { CreateRuleHelperError } from './create-rule-helper-error.js';
|
|
3
|
+
import { glob } from './glob.js';
|
|
4
|
+
import { installScaffold } from './install-scaffold.js';
|
|
5
|
+
export async function createRulePackage({ pluginName, ruleName, lang, needTest, }) {
|
|
6
|
+
const newRuleDir = path.resolve(process.cwd(), '*');
|
|
7
|
+
const files = await glob(newRuleDir);
|
|
8
|
+
if (files.length > 0) {
|
|
9
|
+
throw new CreateRuleHelperError('The directory is not empty');
|
|
10
|
+
}
|
|
11
|
+
return await installScaffold('package', process.cwd(), {
|
|
12
|
+
pluginName,
|
|
13
|
+
ruleName,
|
|
14
|
+
lang,
|
|
15
|
+
needTest,
|
|
16
|
+
packageJson: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { CreateRuleHelperError } from './create-rule-helper-error.js';
|
|
3
|
+
import { fsExists } from './fs-exists.js';
|
|
4
|
+
import { installScaffold } from './install-scaffold.js';
|
|
5
|
+
import { searchCoreRepository } from './search-core-repository.js';
|
|
6
|
+
const rulesRelDir = ['packages', '@markuplint', 'rules', 'src'];
|
|
7
|
+
export async function createRuleToCore({ ruleName, core }) {
|
|
8
|
+
if (!core) {
|
|
9
|
+
throw new CreateRuleHelperError('Core options are not defined');
|
|
10
|
+
}
|
|
11
|
+
const rulesDir = await getRulesDir();
|
|
12
|
+
const newRuleDir = path.resolve(rulesDir, ruleName);
|
|
13
|
+
const exists = await fsExists(newRuleDir);
|
|
14
|
+
if (exists) {
|
|
15
|
+
throw new CreateRuleHelperError(`A new rule "${ruleName}" already exists`);
|
|
16
|
+
}
|
|
17
|
+
return await installScaffold('core', newRuleDir, {
|
|
18
|
+
pluginName: '',
|
|
19
|
+
ruleName,
|
|
20
|
+
lang: 'TYPESCRIPT',
|
|
21
|
+
needTest: true,
|
|
22
|
+
core,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function getRulesDir() {
|
|
26
|
+
const rootDir = await searchCoreRepository();
|
|
27
|
+
if (!rootDir) {
|
|
28
|
+
throw new CreateRuleHelperError('The repository of markuplint is not found');
|
|
29
|
+
}
|
|
30
|
+
const rulesDir = path.resolve(rootDir, ...rulesRelDir);
|
|
31
|
+
const exists = await fsExists(rulesDir);
|
|
32
|
+
if (!exists) {
|
|
33
|
+
throw new CreateRuleHelperError(`Core rules directory (${rulesDir}) is not found`);
|
|
34
|
+
}
|
|
35
|
+
return rulesDir;
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { CreateRuleHelperError } from './create-rule-helper-error.js';
|
|
3
|
+
import { fsExists } from './fs-exists.js';
|
|
4
|
+
import { installScaffold } from './install-scaffold.js';
|
|
5
|
+
export async function createRuleToProject({ pluginName, ruleName, lang, needTest, }) {
|
|
6
|
+
const pluginDir = resolve(process.cwd(), pluginName);
|
|
7
|
+
if (await fsExists(pluginDir)) {
|
|
8
|
+
throw new CreateRuleHelperError(`The directory exists: ${pluginDir}`);
|
|
9
|
+
}
|
|
10
|
+
return await installScaffold('project', pluginDir, {
|
|
11
|
+
pluginName,
|
|
12
|
+
ruleName,
|
|
13
|
+
lang,
|
|
14
|
+
needTest,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fsExists(path: string): Promise<boolean>;
|
package/lib/fs-exists.js
ADDED
package/lib/glob.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const glob: (pattern: string) => Promise<string[]>;
|
package/lib/glob.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CreateRuleCreatorParams, CreateRuleHelperResult } from './types.js';
|
|
2
|
+
export declare function installScaffold(scaffoldType: 'core' | 'project' | 'package', dest: string, params: CreateRuleCreatorParams & {
|
|
3
|
+
readonly packageJson?: boolean;
|
|
4
|
+
}): Promise<CreateRuleHelperResult>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { resolve, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { fsExists } from './fs-exists.js';
|
|
5
|
+
import { transfer } from './transfer.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
export async function installScaffold(scaffoldType, dest, params) {
|
|
9
|
+
const exists = await fsExists(dest);
|
|
10
|
+
if (!exists) {
|
|
11
|
+
await fs.mkdir(dest);
|
|
12
|
+
}
|
|
13
|
+
const scaffoldDir = resolve(__dirname, '..', 'scaffold', scaffoldType);
|
|
14
|
+
const transferred = await transfer(scaffoldType, scaffoldDir, dest, {
|
|
15
|
+
transpile: params.lang === 'JAVASCRIPT',
|
|
16
|
+
test: params.needTest,
|
|
17
|
+
replacer: {
|
|
18
|
+
pluginName: params.pluginName,
|
|
19
|
+
ruleName: params.ruleName,
|
|
20
|
+
description: params.core?.description,
|
|
21
|
+
category: params.core?.category,
|
|
22
|
+
severity: params.core?.severity,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
const packageJson = params.packageJson ? resolve(dest, 'package.json') : null;
|
|
26
|
+
const dependencies = [];
|
|
27
|
+
const devDependencies = [];
|
|
28
|
+
if (packageJson) {
|
|
29
|
+
// const ext = params.lang === 'JAVASCRIPT' ? 'js' : 'ts';
|
|
30
|
+
const packageContent = {
|
|
31
|
+
name: params.ruleName,
|
|
32
|
+
scripts: {},
|
|
33
|
+
};
|
|
34
|
+
if (params.lang === 'TYPESCRIPT') {
|
|
35
|
+
packageContent.scripts.build = 'tsc';
|
|
36
|
+
}
|
|
37
|
+
dependencies.push('@markuplint/ml-core');
|
|
38
|
+
devDependencies.push('markuplint');
|
|
39
|
+
if (params.needTest) {
|
|
40
|
+
packageContent.scripts.test = 'vitest';
|
|
41
|
+
devDependencies.push('vitest');
|
|
42
|
+
}
|
|
43
|
+
if (params.lang === 'TYPESCRIPT') {
|
|
44
|
+
devDependencies.push('typescript');
|
|
45
|
+
}
|
|
46
|
+
await fs.writeFile(packageJson, JSON.stringify(packageContent, null, 2), { encoding: 'utf8' });
|
|
47
|
+
transferred.push({
|
|
48
|
+
ext: '.json',
|
|
49
|
+
name: 'package',
|
|
50
|
+
fileName: 'package',
|
|
51
|
+
test: false,
|
|
52
|
+
destDir: dest,
|
|
53
|
+
filePath: packageJson,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
files: transferred,
|
|
58
|
+
dependencies,
|
|
59
|
+
devDependencies,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isMarkuplintRepo(): Promise<boolean>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readPackageJson(dir: string): Promise<string | null>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export async function readPackageJson(dir) {
|
|
4
|
+
const filePath = path.resolve(dir, 'package.json');
|
|
5
|
+
try {
|
|
6
|
+
const json = await fs.readFile(filePath, { encoding: 'utf8' });
|
|
7
|
+
const data = JSON.parse(json);
|
|
8
|
+
return data?.name ?? null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function searchCoreRepository(): Promise<string | null>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readPackageJson } from './read-package-json.js';
|
|
3
|
+
export async function searchCoreRepository() {
|
|
4
|
+
const paths = path.resolve(process.cwd()).split(path.sep);
|
|
5
|
+
// eslint-disable-next-line no-constant-condition
|
|
6
|
+
while (true) {
|
|
7
|
+
const currentDir = paths.join(path.sep);
|
|
8
|
+
const name = await readPackageJson(currentDir);
|
|
9
|
+
if (name === 'markuplint-packages') {
|
|
10
|
+
return currentDir;
|
|
11
|
+
}
|
|
12
|
+
const dir = paths.pop();
|
|
13
|
+
if (!dir) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { File } from './types.js';
|
|
2
|
+
type TransferOptions = {
|
|
3
|
+
readonly transpile?: boolean;
|
|
4
|
+
readonly test?: boolean;
|
|
5
|
+
readonly replacer?: Readonly<Record<string, string | void>>;
|
|
6
|
+
};
|
|
7
|
+
export declare function transfer(scaffoldType: 'core' | 'project' | 'package', baseDir: string, destDir: string, options?: TransferOptions): Promise<File[]>;
|
|
8
|
+
export {};
|
package/lib/transfer.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { resolve, extname, basename, relative, dirname, sep } from 'node:path';
|
|
4
|
+
import { format } from 'prettier';
|
|
5
|
+
import tsc from 'typescript';
|
|
6
|
+
import { fsExists } from './fs-exists.js';
|
|
7
|
+
import { glob } from './glob.js';
|
|
8
|
+
// eslint-disable-next-line import/no-named-as-default-member
|
|
9
|
+
const { transpile, ScriptTarget } = tsc;
|
|
10
|
+
export async function transfer(scaffoldType, baseDir, destDir, options) {
|
|
11
|
+
const files = await scan(baseDir, destDir);
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const result = await transferFile(scaffoldType, file, options);
|
|
15
|
+
if (result) {
|
|
16
|
+
results.push(result);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return results;
|
|
20
|
+
}
|
|
21
|
+
async function transferFile(scaffoldType, file, options) {
|
|
22
|
+
if (!(await fsExists(file.filePath))) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (file.test && !options?.test) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
let contents = await fs.readFile(file.filePath, { encoding: 'utf8' });
|
|
29
|
+
if (options?.replacer) {
|
|
30
|
+
for (const [before, after] of Object.entries(options?.replacer)) {
|
|
31
|
+
if (!after) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Hyphenation to camel-case for variables
|
|
35
|
+
// `rule-name` => `ruleName`
|
|
36
|
+
contents = contents.replaceAll(new RegExp(`__${before}__c`, 'g'),
|
|
37
|
+
// Camelize
|
|
38
|
+
after.replaceAll(/-+([a-z])/gi, (_, $1) => $1.toUpperCase()).replace(/^[a-z]/, $0 => $0.toLowerCase()));
|
|
39
|
+
contents = contents.replaceAll(new RegExp(`__${before}__`, 'g'), after);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Remove prettier ignore comment
|
|
43
|
+
contents = contents.replace(/\n\s*\/\/ prettier-ignore/, '');
|
|
44
|
+
contents = contents.replace(/\n\s*<!-- prettier-ignore(?:-(?:start|end))? -->/, '');
|
|
45
|
+
const newFile = { ...file };
|
|
46
|
+
if (scaffoldType === 'core' && file.test) {
|
|
47
|
+
const name = options?.replacer?.ruleName;
|
|
48
|
+
if (!name) {
|
|
49
|
+
throw new Error('Rule name is empty');
|
|
50
|
+
}
|
|
51
|
+
newFile.destDir = newFile.destDir.replace(`${sep}rules${sep}src${sep}`, `${sep}rules${sep}test${sep}`);
|
|
52
|
+
contents = contents.replace("require('./').default", `require('../../lib/${name}').default`);
|
|
53
|
+
}
|
|
54
|
+
// TypeScript transpiles to JS
|
|
55
|
+
if (newFile.ext === '.ts' && options?.transpile) {
|
|
56
|
+
newFile.ext = '.js';
|
|
57
|
+
contents = transpile(contents, {
|
|
58
|
+
target: ScriptTarget.ESNext,
|
|
59
|
+
}, newFile.filePath);
|
|
60
|
+
// Insert new line before comments and the export keyword
|
|
61
|
+
contents = contents.replaceAll(/(\n)(\s+\/\*\*|export)/g, '$1\n$2');
|
|
62
|
+
}
|
|
63
|
+
const candidateName = options?.replacer?.[newFile.name.replaceAll('_', '')];
|
|
64
|
+
if (candidateName) {
|
|
65
|
+
newFile.name = candidateName;
|
|
66
|
+
newFile.fileName = candidateName + (newFile.test ? '.spec' : '');
|
|
67
|
+
}
|
|
68
|
+
const dest = resolve(newFile.destDir, newFile.fileName + newFile.ext);
|
|
69
|
+
// Prettier
|
|
70
|
+
const parser = newFile.ext === '.md'
|
|
71
|
+
? 'markdown'
|
|
72
|
+
: newFile.ext === '.json'
|
|
73
|
+
? 'json'
|
|
74
|
+
: newFile.ext === '.ts'
|
|
75
|
+
? options?.transpile
|
|
76
|
+
? 'babel'
|
|
77
|
+
: 'typescript'
|
|
78
|
+
: undefined;
|
|
79
|
+
contents = await format(contents, { parser, filepath: dest });
|
|
80
|
+
if (!(await fsExists(newFile.destDir))) {
|
|
81
|
+
await fs.mkdir(newFile.destDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
await fs.writeFile(dest, contents, { encoding: 'utf8' });
|
|
84
|
+
return newFile;
|
|
85
|
+
}
|
|
86
|
+
async function scan(baseDir, destDir) {
|
|
87
|
+
const fileList = await glob(resolve(baseDir, '**', '*'));
|
|
88
|
+
const destList = fileList
|
|
89
|
+
.map(filePath => {
|
|
90
|
+
const stat = statSync(filePath);
|
|
91
|
+
if (!stat.isFile()) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const relPath = relative(baseDir, filePath);
|
|
95
|
+
const destPath = resolve(destDir, relPath);
|
|
96
|
+
const ext = extname(destPath);
|
|
97
|
+
const fileName = basename(destPath, ext);
|
|
98
|
+
const test = extname(fileName) === '.spec';
|
|
99
|
+
const name = basename(fileName, '.spec');
|
|
100
|
+
const destFileDir = dirname(destPath);
|
|
101
|
+
return {
|
|
102
|
+
ext,
|
|
103
|
+
fileName,
|
|
104
|
+
name,
|
|
105
|
+
test,
|
|
106
|
+
destDir: destFileDir,
|
|
107
|
+
filePath,
|
|
108
|
+
};
|
|
109
|
+
})
|
|
110
|
+
.filter((f) => !!f);
|
|
111
|
+
return destList;
|
|
112
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type CreateRuleHelperParams = CreateRuleCreatorParams & {
|
|
2
|
+
readonly purpose: CreateRulePurpose;
|
|
3
|
+
};
|
|
4
|
+
export type CreateRuleCreatorParams = {
|
|
5
|
+
readonly pluginName: string;
|
|
6
|
+
readonly ruleName: string;
|
|
7
|
+
readonly lang: CreateRuleLanguage;
|
|
8
|
+
readonly needTest: boolean;
|
|
9
|
+
readonly core?: CreateRuleCreatorCoreParams;
|
|
10
|
+
};
|
|
11
|
+
export type CreateRuleCreatorCoreParams = {
|
|
12
|
+
readonly description: string;
|
|
13
|
+
readonly category: string;
|
|
14
|
+
readonly severity: string;
|
|
15
|
+
};
|
|
16
|
+
export type CreateRuleHelperResult = {
|
|
17
|
+
readonly files: readonly File[];
|
|
18
|
+
readonly dependencies: readonly string[];
|
|
19
|
+
readonly devDependencies: readonly string[];
|
|
20
|
+
};
|
|
21
|
+
export type CreateRuleLanguage = 'JAVASCRIPT' | 'TYPESCRIPT';
|
|
22
|
+
export type CreateRulePurpose = 'ADD_TO_PROJECT' | 'PUBLISH_AS_PACKAGE' | 'CONTRIBUTE_TO_CORE';
|
|
23
|
+
export type File = {
|
|
24
|
+
readonly ext: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
readonly fileName: string;
|
|
27
|
+
readonly test: boolean;
|
|
28
|
+
readonly destDir: string;
|
|
29
|
+
readonly filePath: string;
|
|
30
|
+
};
|
package/lib/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markuplint/create-rule",
|
|
3
|
+
"version": "4.0.0-alpha.7",
|
|
4
|
+
"description": "Rule generator for markuplint",
|
|
5
|
+
"repository": "git@github.com:markuplint/markuplint.git",
|
|
6
|
+
"author": "Yusuke Hirao <yusukehirao@me.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"private": false,
|
|
9
|
+
"type": "module",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./lib/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"types": "./lib/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"create-rule": "./bin/create-rule.mjs"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc --build --watch",
|
|
25
|
+
"clean": "tsc --build --clean"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@markuplint/cli-utils": "4.0.0-alpha.7",
|
|
29
|
+
"@markuplint/ml-core": "4.0.0-alpha.7",
|
|
30
|
+
"glob": "^10.3.6",
|
|
31
|
+
"prettier": "^3.1.1",
|
|
32
|
+
"ts-node": "^10.9.2",
|
|
33
|
+
"typescript": "^5.3.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/fs-extra": "^11.0.4",
|
|
37
|
+
"fs-extra": "^11.2.0"
|
|
38
|
+
},
|
|
39
|
+
"gitHead": "571129bf6498541125e1e7c3907d5ed9af53459a"
|
|
40
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: TODO/翻訳 __description__
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# `__ruleName__`
|
|
6
|
+
|
|
7
|
+
<!-- textlint-disable ja-technical-writing/ja-no-mixed-period -->
|
|
8
|
+
|
|
9
|
+
<!-- prettier-ignore-start -->
|
|
10
|
+
TODO: 翻訳
|
|
11
|
+
__description__
|
|
12
|
+
<!-- prettier-ignore-end -->
|
|
13
|
+
|
|
14
|
+
❌ 間違ったコード例
|
|
15
|
+
|
|
16
|
+
```html
|
|
17
|
+
<todo>Write incorrect codes</todo>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
✅ 正しいコード例
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<todo>Write correct codes</todo>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
<!-- textlint-enable ja-technical-writing/ja-no-mixed-period -->
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: __ruleName__
|
|
3
|
+
description: __description__
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `__ruleName__`
|
|
7
|
+
|
|
8
|
+
<!-- prettier-ignore-start -->
|
|
9
|
+
__description__
|
|
10
|
+
<!-- prettier-ignore-end -->
|
|
11
|
+
|
|
12
|
+
❌ Examples of **incorrect** code for this rule
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<todo>Write incorrect codes</todo>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
✅ Examples of **correct** code for this rule
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<todo>Write correct codes</todo>
|
|
22
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mlRuleTest } from 'markuplint';
|
|
2
|
+
import { test, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import rule from './index.js';
|
|
5
|
+
|
|
6
|
+
test('It is test', async () => {
|
|
7
|
+
const { violations } = await mlRuleTest(rule, '<x-foo></x-foo>');
|
|
8
|
+
expect(violations.length).toBe(1);
|
|
9
|
+
// expect(violations).toStrictEqual([
|
|
10
|
+
// {
|
|
11
|
+
// severity: 'error',
|
|
12
|
+
// line: 1,
|
|
13
|
+
// col: 1,
|
|
14
|
+
// raw: '',
|
|
15
|
+
// message: 'It is issue',
|
|
16
|
+
// },
|
|
17
|
+
// ]);
|
|
18
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createRule } from '@markuplint/ml-core';
|
|
2
|
+
|
|
3
|
+
export default createRule<boolean, null>({
|
|
4
|
+
defaultValue: true,
|
|
5
|
+
defaultOptions: null,
|
|
6
|
+
async verify({ document, report, t }) {
|
|
7
|
+
// Element
|
|
8
|
+
await document.walkOn('Element', el => {
|
|
9
|
+
const raw = el.raw.trim();
|
|
10
|
+
if (/./.test(raw)) {
|
|
11
|
+
report({
|
|
12
|
+
scope: el,
|
|
13
|
+
message: t('It is {0}', 'issue'),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Attribute
|
|
19
|
+
await document.walkOn('Attr', attr => {
|
|
20
|
+
if (/./.test(attr.name)) {
|
|
21
|
+
report({
|
|
22
|
+
scope: attr,
|
|
23
|
+
line: attr.nameNode?.startLine,
|
|
24
|
+
col: attr.nameNode?.startCol,
|
|
25
|
+
raw: attr.nameNode?.raw,
|
|
26
|
+
message: t('It is {0}', 'issue'),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
},
|
|
31
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"_category": "__category__",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"value": {
|
|
6
|
+
"type": "boolean",
|
|
7
|
+
"description": "__VALUE_DESCRIPTION__",
|
|
8
|
+
"description:ja": "__VALUE_DESCRIPTION_IN_JAPANESE__"
|
|
9
|
+
},
|
|
10
|
+
"options": {
|
|
11
|
+
"type": "object",
|
|
12
|
+
"additionalProperties": false,
|
|
13
|
+
"properties": {
|
|
14
|
+
"__OPTIONS_PROP_NAME__": {
|
|
15
|
+
"type": "boolean",
|
|
16
|
+
"default": "true",
|
|
17
|
+
"description": "__OPTIONS_PROP_DESCRIPTION__",
|
|
18
|
+
"description:ja": "__OPTIONS_PROP_DESCRIPTION_IN_JAPANESE__"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"oneOf": [
|
|
24
|
+
{
|
|
25
|
+
"type": "boolean"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"$ref": "#/definitions/value"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "object",
|
|
32
|
+
"additionalProperties": false,
|
|
33
|
+
"properties": {
|
|
34
|
+
"value": { "$ref": "#/definitions/value" },
|
|
35
|
+
"options": { "$ref": "#/definitions/options" },
|
|
36
|
+
"option": { "$ref": "#/definitions/options", "deprecated": true },
|
|
37
|
+
"severity": {
|
|
38
|
+
"$ref": "https://raw.githubusercontent.com/markuplint/markuplint/main/packages/%40markuplint/ml-config/schema.json#/definitions/severity",
|
|
39
|
+
"default": "__severity__"
|
|
40
|
+
},
|
|
41
|
+
"reason": {
|
|
42
|
+
"type": "string"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# The `__pluginName__` rule
|
|
2
|
+
|
|
3
|
+
TODO: Write a description
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```shell
|
|
8
|
+
npm install --save-dev {{ name }}
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Applying rules
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"plugins": ["__pluginName__"],
|
|
16
|
+
"rules": {
|
|
17
|
+
"__pluginName__/__ruleName__": {
|
|
18
|
+
"value": "__MAIN_VALUE__",
|
|
19
|
+
"options": {
|
|
20
|
+
"foo": "__OPTIONAL_VALUE__",
|
|
21
|
+
"bar": [123, 456, 789]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Rule Details
|
|
29
|
+
|
|
30
|
+
👎 Examples of **incorrect** code for this rule
|
|
31
|
+
|
|
32
|
+
```html
|
|
33
|
+
<todo>Write incorrect codes</todo>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
👍 Examples of **correct** code for this rule
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<todo>Write correct codes</todo>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Interface
|
|
43
|
+
|
|
44
|
+
- Type: `string`
|
|
45
|
+
- Default Value: `"__DEFAULT_MAIN_VALUE__"`
|
|
46
|
+
|
|
47
|
+
### Options
|
|
48
|
+
|
|
49
|
+
TODO: Write a description
|
|
50
|
+
|
|
51
|
+
#### Interface
|
|
52
|
+
|
|
53
|
+
| Property | Type | Optional | Default Value | Description |
|
|
54
|
+
| -------- | ---------- | -------- | ------------- | ------------------------- |
|
|
55
|
+
| `foo` | `string` | ✔ | `undefined` | TODO: Write a description |
|
|
56
|
+
| `bar` | `number[]` | ✔ | `undefined` | TODO: Write a description |
|
|
57
|
+
|
|
58
|
+
### Default severity
|
|
59
|
+
|
|
60
|
+
TODO: Choose `error` or `warning`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createPlugin } from '@markuplint/ml-core';
|
|
2
|
+
|
|
3
|
+
import { __ruleName__c } from './rules/__ruleName__.js';
|
|
4
|
+
|
|
5
|
+
export default createPlugin({
|
|
6
|
+
name: '__pluginName__',
|
|
7
|
+
create(settings) {
|
|
8
|
+
return {
|
|
9
|
+
rules: {
|
|
10
|
+
// prettier-ignore
|
|
11
|
+
'__ruleName__': __ruleName__c(settings),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CreatePluginSettings } from '@markuplint/ml-core';
|
|
2
|
+
|
|
3
|
+
import { createRule } from '@markuplint/ml-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Step 0-1. Define the type of principal value
|
|
7
|
+
*
|
|
8
|
+
* You can define it as either `string` or `number` or `boolean` or `Array`.
|
|
9
|
+
* If you define a complex structure, you should define `boolean` to this
|
|
10
|
+
* and define that structure to the options.
|
|
11
|
+
*/
|
|
12
|
+
type MainValue = string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Step 0-2. Define the type of options
|
|
16
|
+
*
|
|
17
|
+
* You can define it as an _Object_. Set `null` if it is empty.
|
|
18
|
+
*/
|
|
19
|
+
type Options = {
|
|
20
|
+
foo?: string;
|
|
21
|
+
bar?: number[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const __ruleName__c = (settings: CreatePluginSettings) =>
|
|
25
|
+
createRule<MainValue, Options>({
|
|
26
|
+
/**
|
|
27
|
+
* Step 1-1. Choose the severity from `error` or `warning`
|
|
28
|
+
*
|
|
29
|
+
* Default is `error`
|
|
30
|
+
*/
|
|
31
|
+
defaultSeverity: 'error',
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Step 1-2. Set the default principal value
|
|
35
|
+
*
|
|
36
|
+
* It adopts this value in the evaluation
|
|
37
|
+
* if it sets `true` or undefined (in other words, it doesn't set)
|
|
38
|
+
* to the configuration.
|
|
39
|
+
*/
|
|
40
|
+
defaultValue: '__DEFAULT_MAIN_VALUE__',
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Step 1-3. Set the default options
|
|
44
|
+
*
|
|
45
|
+
* It adopts this value in the evaluation
|
|
46
|
+
* if it doesn't set to the configuration.
|
|
47
|
+
*/
|
|
48
|
+
defaultOptions: {},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Step 2. Write a process.
|
|
52
|
+
*
|
|
53
|
+
* @param context
|
|
54
|
+
*/
|
|
55
|
+
async verify({ document, report, t }) {
|
|
56
|
+
/**
|
|
57
|
+
* Example: Use `walkOn` method to traverse the node tree
|
|
58
|
+
*/
|
|
59
|
+
await document.walkOn('Comment', comment => {
|
|
60
|
+
/**
|
|
61
|
+
* Example: Access the property of the node to get needed data
|
|
62
|
+
*/
|
|
63
|
+
const commentText = comment.raw.trim();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Example: Compare data according to your design to report the violation
|
|
67
|
+
*/
|
|
68
|
+
if (/^<!--\s*todo:/i.test(commentText)) {
|
|
69
|
+
/**
|
|
70
|
+
* Example: It delivers the violation to the linter engine
|
|
71
|
+
*
|
|
72
|
+
* This `report` method can call many times.
|
|
73
|
+
*/
|
|
74
|
+
report({
|
|
75
|
+
/**
|
|
76
|
+
* Example: Define the scope.
|
|
77
|
+
*
|
|
78
|
+
* Set the _node_ that gives
|
|
79
|
+
* the location (line number and column number)
|
|
80
|
+
* to the linter engine.
|
|
81
|
+
*/
|
|
82
|
+
scope: comment,
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Example: The message that is output to a user.
|
|
86
|
+
*
|
|
87
|
+
* You can set just strings without through the translator.
|
|
88
|
+
*/
|
|
89
|
+
message: t('It is {0}', 'TODO'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "commonjs",
|
|
4
|
+
"target": "es2019",
|
|
5
|
+
"strict": true,
|
|
6
|
+
"strictNullChecks": true,
|
|
7
|
+
"strictPropertyInitialization": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"experimentalDecorators": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"lib": ["dom", "es2015", "es2016", "es2017", "es2018", "es2019", "esnext"],
|
|
14
|
+
"skipLibCheck": true,
|
|
15
|
+
"outDir": "./lib",
|
|
16
|
+
"rootDir": "./src"
|
|
17
|
+
},
|
|
18
|
+
"include": ["./src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "lib", "./src/**/*.spec.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createPlugin } from '@markuplint/ml-core';
|
|
2
|
+
|
|
3
|
+
import { __ruleName__c } from './rules/__ruleName__.js';
|
|
4
|
+
|
|
5
|
+
export default createPlugin({
|
|
6
|
+
name: '__pluginName__',
|
|
7
|
+
create(setting) {
|
|
8
|
+
return {
|
|
9
|
+
rules: {
|
|
10
|
+
// prettier-ignore
|
|
11
|
+
'__ruleName__': __ruleName__c(setting),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mlRuleTest } from 'markuplint';
|
|
2
|
+
import { test, expect } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { __ruleName__c } from './__ruleName__.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Example: Write tests
|
|
8
|
+
*/
|
|
9
|
+
test('It is test', async () => {
|
|
10
|
+
const { violations } = await mlRuleTest(
|
|
11
|
+
__ruleName__c({
|
|
12
|
+
/* Plugin settings */
|
|
13
|
+
}),
|
|
14
|
+
/**
|
|
15
|
+
* Example: The target HTML that is evaluated
|
|
16
|
+
*/
|
|
17
|
+
'<div><!-- TODO: I will do something --></div>',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Example: Set expected results.
|
|
22
|
+
*/
|
|
23
|
+
expect(violations).toStrictEqual([
|
|
24
|
+
{
|
|
25
|
+
severity: 'error',
|
|
26
|
+
line: 1,
|
|
27
|
+
col: 6,
|
|
28
|
+
raw: '<!-- TODO: I will do something -->',
|
|
29
|
+
message: 'It is TODO',
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CreatePluginSettings } from '@markuplint/ml-core';
|
|
2
|
+
|
|
3
|
+
import { createRule } from '@markuplint/ml-core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Step 0-1. Define the type of principal value
|
|
7
|
+
*
|
|
8
|
+
* You can define it as either `string` or `number` or `boolean` or `Array`.
|
|
9
|
+
* If you define a complex structure, you should define `boolean` to this
|
|
10
|
+
* and define that structure to the options.
|
|
11
|
+
*/
|
|
12
|
+
type MainValue = string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Step 0-2. Define the type of options
|
|
16
|
+
*
|
|
17
|
+
* You can define it as an _Object_. Set `null` if it is empty.
|
|
18
|
+
*/
|
|
19
|
+
type Options = {
|
|
20
|
+
foo?: string;
|
|
21
|
+
bar?: number[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const __ruleName__c = (settings: CreatePluginSettings = {}) =>
|
|
25
|
+
createRule<MainValue, Options>({
|
|
26
|
+
/**
|
|
27
|
+
* Step 1-1. Choose the severity from `error` or `warning`
|
|
28
|
+
*
|
|
29
|
+
* Default is `error`
|
|
30
|
+
*/
|
|
31
|
+
defaultSeverity: 'error',
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Step 1-2. Set the default principal value
|
|
35
|
+
*
|
|
36
|
+
* It adopts this value in the evaluation
|
|
37
|
+
* if it sets `true` or undefined (in other words, it doesn't set)
|
|
38
|
+
* to the configuration.
|
|
39
|
+
*/
|
|
40
|
+
defaultValue: '__DEFAULT_MAIN_VALUE__',
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Step 1-3. Set the default options
|
|
44
|
+
*
|
|
45
|
+
* It adopts this value in the evaluation
|
|
46
|
+
* if it doesn't set to the configuration.
|
|
47
|
+
*/
|
|
48
|
+
defaultOptions: {},
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Step 2. Write a process.
|
|
52
|
+
*
|
|
53
|
+
* @param context
|
|
54
|
+
*/
|
|
55
|
+
async verify({ document, report, t }) {
|
|
56
|
+
/**
|
|
57
|
+
* Example: Use `walkOn` method to traverse the node tree
|
|
58
|
+
*/
|
|
59
|
+
await document.walkOn('Comment', comment => {
|
|
60
|
+
/**
|
|
61
|
+
* Example: Access the property of the node to get needed data
|
|
62
|
+
*/
|
|
63
|
+
const commentText = comment.raw.trim();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Example: Compare data according to your design to report the violation
|
|
67
|
+
*/
|
|
68
|
+
if (/^<!--\s*todo:/i.test(commentText)) {
|
|
69
|
+
/**
|
|
70
|
+
* Example: It delivers the violation to the linter engine
|
|
71
|
+
*
|
|
72
|
+
* This `report` method can call many times.
|
|
73
|
+
*/
|
|
74
|
+
report({
|
|
75
|
+
/**
|
|
76
|
+
* Example: Define the scope.
|
|
77
|
+
*
|
|
78
|
+
* Set the _node_ that gives
|
|
79
|
+
* the location (line number and column number)
|
|
80
|
+
* to the linter engine.
|
|
81
|
+
*/
|
|
82
|
+
scope: comment,
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Example: The message that is output to a user.
|
|
86
|
+
*
|
|
87
|
+
* You can set just strings without through the translator.
|
|
88
|
+
*/
|
|
89
|
+
message: t('It is {0}', 'TODO'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
});
|