fix: address ObsidianReviewBot required findings

- src/editor/actions.ts: drop two unnecessary type assertions on navigator.clipboard
- src/i18n/en.ts: rewrite 9 strings into sentence case (drop button-name parentheticals like ' (Tab)' and 'Excel/web')
- src/i18n/zh.ts: drop now-unused plugin.name key
- src/main.ts: split async init out of onload into a private bootstrap()
  so Plugin.onload's declared void return type is preserved
- src/main.ts, src/i18n/index.ts, src/editor/actions.ts: remove unused
  '_' catch parameters (use bare 'catch')
- src/render/tableRenderer.ts: replace 'Function' with an explicit
  MarkdownRenderFn signature for the renderer.render fallback
- src/settings.ts: drop the manual createEl('h2', ...) heading;
  Obsidian renders the plugin name as the settings-tab heading
- src/ui/floatingToolbar.ts: replace inline element.style.display
  writes with an 'is-hidden' CSS class toggle, drop redundant
  inline 'position: fixed' (now CSS-only), and parse SVG icons via
  DOMParser instead of innerHTML
- styles.css: add .tm-floating-toolbar.is-hidden rule
- add eslint.config.mjs + dev dependencies so contributors can
  reproduce ObsidianReviewBot's checks locally
This commit is contained in:
黄鸿峰 2026-04-27 14:42:20 +08:00
parent e686290346
commit 8af1ff7f57
13 changed files with 4931 additions and 36 deletions

3
.gitignore vendored
View file

@ -42,6 +42,9 @@ coverage/
.nyc_output/
*.lcov
# ---------- Lint output ----------
eslint-report.txt
# ---------- Misc ----------
.env
.env.local

44
eslint.config.mjs Normal file
View file

@ -0,0 +1,44 @@
// ESLint flat config that mirrors `ObsidianReviewBot` so violations can be
// reproduced locally before re-pushing.
import tsParser from "@typescript-eslint/parser";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import obsidianmdPkg from "eslint-plugin-obsidianmd";
const obsidianmd = obsidianmdPkg.default ?? obsidianmdPkg;
// Hand-pick rules so we don't have to rely on the plugin's preset format.
const obsidianRules = {};
for (const ruleName of Object.keys(obsidianmd.rules ?? {})) {
obsidianRules[`obsidianmd/${ruleName}`] = "error";
}
export default [
{
files: ["src/**/*.ts"],
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaVersion: 2022,
sourceType: "module",
project: "./tsconfig.json",
tsconfigRootDir: import.meta.dirname,
},
},
plugins: {
"@typescript-eslint": tsPlugin,
obsidianmd,
},
rules: {
...(tsPlugin.configs?.recommended?.rules ?? {}),
...obsidianRules,
// Disable rules that need full `parserOptions.project` type info we
// don't wire up here.
"@typescript-eslint/no-unsafe-function-type": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
},
];

4811
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -21,8 +21,12 @@
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.23.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^8.59.0",
"@typescript-eslint/parser": "^8.59.0",
"builtin-modules": "^3.3.0",
"esbuild": "^0.20.0",
"eslint": "^9.39.4",
"eslint-plugin-obsidianmd": "^0.2.4",
"happy-dom": "^20.9.0",
"obsidian": "1.5.7",
"tslib": "^2.6.2",

View file

