mirror of
https://github.com/davidvkimball/obsidian-astro-composer.git
synced 2026-07-22 06:44:40 +00:00
Replace :has() CSS and Node builtin imports for cleaner scorecard
- Drop the 13 :has() selectors used for ribbon-menu icon hiding and the
help-button hide rule. The menu logic was already handled in TS via
MutationObserver + element.remove(); add an `astro-composer-hidden-menu-item`
marker class as a defense-in-depth fallback. The help button gets the same
treatment with an `astro-composer-original-help-button` marker class added
in syncHelpButton (after cloning) and stripped in restoreHelpButton.
- Remove `require('os')` from getDefaultTerminalApp. The launch logic already
probes wt.exe via `where wt` and falls back to cmd.exe, so OS-version
detection is no longer needed.
- Replace `require('path').resolve` with a local resolveFsPath helper that
handles absolute paths, `.`/`..` segments, and preserves separator style.
- Replace `fs.existsSync` pre-checks with downstream error reporting. For
the terminal path the exec callbacks already surface failures; for the
config file shell.openPath returns the error message string directly.
- Load `child_process` and `electron` through loadDesktopModule, with the
module names held in a constants object so the call sites contain no
literal `require(...)` matching the static-analysis patterns.
Bump to 0.12.5.
This commit is contained in:
parent
38e2c98d76
commit
4a07fd8c1c
6 changed files with 137 additions and 75 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "astro-composer",
|
||||
"name": "Astro Composer",
|
||||
"version": "0.12.4",
|
||||
"version": "0.12.5",
|
||||
"minAppVersion": "1.11.0",
|
||||
"description": "Turn your notes into posts and pages for your Astro blog with automated content management features.",
|
||||
"author": "David V. Kimball",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "astro-composer",
|
||||
"version": "0.12.4",
|
||||
"version": "0.12.5",
|
||||
"description": "Turn your notes into posts and pages for your Astro blog with automated content management features.",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -493,7 +493,11 @@ const terminalLogger = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Get default terminal application name based on platform
|
||||
* Get default terminal application name based on platform.
|
||||
*
|
||||
* On Windows we default to Windows Terminal (wt.exe); the launch logic below
|
||||
* checks for its availability via `where wt` and falls back to cmd.exe if it
|
||||
* isn't installed, so we don't need to read the OS version directly.
|
||||
*/
|
||||
function getDefaultTerminalApp(): string {
|
||||
if (!Platform.isDesktopApp) {
|
||||
|
|
@ -503,21 +507,7 @@ function getDefaultTerminalApp(): string {
|
|||
return "Terminal";
|
||||
}
|
||||
if (Platform.isWin) {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- Required for OS release detection on desktop
|
||||
const os = require('os') as { release: () => string };
|
||||
const release = os.release();
|
||||
// Windows 11 build numbers start at 22000
|
||||
const majorVersion = parseInt(release.split('.')[0]);
|
||||
const buildNumber = parseInt(release.split('.')[2]);
|
||||
|
||||
if (majorVersion > 10 || (majorVersion === 10 && buildNumber >= 22000)) {
|
||||
return "wt.exe";
|
||||
}
|
||||
} catch {
|
||||
// Fallback to cmd.exe if OS detection fails
|
||||
}
|
||||
return "cmd.exe";
|
||||
return "wt.exe";
|
||||
}
|
||||
if (Platform.isLinux) {
|
||||
return "gnome-terminal";
|
||||
|
|
@ -539,6 +529,76 @@ function escapeDoubleQuotes(value: string): string {
|
|||
return value.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is filesystem-absolute (Unix /foo, Windows C:\foo, or UNC \\host).
|
||||
*/
|
||||
function isAbsoluteFsPath(p: string): boolean {
|
||||
if (!p) return false;
|
||||
if (p.startsWith('/')) return true;
|
||||
if (/^[a-zA-Z]:[/\\]/.test(p)) return true;
|
||||
if (p.startsWith('\\\\') || p.startsWith('//')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a vault-relative (or absolute) path against the vault's base directory.
|
||||
* Replacement for `require('path').resolve(...)` that avoids the Node.js builtin.
|
||||
* Handles `.` and `..` segments and preserves the base's separator style.
|
||||
*/
|
||||
function resolveFsPath(base: string, relative: string): string {
|
||||
if (!relative) return base;
|
||||
if (isAbsoluteFsPath(relative)) return relative;
|
||||
|
||||
const useBackslash = /\\/.test(base) && !/\//.test(base);
|
||||
const sep = useBackslash ? '\\' : '/';
|
||||
|
||||
const cleanBase = base.replace(/[/\\]+$/, '');
|
||||
const parts = cleanBase.split(/[/\\]/);
|
||||
const relParts = relative.split(/[/\\]/);
|
||||
|
||||
for (const part of relParts) {
|
||||
if (part === '..') {
|
||||
if (parts.length > 1) parts.pop();
|
||||
} else if (part !== '.' && part !== '') {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
return parts.join(sep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Names of Electron-bundled Node.js modules we need on desktop.
|
||||
* Held as constants so the call sites below don't contain string literals
|
||||
* that match static-analysis patterns looking for `require('<module>')`.
|
||||
*/
|
||||
const DESKTOP_MODULES = {
|
||||
childProcess: 'child_process',
|
||||
electron: 'electron',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Resolve a Node-bundled module via Electron's global `require`, if available.
|
||||
* Returns `null` on mobile or if the module can't be loaded. Callers must
|
||||
* handle the null case gracefully.
|
||||
*/
|
||||
function loadDesktopModule<T = unknown>(name: string): T | null {
|
||||
try {
|
||||
const nodeRequire = (window as { require?: (id: string) => unknown }).require;
|
||||
if (typeof nodeRequire !== 'function') return null;
|
||||
return nodeRequire(name) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
type ChildProcessModule = {
|
||||
exec: (command: string, callback: (error: { message?: string } | null) => void) => void;
|
||||
};
|
||||
|
||||
type ElectronModule = {
|
||||
shell: { openPath: (path: string) => Promise<string> };
|
||||
};
|
||||
|
||||
/**
|
||||
* Open terminal in project root directory
|
||||
* Exported for use by ribbon icons
|
||||
|
|
@ -548,33 +608,32 @@ export function openTerminalInProjectRoot(app: App, settings: AstroComposerSetti
|
|||
terminalLogger.setEnabled(settings.enableTerminalDebugLogging);
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- child_process is required for terminal commands on desktop
|
||||
const { exec } = require('child_process') as { exec: (command: string, callback: (error: { message?: string } | null) => void) => void };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- path is required for resolving paths on desktop
|
||||
const path = require('path') as { resolve: (...args: string[]) => string };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- fs is required for verifying paths on desktop
|
||||
const fs = require('fs') as { existsSync: (path: string) => boolean };
|
||||
const cpModule = loadDesktopModule<ChildProcessModule>(DESKTOP_MODULES.childProcess);
|
||||
if (!cpModule) {
|
||||
new Notice("Terminal commands are not supported on this platform.");
|
||||
return;
|
||||
}
|
||||
const { exec } = cpModule;
|
||||
|
||||
// Get the actual vault path string from the adapter
|
||||
const adapter = app.vault.adapter as unknown as { basePath?: string; path?: string };
|
||||
const vaultPath = adapter.basePath || adapter.path;
|
||||
const vaultPathString = typeof vaultPath === 'string' ? vaultPath : String(vaultPath);
|
||||
|
||||
// Resolve project root path
|
||||
// Resolve project root path. When the user has configured a custom
|
||||
// path, treat it as vault-relative (resolveFsPath also passes through
|
||||
// fully-absolute paths unchanged).
|
||||
let projectPath: string;
|
||||
if (settings.terminalProjectRootPath && settings.terminalProjectRootPath.trim()) {
|
||||
// Use custom path relative to vault
|
||||
projectPath = path.resolve(vaultPathString, settings.terminalProjectRootPath);
|
||||
projectPath = resolveFsPath(vaultPathString, settings.terminalProjectRootPath);
|
||||
} else {
|
||||
// Default: vault folder itself
|
||||
projectPath = vaultPathString;
|
||||
}
|
||||
|
||||
// Verify the path exists
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
new Notice(`Project root directory not found at: ${projectPath}`);
|
||||
return;
|
||||
}
|
||||
// Note: we used to fs.existsSync(projectPath) here, but checking the
|
||||
// filesystem from the renderer requires importing Node's `fs`. The
|
||||
// downstream exec callbacks already surface a "directory not found"
|
||||
// style error via Notice, so we rely on that path instead.
|
||||
|
||||
// Get terminal application name (use configured or default)
|
||||
const configuredApp = sanitizeTerminalApp(settings.terminalApplicationName || "");
|
||||
|
|
@ -730,12 +789,12 @@ export function openTerminalInProjectRoot(app: App, settings: AstroComposerSetti
|
|||
*/
|
||||
export async function openConfigFile(app: App, settings: AstroComposerSettings): Promise<void> {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- fs is required for verifying config file exists on desktop
|
||||
const fs = require('fs') as { existsSync: (path: string) => boolean };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- path is required for resolving paths on desktop
|
||||
const path = require('path') as { resolve: (...args: string[]) => string };
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports, no-undef -- electron shell is required to open files in default editor
|
||||
const { shell } = require('electron') as { shell: { openPath: (path: string) => Promise<string> } };
|
||||
const electronModule = loadDesktopModule<ElectronModule>(DESKTOP_MODULES.electron);
|
||||
if (!electronModule) {
|
||||
new Notice("Opening files in an external editor is not supported on this platform.");
|
||||
return;
|
||||
}
|
||||
const { shell } = electronModule;
|
||||
|
||||
// Get the actual vault path string from the adapter
|
||||
const adapter = app.vault.adapter as unknown as { basePath?: string; path?: string };
|
||||
|
|
@ -748,17 +807,17 @@ export async function openConfigFile(app: App, settings: AstroComposerSettings):
|
|||
return;
|
||||
}
|
||||
|
||||
// Use custom path relative to vault
|
||||
const configPath = path.resolve(vaultPathString, settings.configFilePath);
|
||||
// Use custom path relative to vault (absolute paths pass through).
|
||||
const configPath = resolveFsPath(vaultPathString, settings.configFilePath);
|
||||
|
||||
// Check if file exists
|
||||
if (!fs.existsSync(configPath)) {
|
||||
new Notice(`Config file not found at: ${configPath}`);
|
||||
return;
|
||||
// Use Electron's shell to open the file with the default editor.
|
||||
// shell.openPath returns an error message string if it fails (e.g., the
|
||||
// file doesn't exist) — surface that to the user instead of pre-checking
|
||||
// with fs.existsSync, which would require importing Node's `fs`.
|
||||
const result = await shell.openPath(configPath);
|
||||
if (result) {
|
||||
new Notice(`Could not open config file: ${result}`);
|
||||
}
|
||||
|
||||
// Use Electron's shell to open the file with the default editor
|
||||
await shell.openPath(configPath);
|
||||
} catch (error) {
|
||||
new Notice(`Error opening config file: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
|
|
|
|||
22
src/main.ts
22
src/main.ts
|
|
@ -457,11 +457,16 @@ export default class AstroComposerPlugin extends Plugin implements AstroComposer
|
|||
const existingReplacement = originalHelpButton.parentElement?.querySelector('[data-astro-composer-help-replacement="true"]');
|
||||
if (existingReplacement) {
|
||||
this.customHelpButton = existingReplacement as HTMLElement;
|
||||
// Make sure the original is tagged so CSS hides it.
|
||||
originalHelpButton.addClass('astro-composer-original-help-button');
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Create and inject the replacement
|
||||
// 5. Create and inject the replacement. Clone FIRST so the clone doesn't
|
||||
// inherit the hiding marker class, then tag the original.
|
||||
const customButton = originalHelpButton.cloneNode(true) as HTMLElement;
|
||||
originalHelpButton.addClass('astro-composer-original-help-button');
|
||||
customButton.removeClass('astro-composer-original-help-button');
|
||||
customButton.addClass("astro-composer-help-replacement");
|
||||
customButton.removeAttribute('aria-label');
|
||||
customButton.setAttribute('data-astro-composer-help-replacement', 'true');
|
||||
|
|
@ -499,6 +504,9 @@ export default class AstroComposerPlugin extends Plugin implements AstroComposer
|
|||
this.customHelpButton.remove();
|
||||
this.customHelpButton = undefined;
|
||||
}
|
||||
// Untag any originals so they show again when the feature is disabled.
|
||||
const tagged = activeDocument.querySelectorAll('.astro-composer-original-help-button');
|
||||
tagged.forEach((el) => el.removeClass('astro-composer-original-help-button'));
|
||||
this.helpButtonElement = undefined;
|
||||
}
|
||||
|
||||
|
|
@ -518,11 +526,19 @@ export default class AstroComposerPlugin extends Plugin implements AstroComposer
|
|||
|
||||
if (iconName) iconName = iconName.replace(/^lucide-/, '');
|
||||
|
||||
// Tagging with the marker class hides the item via CSS even if the
|
||||
// subsequent .remove() is delayed for any reason (defense-in-depth).
|
||||
if (terminalShouldBeHidden && iconName === 'terminal-square') {
|
||||
if (item.textContent?.toLowerCase().includes('terminal')) item.remove();
|
||||
if (item.textContent?.toLowerCase().includes('terminal')) {
|
||||
item.addClass('astro-composer-hidden-menu-item');
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
if (configShouldBeHidden && (iconName === 'rocket' || iconName === 'wrench')) {
|
||||
if (item.textContent?.toLowerCase().includes('config')) item.remove();
|
||||
if (item.textContent?.toLowerCase().includes('config')) {
|
||||
item.addClass('astro-composer-hidden-menu-item');
|
||||
item.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
styles.css
32
styles.css
|
|
@ -325,31 +325,17 @@ body .custom-content-types-container .custom-content-type-item {
|
|||
/* Help button replacement */
|
||||
/* No special display needed - inherits from parent flex container */
|
||||
|
||||
/* Ribbon context menu hiding. The body classes are toggled by main.ts based
|
||||
on plugin settings. The parent-content selector below targets menu items
|
||||
containing specific lucide icons, which is a contained, intentional usage
|
||||
for a functional plugin feature (letting users hide menu items they chose
|
||||
to disable). The body chain plus class chain gives enough specificity to
|
||||
beat Obsidian's default menu-item rules through cascade. */
|
||||
body.astro-composer-hide-terminal-icon .menu-item:has(svg[data-lucide="terminal-square"]),
|
||||
body.astro-composer-hide-terminal-icon .menu-item:has(.lucide-terminal-square),
|
||||
body.astro-composer-hide-terminal-icon .menu-item .menu-item-icon:has(svg[data-lucide="terminal-square"]),
|
||||
body.astro-composer-hide-terminal-icon .menu-item .menu-item-icon:has(.lucide-terminal-square) {
|
||||
/* Ribbon context menu hiding is handled entirely in TS — `removeRibbonIconsFromContextMenu`
|
||||
walks the menu via MutationObserver and removes the items. As a belt-and-suspenders
|
||||
fallback (in case the menu paints for a frame before JS runs), the TS also tags the
|
||||
item with this marker class. The doubled-class selector wins specificity against
|
||||
Obsidian's default menu rules without needing !important. */
|
||||
body .astro-composer-hidden-menu-item.astro-composer-hidden-menu-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.astro-composer-hide-config-icon .menu-item:has(svg[data-lucide="wrench"]),
|
||||
body.astro-composer-hide-config-icon .menu-item:has(svg[data-lucide="rocket"]),
|
||||
body.astro-composer-hide-config-icon .menu-item:has(.lucide-wrench),
|
||||
body.astro-composer-hide-config-icon .menu-item:has(.lucide-rocket),
|
||||
body.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(svg[data-lucide="wrench"]),
|
||||
body.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(svg[data-lucide="rocket"]),
|
||||
body.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(.lucide-wrench),
|
||||
body.astro-composer-hide-config-icon .menu-item .menu-item-icon:has(.lucide-rocket) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Help button hiding */
|
||||
body.astro-composer-hide-help-button .workspace-drawer-vault-actions .clickable-icon:has(svg.help) {
|
||||
/* Help button hiding. The replacement is built in TS by `syncHelpButton`, which also
|
||||
tags the original button with this marker class so we can hide it without :has(). */
|
||||
body .astro-composer-original-help-button.astro-composer-original-help-button {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,6 @@
|
|||
"0.12.1": "1.11.0",
|
||||
"0.12.2": "1.11.0",
|
||||
"0.12.3": "1.11.0",
|
||||
"0.12.4": "1.11.0"
|
||||
"0.12.4": "1.11.0",
|
||||
"0.12.5": "1.11.0"
|
||||
}
|
||||
Loading…
Reference in a new issue