Skip to content

Commit 408a142

Browse files
committed
refactor: use typescript
1 parent 0e436f0 commit 408a142

File tree

9 files changed

+374
-304
lines changed

9 files changed

+374
-304
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
},
1414
"version": "6.1.0",
1515
"description": "textlint rule for prh.",
16-
"main": "lib/textlint-rule-prh.js",
16+
"main": "lib/node.js",
1717
"files": [
1818
"lib",
1919
"src"
@@ -42,6 +42,7 @@
4242
},
4343
"devDependencies": {
4444
"@textlint/legacy-textlint-core": "^15.2.1",
45+
"@textlint/types": "^15.2.1",
4546
"lint-staged": "^16.1.5",
4647
"prettier": "^3.6.2",
4748
"textlint": "15.2.1",

src/core.ts

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// LICENSE : MIT
2+
import { RuleHelper } from "textlint-rule-helper";
3+
import { parse } from "@babel/parser";
4+
import { ChangeSet, Engine } from "prh";
5+
import {
6+
TextlintRuleContext,
7+
TextlintRuleOptions,
8+
TextlintRuleReporter,
9+
TextlintRuleReportHandler
10+
} from "@textlint/types";
11+
import { ASTNodeTypes, TxtCodeBlockNode } from "@textlint/ast-node-types";
12+
import { CommentBlock, CommentLine } from "@babel/types";
13+
14+
const defaultOptions = {
15+
checkLink: false,
16+
checkBlockQuote: false,
17+
checkEmphasis: false,
18+
checkHeader: true,
19+
checkParagraph: true,
20+
/**
21+
* Check CodeBlock text
22+
* Default: []
23+
*/
24+
checkCodeComment: [],
25+
/**
26+
* Report parsing error for debug
27+
*/
28+
debug: false
29+
};
30+
31+
const assertOptions = (options: TextlintRuleOptions) => {
32+
if (typeof options.ruleContents === "undefined" && typeof options.rulePaths === "undefined") {
33+
throw new Error(`textlint-rule-prh require Rule Options.
34+
Please set .textlintrc:
35+
{
36+
"rules": {
37+
"prh": {
38+
"rulePaths" :["path/to/prh.yml"]
39+
}
40+
}
41+
}
42+
`);
43+
}
44+
};
45+
46+
const createIgnoreNodeTypes = (options: TextlintRuleOptions, Syntax: typeof ASTNodeTypes) => {
47+
const nodeTypes = [];
48+
if (!options.checkLink) {
49+
nodeTypes.push(Syntax.Link);
50+
}
51+
if (!options.checkBlockQuote) {
52+
nodeTypes.push(Syntax.BlockQuote);
53+
}
54+
if (!options.checkEmphasis) {
55+
nodeTypes.push(Syntax.Emphasis);
56+
}
57+
if (!options.checkHeader) {
58+
nodeTypes.push(Syntax.Header);
59+
}
60+
if (!options.checkParagraph) {
61+
nodeTypes.push(Syntax.Paragraph);
62+
}
63+
return nodeTypes;
64+
};
65+
66+
/**
67+
* for each diff of changeSet
68+
*/
69+
const forEachChange = (
70+
changeSet: ChangeSet,
71+
str: string,
72+
onChangeOfMatch: (arg: {
73+
matchStartIndex: number;
74+
matchEndIndex: number;
75+
actual: string;
76+
expected: string;
77+
prh?: string;
78+
}) => void
79+
) => {
80+
const sortedDiffs = changeSet.diffs.sort(function (a, b) {
81+
return a.index - b.index;
82+
});
83+
let delta = 0;
84+
sortedDiffs.forEach(function (diff) {
85+
// TODO: What should I use `!` or `?`
86+
const result = diff.expected!.replace(/\$([0-9]{1,2})/g, function (match, g1) {
87+
const index = parseInt(g1);
88+
if (index === 0 || diff.matches.length - 1 < index) {
89+
return match;
90+
}
91+
return diff.matches[index] || "";
92+
});
93+
// matchStartIndex/matchEndIndex value is original position, not replaced position
94+
// textlint use original position
95+
const matchStartIndex = diff.index;
96+
const matchEndIndex = matchStartIndex + diff.matches[0].length;
97+
// actual => expected
98+
const actual = str.slice(diff.index + delta, diff.index + delta + diff.matches[0].length);
99+
// TODO: What should I use `!` or `?`
100+
const prh = diff.rule!.raw.prh || null;
101+
onChangeOfMatch({
102+
matchStartIndex,
103+
matchEndIndex,
104+
actual: actual,
105+
expected: result,
106+
prh
107+
});
108+
str = str.slice(0, diff.index + delta) + result + str.slice(diff.index + delta + diff.matches[0].length);
109+
delta += result.length - diff.matches[0].length;
110+
});
111+
};
112+
113+
/**
114+
* [Markdown] get actual code value from CodeBlock node
115+
* @param node
116+
* @param raw raw value include CodeBlock syntax
117+
*/
118+
function getUntrimmedCode(node: TxtCodeBlockNode, raw: string): string {
119+
if (node.type !== "CodeBlock") {
120+
return node.value;
121+
}
122+
// Space indented CodeBlock that has not lang
123+
if (!node.lang) {
124+
return node.value;
125+
}
126+
127+
// If it is not markdown codeBlock, just use node.value
128+
if (!(raw.startsWith("```") && raw.endsWith("```"))) {
129+
if (node.value.endsWith("\n")) {
130+
return node.value;
131+
}
132+
return node.value + "\n";
133+
}
134+
// Markdown(remark) specific hack
135+
// https://github.com/wooorm/remark/issues/207#issuecomment-244620590
136+
const lines = raw.split("\n");
137+
// code lines without the first line and the last line
138+
const codeLines = lines.slice(1, lines.length - 1);
139+
// add last new line
140+
// \n```
141+
return codeLines.join("\n") + "\n";
142+
}
143+
144+
export function createReporter(
145+
createPrhEngine: (context: TextlintRuleContext, options: TextlintRuleOptions) => Engine
146+
): TextlintRuleReporter {
147+
function reporter(context: TextlintRuleContext, userOptions: TextlintRuleOptions = {}): TextlintRuleReportHandler {
148+
assertOptions(userOptions);
149+
const options = Object.assign({}, defaultOptions, userOptions);
150+
151+
const prhEngine = createPrhEngine(context, options);
152+
153+
const helper = new RuleHelper(context);
154+
const { Syntax, getSource, report, fixer, RuleError } = context;
155+
const ignoreNodeTypes = createIgnoreNodeTypes(options, Syntax);
156+
const codeCommentTypes = options.checkCodeComment ? options.checkCodeComment : defaultOptions.checkCodeComment;
157+
const isDebug = options.debug ? options.debug : defaultOptions.debug;
158+
return {
159+
[Syntax.Str](node) {
160+
if (helper.isChildNode(node, ignoreNodeTypes)) {
161+
return;
162+
}
163+
const text = getSource(node);
164+
// to get position from index
165+
// https://github.com/prh/prh/issues/29
166+
const dummyFilePath = "";
167+
const makeChangeSet = prhEngine.makeChangeSet(dummyFilePath, text);
168+
forEachChange(makeChangeSet, text, ({ matchStartIndex, matchEndIndex, actual, expected, prh }) => {
169+
// If result is not changed, should not report
170+
if (actual === expected) {
171+
return;
172+
}
173+
174+
const suffix = prh !== null ? "\n" + prh : "";
175+
const messages = actual + " => " + expected + suffix;
176+
report(
177+
node,
178+
new RuleError(messages, {
179+
index: matchStartIndex,
180+
fix: fixer.replaceTextRange([matchStartIndex, matchEndIndex], expected)
181+
})
182+
);
183+
});
184+
},
185+
[Syntax.CodeBlock](node) {
186+
const lang = node.lang;
187+
if (!lang) {
188+
return;
189+
}
190+
const checkLang = codeCommentTypes.some((type) => {
191+
return type === node.lang;
192+
});
193+
if (!checkLang) {
194+
return;
195+
}
196+
const rawText = getSource(node);
197+
const codeText = getUntrimmedCode(node, rawText);
198+
const sourceBlockDiffIndex = rawText !== node.value ? rawText.indexOf(codeText) : 0;
199+
const reportComment = (comment: CommentBlock | CommentLine) => {
200+
// to get position from index
201+
// https://github.com/prh/prh/issues/29
202+
const dummyFilePath = "";
203+
// TODO: trim option for value?
204+
const text = comment.value;
205+
const makeChangeSet = prhEngine.makeChangeSet(dummyFilePath, text);
206+
forEachChange(makeChangeSet, text, ({ matchStartIndex, matchEndIndex, actual, expected, prh }) => {
207+
// If result is not changed, should not report
208+
if (actual === expected) {
209+
return;
210+
}
211+
212+
const suffix = prh !== null ? "\n" + prh : "";
213+
const messages = actual + " => " + expected + suffix;
214+
const commentIdentifier = comment.type === "CommentBlock" ? "/*" : "//";
215+
// TODO: What should I use `!` or `?`
216+
const commentStart = sourceBlockDiffIndex + comment.start! + commentIdentifier.length;
217+
report(
218+
node,
219+
new RuleError(messages, {
220+
index: commentStart + matchStartIndex,
221+
fix: fixer.replaceTextRange(
222+
[commentStart + matchStartIndex, commentStart + matchEndIndex],
223+
expected
224+
)
225+
})
226+
);
227+
});
228+
};
229+
try {
230+
const AST = parse(codeText, {
231+
ranges: true,
232+
allowReturnOutsideFunction: true,
233+
allowAwaitOutsideFunction: true,
234+
allowUndeclaredExports: true,
235+
allowSuperOutsideMethod: true
236+
});
237+
const comments = AST.comments;
238+
if (!comments) {
239+
return;
240+
}
241+
comments.forEach((comment) => {
242+
reportComment(comment);
243+
});
244+
} catch (error) {
245+
if (isDebug) {
246+
console.error(error);
247+
//@ts-expect-error
248+
report(node, new RuleError(error.message));
249+
}
250+
}
251+
}
252+
};
253+
}
254+
return reporter;
255+
}

src/node.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// LICENSE : MIT
2+
import { homedir } from "node:os";
3+
import { Engine, fromYAML, fromYAMLFilePath } from "prh";
4+
import { resolve, dirname } from "path";
5+
import { createReporter } from "./core";
6+
import { TextlintRuleContext, TextlintRuleReporter } from "@textlint/types";
7+
8+
const homeDirectory = homedir();
9+
10+
const untildify = (filePath: string): string => {
11+
return homeDirectory ? filePath.replace(/^~(?=$|\/|\\)/, homeDirectory) : filePath;
12+
};
13+
14+
function createPrhEngine(rulePaths: string[], baseDir: string): Engine | null {
15+
if (rulePaths.length === 0) {
16+
return null;
17+
}
18+
const expandedRulePaths = rulePaths.map((rulePath) => untildify(rulePath));
19+
const prhEngine = fromYAMLFilePath(resolve(baseDir, expandedRulePaths[0]));
20+
expandedRulePaths.slice(1).forEach((ruleFilePath) => {
21+
const config = fromYAMLFilePath(resolve(baseDir, ruleFilePath));
22+
prhEngine.merge(config);
23+
});
24+
return prhEngine;
25+
}
26+
27+
function createPrhEngineFromContents(yamlContents: string[]) {
28+
if (yamlContents.length === 0) {
29+
return null;
30+
}
31+
const dummyFilePath = "";
32+
const prhEngine = fromYAML(dummyFilePath, yamlContents[0]);
33+
yamlContents.slice(1).forEach((content) => {
34+
const config = fromYAML(dummyFilePath, content);
35+
prhEngine.merge(config);
36+
});
37+
return prhEngine;
38+
}
39+
40+
function mergePrh(...engines: (Engine | null)[]) {
41+
const engines_ = engines.filter((engine) => !!engine);
42+
const mainEngine = engines_[0];
43+
engines_.slice(1).forEach((engine) => {
44+
mainEngine.merge(engine);
45+
});
46+
return mainEngine;
47+
}
48+
49+
const getConfigBaseDir = (context: TextlintRuleContext) => {
50+
if (typeof context.getConfigBaseDir === "function") {
51+
return context.getConfigBaseDir() || process.cwd();
52+
}
53+
// @ts-expect-error Old fallback that use deprecated `config` value
54+
// https://github.com/textlint/textlint/issues/294
55+
const textlintRcFilePath = context.config ? context.config.configFile : null;
56+
// .textlintrc directory
57+
return textlintRcFilePath ? dirname(textlintRcFilePath) : process.cwd();
58+
};
59+
60+
const reporter: TextlintRuleReporter = createReporter((context, options) => {
61+
// .textlintrc directory
62+
const textlintRCDir = getConfigBaseDir(context);
63+
// create prh config
64+
const rulePaths = options.rulePaths || [];
65+
const ruleContents = options.ruleContents || [];
66+
// yaml file + yaml contents
67+
const prhEngineContent = createPrhEngineFromContents(ruleContents);
68+
const prhEngineFiles = createPrhEngine(rulePaths, textlintRCDir);
69+
const prhEngine = mergePrh(prhEngineFiles, prhEngineContent);
70+
71+
return prhEngine;
72+
});
73+
74+
export default {
75+
linter: reporter,
76+
fixer: reporter
77+
};

0 commit comments

Comments
 (0)