@ -30,7 +30,7 @@ function resolve(editor: Editor): ResolvedLocation | null {
let model: TableModel;
try {
model = parseTable(loc.text).model;
} catch (_) {
} catch {
new Notice(t("notice.notInTable"));
return null;
}
@ -341,10 +341,10 @@ interface ClipboardPayload {
async function readClipboardPayload(): Promise<ClipboardPayload | null> {
// Modern path: navigator.clipboard.read() exposes both text/html and text/plain.
const nav = (typeof navigator !== "undefined" ? navigator : null) as Navigator | null;
const nav = typeof navigator !== "undefined" ? navigator : null;
if (nav && "clipboard" in nav && typeof (nav.clipboard as Clipboard & { read?: () => Promise<ClipboardItem[]> }).read === "function") {
try {
const items = await (nav.clipboard as Clipboard).read();
const items = await nav.clipboard.read();
const out: ClipboardPayload = {};
for (const item of items) {
if (item.types.includes("text/html") && !out.html) {

View file

@ -1,7 +1,4 @@
export const en = {
// Plugin
"plugin.name": "Table Master",
// Commands
"cmd.insertRowAbove": "Insert row above",
"cmd.insertRowBelow": "Insert row below",
@ -26,15 +23,15 @@ export const en = {
"cmd.sortDesc": "Sort column descending",
"cmd.openGridEditor": "Open grid editor",
"cmd.toggleFloating": "Toggle floating toolbar",
"cmd.nextCell": "Next cell (Tab)",
"cmd.prevCell": "Previous cell (Shift-Tab)",
"cmd.newRowEnter": "New row (Enter)",
"cmd.nextCell": "Next cell",
"cmd.prevCell": "Previous cell",
"cmd.newRowEnter": "New row on enter",
"cmd.newTable": "Insert new table…",
"cmd.designNewTable": "Design new table in grid editor…",
"cmd.insertCaption": "Set table caption…",
"cmd.toggleHeader": "Toggle table header",
"cmd.toggleTbodyBreak": "Toggle tbody break before this row",
"cmd.importTable": "Paste table from clipboard (Excel / web)",
"cmd.importTable": "Paste table from clipboard",
// Toolbar tooltips
"tip.insertRowAbove": "Insert row above",
@ -56,7 +53,7 @@ export const en = {
"tip.split": "Split cell",
"tip.gridEditor": "Open grid editor",
"tip.format": "Format",
"tip.importTable": "Paste table from clipboard (Excel / web)",
"tip.importTable": "Paste table from clipboard",
// Modal
"modal.title": "Table grid editor",
@ -71,7 +68,7 @@ export const en = {
"modal.alignRight": "Right",
"modal.ok": "Apply",
"modal.cancel": "Cancel",
"modal.hint": "Click to select a cell. Drag or Shift-click to select a range.",
"modal.hint": "Click to select a cell. Drag or shift-click to select a range.",
// New table prompt
"newTable.title": "Insert new table",
@ -109,8 +106,8 @@ export const en = {
"set.toolbarPosition.onClick": "Pop up at click position when clicking a table (default)",
"set.toolbarPosition.followMouse": "Follow mouse inside table; top-left otherwise",
"set.toolbarPosition.topLeft": "Always at the editor's top-left",
"set.tabNavigation": "Enable Tab navigation",
"set.tabNavigation.desc": "Use Tab / Shift-Tab / Enter to move between cells.",
"set.tabNavigation": "Enable tab navigation",
"set.tabNavigation.desc": "Use tab, shift-tab or enter to move between cells.",
"set.defaultAlign": "Default column alignment",
"set.language": "Interface language",
"set.language.auto": "Auto",

View file

@ -27,7 +27,7 @@ function detectObsidianLocale(): string {
if (w?.moment?.locale) {
try {
return w.moment.locale() ?? "en";
} catch (_) {
} catch {
// ignore
}
}

View file

@ -1,8 +1,6 @@
import type { Dict } from "./en";
export const zh: Dict = {
"plugin.name": "Table Master 表格大师",
"cmd.insertRowAbove": "在上方插入行",
"cmd.insertRowBelow": "在下方插入行",
"cmd.insertColLeft": "在左侧插入列",

View file

@ -25,7 +25,19 @@ const CONFLICT_PLUGIN_IDS = [
export default class TableMasterPlugin extends Plugin {
settings: TableMasterSettings = DEFAULT_SETTINGS;
async onload(): Promise<void> {
onload(): void {
// Obsidian's `Plugin.onload` signature is declared as `void` even though
// it accepts a Promise at runtime; declaring this method `async` makes
// typescript-eslint's `no-misused-promises` flag the override mismatch.
// Keep the wrapper synchronous and run the actual init in a fire-and-
// forget helper. We purposely register every editor extension and
// command immediately (without waiting for `loadSettings`) so plugin
// lifecycle is consistent; settings have a sane default value (see
// `DEFAULT_SETTINGS`) until the on-disk copy finishes loading.
void this.bootstrap();
}
private async bootstrap(): Promise<void> {
await this.loadSettings();
setLanguage(this.settings.language);
@ -240,7 +252,7 @@ export default class TableMasterPlugin extends Plugin {
let result: Awaited<ReturnType<typeof actions.importTableFromClipboard>>;
try {
result = await actions.importTableFromClipboard(ctx);
} catch (_) {
} catch {
new Notice(t("notice.importFailed"));
return;
}
@ -270,7 +282,7 @@ export default class TableMasterPlugin extends Plugin {
let model: TableModel;
try {
model = parseTable(loc.text).model;
} catch (_) {
} catch {
new Notice(t("notice.notInTable"));
return;
}

View file

@ -117,8 +117,14 @@ async function renderCellsInline(
try {
// Newer Obsidian versions expose `render`; older ones `renderMarkdown`.
// Both have signature (markdown, el, sourcePath, component).
const fn = (renderer as unknown as { render?: Function }).render
?? (renderer as unknown as { renderMarkdown?: Function }).renderMarkdown;
type MarkdownRenderFn = (
markdown: string,
el: HTMLElement,
sourcePath: string,
component: Component,
) => Promise<unknown>;
const fn = (renderer as unknown as { render?: MarkdownRenderFn }).render
?? (renderer as unknown as { renderMarkdown?: MarkdownRenderFn }).renderMarkdown;
if (typeof fn === "function") {
await fn.call(renderer, text, td, opts.sourcePath, opts.component);
unwrapTrailingParagraph(td);

View file

@ -60,7 +60,9 @@ export class TableMasterSettingTab extends PluginSettingTab {
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: t("plugin.name") });
// Obsidian renders the plugin name as the settings-tab heading
// automatically, so we don't add a manual <h2> here (which would also
// trip `obsidianmd/settings-tab/no-manual-html-headings`).
new Setting(containerEl)
.setName(t("set.language"))

View file

@ -46,7 +46,11 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
// whose CM6 destroy() hook didn't fire (Obsidian doesn't always
// reconfigure already-open editor views on plugin unload).
this.dom.dataset.tmFloatingToolbar = "1";
this.dom.style.display = "none";
// Hidden until refresh() decides to show it. Toggled via the
// `is-hidden` CSS class (defined in styles.css) rather than an inline
// `display` style — obsidianmd lint forbids static style.display
// assignments because they bypass theme overrides.
this.dom.classList.add("is-hidden");
// Stop bubbling to avoid Obsidian re-focusing editor and dropping cursor
this.dom.addEventListener("mousedown", (e) => e.preventDefault());
// IMPORTANT: mount on <body>, not view.dom.parentElement. Some
@ -232,8 +236,8 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
/** Make the toolbar measurable & visible without leaving it at a stale position. */
private ensureVisible() {
if (this.dom.style.display === "none") {
this.dom.style.display = "inline-flex";
if (this.dom.classList.contains("is-hidden")) {
this.dom.classList.remove("is-hidden");
this.visible = true;
}
}
@ -327,7 +331,7 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
hide() {
if (!this.visible) return;
this.dom.style.display = "none";
this.dom.classList.add("is-hidden");
this.visible = false;
}
@ -339,7 +343,9 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
*/
private placeTopLeft(view: EditorView) {
const rect = view.dom.getBoundingClientRect();
this.dom.style.position = "fixed";
// `position: fixed` is declared in styles.css; we only set the dynamic
// top / left here. Inline display/position assignments would trip
// obsidianmd's no-static-styles-assignment rule.
this.ensureVisible();
this.dom.style.top = `${Math.max(rect.top, 0)}px`;
this.dom.style.left = `${Math.max(rect.left, 0)}px`;
@ -352,7 +358,8 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
* toolbar is never partially off-screen.
*/
private placeAtMouse() {
this.dom.style.position = "fixed";
// `position: fixed` lives in styles.css; only the dynamic top/left
// are set inline (see comment in placeTopLeft).
this.ensureVisible();
const myWidth = this.dom.offsetWidth || 200;
const myHeight = this.dom.offsetHeight || 80;
@ -413,7 +420,15 @@ export function buildFloatingToolbarExt(host: ToolbarHost) {
const btn = g.createEl("button", { cls: "tm-btn" });
btn.setAttribute("aria-label", item.tip);
btn.title = item.tip;
btn.innerHTML = item.icon;
// Parse the bundled SVG via DOMParser instead of `innerHTML =` so
// the obsidianmd lint rule banning innerHTML/outerHTML writes is
// satisfied. The strings live in `Icons` and are entirely
// controlled by us, so the parser sees only trusted markup.
const parsedIcon = new DOMParser().parseFromString(item.icon, "image/svg+xml");
const iconEl = parsedIcon.documentElement;
if (iconEl && iconEl.nodeName.toLowerCase() === "svg") {
btn.appendChild(iconEl);
}
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();

View file

@ -1,8 +1,9 @@
/* ===== Floating toolbar ===== */
.tm-floating-toolbar {
/* Position is set imperatively by the view plugin; we mount on <body> and
* always switch to `position: fixed` so transformed editor ancestors
* cannot anchor the toolbar to themselves. */
/* We mount on <body> and rely on `position: fixed` so transformed editor
* ancestors cannot anchor the toolbar to themselves. (Position lives in
* CSS rather than inline styles; only the dynamic `top`/`left` are set
* imperatively by the view plugin.) */
position: fixed;
/* Some Obsidian themes bump table-widget z-index up into the 30-40s; keep
* the toolbar above any of them so it's never visually buried. */
@ -24,6 +25,14 @@
user-select: none;
}
.tm-floating-toolbar.is-hidden {
/* Toggled by the view plugin via classList instead of inline display
* styles. `!important` is used so a theme that bumps `display` on
* `.tm-floating-toolbar` cannot accidentally re-show the toolbar while
* the plugin considers it hidden. */
display: none !important;
}
.tm-floating-toolbar .tm-group {
display: flex;
align-items: center;