forked from sindresorhus/awesome-lint
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathno-repeat-item-in-description.js
More file actions
112 lines (89 loc) · 2.91 KB
/
Copy pathno-repeat-item-in-description.js
File metadata and controls
112 lines (89 loc) · 2.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import {lintRule} from 'unified-lint-rule';
import {toString} from 'mdast-util-to-string';
import {visit} from 'unist-util-visit';
const WORD_BOUNDARY_PATTERN = /^[\s.,!?:-]/;
const SEPARATOR = ' - ';
const isBadgeLink = (link, nextNode) => {
const hasImage = link.children?.some(node => node.type === 'image');
const followedByLink = nextNode?.type === 'link';
return hasImage || followedByLink;
};
const findMainLink = paragraphChildren => {
for (const [index, child] of paragraphChildren.entries()) {
if (child.type !== 'link') {
continue;
}
const nextNonWhitespace = paragraphChildren
.slice(index + 1)
.find(node => node.type !== 'text' || node.value.trim());
if (!isBadgeLink(child, nextNonWhitespace)) {
return {link: child, index};
}
}
return {link: null, index: -1};
};
const extractDescription = (nodes, separatorIndex) => {
const separatorNode = nodes[separatorIndex];
const parts = separatorNode.value.split(SEPARATOR);
if (parts.length < 2) {
return '';
}
// Get text after separator plus all remaining nodes
const afterSeparator = parts.slice(1).join(SEPARATOR);
const remainingText = nodes
.slice(separatorIndex + 1)
.map(node => toString(node))
.join('');
return (afterSeparator + remainingText).trim();
};
const findSeparatorIndex = nodes => {
for (const [index, node] of nodes.entries()) {
if (node.type === 'text' && node.value.includes(SEPARATOR)) {
return index;
}
// Allow formatting nodes before separator
const isFormatting = ['emphasis', 'strong', 'inlineCode'].includes(node.type);
const isWhitespace = node.type === 'text' && !node.value.trim();
if (!isFormatting && !isWhitespace) {
return -1;
}
}
return -1;
};
const noRepeatItemInDescriptionRule = lintRule('remark-lint:no-repeat-item-in-description', (ast, file) => {
visit(ast, 'listItem', node => {
const paragraph = node.children?.find(child => child.type === 'paragraph');
if (!paragraph?.children) {
return;
}
const {link: mainLink, index: mainLinkIndex} = findMainLink(paragraph.children);
if (!mainLink) {
return;
}
const itemName = toString(mainLink);
if (!itemName) {
return;
}
const nodesAfterLink = paragraph.children.slice(mainLinkIndex + 1);
const separatorIndex = findSeparatorIndex(nodesAfterLink);
if (separatorIndex === -1) {
return;
}
const description = extractDescription(nodesAfterLink, separatorIndex);
if (!description) {
return;
}
// Check if description starts with item name (case-insensitive)
if (description.toLowerCase().startsWith(itemName.toLowerCase())) {
const afterItemName = description.slice(itemName.length);
// Check for word boundary to avoid false positives
if (!afterItemName || WORD_BOUNDARY_PATTERN.test(afterItemName)) {
file.message(
`List item description should not start with the item name "${itemName}"`,
mainLink,
);
}
}
});
});
export default noRepeatItemInDescriptionRule;