Files
RSSHub/eslint-plugins/nsfw-flag.js
2025-09-30 12:30:30 +08:00

215 lines
7.8 KiB
JavaScript

/**
* ESLint 9 plugin to automatically mark NSFW routes with the nsfw flag
*/
const nsfwRoutes = [
'141jav',
'141ppv',
'18comic',
'2048',
'7mmtv',
'8kcos',
'91porn',
'95mm',
'abskoop',
'asiantolick',
'asmr-200',
'booru',
'chikubi',
'chub',
'civitai',
'cool18',
'coomer',
'copymanga',
'cosplaytele',
'dlsite',
'e-hentai',
'ehentai',
'everia',
'fanbox',
'fansly',
'fantia',
'freexcomic',
'furaffinity',
'gelbooru',
'hanime1',
'iwara',
'javbus',
'javdb',
'javlibrary',
'javtiful',
'javtrailers',
'jpxgmn',
'kemono',
'kisskiss',
'komiic',
'konachan',
'koyso',
'laimanhua',
'literotica',
'mangadex',
'manhuagui',
'manyvids',
'missav',
'netflav',
'nhentai',
'olevod',
'oreno3d',
'patreon',
'pixiv',
'pornhub',
'rawkuma',
'sehuatang',
'shuiguopai',
'sis001',
'skeb',
'skebetter',
'spankbang',
't66y',
'uraaka-joshi',
'wnacg',
'xbookcn',
'xmanhua',
'xsijishe',
'yande',
'zaimanhua',
'zodgame',
'4kup',
'misskon',
'4khd',
];
// 检查是否是 NSFW 路由文件
function isNsfwRoute(filePath) {
const normalizedPath = filePath.replaceAll('\\', '/');
return nsfwRoutes.some((nsfwKey) => {
const routePattern = `/lib/routes/${nsfwKey}/`;
return normalizedPath.includes(routePattern);
});
}
export default {
meta: {
name: '@rsshub/nsfw-flag',
version: '1.0.0',
},
configs: {
recommended: {
plugins: {
'@rsshub/nsfw-flag': 'self',
},
rules: {
'@rsshub/nsfw-flag/add-nsfw-flag': 'error',
},
},
},
rules: {
'add-nsfw-flag': {
meta: {
type: 'problem',
docs: {
description: 'Automatically add nsfw flag to NSFW routes',
category: 'Best Practices',
recommended: true,
},
fixable: 'code',
schema: [],
messages: {
missingNsfwFlag: 'NSFW route is missing the nsfw flag in features',
},
},
create(context) {
const filename = context.filename || context.getFilename();
// 如果不是 NSFW 路由,跳过检查
if (!isNsfwRoute(filename)) {
return {};
}
return {
ExportNamedDeclaration(node) {
// 查找 export const route: Route = {...}
if (
node.declaration &&
node.declaration.type === 'VariableDeclaration' &&
node.declaration.declarations &&
node.declaration.declarations[0] &&
node.declaration.declarations[0].id &&
node.declaration.declarations[0].id.name === 'route'
) {
const routeDeclaration = node.declaration.declarations[0];
const routeObject = routeDeclaration.init;
if (routeObject && routeObject.type === 'ObjectExpression') {
let featuresProperty = null;
let nsfwProperty = null;
// 查找 features 属性
for (const prop of routeObject.properties) {
if (prop.type === 'Property' && prop.key && prop.key.name === 'features') {
featuresProperty = prop;
// 在 features 中查找 nsfw 属性
if (prop.value && prop.value.type === 'ObjectExpression') {
for (const featureProp of prop.value.properties) {
if (featureProp.type === 'Property' && featureProp.key && featureProp.key.name === 'nsfw') {
nsfwProperty = featureProp;
break;
}
}
}
break;
}
}
// 检查是否需要添加或修复 nsfw 标志
if (!featuresProperty) {
// 没有 features 属性,需要添加整个 features 对象
context.report({
node: routeObject,
messageId: 'missingNsfwFlag',
fix(fixer) {
// 在对象的最后添加 features 属性
const lastProperty = routeObject.properties.at(-1);
return lastProperty
? fixer.insertTextAfter(lastProperty, ',\n features: {\n nsfw: true,\n }')
: // 空对象的情况
fixer.insertTextAfter(routeObject.properties.length > 0 ? routeObject.properties.at(-1) : routeObject, '\n features: {\n nsfw: true,\n }\n');
},
});
} else if (!nsfwProperty) {
// 有 features 属性但没有 nsfw 属性
context.report({
node: featuresProperty.value,
messageId: 'missingNsfwFlag',
fix(fixer) {
const featuresObject = featuresProperty.value;
if (featuresObject.properties.length > 0) {
const lastFeatureProp = featuresObject.properties.at(-1);
return fixer.insertTextAfter(lastFeatureProp, ',\n nsfw: true');
} else {
// features 是空对象
return fixer.replaceTextRange([featuresObject.range[0] + 1, featuresObject.range[1] - 1], '\n nsfw: true,\n ');
}
},
});
} else if (nsfwProperty.value && (nsfwProperty.value.type !== 'Literal' || nsfwProperty.value.value !== true)) {
// nsfw 属性存在但不是 true
context.report({
node: nsfwProperty.value,
messageId: 'missingNsfwFlag',
fix(fixer) {
return fixer.replaceText(nsfwProperty.value, 'true');
},
});
}
}
}
},
};
},
},
},
};