mirror of
https://github.com/moranrs/table-master.git
synced 2026-07-22 06:53:11 +00:00
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:
parent
e686290346
commit
8af1ff7f57
13 changed files with 4931 additions and 36 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -42,6 +42,9 @@ coverage/
|
|||
.nyc_output/
|
||||
*.lcov
|
||||
|
||||
# ---------- Lint output ----------
|
||||
eslint-report.txt
|
||||
|
||||
# ---------- Misc ----------
|
||||
.env
|
||||
.env.local
|
||||
|
|
|
|||
44
eslint.config.mjs
Normal file
44
eslint.config.mjs
Normal 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
4811
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ function detectObsidianLocale(): string {
|
|||
if (w?.moment?.locale) {
|
||||
try {
|
||||
return w.moment.locale() ?? "en";
|
||||
} catch (_) {
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import type { Dict } from "./en";
|
||||
|
||||
export const zh: Dict = {
|
||||
"plugin.name": "Table Master 表格大师",
|
||||
|
||||
"cmd.insertRowAbove": "在上方插入行",
|
||||
"cmd.insertRowBelow": "在下方插入行",
|
||||
"cmd.insertColLeft": "在左侧插入列",
|
||||
|
|
|
|||
18
src/main.ts
18
src/main.ts
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
15
styles.css
15
styles.css
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue