eslint-plugin-obsidianmd
Version:
Validates guidelines for Obsidian plugins
149 lines (148 loc) • 6.38 kB
JavaScript
import { ESLintUtils } from "@typescript-eslint/utils";
import { createSentenceCaseReporter, getContextFilename, isEnglishLocalePath, resolveSentenceCaseConfig, unwrapExpression, } from "./sentenceCaseUtil.js";
const ruleCreator = ESLintUtils.RuleCreator((name) => `https://github.com/obsidianmd/eslint-plugin/blob/master/docs/rules/ui/${name}.md`);
// Default configuration with no custom options specified
const defaultOptions = [{}];
// Checks if the current file is an English locale TypeScript or JavaScript module
function isEnglishLocaleModule(context) {
const file = getContextFilename(context);
return file != null && isEnglishLocalePath(file, ["ts", "js", "cjs", "mjs"]);
}
// Recursively collects all string literals from nested objects and arrays
function collectStringLiterals(root, cb) {
const stack = [root];
while (stack.length > 0) {
const popped = stack.pop();
if (!popped)
continue;
const current = unwrapExpression(popped);
if (!current)
continue;
if (current.type === "Literal") {
if (typeof current.value === "string")
cb(current, current.value);
continue;
}
if (current.type === "TemplateLiteral") {
if (current.expressions.length === 0) {
const raw = current.quasis.map((q) => q.value.raw).join("");
cb(current, raw);
}
continue;
}
if (current.type === "ObjectExpression") {
for (const property of current.properties) {
if (property.type === "Property") {
const value = property.value;
stack.push(value);
}
else if (property.type === "SpreadElement") {
stack.push(property.argument);
}
}
continue;
}
if (current.type === "ArrayExpression") {
for (const element of current.elements) {
if (!element || element.type === "SpreadElement")
continue;
stack.push(element);
}
}
}
}
export default ruleCreator({
name: "sentence-case-locale-module",
meta: {
type: "suggestion",
docs: {
description: "Enforce sentence case for English TS/JS locale module strings",
url: "https://github.com/obsidianmd/eslint-plugin/blob/master/docs/rules/ui/sentence-case-locale-module.md",
},
fixable: "code",
hasSuggestions: false,
schema: [
{
type: "object",
properties: {
mode: { type: "string", enum: ["loose", "strict"] },
brands: { type: "array", items: { type: "string" } },
acronyms: { type: "array", items: { type: "string" } },
ignoreWords: { type: "array", items: { type: "string" } },
ignoreRegex: { type: "array", items: { type: "string" } },
allowAutoFix: { type: "boolean" },
enforceCamelCaseLower: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
useSentenceCase: "Use sentence case for UI text.",
},
},
defaultOptions,
create(context) {
if (!isEnglishLocaleModule(context))
return {};
const { evaluatorOptions, allowAutoFix } = resolveSentenceCaseConfig(context.options);
const varObjects = new Map();
const reportIfNeeded = createSentenceCaseReporter({
context,
evaluatorOptions,
allowAutoFix,
messageId: "useSentenceCase",
debugLabel: "ui/sentence-case-locale-module",
});
return {
Program(program) {
// Track top-level variable declarations that are objects
for (const stmt of program.body) {
if (stmt.type === "VariableDeclaration") {
for (const d of stmt.declarations) {
if (d.id.type === "Identifier" &&
d.init) {
const unwrapped = unwrapExpression(d.init);
if (unwrapped && unwrapped.type === "ObjectExpression") {
varObjects.set(d.id.name, unwrapped);
}
}
}
}
}
},
ExportDefaultDeclaration(node) {
if (!node.declaration)
return;
if (node.declaration.type === "ObjectExpression") {
collectStringLiterals(node.declaration, (n, v) => reportIfNeeded(n, v));
}
else if (node.declaration.type === "Identifier") {
const obj = varObjects.get(node.declaration.name);
if (obj)
collectStringLiterals(obj, (n, v) => reportIfNeeded(n, v));
}
},
ExportNamedDeclaration(node) {
if (node.declaration && node.declaration.type === "VariableDeclaration") {
for (const decl of node.declaration.declarations) {
if (decl.init) {
const unwrapped = unwrapExpression(decl.init);
if (unwrapped && unwrapped.type === "ObjectExpression") {
collectStringLiterals(unwrapped, (n, v) => reportIfNeeded(n, v));
}
}
}
}
else if (!node.source && node.specifiers && node.specifiers.length > 0) {
for (const spec of node.specifiers) {
if (spec.type === "ExportSpecifier" && spec.local.type === "Identifier") {
const obj = varObjects.get(spec.local.name);
if (obj)
collectStringLiterals(obj, (n, v) => reportIfNeeded(n, v));
}
}
}
},
};
},
});