Compare commits

..
9 Commits
Author SHA1 Message Date
amitwh 69afd5fc54 chore(release): bump version to 5.0.1 2026-06-06 23:08:20 +05:30
amitwh 9e315af1b9 fix(release): walk-and-find pandoc binary in extracted archive
Pandoc 3.9 macOS zip uses an arch-suffixed inner directory
(pandoc-3.9.0.2-x86_64/) where Linux/Windows use the bare
pandoc-3.9.0.2/ dir. Hard-coding the intermediate path broke
the macOS build in v5.0.1-rc1.

Replace the hard-coded path.join(tmpDir, PANDOC_VERSION, ...)
with a recursive walk that locates the binary by name. This
is robust to future pandoc layout shifts (e.g., universal
binaries renaming the inner dir).

Verified:
  - Linux: download + extract produces a working pandoc 3.9.0.2
  - macOS: walker finds pandoc in pandoc-3.9.0.2-x86_64/bin/
  - Windows: walker finds pandoc.exe in pandoc-3.9.0.2/
  - 306/306 tests pass
2026-06-06 23:07:30 +05:30
amitwh 78914a4d65 chore(renderer): default to light theme
User prefers light mode. next-themes previously defaulted to dark
which made first-load users (no localStorage) see a dark UI. With
'enableSystem' on, 'system' is still available as a choice — only
the initial value changed.

Existing users with 'dark' in localStorage keep dark until they toggle.
New users get light.
2026-06-06 22:43:16 +05:30
amitwh 3c4cc985fb feat(packaging): add macOS icon.icns + build:icon-icns script
electron-builder's mac block declared 'icon: assets/icon.icns' but the
file was never generated, so macOS builds shipped with the default
Electron icon. The build would succeed but the .app looked generic.

Add scripts/build-icon-icns.js — a 50-line node script that packs the
existing PNGs from assets/icons/ (16/32/64/128/256/512/1024 + 48 small
+ 16/32/128/256 @2x retina) into a valid ICNS file. No native deps;
works on Linux, mac, and Windows runners alike (avoids the mac-only
'sips' that png2icns requires).

Wired into npm via 'npm run build:icon-icns'. Local build:linux now
parses the ICNS correctly and produces AppImage/deb/snap.
2026-06-06 22:43:06 +05:30
amitwh 09d9f6bdd1 fix(window): renderer path off-by-one; use 'close' for state save
Two related bugs in src/main/window/index.js:

1. Renderer path was ../../dist/renderer/index.html which resolves to
   src/dist/renderer/index.html (one level too deep). Production builds
   load from <project>/dist/renderer/index.html. Fix: ../../..

   This was never caught because dev mode uses VITE_DEV_SERVER_URL and
   never touches the file path; only 'electron .' (no Vite) hits it.

2. win.on('closed', state.save) calls win.getBounds() on a destroyed
   BrowserWindow. The 'closed' event fires AFTER destruction, so
   getBounds() throws 'Object has been destroyed'. Use 'close' (fires
   before destruction) and guard with isDestroyed() as a belt-and-suspenders.

Both bugs surfaced when the user clicked File → Open. The renderer
loaded a missing HTML (blank body); the openFile handler then triggered
the closed-event state save which threw.
2026-06-06 22:42:54 +05:30
amitwh f36d918871 fix(menu): require('../main') → require('../index') after main.js→index.js rename
The menu items file was using require('../main') which resolved to a
non-existent src/main/main.js. After the 054ce23 refactor that renamed
main.js → index.js, every menu click tried to require a missing module.

All 65 occurrences fixed; fileItems() now loads cleanly at app startup
and the menu doesn't throw on construction.

Caught by user clicking File → Open: showOpenDialogSync emitted through
to the lazy require at items.js:52, which failed with module-not-found
before main process handlers ran.
2026-06-06 22:42:42 +05:30
amitwh ab16998922 feat(packaging): all-platform builds with GitHub Releases
Closes the gap from the previous handoff: `npm run dist:all` now
produces Linux .deb/AppImage/snap, Windows NSIS/portable/zip, and
macOS dmg/zip artifacts. `.github/workflows/release.yml` builds all
three on tag push (v*) and attaches the union to a GitHub Release.

Bug fix in the same commit (would have broken any packaged build):

* `build.files` was `["src/**/*", ...]` — shipped `src/renderer/`
  .tsx dev source into the asar. Replaced with a whitelist
  (`src/main/**/*`, `src/preload.js`, `src/plugins/**/*`,
  `package.json`) and moved the built renderer to
  `build.extraResources` (`dist/renderer` -> `renderer/`).

* `src/main/window/index.js` now uses `process.resourcesPath` when
  `app.isPackaged` is true, plus a friendly error if the renderer is
  missing. Verified by extracting the .AppImage and launching the
  binary — main process logs
  `Production mode — loading .../resources/renderer/index.html`.

New in `package.json` build config:

* `mac.target`: dmg + zip for x64 and arm64; `darkModeSupport: true`,
  `hardenedRuntime: false`, `gatekeeperAssess: false`,
  `entitlements: null`. `mac.identity: null` retained (unsigned).
* `publish`: GitHub Releases provider. CI overrides with
  `--publish=never`.

Known limitations (intentional follow-ups, not blockers):

* No `assets/icon.icns` — macOS app uses the default Electron icon.
  This box lacks `iconutil`/`png2icns`; generating a proper .icns is
  a one-line follow-up.
* No code signing — Windows SmartScreen will warn, macOS Gatekeeper
  will quarantine. `CSC_LINK_BASE64`/`CSC_KEY_PASSWORD` secrets are
  already wired in the Windows job for when the cert is added.
* No auto-update — user decision. The `publish` block writes the
  metadata that a future `electron-updater` integration will consume.

Verified on this Linux box:

* `npm run build:renderer` then `npm run build:linux` — produced
  AppImage (291M), deb (220M), snap (245M).
* `resources/app.asar` has 0 `src/renderer/*` entries (was the
  pre-fix bug). 44 .tsx files remain in asar — all from
  `node_modules/@hookform/resolvers` and `node_modules/immer` test
  fixtures, not our code.
* Extracted `squashfs-root/resources/renderer/index.html` exists and
  is what `window/index.js` loads.
* Binary runs for >12s without crashing — `loadFile` resolves
  `process.resourcesPath/renderer/index.html` correctly.

Tests: 306/306 pass. No regressions.

Plan doc: docs/plans/2026-06-06-packaging-all-platforms.md (the
task-by-task execution guide I followed; included for traceability).
2026-06-06 21:06:20 +05:30
amitwh 58ca3014d8 fix(modals): drop shadcn-style aria-describedby to silence Radix warning
Every DialogContent had aria-describedby="X-desc" and every
DialogDescription had a matching id="X-desc". That pattern is the
default shadcn/ui recipe, but it breaks Radix 1.1.x.

Root cause: Radix auto-generates a descriptionId via useId() (e.g. :r0:)
and stores it in the Dialog context. The DescriptionWarning effect does
`document.getElementById(descriptionId)` and warns if the element is
missing. When the description element's id is overridden to
"welcome-desc", the DOM has id="welcome-desc" but the context still has
:r0:, so the lookup misses and the warning fires on every dialog open.

Fix: drop both overrides. Let Radix manage the id and aria-describedby
itself. Screen-reader semantics are unchanged (Radix still wires
Content -> Description via the auto-generated id).

Touched: 12 modal files (11 DialogContent/DialogDescription pairs +
SettingsSheet which uses Sheet). +1 regression test in
WelcomeDialog.test.tsx that spies on console.warn and asserts the
"Missing Description" message is never emitted.

Tests: 306/306 pass (was 305).

Long-term memory: radix-aria-describedby-anti-pattern.md captures the
do-not-reintroduce rule for future agents.
2026-06-06 20:45:13 +05:30
amitwh 7c1a79c724 fix(app): add Vite dev workflow, fix onLayout prop, fix icon path, allow Google Fonts CSP
The v5.0.0 React UI redesign shipped without a working dev workflow —
`npm start` only ran `electron .` which loaded the source index.html
that references raw .tsx files. The browser couldn't execute them, so
the app rendered an empty #root and the user saw 'lot of errors'.

