mirror of
https://github.com/anthonyfitzpatrick/manuscript-compiler.git
synced 2026-07-22 08:31:19 +00:00
Initial Manuscript Compiler MVP
This commit is contained in:
commit
1abb780cb7
16 changed files with 1115 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
main.js
|
||||
*.map
|
||||
Icon?
|
||||
24
README.md
Normal file
24
README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Manuscript Compiler
|
||||
|
||||
Manuscript Compiler is an Obsidian plugin that recursively compiles a book folder into one clean Markdown file for later import into tools such as Vellum.
|
||||
|
||||
## Stage 1 features
|
||||
|
||||
- Compiles front matter, parts, chapters, scenes, and back matter in natural filename order.
|
||||
- Optionally removes YAML frontmatter and includes scene titles.
|
||||
- Uses a configurable separator between scenes.
|
||||
- Creates the export folder when needed and confirms before overwriting.
|
||||
- Reports structural counts, word count, and warnings without modifying source notes.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
Copy `manifest.json`, `main.js`, and (if present) `styles.css` to `.obsidian/plugins/manuscript-compiler/` in a test vault, then enable the plugin in Obsidian.
|
||||
|
||||
## Book structure
|
||||
|
||||
The selected book folder may contain `Ebook Front Matter`, part folders containing chapter folders and Markdown scene files, and `Ebook Back Matter`. Hidden files and folders are ignored. Files and folders are sorted using natural, case-insensitive filename order.
|
||||
23
esbuild.config.mjs
Normal file
23
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const production = process.argv[2] === "production";
|
||||
const context = await esbuild.context({
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
external: ["obsidian", "electron", "@codemirror/autocomplete", "@codemirror/collab", "@codemirror/commands", "@codemirror/language", "@codemirror/lint", "@codemirror/search", "@codemirror/state", "@codemirror/view", "@lezer/common", "@lezer/highlight", "@lezer/lr", ...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: production ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js"
|
||||
});
|
||||
|
||||
if (production) {
|
||||
await context.rebuild();
|
||||
await context.dispose();
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "manuscript-compiler",
|
||||
"name": "Manuscript Compiler",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "1.5.0",
|
||||
"description": "Compile a book folder into a single Markdown manuscript.",
|
||||
"author": "Anthony Fitzpatrick",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
639
package-lock.json
generated
Normal file
639
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,639 @@
|
|||
{
|
||||
"name": "obsidian-manuscript-compiler",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "obsidian-manuscript-compiler",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"obsidian": "^1.8.7",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz",
|
||||
"integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@marijn/find-cluster-break": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/view": {
|
||||
"version": "6.38.6",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz",
|
||||
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@codemirror/state": "^6.5.0",
|
||||
"crelt": "^1.0.6",
|
||||
"style-mod": "^4.1.0",
|
||||
"w3c-keyname": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@marijn/find-cluster-break": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/codemirror": {
|
||||
"version": "5.60.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz",
|
||||
"integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/tern": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz",
|
||||
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tern": {
|
||||
"version": "0.23.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz",
|
||||
"integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
|
||||
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.24.2",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
|
||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.24.2",
|
||||
"@esbuild/android-arm": "0.24.2",
|
||||
"@esbuild/android-arm64": "0.24.2",
|
||||
"@esbuild/android-x64": "0.24.2",
|
||||
"@esbuild/darwin-arm64": "0.24.2",
|
||||
"@esbuild/darwin-x64": "0.24.2",
|
||||
"@esbuild/freebsd-arm64": "0.24.2",
|
||||
"@esbuild/freebsd-x64": "0.24.2",
|
||||
"@esbuild/linux-arm": "0.24.2",
|
||||
"@esbuild/linux-arm64": "0.24.2",
|
||||
"@esbuild/linux-ia32": "0.24.2",
|
||||
"@esbuild/linux-loong64": "0.24.2",
|
||||
"@esbuild/linux-mips64el": "0.24.2",
|
||||
"@esbuild/linux-ppc64": "0.24.2",
|
||||
"@esbuild/linux-riscv64": "0.24.2",
|
||||
"@esbuild/linux-s390x": "0.24.2",
|
||||
"@esbuild/linux-x64": "0.24.2",
|
||||
"@esbuild/netbsd-arm64": "0.24.2",
|
||||
"@esbuild/netbsd-x64": "0.24.2",
|
||||
"@esbuild/openbsd-arm64": "0.24.2",
|
||||
"@esbuild/openbsd-x64": "0.24.2",
|
||||
"@esbuild/sunos-x64": "0.24.2",
|
||||
"@esbuild/win32-arm64": "0.24.2",
|
||||
"@esbuild/win32-ia32": "0.24.2",
|
||||
"@esbuild/win32-x64": "0.24.2"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/obsidian": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.13.1.tgz",
|
||||
"integrity": "sha512-qtTEA2pmhJzhuhJqzbBFRYhpIOqvW+krDYjtFynv66KbxBbumHBlsJfWw3I4jtnK/6fZwbQhCrmmDdRwXmX56w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/codemirror": "5.60.8",
|
||||
"moment": "2.29.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/state": "6.5.0",
|
||||
"@codemirror/view": "6.38.6"
|
||||
}
|
||||
},
|
||||
"node_modules/style-mod": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
20
package.json
Normal file
20
package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "obsidian-manuscript-compiler",
|
||||
"version": "0.1.0",
|
||||
"description": "Compile an Obsidian book folder into a single Markdown manuscript.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||
"typecheck": "tsc -noEmit -skipLibCheck"
|
||||
},
|
||||
"keywords": ["obsidian", "manuscript", "markdown"],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.2",
|
||||
"builtin-modules": "^4.0.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"obsidian": "^1.8.7",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
82
src/compiler.ts
Normal file
82
src/compiler.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Vault } from "obsidian";
|
||||
import type { ManuscriptCompilerSettings } from "./settings";
|
||||
import type { CompileResult, ScannedBook } from "./types";
|
||||
|
||||
export class ManuscriptCompiler {
|
||||
constructor(private readonly vault: Vault) {}
|
||||
|
||||
async compile(book: ScannedBook, settings: ManuscriptCompilerSettings): Promise<CompileResult> {
|
||||
const sections: string[] = [];
|
||||
const warnings = [...book.warnings];
|
||||
let scenes = 0;
|
||||
let chapters = 0;
|
||||
|
||||
if (settings.includeFrontMatter) {
|
||||
const content = await this.readFiles(book.frontMatter, settings, false);
|
||||
sections.push(...content);
|
||||
}
|
||||
|
||||
for (const part of book.parts) {
|
||||
sections.push(`# ${part.folder.name}`);
|
||||
const loose = await this.readFiles(part.looseScenes, settings, true);
|
||||
sections.push(...this.joinScenes(loose, settings.sceneSeparator));
|
||||
scenes += loose.length;
|
||||
|
||||
for (const chapter of part.chapters) {
|
||||
sections.push(`## ${chapter.folder.name}`);
|
||||
chapters += 1;
|
||||
const content = await this.readFiles(chapter.scenes, settings, true);
|
||||
sections.push(...this.joinScenes(content, settings.sceneSeparator));
|
||||
scenes += content.length;
|
||||
if (content.length === 0) warnings.push(`Chapter “${chapter.folder.name}” contains no Markdown files.`);
|
||||
}
|
||||
}
|
||||
|
||||
const rootScenes = await this.readFiles(book.looseScenes, settings, true);
|
||||
sections.push(...this.joinScenes(rootScenes, settings.sceneSeparator));
|
||||
scenes += rootScenes.length;
|
||||
|
||||
if (settings.includeBackMatter) {
|
||||
const content = await this.readFiles(book.backMatter, settings, false);
|
||||
sections.push(...content);
|
||||
}
|
||||
|
||||
const markdown = `${sections.filter(Boolean).join("\n\n").trim()}\n`;
|
||||
return { markdown, parts: book.parts.length, chapters, scenes, wordCount: this.countWords(markdown), warnings };
|
||||
}
|
||||
|
||||
private async readFiles(files: ScannedBook["frontMatter"], settings: ManuscriptCompilerSettings, titleScenes: boolean): Promise<string[]> {
|
||||
return Promise.all(files.map(async (file) => {
|
||||
let body = await this.vault.cachedRead(file);
|
||||
if (settings.stripYamlFrontmatter) body = this.stripYaml(body);
|
||||
body = body.trim();
|
||||
if (titleScenes && settings.includeSceneTitles && body) return `### ${file.basename}\n\n${body}`;
|
||||
return body;
|
||||
}));
|
||||
}
|
||||
|
||||
private joinScenes(scenes: string[], separator: string): string[] {
|
||||
const nonEmpty = scenes.filter(Boolean);
|
||||
const result: string[] = [];
|
||||
nonEmpty.forEach((scene, index) => {
|
||||
if (index > 0 && separator.trim()) result.push(separator.trim());
|
||||
result.push(scene);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
private stripYaml(markdown: string): string {
|
||||
const normalized = markdown.replace(/^\uFEFF/, "");
|
||||
return normalized.replace(/^---[\t ]*\r?\n[\s\S]*?\r?\n(?:---|\.\.\.)[\t ]*(?:\r?\n|$)/, "");
|
||||
}
|
||||
|
||||
private countWords(markdown: string): number {
|
||||
const text = markdown
|
||||
.replace(/```[\s\S]*?```/g, " ")
|
||||
.replace(/`[^`]*`/g, " ")
|
||||
.replace(/^#{1,6}\s+.*$/gm, " ")
|
||||
.replace(/[\p{P}\p{S}]+/gu, " ")
|
||||
.trim();
|
||||
return text ? text.split(/\s+/u).length : 0;
|
||||
}
|
||||
}
|
||||
34
src/exporter.ts
Normal file
34
src/exporter.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { normalizePath, TFile, Vault } from "obsidian";
|
||||
|
||||
export class MarkdownExporter {
|
||||
constructor(private readonly vault: Vault) {}
|
||||
|
||||
getOutputPath(exportFolder: string, bookName: string): string {
|
||||
const folder = normalizePath(exportFolder.trim().replace(/^\/+|\/+$/g, ""));
|
||||
const safeName = bookName.replace(/[\\/:*?"<>|]/g, "-").trim() || "Manuscript";
|
||||
return normalizePath(folder ? `${folder}/${safeName} Manuscript.md` : `${safeName} Manuscript.md`);
|
||||
}
|
||||
|
||||
exists(path: string): boolean {
|
||||
return this.vault.getAbstractFileByPath(path) instanceof TFile;
|
||||
}
|
||||
|
||||
async write(path: string, markdown: string): Promise<TFile> {
|
||||
const lastSlash = path.lastIndexOf("/");
|
||||
if (lastSlash >= 0) await this.ensureFolder(path.slice(0, lastSlash));
|
||||
const existing = this.vault.getAbstractFileByPath(path);
|
||||
if (existing instanceof TFile) {
|
||||
await this.vault.modify(existing, markdown);
|
||||
return existing;
|
||||
}
|
||||
return this.vault.create(path, markdown);
|
||||
}
|
||||
|
||||
private async ensureFolder(path: string): Promise<void> {
|
||||
const parts = normalizePath(path).split("/");
|
||||
for (let index = 1; index <= parts.length; index += 1) {
|
||||
const current = parts.slice(0, index).join("/");
|
||||
if (!this.vault.getAbstractFileByPath(current)) await this.vault.createFolder(current);
|
||||
}
|
||||
}
|
||||
}
|
||||
67
src/main.ts
Normal file
67
src/main.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Notice, Plugin, TFolder } from "obsidian";
|
||||
import { ManuscriptCompiler } from "./compiler";
|
||||
import { MarkdownExporter } from "./exporter";
|
||||
import { DEFAULT_SETTINGS, ManuscriptCompilerSettings } from "./settings";
|
||||
import { CompileReportModal, ConfirmOverwriteModal, FolderSuggestModal, ManuscriptCompilerSettingTab, showError } from "./ui";
|
||||
import { VaultScanner } from "./vault-scanner";
|
||||
|
||||
export default class ManuscriptCompilerPlugin extends Plugin {
|
||||
settings: ManuscriptCompilerSettings = DEFAULT_SETTINGS;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
await this.loadSettings();
|
||||
this.addSettingTab(new ManuscriptCompilerSettingTab(this.app, this));
|
||||
this.addCommand({ id: "compile-current-book", name: "Compile Current Book", callback: () => { void this.compileCurrentBook(); } });
|
||||
this.addCommand({ id: "compile-selected-folder", name: "Compile Selected Folder", callback: () => { new FolderSuggestModal(this.app, (folder) => { void this.compileFolder(folder); }).open(); } });
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> { this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData() as Partial<ManuscriptCompilerSettings> | null); }
|
||||
async saveSettings(): Promise<void> { await this.saveData(this.settings); }
|
||||
|
||||
private async compileCurrentBook(): Promise<void> {
|
||||
try {
|
||||
const folder = this.resolveCurrentBook();
|
||||
if (!folder) throw new Error("Set a default manuscript folder, or open a note inside a book folder containing Part folders.");
|
||||
await this.compileFolder(folder);
|
||||
} catch (error) { showError(error); }
|
||||
}
|
||||
|
||||
private resolveCurrentBook(): TFolder | null {
|
||||
if (this.settings.defaultManuscriptFolder) {
|
||||
const configured = this.app.vault.getAbstractFileByPath(this.settings.defaultManuscriptFolder);
|
||||
if (configured instanceof TFolder) return configured;
|
||||
}
|
||||
const active = this.app.workspace.getActiveFile();
|
||||
let folder = active?.parent ?? null;
|
||||
while (folder && folder.path !== "/") {
|
||||
const hasBookStructure = folder.children.some((child) => child instanceof TFolder && (/^part\b/i.test(child.name) || /^ebook (front|back) matter$/i.test(child.name)));
|
||||
if (hasBookStructure) return folder;
|
||||
folder = folder.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async compileFolder(folder: TFolder): Promise<void> {
|
||||
try {
|
||||
new Notice(`Compiling “${folder.name}”…`);
|
||||
const scan = new VaultScanner().scan(folder);
|
||||
const result = await new ManuscriptCompiler(this.app.vault).compile(scan, this.settings);
|
||||
const exporter = new MarkdownExporter(this.app.vault);
|
||||
const outputPath = exporter.getOutputPath(this.settings.defaultExportFolder, folder.name);
|
||||
if (exporter.exists(outputPath) && !await this.confirmOverwrite(outputPath)) { new Notice("Compilation cancelled."); return; }
|
||||
const output = await exporter.write(outputPath, result.markdown);
|
||||
new CompileReportModal(this.app, { output: output.path, parts: result.parts, chapters: result.chapters, scenes: result.scenes, wordCount: result.wordCount, warnings: result.warnings }).open();
|
||||
} catch (error) { showError(error); }
|
||||
}
|
||||
|
||||
private confirmOverwrite(path: string): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (value: boolean): void => { if (!settled) { settled = true; resolve(value); } };
|
||||
const modal = new ConfirmOverwriteModal(this.app, path, finish);
|
||||
const originalClose = modal.onClose.bind(modal);
|
||||
modal.onClose = (): void => { originalClose(); finish(false); };
|
||||
modal.open();
|
||||
});
|
||||
}
|
||||
}
|
||||
19
src/settings.ts
Normal file
19
src/settings.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export interface ManuscriptCompilerSettings {
|
||||
defaultManuscriptFolder: string;
|
||||
defaultExportFolder: string;
|
||||
includeFrontMatter: boolean;
|
||||
includeBackMatter: boolean;
|
||||
stripYamlFrontmatter: boolean;
|
||||
includeSceneTitles: boolean;
|
||||
sceneSeparator: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ManuscriptCompilerSettings = {
|
||||
defaultManuscriptFolder: "",
|
||||
defaultExportFolder: "Manuscript Exports",
|
||||
includeFrontMatter: true,
|
||||
includeBackMatter: true,
|
||||
stripYamlFrontmatter: true,
|
||||
includeSceneTitles: false,
|
||||
sceneSeparator: "#"
|
||||
};
|
||||
30
src/types.ts
Normal file
30
src/types.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { TFile, TFolder } from "obsidian";
|
||||
|
||||
export interface ScannedBook {
|
||||
root: TFolder;
|
||||
frontMatter: TFile[];
|
||||
parts: ScannedPart[];
|
||||
looseScenes: TFile[];
|
||||
backMatter: TFile[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export interface ScannedPart {
|
||||
folder: TFolder;
|
||||
chapters: ScannedChapter[];
|
||||
looseScenes: TFile[];
|
||||
}
|
||||
|
||||
export interface ScannedChapter {
|
||||
folder: TFolder;
|
||||
scenes: TFile[];
|
||||
}
|
||||
|
||||
export interface CompileResult {
|
||||
markdown: string;
|
||||
parts: number;
|
||||
chapters: number;
|
||||
scenes: number;
|
||||
wordCount: number;
|
||||
warnings: string[];
|
||||
}
|
||||
65
src/ui.ts
Normal file
65
src/ui.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { App, FuzzySuggestModal, Modal, Notice, PluginSettingTab, Setting, TFolder } from "obsidian";
|
||||
import type ManuscriptCompilerPlugin from "./main";
|
||||
|
||||
export class FolderSuggestModal extends FuzzySuggestModal<TFolder> {
|
||||
constructor(app: App, private readonly onChoose: (folder: TFolder) => void) { super(app); }
|
||||
getItems(): TFolder[] { return this.app.vault.getAllLoadedFiles().filter((file): file is TFolder => file instanceof TFolder && file.path !== "/"); }
|
||||
getItemText(folder: TFolder): string { return folder.path; }
|
||||
onChooseItem(folder: TFolder): void { this.onChoose(folder); }
|
||||
}
|
||||
|
||||
export class ConfirmOverwriteModal extends Modal {
|
||||
constructor(app: App, private readonly path: string, private readonly resolve: (confirmed: boolean) => void) { super(app); }
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Overwrite manuscript?");
|
||||
this.contentEl.createEl("p", { text: `The file “${this.path}” already exists. Replace it?` });
|
||||
new Setting(this.contentEl)
|
||||
.addButton((button) => button.setButtonText("Cancel").onClick(() => { this.resolve(false); this.close(); }))
|
||||
.addButton((button) => button.setButtonText("Overwrite").setWarning().onClick(() => { this.resolve(true); this.close(); }));
|
||||
}
|
||||
onClose(): void { this.contentEl.empty(); }
|
||||
}
|
||||
|
||||
export class CompileReportModal extends Modal {
|
||||
constructor(app: App, private readonly report: { output: string; parts: number; chapters: number; scenes: number; wordCount: number; warnings: string[] }) { super(app); }
|
||||
onOpen(): void {
|
||||
this.titleEl.setText("Compilation complete");
|
||||
const list = this.contentEl.createEl("dl", { cls: "manuscript-compiler-report" });
|
||||
this.addRow(list, "Output file", this.report.output);
|
||||
this.addRow(list, "Parts", String(this.report.parts));
|
||||
this.addRow(list, "Chapters", String(this.report.chapters));
|
||||
this.addRow(list, "Scenes", String(this.report.scenes));
|
||||
this.addRow(list, "Word count", this.report.wordCount.toLocaleString());
|
||||
this.contentEl.createEl("h3", { text: "Warnings" });
|
||||
if (this.report.warnings.length === 0) this.contentEl.createEl("p", { text: "None" });
|
||||
else {
|
||||
const warnings = this.contentEl.createEl("ul");
|
||||
this.report.warnings.forEach((warning) => warnings.createEl("li", { text: warning }));
|
||||
}
|
||||
}
|
||||
private addRow(list: HTMLElement, label: string, value: string): void { list.createEl("dt", { text: label }); list.createEl("dd", { text: value }); }
|
||||
}
|
||||
|
||||
export class ManuscriptCompilerSettingTab extends PluginSettingTab {
|
||||
constructor(app: App, private readonly plugin: ManuscriptCompilerPlugin) { super(app, plugin); }
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
new Setting(containerEl).setName("Default manuscript folder").setDesc("Vault-relative path to the book folder used by Compile Current Book.").addText((text) => text.setPlaceholder("Books/My Book").setValue(this.plugin.settings.defaultManuscriptFolder).onChange(async (value) => { this.plugin.settings.defaultManuscriptFolder = value.trim(); await this.plugin.saveSettings(); }));
|
||||
new Setting(containerEl).setName("Default export folder").setDesc("Vault-relative folder where compiled manuscripts are written.").addText((text) => text.setPlaceholder("Manuscript Exports").setValue(this.plugin.settings.defaultExportFolder).onChange(async (value) => { this.plugin.settings.defaultExportFolder = value.trim(); await this.plugin.saveSettings(); }));
|
||||
this.addToggle(containerEl, "Include front matter", "Include Markdown files under Ebook Front Matter.", "includeFrontMatter");
|
||||
this.addToggle(containerEl, "Include back matter", "Include Markdown files under Ebook Back Matter.", "includeBackMatter");
|
||||
this.addToggle(containerEl, "Strip YAML frontmatter", "Remove leading YAML frontmatter from every source note.", "stripYamlFrontmatter");
|
||||
this.addToggle(containerEl, "Include scene titles", "Add each scene filename as a level-three heading.", "includeSceneTitles");
|
||||
new Setting(containerEl).setName("Scene separator").setDesc("Markdown inserted between consecutive scenes. The default is #.").addText((text) => text.setValue(this.plugin.settings.sceneSeparator).onChange(async (value) => { this.plugin.settings.sceneSeparator = value; await this.plugin.saveSettings(); }));
|
||||
}
|
||||
private addToggle(container: HTMLElement, name: string, description: string, key: "includeFrontMatter" | "includeBackMatter" | "stripYamlFrontmatter" | "includeSceneTitles"): void {
|
||||
new Setting(container).setName(name).setDesc(description).addToggle((toggle) => toggle.setValue(this.plugin.settings[key]).onChange(async (value) => { this.plugin.settings[key] = value; await this.plugin.saveSettings(); }));
|
||||
}
|
||||
}
|
||||
|
||||
export function showError(error: unknown): void {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
new Notice(`Manuscript Compiler: ${message}`, 8000);
|
||||
console.error("Manuscript Compiler", error);
|
||||
}
|
||||
65
src/vault-scanner.ts
Normal file
65
src/vault-scanner.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { TAbstractFile, TFile, TFolder } from "obsidian";
|
||||
import type { ScannedBook, ScannedChapter, ScannedPart } from "./types";
|
||||
|
||||
const FRONT_MATTER = "ebook front matter";
|
||||
const BACK_MATTER = "ebook back matter";
|
||||
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
|
||||
|
||||
export class VaultScanner {
|
||||
scan(root: TFolder): ScannedBook {
|
||||
const warnings: string[] = [];
|
||||
const visibleChildren = this.sortedVisibleChildren(root);
|
||||
const frontFolder = visibleChildren.find((item): item is TFolder => item instanceof TFolder && item.name.toLowerCase() === FRONT_MATTER);
|
||||
const backFolder = visibleChildren.find((item): item is TFolder => item instanceof TFolder && item.name.toLowerCase() === BACK_MATTER);
|
||||
const partFolders = visibleChildren.filter((item): item is TFolder => item instanceof TFolder && item !== frontFolder && item !== backFolder);
|
||||
const looseScenes = visibleChildren.filter((item): item is TFile => item instanceof TFile && this.isMarkdown(item));
|
||||
|
||||
if (looseScenes.length > 0) warnings.push(`${looseScenes.length} Markdown file(s) found directly in the book folder; included after parts.`);
|
||||
if (!frontFolder) warnings.push("No Ebook Front Matter folder found.");
|
||||
if (!backFolder) warnings.push("No Ebook Back Matter folder found.");
|
||||
|
||||
return {
|
||||
root,
|
||||
frontMatter: frontFolder ? this.collectMarkdown(frontFolder) : [],
|
||||
parts: partFolders.map((folder) => this.scanPart(folder, warnings)),
|
||||
looseScenes,
|
||||
backMatter: backFolder ? this.collectMarkdown(backFolder) : [],
|
||||
warnings
|
||||
};
|
||||
}
|
||||
|
||||
private scanPart(folder: TFolder, warnings: string[]): ScannedPart {
|
||||
const children = this.sortedVisibleChildren(folder);
|
||||
const chapterFolders = children.filter((item): item is TFolder => item instanceof TFolder);
|
||||
const looseScenes = children.filter((item): item is TFile => item instanceof TFile && this.isMarkdown(item));
|
||||
if (looseScenes.length > 0) warnings.push(`${looseScenes.length} Markdown file(s) found directly in “${folder.name}”; included before its chapters.`);
|
||||
return { folder, chapters: chapterFolders.map((chapter) => this.scanChapter(chapter)), looseScenes };
|
||||
}
|
||||
|
||||
private scanChapter(folder: TFolder): ScannedChapter {
|
||||
return { folder, scenes: this.collectMarkdown(folder) };
|
||||
}
|
||||
|
||||
private collectMarkdown(folder: TFolder): TFile[] {
|
||||
const files: TFile[] = [];
|
||||
for (const child of this.sortedVisibleChildren(folder)) {
|
||||
if (child instanceof TFile && this.isMarkdown(child)) files.push(child);
|
||||
if (child instanceof TFolder) files.push(...this.collectMarkdown(child));
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
private sortedVisibleChildren(folder: TFolder): TAbstractFile[] {
|
||||
return folder.children
|
||||
.filter((item) => !this.isHidden(item.path))
|
||||
.sort((a, b) => collator.compare(a.name, b.name));
|
||||
}
|
||||
|
||||
private isHidden(path: string): boolean {
|
||||
return path.split("/").some((segment) => segment.startsWith("."));
|
||||
}
|
||||
|
||||
private isMarkdown(file: TFile): boolean {
|
||||
return file.extension.toLowerCase() === "md" && !this.isHidden(file.path);
|
||||
}
|
||||
}
|
||||
8
styles.css
Normal file
8
styles.css
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
.manuscript-compiler-report {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: var(--size-4-2) var(--size-4-4);
|
||||
}
|
||||
|
||||
.manuscript-compiler-report dt { font-weight: var(--font-semibold); }
|
||||
.manuscript-compiler-report dd { margin: 0; overflow-wrap: anywhere; }
|
||||
23
tsconfig.json
Normal file
23
tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] },
|
||||
"inlineSourceMap": true,
|
||||
"inlineSources": true,
|
||||
"module": "ESNext",
|
||||
"target": "ES2018",
|
||||
"allowJs": false,
|
||||
"noImplicitAny": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"downlevelIteration": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["DOM", "ES2018"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
3
versions.json
Normal file
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"0.1.0": "1.5.0"
|
||||
}
|
||||
Loading…
Reference in a new issue