This commit restores a real dev workflow:

1. **Vite dev server wired into npm scripts** — added `concurrently`
   + `wait-on` as devDeps. `npm run dev` now runs Vite (port 5173)
   and Electron together with `wait-on tcp:5173` so Electron only
   starts after the dev server is ready.

2. **Main process loads Vite URL in dev, dist/ in prod** — window
   module now checks `process.env.VITE_DEV_SERVER_URL` and
   `app.isPackaged` to decide what to load. Production still
   loads the built `dist/renderer/index.html`.

3. **Fixed ReferenceError: app is not defined** — window module
   referenced `app.isPackaged` but only imported `BrowserWindow`.
   Added `app` to the destructured import.

4. **Fixed wrong icon path** — window creation used
   `../../assets/icon.png` from src/main/window/ which resolved
   to `src/assets/icon.png` (doesn't exist). Icon lives at
   project root, so path is now `../../../assets/icon.png`.

5. **Allowed Google Fonts in CSP** — the strict CSP
   (`font-src 'self' data:`, `connect-src 'self'`) blocked
   the preconnect/css link to fonts.googleapis.com / .gstatic.com.
   Added both domains so Plus Jakarta Sans loads.

6. **Renamed onLayout → onLayoutChange** in AppShell — react-resizable-panels
   v4 renamed the prop. The v1 name was being spread to a DOM
   div and React logged 'Unknown event handler property' warnings.
   Test mock updated for parity.

7. **show: false → show: true** — ready-to-show was hanging on
   this Wayland/container combo. Now the window is shown
   immediately on create.

Verified end-to-end:
- 305/305 vitest tests pass
- Vite dev server starts on :5173
- Electron loads localhost:5173 and React mounts
- Header 'MarkdownConverter' + sidebar with 'No files open' visible
- No onLayout warnings after HMR
- All blocked errors resolved
2026-06-06 16:17:16 +05:30
26 changed files with 1005 additions and 116 deletions
+42 -1
View File
@@ -94,8 +94,42 @@ jobs:
dist/*.zip dist/*.zip
retention-days: 5 retention-days: 5
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Download external tools (pandoc)
run: node scripts/download-tools.js
- name: Run tests
run: npm test
- name: Build macOS packages (unsigned)
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
run: npm run build:mac -- --publish=never
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos-artifacts
path: |
dist/*.dmg
dist/*.zip
retention-days: 5
release: release:
needs: [build-linux, build-windows] needs: [build-linux, build-windows, build-macos]
if: always() if: always()
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -115,6 +149,13 @@ jobs:
name: windows-artifacts name: windows-artifacts
path: dist path: dist
- name: Download macOS artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: macos-artifacts
path: dist
- name: Create GitHub Release - name: Create GitHub Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
BIN
View File
Binary file not shown.
@@ -0,0 +1,270 @@
# Packaging — All-Platform Build + GitHub Releases
**Goal:** Make `npm run dist:all` produce Linux + Windows + macOS artifacts, and have the existing `.github/workflows/release.yml` create a GitHub Release on tag push with those artifacts. No code signing in this iteration.
**Scope (user decisions 2026-06-06):**
- Target platforms: **all three** (Linux .deb/AppImage/snap, Windows NSIS/portable/zip, macOS dmg/zip)
- Distribution: **GitHub Releases** (no auto-update; static artifacts only)
- Code signing: **skip for now** (unsigned binaries; Windows SmartScreen will warn, macOS Gatekeeper will quarantine)
**Architecture:** This is mostly about completing the existing scaffold. `electron-builder` 26.0.12, `scripts/download-tools.js`, `scripts/generate-icons.js`, the `package.json` scripts, and `release.yml` are all already in place. We need to (1) close a real bug in the `files` config, (2) add the macOS job to the workflow, and (3) declare macOS targets in the package config.
**Tech stack:** electron-builder 26.0.12, GitHub Actions `ubuntu-latest`/`windows-latest`/`macos-latest` runners, softprops/action-gh-release@v2 for the release creation.
---
## Task 1 — Fix the `files` config in `package.json`
The current `files: ["src/**/*", ...]` matches `src/renderer/**/*.tsx` and ships dev source into the asar. The shipped renderer should be the built `dist/renderer/**/*` output, not TS source. asarUnpack for ffmpeg-static is correct and stays.
**Files:** `package.json` (build.files, build.extraResources)
### Step 1.1 — Replace `files` block
```json
"files": [
"src/main/**/*",
"src/preload.js",
"src/plugins/**/*",
"package.json"
],
"extraResources": [
{
"from": "dist/renderer",
"to": "renderer"
}
]
```
- `src/main/**/*` — main process source (entry point + all main-process modules)
- `src/preload.js` — single preload file at the root of `src/`
- `src/plugins/**/*` — built-in plugins (referenced from main process; the `extraFiles: []` line in the current config is empty, so this is the right pattern)
- `package.json` — required for the asar `app.asar/package.json` lookup electron does at runtime
- `dist/renderer` moves to `extraResources` because it should live alongside the asar (not inside it) — the asar is sealed and we want the renderer to be a separately-updatable resource path. Production main loads it via `loadFile(path.join(__dirname, '../../dist/renderer/index.html'))` which, in the packaged app, resolves to `process.resourcesPath/renderer/index.html`. **This requires changing the loadFile path in `src/main/window/index.js` too** — see Task 4.
### Step 1.2 — Verify
- `npm run build:linux` should produce a working .AppImage
- Inside the .AppImage (extract with `--appimage-extract`), check `resources/` has `app.asar` and a `renderer/` directory containing `index.html` and `assets/`
- Inside `app.asar`, check `src/main/index.js` is present and `src/renderer/` is absent
---
## Task 2 — Add macOS targets + icon to `package.json` build config
**Files:** `package.json`
### Step 2.1 — Replace the existing `mac` block
```json
"mac": {
"category": "public.app-category.productivity",
"identity": null,
"target": [
{ "target": "dmg", "arch": ["x64", "arm64"] },
{ "target": "zip", "arch": ["x64", "arm64"] }
],
"icon": "assets/icon.icns",
"darkModeSupport": true,
"hardenedRuntime": false,
"gatekeeperAssess": false,
"entitlements": null
}
```
- `identity: null` is intentional for unsigned builds
- `darkModeSupport: true` — the app already supports dark mode; this is just metadata
- `hardenedRuntime: false` and `gatekeeperAssess: false` — without signing, these are the only way the build succeeds
- `entitlements: null` — no entitlements file; some features (jit) won't work but Electron's main process doesn't need them in this app
- **`.icns` is missing.** For this iteration we'll use the default Electron icon and add a follow-up to generate one. The build will succeed without it but the produced .app will have the default icon.
### Step 2.2 — Generate a placeholder `.icns` (optional, can be deferred)
If we want a proper icon, the path is:
1. `npm run generate-icons` to produce `assets/icons/*.png` (already done)
2. Use `png2icns` (Linux) or `iconutil` (mac) to bundle them into `assets/icon.icns`
3. We don't have either on this Linux container; defer to a follow-up plan
For this iteration: ship without `.icns` and accept the default icon.
### Step 2.3 — Add `publish` config
```json
"publish": {
"provider": "github",
"owner": "amitwh",
"repo": "markdown-converter",
"releaseType": "release"
}
```
Even without auto-update, this tells `electron-builder` to write `latest-mac.yml` / `latest-linux.yml` next to the artifacts. Useful for future auto-update wiring. CI passes `--publish=never` to override.
---
## Task 3 — Add macOS job to `.github/workflows/release.yml`
**Files:** `.github/workflows/release.yml`
### Step 3.1 — Add a third job between `build-windows` and `release`
Insert after the `build-windows` job (around line 96):
```yaml
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Download external tools (pandoc)
run: node scripts/download-tools.js
- name: Run tests
run: npm test
- name: Build macOS packages (unsigned)
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
run: npm run build:mac -- --publish=never
- name: Upload macOS artifacts
uses: actions/upload-artifact@v4
with:
name: macos-artifacts
path: |
dist/*.dmg
dist/*.zip
retention-days: 5
```
### Step 3.2 — Wire macOS into the `release` job's `needs` and downloads
Change the `release` job:
```yaml
release:
needs: [build-linux, build-windows, build-macos]
...
```
And add a third download step (after the Windows download):
```yaml
- name: Download macOS artifacts
uses: actions/download-artifact@v4
continue-on-error: true
with:
name: macos-artifacts
path: dist
```
`continue-on-error: true` is intentional — a failed macOS build shouldn't block a release of working Linux+Windows artifacts.
---
## Task 4 — Update `src/main/window/index.js` for the resource path
**Files:** `src/main/window/index.js`
### Step 4.1 — Update the loadFile path
The current code (line 35):
```js
const prodPath = path.join(__dirname, '../../dist/renderer/index.html');
```
Inside the packaged app, `__dirname` points into the asar. `process.resourcesPath` points to the directory holding `app.asar` + the `renderer/` extraResources dir. Change to:
```js
const rendererIndex = app.isPackaged
? path.join(process.resourcesPath, 'renderer', 'index.html')
: path.join(__dirname, '../../dist/renderer/index.html');
win.loadFile(rendererIndex);
```
This needs to happen after the dev/prod branch is decided. The existing `if (!app.isPackaged && devServerUrl)` block already handles dev mode; the else branch needs this update.
### Step 4.2 — Add a guard for missing renderer in prod
```js
if (app.isPackaged) {
try {
fs.accessSync(path.join(process.resourcesPath, 'renderer', 'index.html'));
} catch {
console.error('[WINDOW] Renderer not found at', process.resourcesPath, '— did you run `npm run build:renderer` before packaging?');
}
}
```
Failure mode: someone runs `npm run build` without first building the renderer. Without this guard, the window opens blank and the user has no idea why.
---
## Task 5 — Test on this Linux box
### Step 5.1 — Local verification
```bash
npm ci
npm run download-tools # idempotent; bin/linux/pandoc already exists
npm run build:renderer
npm run build:linux # produces .deb, .AppImage in dist/
```
Inspect the .AppImage:
```bash
chmod +x dist/*.AppImage
./dist/MarkdownConverter-*.AppImage --appimage-extract
ls squashfs-root/resources/
# Expect: app.asar renderer/
ls squashfs-root/resources/renderer/
# Expect: index.html assets/
```
### Step 5.2 — Run the produced app
```bash
./dist/MarkdownConverter-*.AppImage
```
The app should:
- Launch with the AppShell, header, sidebar
- Allow opening a folder / file
- Export to PDF/DOCX/HTML work (pandoc bundled)
### Step 5.3 — Tag + push to test the release workflow (optional)
Only do this if the user wants to verify the full pipeline. A dry-run on the CI side via `act` would be cleaner but `act` doesn't run macos-latest jobs locally. If the user wants to verify the workflow, push a `v5.0.1-rc1` tag and watch the Actions tab.
---
## Success criteria
1. `npm run dist:all` produces all three platform families on this Linux box (or at least Linux + mac, since Windows is built on Windows)
2. The .AppImage launches and the app works end-to-end
3. `release.yml` runs all three jobs on a tag push and creates a GitHub release with the union of artifacts
4. No regressions: 306/306 tests still pass, dev workflow still works
5. The `files` bug is fixed: no `.tsx` in the packaged asar
## Known limitations (intentional, can be follow-ups)
- **No `.icns`** — the macOS app will have the default Electron icon. Generating a proper `.icns` requires `iconutil` (mac) or `png2icns` (cross-platform); neither is on this Linux box.
- **No code signing** — Windows SmartScreen will warn; macOS Gatekeeper will quarantine unsigned `.dmg`. Setting up signing later is a config-only change (no structural rework): add `CSC_LINK`/`CSC_KEY_PASSWORD` for Windows, `CSC_LINK` (Apple Developer ID) for mac.
- **No auto-update** — user decisions for this iteration. Wiring `electron-updater` is a follow-up; the `publish` config from Task 2 puts metadata in the right place.
- **Cross-build quirks** — building macOS from Linux produces a `.dmg` that is generally usable but may show a "this app is from an unidentified developer" prompt the first time. Right-click → Open to bypass once. Documented in the release notes.
## Out of scope
- CHANGELOG.md updates (not part of packaging)
- electron-updater wiring
- Code signing cert procurement
- Snap store / Microsoft Store / Mac App Store submission
+394 -3
View File
@@ -1,12 +1,12 @@
{ {
"name": "markdown-converter", "name": "markdown-converter",
"version": "4.4.2", "version": "5.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "markdown-converter", "name": "markdown-converter",
"version": "4.4.2", "version": "5.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.1", "@codemirror/autocomplete": "^6.20.1",
@@ -86,6 +86,7 @@
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^10.0.3",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"electron": "^41.1.1", "electron": "^41.1.1",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
@@ -104,7 +105,8 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.16", "vite": "^8.0.16",
"vitest": "^4.1.8" "vitest": "^4.1.8",
"wait-on": "^9.0.10"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@@ -1902,6 +1904,60 @@
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz",
"integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
}
},
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "5.4.0", "version": "5.4.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz",
@@ -6982,6 +7038,46 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz",
"integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==",
"dev": true,
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.16.0",
"form-data": "^4.0.5",
"https-proxy-agent": "^5.0.1",
"proxy-from-env": "^2.1.0"
}
},
"node_modules/axios/node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/axios/node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"dev": true,
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/babel-jest": { "node_modules/babel-jest": {
"version": "30.3.0", "version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
@@ -7889,6 +7985,185 @@
"typedarray": "^0.0.6" "typedarray": "^0.0.6"
} }
}, },
"node_modules/concurrently": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.3.tgz",
"integrity": "sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "5.6.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.4",
"supports-color": "10.2.2",
"tree-kill": "1.2.2",
"yargs": "18.0.0"
},
"bin": {
"conc": "dist/bin/index.js",
"concurrently": "dist/bin/index.js"
},
"engines": {
"node": ">=22"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/concurrently/node_modules/ansi-styles": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concurrently/node_modules/cliui": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
"integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^7.2.0",
"strip-ansi": "^7.1.0",
"wrap-ansi": "^9.0.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/concurrently/node_modules/emoji-regex": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently/node_modules/string-width": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^10.3.0",
"get-east-asian-width": "^1.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/concurrently/node_modules/strip-ansi": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
"integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.2.2"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/concurrently/node_modules/wrap-ansi": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
"integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^6.2.1",
"string-width": "^7.0.0",
"strip-ansi": "^7.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/concurrently/node_modules/yargs": {
"version": "18.0.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz",
"integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^9.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"string-width": "^7.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^22.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/concurrently/node_modules/yargs-parser": {
"version": "22.0.0",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
"integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
},
"node_modules/conf": { "node_modules/conf": {
"version": "14.0.0", "version": "14.0.0",
"resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz", "resolved": "https://registry.npmjs.org/conf/-/conf-14.0.0.tgz",
@@ -10378,6 +10653,27 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"dev": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fontkit": { "node_modules/fontkit": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz", "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-2.0.4.tgz",
@@ -10563,6 +10859,19 @@
"node": "6.* || 8.* || >= 10.*" "node": "6.* || 8.* || >= 10.*"
} }
}, },
"node_modules/get-east-asian-width": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-intrinsic": { "node_modules/get-intrinsic": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -12696,6 +13005,25 @@
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
}, },
"node_modules/joi": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
"integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
"@hapi/formula": "^3.0.2",
"@hapi/hoek": "^11.0.7",
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
"@standard-schema/spec": "^1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/jpeg-exif": { "node_modules/jpeg-exif": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
@@ -15183,6 +15511,16 @@
"signal-exit": "^3.0.2" "signal-exit": "^3.0.2"
} }
}, },
"node_modules/proxy-from-env": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -15758,6 +16096,16 @@
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -15958,6 +16306,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/shell-quote": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
"integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/siginfo": { "node_modules/siginfo": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -16878,6 +17239,16 @@
"node": ">=20" "node": ">=20"
} }
}, },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/truncate-utf8-bytes": { "node_modules/truncate-utf8-bytes": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
@@ -17441,6 +17812,26 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/wait-on": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz",
"integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"axios": "^1.16.0",
"joi": "^18.2.1",
"lodash": "^4.18.1",
"minimist": "^1.2.8",
"rxjs": "^7.8.2"
},
"bin": {
"wait-on": "bin/wait-on"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/walker": { "node_modules/walker": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
+36 -8
View File
@@ -1,10 +1,15 @@
{ {
"name": "markdown-converter", "name": "markdown-converter",
"version": "5.0.0", "version": "5.0.1",
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting", "description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
"main": "src/main/index.js", "main": "src/main/index.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"dev:renderer": "vite --config vite.renderer.config.ts",
"dev:electron": "wait-on tcp:5173 && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron . -- --disable-gpu --disable-software-rasterizer --disable-dev-shm-usage",
"dev": "concurrently -k -n vite,electron -c blue,green \"npm:dev:renderer\" \"npm:dev:electron\"",
"build:renderer": "vite build --config vite.renderer.config.ts",
"preview": "npm run build:renderer && electron .",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:coverage": "jest --coverage", "test:coverage": "jest --coverage",
@@ -27,7 +32,8 @@
"dist": "electron-builder --publish=never", "dist": "electron-builder --publish=never",
"dist:all": "electron-builder -mwl", "dist:all": "electron-builder -mwl",
"download-tools": "node scripts/download-tools.js", "download-tools": "node scripts/download-tools.js",
"generate-icons": "node scripts/generate-icons.js" "generate-icons": "node scripts/generate-icons.js",
"build:icon-icns": "node scripts/build-icon-icns.js"
}, },
"keywords": [ "keywords": [
"markdown", "markdown",
@@ -60,6 +66,7 @@
"autoprefixer": "^10.5.0", "autoprefixer": "^10.5.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"concurrently": "^10.0.3",
"cross-env": "^10.0.0", "cross-env": "^10.0.0",
"electron": "^41.1.1", "electron": "^41.1.1",
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
@@ -78,7 +85,8 @@
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"vite": "^8.0.16", "vite": "^8.0.16",
"vitest": "^4.1.8" "vitest": "^4.1.8",
"wait-on": "^9.0.10"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.20.1", "@codemirror/autocomplete": "^6.20.1",
@@ -159,15 +167,20 @@
}, },
"icon": "assets/icon", "icon": "assets/icon",
"files": [ "files": [
"src/**/*", "src/main/**/*",
"assets/**/*", "src/preload.js",
"scripts/**/*", "src/plugins/**/*",
"node_modules/**/*",
"package.json" "package.json"
], ],
"asarUnpack": [ "asarUnpack": [
"node_modules/ffmpeg-static/**" "node_modules/ffmpeg-static/**"
], ],
"extraResources": [
{
"from": "dist/renderer",
"to": "renderer"
}
],
"extraFiles": [], "extraFiles": [],
"fileAssociations": [ "fileAssociations": [
{ {
@@ -194,7 +207,16 @@
], ],
"mac": { "mac": {
"category": "public.app-category.productivity", "category": "public.app-category.productivity",
"identity": null "identity": null,
"target": [
{ "target": "dmg", "arch": ["x64", "arm64"] },
{ "target": "zip", "arch": ["x64", "arm64"] }
],
"icon": "assets/icon.icns",
"darkModeSupport": true,
"hardenedRuntime": false,
"gatekeeperAssess": false,
"entitlements": null
}, },
"win": { "win": {
"target": [ "target": [
@@ -267,6 +289,12 @@
], ],
"description": "Professional Markdown editor and universal file converter", "description": "Professional Markdown editor and universal file converter",
"maintainer": "ConcreteInfo <amit.wh@gmail.com>" "maintainer": "ConcreteInfo <amit.wh@gmail.com>"
},
"publish": {
"provider": "github",
"owner": "amitwh",
"repo": "markdown-converter",
"releaseType": "release"
} }
} }
} }
+75
View File
@@ -0,0 +1,75 @@
/**
* Build assets/icon.icns from the PNGs in assets/icons/.
*
* ICNS file format (Apple Icon Image):
* Header: 'icns' (4 bytes) + total file length (uint32 BE, includes header)
* For each size:
* Type tag (4 bytes) + length (uint32 BE, includes the 8-byte entry header) + PNG data
*
* Standard PNG type tags we'll include:
* icp4 = 16x16, icp5 = 32x32, icp6 = 64x64
* ic07 = 128x128, ic08 = 256x256, ic09 = 512x512, ic10 = 1024x1024
* icsb = 48x48 (small)
* ic11 = 16x16@2x (32x32 retina), ic12 = 32x32@2x (64x64 retina)
* ic13 = 128x128@2x (256x256 retina), ic14 = 256x256@2x (512x512 retina)
*/
const fs = require('fs');
const path = require('path');
const iconsDir = path.join(__dirname, '..', 'assets', 'icons');
const outFile = path.join(__dirname, '..', 'assets', 'icon.icns');
// (size-in-pixels, icns type tag) — we skip sizes we don't have on disk
const sizes = [
{ size: 16, type: 'icp4' },
{ size: 32, type: 'icp5' },
{ size: 64, type: 'icp6' },
{ size: 128, type: 'ic07' },
{ size: 256, type: 'ic08' },
{ size: 512, type: 'ic09' },
{ size: 1024, type: 'ic10' },
{ size: 48, type: 'icsb' },
{ size: 32, type: 'ic11' }, // 16@2x
{ size: 64, type: 'ic12' }, // 32@2x
{ size: 256, type: 'ic13' }, // 128@2x
{ size: 512, type: 'ic14' }, // 256@2x
];
const entries = [];
for (const { size, type } of sizes) {
const pngPath = path.join(iconsDir, `${size}x${size}.png`);
if (!fs.existsSync(pngPath)) continue;
const png = fs.readFileSync(pngPath);
// ICNS requires 8-byte alignment per entry. paddedLen is the TOTAL entry size
// (8-byte type+length header + PNG + padding), rounded up to a multiple of 8.
const paddedLen = 8 + Math.ceil(png.length / 8) * 8;
entries.push({ type, png, paddedLen });
console.log(` ${type} (${size}x${size}) — ${png.length} bytes`);
}
if (entries.length === 0) {
console.error('No source PNGs found in', iconsDir);
process.exit(1);
}
const totalLen = 8 + entries.reduce((s, e) => s + e.paddedLen, 0);
const buf = Buffer.alloc(totalLen);
let off = 0;
// Header
buf.write('icns', off, 4, 'ascii'); off += 4;
buf.writeUInt32BE(totalLen, off); off += 4;
// Entries
for (const { type, png, paddedLen } of entries) {
buf.write(type, off, 4, 'ascii'); off += 4;
buf.writeUInt32BE(paddedLen, off); off += 4;
png.copy(buf, off);
off += png.length;
// Pad to 8-byte boundary within the entry. paddedLen already includes the 8-byte header.
const pad = paddedLen - 8 - png.length;
if (pad > 0) off += pad;
}
fs.writeFileSync(outFile, buf);
console.log(`\nWrote ${outFile} (${buf.length} bytes, ${entries.length} sizes)`);
+31 -4
View File
@@ -14,6 +14,30 @@ const { execSync } = require('child_process');
const PANDOC_VERSION = '3.9.0.2'; const PANDOC_VERSION = '3.9.0.2';
/**
* Pandoc's archive inner layout has shifted across releases and platforms:
* linux tarball: pandoc-3.9.0.2/bin/pandoc
* win32 zip: pandoc-3.9.0.2/pandoc.exe
* darwin zip: pandoc-3.9.0.2-x86_64/bin/pandoc (arch-suffixed inner dir)
* pandoc-3.9.0.2-arm64/bin/pandoc (on Apple Silicon builds)
* Rather than hard-coding the intermediate path — which has already broken
* once when pandoc 3.9 added the arch suffix to the macOS archive — we walk
* the extracted tree and find the binary by name. This is robust to future
* layout shifts (e.g., universal binaries renaming the inner dir).
*/
function findFile(rootDir, targetName) {
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
for (const entry of entries) {
const full = path.join(rootDir, entry.name);
if (entry.isFile() && entry.name === targetName) return full;
if (entry.isDirectory()) {
const found = findFile(full, targetName);
if (found) return found;
}
}
return null;
}
const PANDOC_CONFIG = { const PANDOC_CONFIG = {
linux: { linux: {
url: `https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz`, url: `https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz`,
@@ -22,8 +46,9 @@ const PANDOC_CONFIG = {
extract(archivePath, destDir) { extract(archivePath, destDir) {
const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`); const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true }); fs.mkdirSync(tmpDir, { recursive: true });
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}" pandoc-${PANDOC_VERSION}/bin/pandoc`); execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`);
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'bin', 'pandoc'); const src = findFile(tmpDir, 'pandoc');
if (!src) throw new Error(`pandoc binary not found under ${tmpDir}`);
fs.copyFileSync(src, path.join(destDir, 'pandoc')); fs.copyFileSync(src, path.join(destDir, 'pandoc'));
fs.chmodSync(path.join(destDir, 'pandoc'), 0o755); fs.chmodSync(path.join(destDir, 'pandoc'), 0o755);
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true });
@@ -39,7 +64,8 @@ const PANDOC_CONFIG = {
execSync( execSync(
`powershell -Command "Expand-Archive -Force '${archivePath}' '${tmpDir}'"`, `powershell -Command "Expand-Archive -Force '${archivePath}' '${tmpDir}'"`,
); );
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'pandoc.exe'); const src = findFile(tmpDir, 'pandoc.exe');
if (!src) throw new Error(`pandoc.exe not found under ${tmpDir}`);
fs.copyFileSync(src, path.join(destDir, 'pandoc.exe')); fs.copyFileSync(src, path.join(destDir, 'pandoc.exe'));
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true });
}, },
@@ -52,7 +78,8 @@ const PANDOC_CONFIG = {
const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`); const tmpDir = path.join(os.tmpdir(), `pandoc-${Date.now()}`);
fs.mkdirSync(tmpDir, { recursive: true }); fs.mkdirSync(tmpDir, { recursive: true });
execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`); execSync(`unzip -o "${archivePath}" -d "${tmpDir}"`);
const src = path.join(tmpDir, `pandoc-${PANDOC_VERSION}`, 'bin', 'pandoc'); const src = findFile(tmpDir, 'pandoc');
if (!src) throw new Error(`pandoc binary not found under ${tmpDir}`);
fs.copyFileSync(src, path.join(destDir, 'pandoc')); fs.copyFileSync(src, path.join(destDir, 'pandoc'));
fs.chmodSync(path.join(destDir, 'pandoc'), 0o755); fs.chmodSync(path.join(destDir, 'pandoc'), 0o755);
fs.rmSync(tmpDir, { recursive: true, force: true }); fs.rmSync(tmpDir, { recursive: true, force: true });
+65 -65
View File
@@ -16,7 +16,7 @@ function buildRecentFilesMenu(mainWindow) {
const items = existing.map(file => ({ const items = existing.map(file => ({
label: path.basename(file), label: path.basename(file),
click: () => { click: () => {
const { openFileFromPath } = require('../main'); const { openFileFromPath } = require('../index');
openFileFromPath(file); openFileFromPath(file);
} }
})); }));
@@ -48,7 +48,7 @@ function fileItems(mainWindow) {
label: 'Open', label: 'Open',
accelerator: 'CmdOrCtrl+O', accelerator: 'CmdOrCtrl+O',
click: () => { click: () => {
const { openFile } = require('../main'); const { openFile } = require('../index');
openFile(); openFile();
} }
}, },
@@ -56,7 +56,7 @@ function fileItems(mainWindow) {
label: 'Open PDF', label: 'Open PDF',
accelerator: 'CmdOrCtrl+Shift+O', accelerator: 'CmdOrCtrl+Shift+O',
click: () => { click: () => {
const { openPdfFile } = require('../main'); const { openPdfFile } = require('../index');
openPdfFile(); openPdfFile();
} }
}, },
@@ -69,7 +69,7 @@ function fileItems(mainWindow) {
label: 'Save As', label: 'Save As',
accelerator: 'CmdOrCtrl+Shift+S', accelerator: 'CmdOrCtrl+Shift+S',
click: () => { click: () => {
const { saveAsFile } = require('../main'); const { saveAsFile } = require('../index');
saveAsFile(); saveAsFile();
} }
}, },
@@ -101,7 +101,7 @@ function fileItems(mainWindow) {
label: 'Import Document...', label: 'Import Document...',
accelerator: 'CmdOrCtrl+I', accelerator: 'CmdOrCtrl+I',
click: () => { click: () => {
const { importDocument } = require('../main'); const { importDocument } = require('../index');
importDocument(); importDocument();
} }
}, },
@@ -110,64 +110,64 @@ function fileItems(mainWindow) {
submenu: [ submenu: [
{ {
label: 'HTML', click: () => { label: 'HTML', click: () => {
const { exportFile } = require('../main'); const { exportFile } = require('../index');
exportFile('html'); exportFile('html');
} }
}, },
{ {
label: 'PDF', click: () => { label: 'PDF', click: () => {
const { exportFile } = require('../main'); const { exportFile } = require('../index');
exportFile('pdf'); exportFile('pdf');
} }
}, },
{ {
label: 'PDF (Enhanced)', click: () => { label: 'PDF (Enhanced)', click: () => {
const { exportPDFViaWordTemplate } = require('../main'); const { exportPDFViaWordTemplate } = require('../index');
exportPDFViaWordTemplate(); exportPDFViaWordTemplate();
}, accelerator: 'Ctrl+Shift+P' }, accelerator: 'Ctrl+Shift+P'
}, },
{ {
label: 'DOCX', click: () => { label: 'DOCX', click: () => {
const { exportFile } = require('../main'); const { exportFile } = require('../index');
exportFile('docx'); exportFile('docx');
} }
}, },
{ {
label: 'DOCX (Enhanced)', click: () => { label: 'DOCX (Enhanced)', click: () => {
const { exportWordWithTemplate } = require('../main'); const { exportWordWithTemplate } = require('../index');
exportWordWithTemplate(); exportWordWithTemplate();
}, accelerator: 'Ctrl+Shift+W' }, accelerator: 'Ctrl+Shift+W'
}, },
{ label: 'LaTeX', click: () => { const { exportFile } = require('../main'); exportFile('latex'); } }, { label: 'LaTeX', click: () => { const { exportFile } = require('../index'); exportFile('latex'); } },
{ label: 'RTF', click: () => { const { exportFile } = require('../main'); exportFile('rtf'); } }, { label: 'RTF', click: () => { const { exportFile } = require('../index'); exportFile('rtf'); } },
{ label: 'ODT', click: () => { const { exportFile } = require('../main'); exportFile('odt'); } }, { label: 'ODT', click: () => { const { exportFile } = require('../index'); exportFile('odt'); } },
{ label: 'EPUB', click: () => { const { exportFile } = require('../main'); exportFile('epub'); } }, { label: 'EPUB', click: () => { const { exportFile } = require('../index'); exportFile('epub'); } },
{ type: 'separator' }, { type: 'separator' },
{ label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../main'); exportFile('pptx'); } }, { label: 'PowerPoint (PPTX)', click: () => { const { exportFile } = require('../index'); exportFile('pptx'); } },
{ label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../main'); exportFile('odp'); } }, { label: 'OpenDocument Presentation (ODP)', click: () => { const { exportFile } = require('../index'); exportFile('odp'); } },
{ type: 'separator' }, { type: 'separator' },
{ label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../main'); exportSpreadsheet('csv'); } }, { label: 'CSV (Tables)', click: () => { const { exportSpreadsheet } = require('../index'); exportSpreadsheet('csv'); } },
{ type: 'separator' }, { type: 'separator' },
{ label: 'JSON (.json)', click: () => { const { exportFile } = require('../main'); exportFile('json'); } }, { label: 'JSON (.json)', click: () => { const { exportFile } = require('../index'); exportFile('json'); } },
{ label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../main'); exportFile('yaml'); } }, { label: 'YAML (.yaml)', click: () => { const { exportFile } = require('../index'); exportFile('yaml'); } },
{ label: 'XML (.xml)', click: () => { const { exportFile } = require('../main'); exportFile('xml'); } }, { label: 'XML (.xml)', click: () => { const { exportFile } = require('../index'); exportFile('xml'); } },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../main'); exportFile('confluence'); } }, { label: 'Confluence Wiki (.txt)', click: () => { const { exportFile } = require('../index'); exportFile('confluence'); } },
{ label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../main'); exportFile('mobi'); } } { label: 'MOBI E-book (.mobi)', click: () => { const { exportFile } = require('../index'); exportFile('mobi'); } }
] ]
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: 'Select Word Template...', label: 'Select Word Template...',
click: () => { click: () => {
const { selectWordTemplate } = require('../main'); const { selectWordTemplate } = require('../index');
selectWordTemplate(); selectWordTemplate();
} }
}, },
{ {
label: 'Template Settings...', label: 'Template Settings...',
click: () => { click: () => {
const { showTemplateSettings } = require('../main'); const { showTemplateSettings } = require('../index');
showTemplateSettings(); showTemplateSettings();
} }
}, },
@@ -240,32 +240,32 @@ function viewItems(mainWindow) {
{ {
label: 'Theme', label: 'Theme',
submenu: [ submenu: [
{ label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../main'); setTheme('atomonelight'); } }, { label: 'Atom One Light (Default)', click: () => { const { setTheme } = require('../index'); setTheme('atomonelight'); } },
{ label: 'GitHub Light', click: () => { const { setTheme } = require('../main'); setTheme('github'); } }, { label: 'GitHub Light', click: () => { const { setTheme } = require('../index'); setTheme('github'); } },
{ label: 'Light', click: () => { const { setTheme } = require('../main'); setTheme('light'); } }, { label: 'Light', click: () => { const { setTheme } = require('../index'); setTheme('light'); } },
{ label: 'Solarized Light', click: () => { const { setTheme } = require('../main'); setTheme('solarized'); } }, { label: 'Solarized Light', click: () => { const { setTheme } = require('../index'); setTheme('solarized'); } },
{ label: 'Gruvbox Light', click: () => { const { setTheme } = require('../main'); setTheme('gruvbox-light'); } }, { label: 'Gruvbox Light', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-light'); } },
{ label: 'Ayu Light', click: () => { const { setTheme } = require('../main'); setTheme('ayu-light'); } }, { label: 'Ayu Light', click: () => { const { setTheme } = require('../index'); setTheme('ayu-light'); } },
{ label: 'Sepia', click: () => { const { setTheme } = require('../main'); setTheme('sepia'); } }, { label: 'Sepia', click: () => { const { setTheme } = require('../index'); setTheme('sepia'); } },
{ label: 'Paper', click: () => { const { setTheme } = require('../main'); setTheme('paper'); } }, { label: 'Paper', click: () => { const { setTheme } = require('../index'); setTheme('paper'); } },
{ label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../main'); setTheme('rosepine-dawn'); } }, { label: 'Rose Pine Dawn', click: () => { const { setTheme } = require('../index'); setTheme('rosepine-dawn'); } },
{ label: 'Concrete Light', click: () => { const { setTheme } = require('../main'); setTheme('concrete-light'); } }, { label: 'Concrete Light', click: () => { const { setTheme } = require('../index'); setTheme('concrete-light'); } },
{ type: 'separator' }, { type: 'separator' },
{ label: 'Dark', click: () => { const { setTheme } = require('../main'); setTheme('dark'); } }, { label: 'Dark', click: () => { const { setTheme } = require('../index'); setTheme('dark'); } },
{ label: 'One Dark', click: () => { const { setTheme } = require('../main'); setTheme('onedark'); } }, { label: 'One Dark', click: () => { const { setTheme } = require('../index'); setTheme('onedark'); } },
{ label: 'Dracula', click: () => { const { setTheme } = require('../main'); setTheme('dracula'); } }, { label: 'Dracula', click: () => { const { setTheme } = require('../index'); setTheme('dracula'); } },
{ label: 'Nord', click: () => { const { setTheme } = require('../main'); setTheme('nord'); } }, { label: 'Nord', click: () => { const { setTheme } = require('../index'); setTheme('nord'); } },
{ label: 'Monokai', click: () => { const { setTheme } = require('../main'); setTheme('monokai'); } }, { label: 'Monokai', click: () => { const { setTheme } = require('../index'); setTheme('monokai'); } },
{ label: 'Material', click: () => { const { setTheme } = require('../main'); setTheme('material'); } }, { label: 'Material', click: () => { const { setTheme } = require('../index'); setTheme('material'); } },
{ label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../main'); setTheme('gruvbox-dark'); } }, { label: 'Gruvbox Dark', click: () => { const { setTheme } = require('../index'); setTheme('gruvbox-dark'); } },
{ label: 'Tokyo Night', click: () => { const { setTheme } = require('../main'); setTheme('tokyonight'); } }, { label: 'Tokyo Night', click: () => { const { setTheme } = require('../index'); setTheme('tokyonight'); } },
{ label: 'Palenight', click: () => { const { setTheme } = require('../main'); setTheme('palenight'); } }, { label: 'Palenight', click: () => { const { setTheme } = require('../index'); setTheme('palenight'); } },
{ label: 'Ayu Dark', click: () => { const { setTheme } = require('../main'); setTheme('ayu-dark'); } }, { label: 'Ayu Dark', click: () => { const { setTheme } = require('../index'); setTheme('ayu-dark'); } },
{ label: 'Ayu Mirage', click: () => { const { setTheme } = require('../main'); setTheme('ayu-mirage'); } }, { label: 'Ayu Mirage', click: () => { const { setTheme } = require('../index'); setTheme('ayu-mirage'); } },
{ label: 'Oceanic Next', click: () => { const { setTheme } = require('../main'); setTheme('oceanic-next'); } }, { label: 'Oceanic Next', click: () => { const { setTheme } = require('../index'); setTheme('oceanic-next'); } },
{ label: 'Cobalt2', click: () => { const { setTheme } = require('../main'); setTheme('cobalt2'); } }, { label: 'Cobalt2', click: () => { const { setTheme } = require('../index'); setTheme('cobalt2'); } },
{ label: 'Concrete Dark', click: () => { const { setTheme } = require('../main'); setTheme('concrete-dark'); } }, { label: 'Concrete Dark', click: () => { const { setTheme } = require('../index'); setTheme('concrete-dark'); } },
{ label: 'Concrete Warm', click: () => { const { setTheme } = require('../main'); setTheme('concrete-warm'); } } { label: 'Concrete Warm', click: () => { const { setTheme } = require('../index'); setTheme('concrete-warm'); } }
] ]
}, },
{ type: 'separator' }, { type: 'separator' },
@@ -327,7 +327,7 @@ function batchItems(mainWindow) {
{ {
label: 'Convert Markdown Folder...', label: 'Convert Markdown Folder...',
click: () => { click: () => {
const { showBatchConversionDialog } = require('../main'); const { showBatchConversionDialog } = require('../index');
showBatchConversionDialog(); showBatchConversionDialog();
} }
}, },
@@ -357,7 +357,7 @@ function convertItems(mainWindow) {
label: 'Universal File Converter...', label: 'Universal File Converter...',
accelerator: 'CmdOrCtrl+Shift+C', accelerator: 'CmdOrCtrl+Shift+C',
click: () => { click: () => {
const { showUniversalConverterDialog } = require('../main'); const { showUniversalConverterDialog } = require('../index');
showUniversalConverterDialog(); showUniversalConverterDialog();
} }
} }
@@ -370,7 +370,7 @@ function pdfEditorItems(mainWindow) {
label: 'Open PDF File...', label: 'Open PDF File...',
accelerator: 'CmdOrCtrl+Shift+O', accelerator: 'CmdOrCtrl+Shift+O',
click: () => { click: () => {
const { openPdfFile } = require('../main'); const { openPdfFile } = require('../index');
openPdfFile(); openPdfFile();
} }
}, },
@@ -378,21 +378,21 @@ function pdfEditorItems(mainWindow) {
{ {
label: 'Merge PDFs...', label: 'Merge PDFs...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('merge'); showPDFEditorDialog('merge');
} }
}, },
{ {
label: 'Split PDF...', label: 'Split PDF...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('split'); showPDFEditorDialog('split');
} }
}, },
{ {
label: 'Compress PDF...', label: 'Compress PDF...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('compress'); showPDFEditorDialog('compress');
} }
}, },
@@ -400,21 +400,21 @@ function pdfEditorItems(mainWindow) {
{ {
label: 'Rotate Pages...', label: 'Rotate Pages...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('rotate'); showPDFEditorDialog('rotate');
} }
}, },
{ {
label: 'Delete Pages...', label: 'Delete Pages...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('delete'); showPDFEditorDialog('delete');
} }
}, },
{ {
label: 'Reorder Pages...', label: 'Reorder Pages...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('reorder'); showPDFEditorDialog('reorder');
} }
}, },
@@ -422,7 +422,7 @@ function pdfEditorItems(mainWindow) {
{ {
label: 'Add Watermark...', label: 'Add Watermark...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('watermark'); showPDFEditorDialog('watermark');
} }
}, },
@@ -433,21 +433,21 @@ function pdfEditorItems(mainWindow) {
{ {
label: 'Add Password Protection...', label: 'Add Password Protection...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('encrypt'); showPDFEditorDialog('encrypt');
} }
}, },
{ {
label: 'Remove Password...', label: 'Remove Password...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('decrypt'); showPDFEditorDialog('decrypt');
} }
}, },
{ {
label: 'Set Permissions...', label: 'Set Permissions...',
click: () => { click: () => {
const { showPDFEditorDialog } = require('../main'); const { showPDFEditorDialog } = require('../index');
showPDFEditorDialog('permissions'); showPDFEditorDialog('permissions');
} }
} }
@@ -457,7 +457,7 @@ function pdfEditorItems(mainWindow) {
{ {
label: 'About PDF Editor', label: 'About PDF Editor',
click: () => { click: () => {
const { showAboutDialog } = require('../main'); const { showAboutDialog } = require('../index');
showAboutDialog(); showAboutDialog();
} }
} }
@@ -482,7 +482,7 @@ function helpItems(mainWindow) {
{ {
label: 'About MarkdownConverter', label: 'About MarkdownConverter',
click: () => { click: () => {
const { showAboutDialog } = require('../main'); const { showAboutDialog } = require('../index');
showAboutDialog(); showAboutDialog();
} }
}, },
@@ -490,7 +490,7 @@ function helpItems(mainWindow) {
{ {
label: 'Dependencies & Requirements', label: 'Dependencies & Requirements',
click: () => { click: () => {
const { showDependenciesDialog } = require('../main'); const { showDependenciesDialog } = require('../index');
showDependenciesDialog(); showDependenciesDialog();
} }
}, },
+40 -6
View File
@@ -1,8 +1,9 @@
// src/main/window/index.js // src/main/window/index.js
// Main window creation // Main window creation
const { BrowserWindow } = require('electron'); const { app, BrowserWindow } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs');
const state = require('./state'); const state = require('./state');
const menu = require('../menu'); const menu = require('../menu');
@@ -13,16 +14,47 @@ function createMainWindow() {
height: bounds.height, height: bounds.height,
x: bounds.x, x: bounds.x,
y: bounds.y, y: bounds.y,
show: false, show: true,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation: false, contextIsolation: false,
spellcheck: true spellcheck: true
}, },
icon: path.join(__dirname, '../../assets/icon.png') icon: path.join(__dirname, '../../../assets/icon.png')
}); });
win.loadFile(path.join(__dirname, '../../renderer/index.html')); // Dev (Vite): load the running dev server so .tsx is transformed on the fly.
// Production (running from dist/ directly): load the built renderer at the
// relative path from this file.
// Production (packaged installer build): the renderer ships under
// process.resourcesPath/renderer (configured via build.extraResources in
// package.json) — not inside the asar.
// VITE_DEV_SERVER_URL is set by `npm run dev` via cross-env.
// app.isPackaged is set by electron-builder for installer builds.
const devServerUrl = process.env.VITE_DEV_SERVER_URL;
if (!app.isPackaged && devServerUrl) {
console.log('[WINDOW] Dev mode — loading', devServerUrl);
win.loadURL(devServerUrl);
} else {
const rendererIndex = app.isPackaged
? path.join(process.resourcesPath, 'renderer', 'index.html')
: path.join(__dirname, '../../../dist/renderer/index.html');
if (app.isPackaged) {
try {
fs.accessSync(rendererIndex);
} catch {
console.error(
'[WINDOW] Renderer not found at',
rendererIndex,
'— did you run `npm run build:renderer` before packaging?',
);
}
}
console.log('[WINDOW] Production mode — loading', rendererIndex);
win.loadFile(rendererIndex);
}
// Show window only after content is ready — avoids blank flash // Show window only after content is ready — avoids blank flash
win.once('ready-to-show', () => { win.once('ready-to-show', () => {
@@ -31,8 +63,10 @@ function createMainWindow() {
menu.register(win); menu.register(win);
win.on('closed', () => { // Use 'close' (fires before destruction) — 'closed' fires after the
state.save(win); // BrowserWindow object is destroyed, so getBounds() would throw.
win.on('close', () => {
if (!win.isDestroyed()) state.save(win);
}); });
// Spell check context menu // Spell check context menu
+2 -2
View File
@@ -27,7 +27,7 @@ export function AppShell() {
<main className="h-screen w-screen overflow-hidden bg-background"> <main className="h-screen w-screen overflow-hidden bg-background">
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
onLayout={(sizes) => setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })} onLayoutChange={(sizes) => setPaneSizes({ sidebar: 0, editor: sizes[0], preview: sizes[1] })}
> >
<ResizablePanel defaultSize={previewVisible ? 50 : 100} minSize={20}> <ResizablePanel defaultSize={previewVisible ? 50 : 100} minSize={20}>
<section className="h-full bg-background"> <section className="h-full bg-background">
@@ -58,7 +58,7 @@ export function AppShell() {
<main className="flex-1 overflow-hidden"> <main className="flex-1 overflow-hidden">
<ResizablePanelGroup <ResizablePanelGroup
direction="horizontal" direction="horizontal"
onLayout={(sizes) => setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })} onLayoutChange={(sizes) => setPaneSizes({ sidebar: sizes[0], editor: sizes[1], preview: sizes[2] })}
> >
{sidebarVisible && ( {sidebarVisible && (
<> <>
@@ -17,10 +17,10 @@ export function AboutDialog() {
return ( return (
<Dialog open={isOpen} onOpenChange={(o) => !o && closeModal()}> <Dialog open={isOpen} onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="about-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>About MarkdownConverter</DialogTitle> <DialogTitle>About MarkdownConverter</DialogTitle>
<DialogDescription id="about-desc"> <DialogDescription>
Professional Markdown editor and universal file converter. Professional Markdown editor and universal file converter.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -33,10 +33,10 @@ export function AsciiGeneratorDialog() {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="ascii-desc" className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>ASCII generator</DialogTitle> <DialogTitle>ASCII generator</DialogTitle>
<DialogDescription id="ascii-desc">Type text, pick a font, see ASCII art</DialogDescription> <DialogDescription>Type text, pick a font, see ASCII art</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
@@ -17,10 +17,10 @@ export function ConfirmDialog(props: ConfirmProps) {
return ( return (
<Dialog open onOpenChange={(o) => !o && handleCancel()}> <Dialog open onOpenChange={(o) => !o && handleCancel()}>
<DialogContent aria-describedby="confirm-body"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>{title}</DialogTitle> <DialogTitle>{title}</DialogTitle>
<DialogDescription id="confirm-body">{body}</DialogDescription> <DialogDescription>{body}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button variant="ghost" onClick={handleCancel}> <Button variant="ghost" onClick={handleCancel}>
@@ -37,10 +37,10 @@ export function ExportBatchDialog({ sourcePaths }: { sourcePaths: string[] }) {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="batch-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Batch export</DialogTitle> <DialogTitle>Batch export</DialogTitle>
<DialogDescription id="batch-desc">{sourcePaths.length} files</DialogDescription> <DialogDescription>{sourcePaths.length} files</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div> <div>
@@ -41,10 +41,10 @@ export function ExportDocxDialog({ sourcePath }: { sourcePath: string }) {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="docx-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Export to DOCX</DialogTitle> <DialogTitle>Export to DOCX</DialogTitle>
<DialogDescription id="docx-desc">{sourcePath}</DialogDescription> <DialogDescription>{sourcePath}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div> <div>
@@ -43,10 +43,10 @@ export function ExportHtmlDialog({ sourcePath }: { sourcePath: string }) {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="html-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Export to HTML</DialogTitle> <DialogTitle>Export to HTML</DialogTitle>
<DialogDescription id="html-desc">{sourcePath}</DialogDescription> <DialogDescription>{sourcePath}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">
@@ -56,10 +56,10 @@ export function ExportPdfDialog({ sourcePath }: { sourcePath: string }) {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="pdf-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Export to PDF</DialogTitle> <DialogTitle>Export to PDF</DialogTitle>
<DialogDescription id="pdf-desc">{sourcePath}</DialogDescription> <DialogDescription>{sourcePath}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div> <div>
@@ -47,10 +47,10 @@ export function FindInFilesDialog() {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="find-desc" className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Find in files</DialogTitle> <DialogTitle>Find in files</DialogTitle>
<DialogDescription id="find-desc"> <DialogDescription>
{rootPath ? `Search in ${rootPath}` : 'No folder open'} {rootPath ? `Search in ${rootPath}` : 'No folder open'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -15,10 +15,10 @@ export function SettingsSheet() {
return ( return (
<Sheet open onOpenChange={(o) => !o && closeModal()}> <Sheet open onOpenChange={(o) => !o && closeModal()}>
<SheetContent aria-describedby="settings-desc" side="right" className="w-full sm:max-w-[480px]"> <SheetContent side="right" className="w-full sm:max-w-[480px]">
<SheetHeader> <SheetHeader>
<SheetTitle>Settings</SheetTitle> <SheetTitle>Settings</SheetTitle>
<SheetDescription id="settings-desc">Editor, theme, and export preferences</SheetDescription> <SheetDescription>Editor, theme, and export preferences</SheetDescription>
</SheetHeader> </SheetHeader>
<Tabs defaultValue="editor" className="mt-4"> <Tabs defaultValue="editor" className="mt-4">
<TabsList className="grid w-full grid-cols-5"> <TabsList className="grid w-full grid-cols-5">
@@ -60,10 +60,10 @@ export function TableGeneratorDialog() {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="table-desc" className="max-w-2xl"> <DialogContent className="max-w-2xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Table generator</DialogTitle> <DialogTitle>Table generator</DialogTitle>
<DialogDescription id="table-desc">Specify rows × columns to generate a markdown table</DialogDescription> <DialogDescription>Specify rows × columns to generate a markdown table</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex gap-3"> <div className="flex gap-3">
@@ -17,10 +17,10 @@ export function WelcomeDialog() {
return ( return (
<Dialog open onOpenChange={(o) => !o && handleClose()}> <Dialog open onOpenChange={(o) => !o && handleClose()}>
<DialogContent aria-describedby="welcome-desc" className="max-w-xl"> <DialogContent className="max-w-xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Welcome to MarkdownConverter</DialogTitle> <DialogTitle>Welcome to MarkdownConverter</DialogTitle>
<DialogDescription id="welcome-desc"> <DialogDescription>
A polished editor for Markdown, with PDF, DOCX, and HTML export. A polished editor for Markdown, with PDF, DOCX, and HTML export.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -65,10 +65,10 @@ export function WordExportDialog({ sourcePath }: { sourcePath: string }) {
return ( return (
<Dialog open onOpenChange={(o) => !o && closeModal()}> <Dialog open onOpenChange={(o) => !o && closeModal()}>
<DialogContent aria-describedby="word-desc"> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Export to Word (.docx)</DialogTitle> <DialogTitle>Export to Word (.docx)</DialogTitle>
<DialogDescription id="word-desc">{sourcePath}</DialogDescription> <DialogDescription>{sourcePath}</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<div> <div>
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self';"> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MarkdownConverter</title> <title>MarkdownConverter</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
+1 -1
View File
@@ -6,7 +6,7 @@ import './styles/globals.css';
ReactDOM.createRoot(document.getElementById('root')!).render( ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider defaultTheme="dark" attribute="class" enableSystem> <ThemeProvider defaultTheme="light" attribute="class" enableSystem>
<App /> <App />
</ThemeProvider> </ThemeProvider>
</React.StrictMode> </React.StrictMode>
+1 -1
View File
@@ -6,7 +6,7 @@ import { useAppStore } from '@/stores/app-store';
// Mock react-resizable-panels for jsdom environment // Mock react-resizable-panels for jsdom environment
vi.mock('@/components/ui/resizable', () => ({ vi.mock('@/components/ui/resizable', () => ({
ResizablePanelGroup: ({ children, direction, onLayout }: any) => ( ResizablePanelGroup: ({ children, direction, onLayoutChange }: any) => (
<div data-testid="resizable-panel-group" data-direction={direction}> <div data-testid="resizable-panel-group" data-direction={direction}>
{children} {children}
</div> </div>
@@ -31,4 +31,27 @@ describe('WelcomeDialog', () => {
await userEvent.click(screen.getByRole('button', { name: /get started/i })); await userEvent.click(screen.getByRole('button', { name: /get started/i }));
expect(useSettingsStore.getState().welcomeDismissed).toBe(true); expect(useSettingsStore.getState().welcomeDismissed).toBe(true);
}); });
// Regression guard: Radix Dialog emits a console.warn when the Content's
// aria-describedby points at an id that isn't in the DOM. The default
// shadcn/ui pattern (custom id="X-desc" on DialogDescription) breaks that
// link because Radix's internal descriptionId is auto-generated via useId.
// The fix is to drop the override and let Radix manage the id itself.
it('does not emit the Radix "Missing Description" warning', async () => {
const warnings: string[] = [];
const spy = vi.spyOn(console, 'warn').mockImplementation((...args) => {
warnings.push(args.map(String).join(' '));
});
try {
render(<WelcomeDialog />);
// Effects that fire the warning run in a microtask; one tick is enough.
await Promise.resolve();
const offending = warnings.filter((w) =>
/Missing\s+`?Description`?|aria-describedby=\{undefined\}/.test(w),
);
expect(offending).toEqual([]);
} finally {
spy.mockRestore();
}
});
}); });