mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
Compare commits
4
Commits
efca458495
..
v4.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be46d4bac | ||
|
|
1e24b52f3e | ||
|
|
58bd19ecd1 | ||
|
|
7ab5a0ddb4 |
@@ -25,6 +25,13 @@ jobs:
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Bundle MarkItDown (optional, best effort)
|
||||
# Freezes markitdown + Python into bin/linux/markitdown; failure is
|
||||
# non-fatal — the build then ships without it and the app falls back
|
||||
# to a system markitdown at runtime.
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
@@ -59,6 +66,12 @@ jobs:
|
||||
- name: Download external tools (pandoc)
|
||||
run: node scripts/download-tools.js
|
||||
|
||||
- name: Bundle MarkItDown (optional, best effort)
|
||||
# Windows runners ship python + venv; non-fatal on failure — the
|
||||
# package then omits the bundled binary by design
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
@@ -94,8 +107,44 @@ jobs:
|
||||
dist/*.zip
|
||||
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: Bundle MarkItDown (optional, best effort)
|
||||
run: npm run bundle:markitdown
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Build macOS packages
|
||||
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:
|
||||
needs: [build-linux, build-windows]
|
||||
needs: [build-linux, build-windows, build-macos]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -115,6 +164,13 @@ jobs:
|
||||
name: windows-artifacts
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
@@ -51,6 +51,7 @@ A powerful cross-platform Markdown editor and document converter powered by Pand
|
||||
- **ASCII Art Generator** - Create text banners and diagrams
|
||||
- **Word templates** - Use custom Word templates for enhanced exports
|
||||
- **Import documents** - Import from 30+ formats (DOCX, PDF, HTML, etc.)
|
||||
- **MarkItDown import** - Any file → Markdown via [Microsoft MarkItDown](https://github.com/microsoft/markitdown): PDF, DOCX, PPTX, XLSX, Outlook .msg, EPUB, images, ZIP (audio/OCR with the `[all]` extras)
|
||||
- **Excel export** - Markdown tables to native .xlsx workbooks (one sheet per table)
|
||||
- **AI Assistant** - Multi-provider AI help (OpenAI/Anthropic/Ollama/LM Studio): chat panel, summarize/improve/translate commands, grammar proofreading
|
||||
- **Inline comments** - Anchor-based document comments in `.comments/` sidecars with F8 navigation
|
||||
@@ -67,6 +68,7 @@ A powerful cross-platform Markdown editor and document converter powered by Pand
|
||||
### Prerequisites
|
||||
- [Node.js](https://nodejs.org/) (v16 or later)
|
||||
- [Pandoc](https://pandoc.org/installing.html) (required for export functionality)
|
||||
- Optional: [MarkItDown](https://github.com/microsoft/markitdown) (`pip install "markitdown[all]"`) for any-file → Markdown import
|
||||
|
||||
### Install Dependencies
|
||||
```bash
|
||||
@@ -159,6 +161,43 @@ Open PDF files directly in MarkdownConverter:
|
||||
- Rotate pages left or right
|
||||
- Close PDF to return to editor
|
||||
|
||||
## Bundled Dependencies, Legal Notices & Credits
|
||||
|
||||
MarkdownConverter ships as a self-contained package. Everything needed for the
|
||||
core workflows is **bundled**; a few large optional tools are detected from
|
||||
the system when present.
|
||||
|
||||
### Bundled with the app
|
||||
|
||||
| Component | License | Role |
|
||||
|---|---|---|
|
||||
| [Pandoc](https://pandoc.org) 3.9 | GPL-2.0+ (separate process) | 25+ export/import formats |
|
||||
| [FFmpeg](https://ffmpeg.org) (via ffmpeg-static) | GPL-3.0+ build (separate process) | audio/video tools |
|
||||
| [MarkItDown](https://github.com/microsoft/markitdown) (MIT) + embedded Python runtime (PSF) | MIT / PSF | any-file → Markdown import (PDF/DOCX/PPTX/XLSX/Outlook/EPUB/images/ZIP) |
|
||||
| [sharp](https://sharp.pixelplumbing.com) + libvips | Apache-2.0 / LGPL-2.1+ (dynamic) | image tools |
|
||||
| [KaTeX](https://katex.org), [marked](https://marked.js.org), [highlight.js], [DOMPurify], [mermaid], [CodeMirror 6], pdf-lib (@cantoo fork), pdfjs-dist, JSZip, simple-git | MIT / Apache-2.0 / BSD-3 / MPL-2.0 | editor, preview, PDF, Git |
|
||||
| JetBrains Mono & Fira Code fonts | SIL OFL 1.1 | editor typography |
|
||||
|
||||
GPL-licensed tools run as **separate processes** (never linked into the app)
|
||||
and their complete corresponding sources are offered in
|
||||
[SOURCES.md](SOURCES.md). Full details: [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md),
|
||||
also available inside the app under **Help → Third-Party Notices & Licenses**.
|
||||
|
||||
### Not bundled (used when installed)
|
||||
|
||||
LibreOffice (Office conversion), MiKTeX/TeX Live (LaTeX PDF), ImageMagick
|
||||
(extra image formats), PlantUML + Java (local diagrams), Calibre (MOBI),
|
||||
MarkItDown `[all]` extras (audio transcription / OCR).
|
||||
|
||||
### Credits
|
||||
|
||||
Built on open source: [Electron], [CodeMirror], [marked], [KaTeX],
|
||||
[highlight.js], [DOMPurify](https://github.com/cure53/DOMPurify),
|
||||
[mermaid](https://mermaid.js.org), [pdf-lib], [pdf.js](https://mozilla.github.io/pdf.js/),
|
||||
[sharp]/libvips, [Pandoc], [FFmpeg], [MarkItDown] by Microsoft,
|
||||
[simple-git], [JSZip], [JetBrains Mono], [Fira Code]. Thank you to all their
|
||||
authors and maintainers.
|
||||
|
||||
## Open Source
|
||||
|
||||
MarkdownConverter is 100% open-source. All dependencies are permissively licensed:
|
||||
@@ -181,4 +220,4 @@ Amit Haridas (amit.wh@gmail.com)
|
||||
|
||||
## Version
|
||||
|
||||
v4.6.0
|
||||
v4.7.0
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# Source Code Availability (GPL / LGPL Written Offer)
|
||||
|
||||
MarkdownConverter distributes the following binaries built from GPL-licensed
|
||||
software. Per GPL §3(b), this document is the written offer: **corresponding
|
||||
source code for the exact versions listed below is available on request for
|
||||
at least three years from each release**, and permanently at the referenced
|
||||
public locations. Write to: amit.wh@gmail.com (or open a GitHub issue at
|
||||
https://github.com/amitwh/markdown-converter/issues).
|
||||
|
||||
## Pandoc — GPL-2.0-or-later
|
||||
|
||||
- Binary shipped: `bin/pandoc` (v3.9.0.2, official upstream release, unmodified)
|
||||
- SHA-256 (linux): `7d124235998ecd3cdd9a463b1e5f6691a178b6461824c29a36170a0882f05597`
|
||||
- Source: <https://github.com/jgm/pandoc/archive/refs/tags/3.9.0.2.tar.gz>
|
||||
- Pandoc statically links Haskell libraries (GHC ecosystem, mostly BSD-3);
|
||||
their sources are included in the upstream release tarball's dependency set.
|
||||
|
||||
## FFmpeg — GPL-3.0-or-later (build configuration)
|
||||
|
||||
- Binary shipped: `ffmpeg` provided by the npm package `ffmpeg-static@5.3.0`
|
||||
(Linux: johnvansickle.com build; Windows: gyan.dev; macOS: evermeet.cx —
|
||||
all `--enable-gpl` builds including x264/x265, per the build banner)
|
||||
- Source:
|
||||
- FFmpeg: <https://ffmpeg.org/releases/> (use the release matching
|
||||
`ffmpeg -version` of the shipped binary)
|
||||
- Build scripts & pinned versions: <https://github.com/eugeneware/ffmpeg-static>
|
||||
- x264: <https://code.videolan.org/videolan/x264> ·
|
||||
x265: <https://bitbucket.org/multicoreware/x265_git/> ·
|
||||
other `--enable-lib*` components: their upstream sources (all free/open)
|
||||
|
||||
## PyInstaller bootloader (inside the bundled MarkItDown binary) — GPL-2.0 with boot-exception
|
||||
|
||||
- Binary shipped: `bin/markitdown` (MarkItDown 0.1.7 frozen with PyInstaller 6.x)
|
||||
- PyInstaller grants a special exception allowing the bootloader to be
|
||||
embedded in non-GPL frozen applications; source anyway:
|
||||
<https://github.com/pyinstaller/pyinstaller>
|
||||
- Everything frozen above the bootloader (markitdown + Python packages +
|
||||
CPython runtime) is permissively licensed (MIT/Apache/BSD/PSF/MPL);
|
||||
see THIRD-PARTY-NOTICES.md §2 for the list.
|
||||
- CPython runtime source: <https://www.python.org/downloads/source/>
|
||||
(PSF License — not GPL, listed here for completeness).
|
||||
|
||||
## libvips (via sharp prebuilt binaries) — LGPL-2.1-or-later
|
||||
|
||||
- Shipped as dynamically-loaded libraries from `@img/*` prebuilts for sharp 0.35.4
|
||||
- Source: <https://github.com/libvips/libvips> · prebuilt bundle sources:
|
||||
<https://github.com/lovell/sharp-builds>
|
||||
- LGPL compliance: the app's own source is public (MIT) and the libraries
|
||||
remain separately replaceable files in the installation directory
|
||||
(`node_modules/@img/`), satisfying the relinking requirement.
|
||||
|
||||
---
|
||||
|
||||
_Versions and hashes above correspond to the release this file ships with;
|
||||
update them when bumping bundled tool versions._
|
||||
@@ -0,0 +1,108 @@
|
||||
# Third-Party Notices & Licenses
|
||||
|
||||
MarkdownConverter (this app) is MIT-licensed. This document lists the
|
||||
third-party components that are **distributed with** the application, their
|
||||
licenses, and where to obtain source code. Full license texts for the
|
||||
copyleft and font licenses referenced here are in the `third-party-licenses/`
|
||||
folder shipped alongside this file (and in the source repository).
|
||||
|
||||
This product includes software developed by third parties under the licenses
|
||||
below. Copyright and license notices are reproduced verbatim or referenced
|
||||
per each license's terms.
|
||||
|
||||
---
|
||||
|
||||
## 1. This application
|
||||
|
||||
**MarkdownConverter** — Copyright (C) 2024-2025 ConcreteInfo (Amit Haridas) —
|
||||
MIT License. See the repository `LICENSE` file.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bundled external binaries
|
||||
|
||||
These run as separate operating-system processes, launched via `execFile`
|
||||
with literal argv (never a shell, never linked into the app).
|
||||
|
||||
| Component | Version | License | Notes |
|
||||
|---|---|---|---|
|
||||
| Pandoc | 3.9.0.2 | GPL-2.0-or-later | Downloaded at build time by `scripts/download-tools.js` (SHA-256 pinned); license: [GPL-2.0](third-party-licenses/GPL-2.0.txt) |
|
||||
| FFmpeg | bundled by `ffmpeg-static` 5.3.0 | **GPL-3.0-or-later build** (`--enable-gpl --enable-libx264/x265`) | License text: <https://ffmpeg.org/legal.html>; source offer below |
|
||||
| MarkItDown | 0.1.7 (+ Python deps) | MIT | Microsoft's any-file→Markdown importer, frozen with PyInstaller by `scripts/bundle-markitdown.js`; includes an embedded CPython runtime (PSF license) |
|
||||
| sharp / libvips prebuilt binaries | 0.35.4 | Apache-2.0 / **LGPL-2.1-or-later** (libvips) | Dynamically loaded native addon; LGPL compliance: this app's full MIT source is public, enabling relinking; license: [LGPL-2.1](third-party-licenses/LGPL-2.1.txt) |
|
||||
| KaTeX (CSS + fonts) | 0.18.5 | MIT | `assets/katex/` |
|
||||
| JetBrains Mono, Fira Code fonts | — | SIL OFL 1.1 | `assets/fonts/`; license: [OFL-1.1](third-party-licenses/OFL-1.1.txt) |
|
||||
| Electron | 41.x | MIT | and its bundled Chromium (BSD-style licenses) / Node.js (MIT) / OpenSSL (Apache-2.0) — see <https://www.electronjs.org/blog/electron-licensing> |
|
||||
|
||||
### GPL source availability (GPL §3 offer)
|
||||
|
||||
Corresponding source for every GPL-licensed binary distributed with this app
|
||||
is available on written request and from these permanent locations — see
|
||||
**[SOURCES.md](SOURCES.md)** for exact versions and URLs.
|
||||
|
||||
### Python packages inside the bundled MarkItDown binary
|
||||
|
||||
The frozen MarkItDown binary embeds CPython and (each MIT/Apache-2.0/BSD-3/
|
||||
PSF/MPL-2.0 licensed unless noted): markitdown, onnxruntime (MIT), numpy
|
||||
(BSD-3), magika (Apache-2.0), beautifulsoup4 / soupsieve (MIT), requests
|
||||
(Apache-2.0) + urllib3/idna/charset-normalizer, certifi (MPL-2.0),
|
||||
markdownify, defusedxml (PSF), protobuf (BSD-3), flatbuffers (Apache-2.0),
|
||||
pdfminer.six (MIT), python-docx, python-pptx, openpyxl, extract-msg,
|
||||
markdown-it-py / mdurl, pyinstaller (GPL-2.0-with-exception — build tool
|
||||
only; its bootloader is embedded, source offer included in SOURCES.md),
|
||||
Packaging, six, click, chardet (LGPL — dynamically loadable Python module).
|
||||
|
||||
---
|
||||
|
||||
## 3. npm dependencies shipped in the app (runtime)
|
||||
|
||||
| Package | Version | License |
|
||||
|---|---|---|
|
||||
| @cantoo/pdf-lib | 2.9.1 | MIT |
|
||||
| @codemirror/* (autocomplete, commands, lang-*, language, lint, search, state, theme-one-dark, view), codemirror | 6.x | MIT |
|
||||
| @replit/codemirror-vim | 6.4.0 | MIT |
|
||||
| core-util-is | 1.0.3 | MIT |
|
||||
| docx | 9.6.1 | MIT |
|
||||
| dompurify | 3.4.14 | (MPL-2.0 OR Apache-2.0) |
|
||||
| electron-store | 10.1.0 | MIT |
|
||||
| ffmpeg-static | 5.3.0 | GPL-3.0-or-later (binary; see §2) |
|
||||
| highlight.js | 11.11.1 | BSD-3-Clause |
|
||||
| html2pdf.js | 0.14.0 | MIT |
|
||||
| jszip | 3.10.1 | (MIT OR GPL-3.0-or-later) |
|
||||
| katex | 0.18.5 | MIT |
|
||||
| marked, marked-footnote, marked-highlight | 17.x / 1.4 / 2.2 | MIT |
|
||||
| mermaid | 11.17.2 | MIT |
|
||||
| pdfjs-dist | 5.5.207 | Apache-2.0 |
|
||||
| pdfkit | 0.17.2 | MIT |
|
||||
| pizzip | 3.2.0 | (MIT OR GPL-3.0) |
|
||||
| sharp | 0.35.4 | Apache-2.0 (+ LGPL libvips binaries; see §2) |
|
||||
| simple-git | 3.36.0 | MIT |
|
||||
| tslib | 2.8.1 | 0BSD |
|
||||
|
||||
(Development-only dependencies — electron-builder, eslint, prettier, jest,
|
||||
cross-env, @testing-library/dom — are not distributed with the application.)
|
||||
|
||||
## 4. Optional external tools (NOT bundled)
|
||||
|
||||
These are detected and used when the user installs them; no copy is
|
||||
distributed with this app, so no redistribution obligations arise:
|
||||
|
||||
- **LibreOffice** (MPL-2.0) — enhanced Office-format conversion
|
||||
- **MiKTeX / TeX Live** (LPPL/GPL per component) — LaTeX PDF export
|
||||
- **ImageMagick** (Apache-2.0-style) — extra image formats in the universal converter
|
||||
- **PlantUML** (GPL-3.0) + a Java runtime — local diagram rendering
|
||||
- **Calibre** (`ebook-convert`) — MOBI export
|
||||
- **System MarkItDown with `[all]` extras** — audio transcription / OCR
|
||||
|
||||
## 5. Trademarks
|
||||
|
||||
Product names used to describe compatibility (Pandoc, FFmpeg, LibreOffice,
|
||||
MarkItDown, Microsoft, Excel, Word, PowerPoint…) are trademarks of their
|
||||
respective owners and are not affiliated with this project.
|
||||
|
||||
## 6. License texts
|
||||
|
||||
See the `third-party-licenses/` directory: `GPL-2.0.txt`, `LGPL-2.1.txt`,
|
||||
`MPL-2.0.txt`, `Apache-2.0.txt`, `OFL-1.1.txt`, `PSF-Python.txt`. MIT and
|
||||
BSD-3-Clause texts are short and reproduced in each package's own repository;
|
||||
per-package LICENSE files also ship inside `node_modules/` in source form.
|
||||
+54
@@ -1,5 +1,59 @@
|
||||
# PanConverter - Updates & Changelog
|
||||
|
||||
## Version 4.7.0 (2026-09-05)
|
||||
|
||||
### Bundling & Legal Compliance
|
||||
- **MarkItDown is now bundled**: `npm run bundle:markitdown` freezes Microsoft's
|
||||
markitdown (MIT) + embedded Python runtime into a single ~75MB per-platform
|
||||
binary (`bin/<platform>/markitdown`) via PyInstaller (ML extras excluded);
|
||||
the app prefers the bundled binary and falls back to system installs
|
||||
- Packaging copies the bundled markitdown for Windows/macOS/Linux alongside Pandoc
|
||||
- **THIRD-PARTY-NOTICES.md** — full license inventory of everything distributed
|
||||
(bundled binaries, npm runtime deps, fonts, embedded Python packages)
|
||||
- **SOURCES.md** — GPL §3(b) written source offer for Pandoc / FFmpeg (GPL build) /
|
||||
PyInstaller bootloader, with pinned versions + SHA-256; LGPL relinking note for libvips
|
||||
- **third-party-licenses/** — canonical texts: GPL-2.0, LGPL-2.1, MPL-2.0,
|
||||
Apache-2.0, OFL-1.1, PSF-Python
|
||||
- **Help → Third-Party Notices & Licenses** — in-app viewer for both documents
|
||||
- **download-tools.js** now SHA-256 pins and verifies every downloaded artifact
|
||||
(post-download and against the cache on every run; hard-fails on mismatch)
|
||||
- README gains a "Bundled Dependencies, Legal Notices & Credits" section
|
||||
- Large optional tools intentionally NOT bundled (documented): LibreOffice,
|
||||
MiKTeX/TeX Live, ImageMagick, PlantUML+JRE, Calibre
|
||||
|
||||
---
|
||||
|
||||
## Version 4.6.1 (2026-09-05)
|
||||
|
||||
### New Features
|
||||
- **MarkItDown import** — "File → Import with MarkItDown (Any Format)…" embeds
|
||||
Microsoft's [markitdown](https://github.com/microsoft/markitdown) (MIT) as an
|
||||
any-file → Markdown path: PDF, DOCX, PPTX, XLSX, Outlook .msg/.eml, EPUB,
|
||||
images, CSV/JSON/XML, ZIP; audio transcription and OCR with the `[all]` extras
|
||||
- Command auto-resolution: `markitdown` binary, then `python -m markitdown` /
|
||||
`python3 -m markitdown` (probed once, cached)
|
||||
- Same SEC-1 argv discipline as Pandoc (execFile only, paths never through a shell),
|
||||
50MB input cap, 120s timeout, path-sanitized errors that surface markitdown's
|
||||
actionable `pip install 'markitdown[...]'` hints
|
||||
- Output written next to the source as `<name>.md` (numeric suffix instead of
|
||||
overwriting) and opened in a new tab; `markitdown:available` / `markitdown:convert`
|
||||
IPC for future renderer flows
|
||||
- **AI Assistant: Anthropic-compatible provider** — any base URL speaking the
|
||||
Anthropic messages schema (LiteLLM proxies, Bedrock gateways, local servers);
|
||||
x-api-key + Bearer auth, keyless proxies supported, tolerates bases with or
|
||||
without a trailing `/v1`
|
||||
|
||||
### Bug Fixes
|
||||
- File → Open PDF crashed the PDF editor (null operation matched no section; now
|
||||
defaults to Merge)
|
||||
- Backlinks panel required the wrong module path (failed at registration)
|
||||
- writing-studio engines/panels now await their IPC-backed settings/file backends
|
||||
(eliminates `JSON.parse("[object Promise]")` crashes)
|
||||
- Manuscript panel's window.prompt (unsupported in Electron) replaced with an
|
||||
inline dialog; collaboration comment store made async to match its IO
|
||||
|
||||
---
|
||||
|
||||
## Version 4.6.0 (2026-09-05)
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* electron-builder configuration (replaces the static package.json "build"
|
||||
* section so it can be computed dynamically).
|
||||
*
|
||||
* Why a JS config: the bundled markitdown binary is OPTIONAL at build time —
|
||||
* PyInstaller can only build for the host OS, so e.g. a Windows package
|
||||
* built on a machine without bin/win32/markitdown.exe must simply omit it
|
||||
* (the app falls back to system-installed markitdown at runtime) instead of
|
||||
* failing on a missing extraFiles source.
|
||||
*
|
||||
* Everything else mirrors the previous static config verbatim.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/** Common extraFiles for a platform: pandoc always, markitdown when built. */
|
||||
function toolExtraFiles(platform) {
|
||||
const exe = platform === 'win32' ? '.exe' : '';
|
||||
const files = [{ from: `bin/${platform}/pandoc${exe}`, to: `bin/pandoc${exe}` }];
|
||||
const markitdown = `bin/${platform}/markitdown${exe}`;
|
||||
if (fs.existsSync(path.join(__dirname, markitdown))) {
|
||||
files.push({ from: markitdown, to: `bin/markitdown${exe}` });
|
||||
} else {
|
||||
console.warn(
|
||||
`[electron-builder.config] ${markitdown} not found — building WITHOUT the ` +
|
||||
'bundled markitdown (the app will use a system install if present). ' +
|
||||
'Run "npm run bundle:markitdown" on this platform to bundle it.'
|
||||
);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
appId: 'com.concreteinfo.markdownconverter',
|
||||
productName: 'MarkdownConverter',
|
||||
copyright: 'Copyright (C) 2024-2025 ConcreteInfo',
|
||||
directories: { output: 'dist' },
|
||||
icon: 'assets/icon',
|
||||
files: [
|
||||
'src/**/*',
|
||||
'assets/**/*',
|
||||
'scripts/**/*',
|
||||
'node_modules/**/*',
|
||||
'package.json',
|
||||
'THIRD-PARTY-NOTICES.md',
|
||||
'SOURCES.md',
|
||||
'third-party-licenses/**/*',
|
||||
],
|
||||
asarUnpack: [
|
||||
'node_modules/ffmpeg-static/**',
|
||||
'node_modules/sharp/**',
|
||||
'node_modules/@img/**',
|
||||
'node_modules/@napi-rs/**',
|
||||
'assets/fonts/**',
|
||||
],
|
||||
fileAssociations: [
|
||||
{
|
||||
ext: 'md',
|
||||
name: 'Markdown Document',
|
||||
description: 'Markdown Document',
|
||||
mimeType: 'text/markdown',
|
||||
role: 'Editor',
|
||||
},
|
||||
{
|
||||
ext: 'markdown',
|
||||
name: 'Markdown Document',
|
||||
description: 'Markdown Document',
|
||||
mimeType: 'text/markdown',
|
||||
role: 'Editor',
|
||||
},
|
||||
{
|
||||
ext: 'pdf',
|
||||
name: 'PDF Document',
|
||||
description: 'PDF Document',
|
||||
mimeType: 'application/pdf',
|
||||
role: 'Editor',
|
||||
},
|
||||
],
|
||||
mac: {
|
||||
category: 'public.app-category.productivity',
|
||||
identity: null,
|
||||
extraFiles: toolExtraFiles('darwin'),
|
||||
},
|
||||
win: {
|
||||
target: [
|
||||
{ target: 'nsis', arch: ['x64'] },
|
||||
{ target: 'portable', arch: ['x64'] },
|
||||
{ target: 'zip', arch: ['x64'] },
|
||||
],
|
||||
artifactName: '${productName}-${version}-${arch}.${ext}',
|
||||
requestedExecutionLevel: 'asInvoker',
|
||||
legalTrademarks: 'Copyright (C) 2024-2025 ConcreteInfo',
|
||||
verifyUpdateCodeSignature: false,
|
||||
signAndEditExecutable: false,
|
||||
extraFiles: toolExtraFiles('win32'),
|
||||
},
|
||||
nsis: {
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
allowToChangeInstallationDirectory: true,
|
||||
displayLanguageSelector: true,
|
||||
createDesktopShortcut: true,
|
||||
createStartMenuShortcut: true,
|
||||
shortcutName: 'MarkdownConverter',
|
||||
runAfterFinish: true,
|
||||
menuCategory: 'Productivity',
|
||||
license: 'LICENSE',
|
||||
warningsAsErrors: false,
|
||||
artifactName: '${productName}-Setup-${version}.${ext}',
|
||||
deleteAppDataOnUninstall: false,
|
||||
differentialPackage: true,
|
||||
},
|
||||
linux: {
|
||||
target: ['deb', 'AppImage', 'snap'],
|
||||
category: 'Utility',
|
||||
maintainer: 'ConcreteInfo <amit.wh@gmail.com>',
|
||||
extraFiles: toolExtraFiles('linux'),
|
||||
},
|
||||
deb: {
|
||||
depends: ['imagemagick', 'libreoffice-common'],
|
||||
description: 'Professional Markdown editor and universal file converter',
|
||||
maintainer: 'ConcreteInfo <amit.wh@gmail.com>',
|
||||
},
|
||||
};
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "4.6.0",
|
||||
"version": "4.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "markdown-converter",
|
||||
"version": "4.6.0",
|
||||
"version": "4.7.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.9.1",
|
||||
|
||||
+12
-134
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markdown-converter",
|
||||
"version": "4.6.0",
|
||||
"version": "4.7.0",
|
||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
@@ -12,18 +12,19 @@
|
||||
"lint:fix": "eslint src tests --fix",
|
||||
"format": "prettier --write src tests",
|
||||
"format:check": "prettier --check src tests",
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx electron-builder --win",
|
||||
"build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --win",
|
||||
"build": "electron-builder --config electron-builder.config.js",
|
||||
"build:win": "electron-builder --config electron-builder.config.js --win",
|
||||
"build:win-signed": "cross-env CSC_LINK=code-signing-cert.pfx electron-builder --config electron-builder.config.js --win",
|
||||
"build:win-unsigned": "cross-env CSC_IDENTITY_AUTO_DISCOVERY=false electron-builder --config electron-builder.config.js --win",
|
||||
"create-cert": "powershell -ExecutionPolicy Bypass -File scripts/create-selfsigned-cert.ps1",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:linux-ci": "electron-builder --linux deb AppImage",
|
||||
"build:local": "electron-builder --linux --win",
|
||||
"dist": "electron-builder --publish=never",
|
||||
"dist:all": "electron-builder -mwl",
|
||||
"build:mac": "electron-builder --config electron-builder.config.js --mac",
|
||||
"build:linux": "electron-builder --config electron-builder.config.js --linux",
|
||||
"build:linux-ci": "electron-builder --config electron-builder.config.js --linux deb AppImage",
|
||||
"build:local": "electron-builder --config electron-builder.config.js --linux --win",
|
||||
"dist": "electron-builder --config electron-builder.config.js --publish=never",
|
||||
"dist:all": "electron-builder --config electron-builder.config.js -mwl",
|
||||
"download-tools": "node scripts/download-tools.js",
|
||||
"bundle:markitdown": "node scripts/bundle-markitdown.js",
|
||||
"generate-icons": "node scripts/generate-icons.js"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -98,128 +99,5 @@
|
||||
"lodash.pick": "npm:lodash@^4.17.21",
|
||||
"lodash-es": "^4.18.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.concreteinfo.markdownconverter",
|
||||
"productName": "MarkdownConverter",
|
||||
"copyright": "Copyright (C) 2024-2025 ConcreteInfo",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"icon": "assets/icon",
|
||||
"files": [
|
||||
"src/**/*",
|
||||
"assets/**/*",
|
||||
"scripts/**/*",
|
||||
"node_modules/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"asarUnpack": [
|
||||
"node_modules/ffmpeg-static/**",
|
||||
"node_modules/sharp/**",
|
||||
"node_modules/@img/**",
|
||||
"node_modules/@napi-rs/**",
|
||||
"assets/fonts/**"
|
||||
],
|
||||
"extraFiles": [],
|
||||
"fileAssociations": [
|
||||
{
|
||||
"ext": "md",
|
||||
"name": "Markdown Document",
|
||||
"description": "Markdown Document",
|
||||
"mimeType": "text/markdown",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "markdown",
|
||||
"name": "Markdown Document",
|
||||
"description": "Markdown Document",
|
||||
"mimeType": "text/markdown",
|
||||
"role": "Editor"
|
||||
},
|
||||
{
|
||||
"ext": "pdf",
|
||||
"name": "PDF Document",
|
||||
"description": "PDF Document",
|
||||
"mimeType": "application/pdf",
|
||||
"role": "Editor"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.productivity",
|
||||
"identity": null
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "portable",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
},
|
||||
{
|
||||
"target": "zip",
|
||||
"arch": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
],
|
||||
"artifactName": "${productName}-${version}-${arch}.${ext}",
|
||||
"requestedExecutionLevel": "asInvoker",
|
||||
"legalTrademarks": "Copyright (C) 2024-2025 ConcreteInfo",
|
||||
"verifyUpdateCodeSignature": false,
|
||||
"signAndEditExecutable": false,
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/win32/pandoc.exe",
|
||||
"to": "bin/pandoc.exe"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"displayLanguageSelector": true,
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "MarkdownConverter",
|
||||
"runAfterFinish": true,
|
||||
"menuCategory": "Productivity",
|
||||
"license": "LICENSE",
|
||||
"warningsAsErrors": false,
|
||||
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||
"deleteAppDataOnUninstall": false,
|
||||
"differentialPackage": true
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"deb",
|
||||
"AppImage",
|
||||
"snap"
|
||||
],
|
||||
"category": "Utility",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "bin/linux/pandoc",
|
||||
"to": "bin/pandoc"
|
||||
}
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
"depends": [
|
||||
"imagemagick",
|
||||
"libreoffice-common"
|
||||
],
|
||||
"description": "Professional Markdown editor and universal file converter",
|
||||
"maintainer": "ConcreteInfo <amit.wh@gmail.com>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Builds a self-contained MarkItDown binary into bin/<platform>/markitdown
|
||||
* so the app can bundle Microsoft's markitdown (MIT) without requiring users
|
||||
* to have Python installed. Run via `npm run bundle:markitdown` before
|
||||
* packaging (skipped automatically when the binary already exists).
|
||||
*
|
||||
* How: creates a throwaway virtualenv, pip-installs markitdown (with the
|
||||
* document extras) + PyInstaller, then freezes the CLI into a onefile
|
||||
* executable. Heavy optional ML deps (torch/scipy/pandas/…) are excluded —
|
||||
* the resulting ~75MB binary covers PDF/DOCX/PPTX/XLSX/Outlook/HTML/EPUB/
|
||||
* images/CSV/JSON/XML/ZIP. Audio transcription and OCR intentionally stay
|
||||
* unbundled (multi-GB); the app falls back to a system `markitdown[all]`
|
||||
* install for those.
|
||||
*
|
||||
* Requires on the build machine: python3 (with venv + pip) — on Debian/
|
||||
* Ubuntu that is `sudo apt install python3 python3-venv`.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
const platform = process.platform; // linux | win32 | darwin
|
||||
const exeName = platform === 'win32' ? 'markitdown.exe' : 'markitdown';
|
||||
const binDir = path.join(__dirname, '..', 'bin', platform);
|
||||
const destFile = path.join(binDir, exeName);
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mid-bundle-'));
|
||||
|
||||
/** Run a command, inheriting stdout/stderr so build progress is visible. */
|
||||
function run(cmd, args, opts = {}) {
|
||||
execFileSync(cmd, args, { stdio: 'inherit', ...opts });
|
||||
}
|
||||
|
||||
function pythonBin(venvDir, name) {
|
||||
return path.join(venvDir, platform === 'win32' ? 'Scripts' : 'bin', name);
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (fs.existsSync(destFile)) {
|
||||
console.log(`[bundle-markitdown] ${destFile} already present — skipping.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const venvDir = path.join(workDir, 'venv');
|
||||
console.log('[bundle-markitdown] Creating virtualenv...');
|
||||
run('python3', ['-m', 'venv', venvDir]);
|
||||
|
||||
console.log('[bundle-markitdown] Installing markitdown + PyInstaller...');
|
||||
run(pythonBin(venvDir, 'pip'), [
|
||||
'install', '--quiet', '--upgrade', 'pip',
|
||||
]);
|
||||
run(pythonBin(venvDir, 'pip'), [
|
||||
'install', '--quiet',
|
||||
// Document extras only — [all] would pull the ML stack (torch etc.)
|
||||
'markitdown[pdf,docx,pptx,xlsx,outlook]',
|
||||
'pyinstaller',
|
||||
]);
|
||||
|
||||
// PyInstaller entrypoint mirroring the markitdown CLI
|
||||
const entry = path.join(workDir, 'entry.py');
|
||||
fs.writeFileSync(
|
||||
entry,
|
||||
[
|
||||
'# Generated by scripts/bundle-markitdown.js — frozen CLI entrypoint',
|
||||
'from markitdown.__main__ import main',
|
||||
"if __name__ == '__main__':",
|
||||
' main()',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
console.log('[bundle-markitdown] Freezing with PyInstaller (this takes a minute)...');
|
||||
run(pythonBin(venvDir, platform === 'win32' ? 'pyinstaller.exe' : 'pyinstaller'), [
|
||||
'--onefile',
|
||||
'--name', 'markitdown',
|
||||
'--strip',
|
||||
'--clean',
|
||||
// magika ships its ML model as data files — must be collected into the bundle
|
||||
'--collect-data', 'magika',
|
||||
// Heavy optional scientific/ML stacks markitdown never imports for
|
||||
// document conversion — excluding them keeps the binary ~75MB
|
||||
'--exclude-module', 'torch',
|
||||
'--exclude-module', 'scipy',
|
||||
'--exclude-module', 'pandas',
|
||||
'--exclude-module', 'matplotlib',
|
||||
'--exclude-module', 'tkinter',
|
||||
'--exclude-module', 'IPython',
|
||||
'--exclude-module', 'pytest',
|
||||
'--distpath', path.join(workDir, 'dist'),
|
||||
'--workpath', path.join(workDir, 'build'),
|
||||
'--specpath', workDir,
|
||||
entry,
|
||||
]);
|
||||
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
fs.copyFileSync(path.join(workDir, 'dist', exeName), destFile);
|
||||
if (platform !== 'win32') fs.chmodSync(destFile, 0o755);
|
||||
|
||||
// Smoke test before declaring success
|
||||
const version = execFileSync(destFile, ['--version'], { encoding: 'utf-8' }).trim();
|
||||
if (!version.toLowerCase().includes('markitdown')) {
|
||||
throw new Error(`Unexpected --version output: ${version}`);
|
||||
}
|
||||
console.log(`[bundle-markitdown] Built ${destFile} (${version})`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error('[bundle-markitdown] FAILED:', err.message);
|
||||
console.error(
|
||||
'\nPrerequisites: python3 with venv+pip on the PATH.\n' +
|
||||
' Debian/Ubuntu: sudo apt install python3 python3-venv\n' +
|
||||
' macOS: brew install python\n' +
|
||||
'The app still works without the bundled binary — it falls back to a\n' +
|
||||
'system-installed markitdown or python -m markitdown at runtime.'
|
||||
);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -3,6 +3,11 @@
|
||||
* Downloads pandoc binary for the current build platform.
|
||||
* Run automatically via `npm run download-tools` before building.
|
||||
* Skips download if binary already exists (idempotent).
|
||||
*
|
||||
* Security: every artifact with a known hash is verified with SHA-256 after
|
||||
* download AND on every run (defending the build against a tampered cache /
|
||||
* compromised mirror). When a hash is missing for a platform, the computed
|
||||
* hash is printed so CI can pin it — add it to KNOWN_SHA256 below.
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
@@ -10,10 +15,57 @@ const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const PANDOC_VERSION = '3.9.0.2';
|
||||
|
||||
/**
|
||||
* SHA-256 of each downloaded artifact, keyed "<platform>:<file>" or
|
||||
* "fonts:<file>". Computed with `sha256sum` from the pristine upstream
|
||||
* artifacts; keep in sync when bumping versions.
|
||||
*/
|
||||
const KNOWN_SHA256 = {
|
||||
'linux:pandoc': '7d124235998ecd3cdd9a463b1e5f6691a178b6461824c29a36170a0882f05597',
|
||||
'win32:pandoc.exe': 'e83f8354c0f507222b5684797b9c5ae766f03889785995d14aac27816ec456ba',
|
||||
// Fill these from a trusted machine after the first download of each
|
||||
// platform (the script prints the computed hash):
|
||||
// 'darwin:pandoc': '…',
|
||||
'fonts:FiraCode-Regular.ttf': '3c79d234a9161c790410ebb2a80de7efb7c15f581062c130e0fa78503ccdd0da',
|
||||
'fonts:FiraCode-Bold.ttf': '975f26779fac1029c2cbdac1e9fac7e9ddeec05e064675e4aac63bffa121742f',
|
||||
'fonts:FiraCode-LICENSE.txt': '1d41e10031ab125302780a05ec4c91d218e47db0c7e37cf315cce5e608cdc25c',
|
||||
};
|
||||
|
||||
/** sha256 of a file's contents, hex-encoded. */
|
||||
function sha256File(filePath) {
|
||||
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a file against KNOWN_SHA256. Throws on mismatch; prints the computed
|
||||
* hash with a pin-me hint when no hash is recorded yet (soft-fail so new
|
||||
* platforms bootstrap, hard-fail so tampering never passes silently).
|
||||
*/
|
||||
function verifyArtifact(key, filePath) {
|
||||
const actual = sha256File(filePath);
|
||||
const expected = KNOWN_SHA256[key];
|
||||
if (!expected) {
|
||||
console.warn(
|
||||
`[download-tools] WARNING: no pinned SHA-256 for "${key}".\n` +
|
||||
` Computed: ${actual}\n` +
|
||||
' Verify it against the upstream artifact and add it to KNOWN_SHA256.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`SHA-256 mismatch for ${key}!\n expected ${expected}\n actual ${actual}\n` +
|
||||
'Delete the file and re-run; if the mismatch persists, do NOT ship it.'
|
||||
);
|
||||
}
|
||||
console.log(`[download-tools] SHA-256 OK for ${key}`);
|
||||
}
|
||||
|
||||
const PANDOC_CONFIG = {
|
||||
linux: {
|
||||
url: `https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-linux-amd64.tar.gz`,
|
||||
@@ -119,11 +171,14 @@ async function downloadFiraCode() {
|
||||
for (const t of targets) {
|
||||
const destFile = path.join(destDir, t.out);
|
||||
if (fs.existsSync(destFile)) {
|
||||
// Re-verify cached artifacts so a tampered cache never ships
|
||||
verifyArtifact(`fonts:${t.out}`, destFile);
|
||||
console.log(`[download-tools] ${t.out} already present — skipping.`);
|
||||
continue;
|
||||
}
|
||||
console.log(`[download-tools] Downloading ${t.out}...`);
|
||||
await download(t.url, destFile);
|
||||
verifyArtifact(`fonts:${t.out}`, destFile);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +195,7 @@ async function downloadPandoc() {
|
||||
const destFile = path.join(destDir, config.destFile);
|
||||
|
||||
if (fs.existsSync(destFile)) {
|
||||
verifyArtifact(`${platform}:${config.destFile}`, destFile);
|
||||
console.log(`[download-tools] pandoc already present at ${destFile} — skipping.`);
|
||||
return;
|
||||
}
|
||||
@@ -153,6 +209,7 @@ async function downloadPandoc() {
|
||||
|
||||
console.log(`[download-tools] Extracting to ${destDir}...`);
|
||||
config.extract(tmpArchive, destDir);
|
||||
verifyArtifact(`${platform}:${config.destFile}`, destFile);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(tmpArchive);
|
||||
|
||||
+188
-1
@@ -27,11 +27,27 @@ if (process.platform === 'win32') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory of the bundled external tools (pandoc/markitdown) in a PACKAGED
|
||||
* app. electron-builder extraFiles land next to the executable on Linux and
|
||||
* Windows, and inside Contents/ on macOS — NOT in resources/bin (which is
|
||||
* where the lookup incorrectly pointed before v4.7, making packaged builds
|
||||
* silently fall back to system-installed tools).
|
||||
*/
|
||||
function bundledToolDir() {
|
||||
if (process.platform === 'darwin') {
|
||||
// resourcesPath = …/Contents/Resources → tools live in …/Contents/bin
|
||||
return path.join(path.dirname(process.resourcesPath), 'bin');
|
||||
}
|
||||
// exePath = …/MarkdownConverter(.exe) → tools live in …/bin
|
||||
return path.join(path.dirname(process.execPath), 'bin');
|
||||
}
|
||||
|
||||
// Returns path to pandoc: bundled binary when packaged, dev bin or system fallback otherwise.
|
||||
function getPandocPath() {
|
||||
if (app.isPackaged) {
|
||||
const ext = process.platform === 'win32' ? '.exe' : '';
|
||||
return path.join(process.resourcesPath, 'bin', `pandoc${ext}`);
|
||||
return path.join(bundledToolDir(), `pandoc${ext}`);
|
||||
}
|
||||
// Development: prefer locally-downloaded binary in bin/<platform>/
|
||||
const devBin = path.join(
|
||||
@@ -840,6 +856,12 @@ function createMenu() {
|
||||
accelerator: 'CmdOrCtrl+I',
|
||||
click: importDocument,
|
||||
},
|
||||
{
|
||||
// Microsoft MarkItDown: any file → Markdown (PDF/DOCX/PPTX/XLSX/
|
||||
// MSG/EPUB/images/ZIP/…, audio/OCR with the [all] extras)
|
||||
label: 'Import with MarkItDown (Any Format)...',
|
||||
click: importWithMarkItDown,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
submenu: [
|
||||
@@ -1555,6 +1577,12 @@ function createMenu() {
|
||||
label: 'Dependencies & Requirements',
|
||||
click: () => showDependenciesDialog(),
|
||||
},
|
||||
{
|
||||
// Legal: bundled-component licenses + GPL source offer (in-app copy
|
||||
// of THIRD-PARTY-NOTICES.md and SOURCES.md)
|
||||
label: 'Third-Party Notices & Licenses',
|
||||
click: () => showThirdPartyNoticesWindow(),
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
@@ -1679,6 +1707,68 @@ function showAboutDialog() {
|
||||
}
|
||||
|
||||
// Show Dependencies Dialog
|
||||
/**
|
||||
* In-app legal window: renders THIRD-PARTY-NOTICES.md and SOURCES.md (both
|
||||
* ship inside the app — dev: repo root, packaged: asar root via build.files).
|
||||
* The markdown is shown verbatim in a <pre> with light styling rather than
|
||||
* rendered, so license texts stay exactly as written.
|
||||
*/
|
||||
function showThirdPartyNoticesWindow() {
|
||||
const noticesWindow = new BrowserWindow({
|
||||
width: 820,
|
||||
height: 640,
|
||||
parent: mainWindow,
|
||||
title: 'Third-Party Notices & Licenses',
|
||||
icon: path.join(__dirname, '../assets/icon.png'),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
noticesWindow.setMenuBarVisibility(false);
|
||||
|
||||
// Both files are read defensively so a packaging slip degrades gracefully
|
||||
const readDoc = (file) => {
|
||||
try {
|
||||
return fs.readFileSync(path.join(__dirname, '..', file), 'utf-8');
|
||||
} catch {
|
||||
return `(Could not read ${file} in this installation — see the source repository.)`;
|
||||
}
|
||||
};
|
||||
const esc = (s) =>
|
||||
s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
const html =
|
||||
'<!doctype html><html><head><meta charset="utf-8"><title>Third-Party Notices</title><style>' +
|
||||
'body{margin:0;font:13px/1.6 system-ui,sans-serif;background:#fafafa;color:#222}' +
|
||||
'header{position:sticky;top:0;background:#fff;border-bottom:1px solid #e5e7eb;padding:10px 20px;display:flex;gap:16px;align-items:center;z-index:1}' +
|
||||
'header h1{font-size:15px;margin:0;flex:1}' +
|
||||
'header button{padding:6px 14px;cursor:pointer;border:1px solid #d1d5db;border-radius:4px;background:#fff}' +
|
||||
'header button.active{background:#4a90d9;color:#fff;border-color:#4a90d9}' +
|
||||
'pre{white-space:pre-wrap;word-break:break-word;padding:20px 24px;margin:0;font:12px/1.65 ui-monospace,Menlo,Consolas,monospace}' +
|
||||
'body.dark pre{background:#1f2937;color:#e5e7eb}body.dark{background:#111}' +
|
||||
'</style></head><body>' +
|
||||
'<header><h1>MarkdownConverter — Third-Party Notices</h1>' +
|
||||
'<button id="tab-notices" class="active">Notices</button>' +
|
||||
'<button id="tab-sources">Source Offers</button></header>' +
|
||||
`<pre id="content"></pre>` +
|
||||
'<script>' +
|
||||
'const notices=' +
|
||||
JSON.stringify(esc(readDoc('THIRD-PARTY-NOTICES.md'))) +
|
||||
';' +
|
||||
'const sources=' +
|
||||
JSON.stringify(esc(readDoc('SOURCES.md'))) +
|
||||
';' +
|
||||
'const c=document.getElementById("content");' +
|
||||
'c.textContent=notices;' +
|
||||
'document.getElementById("tab-notices").onclick=e=>{c.textContent=notices;swap(e)};' +
|
||||
'document.getElementById("tab-sources").onclick=e=>{c.textContent=sources;swap(e)};' +
|
||||
'function swap(e){document.querySelectorAll("header button").forEach(b=>b.classList.remove("active"));e.target.classList.add("active")}' +
|
||||
'</script></body></html>';
|
||||
|
||||
noticesWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
|
||||
}
|
||||
|
||||
function showDependenciesDialog() {
|
||||
const depsWindow = new BrowserWindow({
|
||||
width: 600,
|
||||
@@ -1763,6 +1853,12 @@ function showDependenciesDialog() {
|
||||
<a class="dep-link" href="https://miktex.org/download" target="_blank">https://miktex.org/download</a>
|
||||
</div>
|
||||
|
||||
<div class="dep-card optional">
|
||||
<div class="dep-name">MarkItDown <span class="tag tag-optional">Optional</span></div>
|
||||
<div class="dep-desc">Microsoft's any-file-to-Markdown importer (PDF, DOCX, PPTX, XLSX, Outlook .msg, EPUB, images, ZIP). Install with: pip install "markitdown[all]"</div>
|
||||
<a class="dep-link" href="https://github.com/microsoft/markitdown" target="_blank">https://github.com/microsoft/markitdown</a>
|
||||
</div>
|
||||
|
||||
<h2>Bundled Libraries</h2>
|
||||
|
||||
<div class="dep-card">
|
||||
@@ -3717,6 +3813,97 @@ function importDocument() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MarkItDown import (Microsoft markitdown, any file → Markdown)
|
||||
// ============================================
|
||||
const MarkItDown = require('./main/MarkItDown');
|
||||
|
||||
// Probed once on first use; {command, argsPrefix, version} or null
|
||||
let markItDownResolved = undefined; // undefined = not probed yet
|
||||
|
||||
/** Resolve (and cache) the markitdown command via the bridge module. */
|
||||
async function getMarkItDown() {
|
||||
if (markItDownResolved === undefined) {
|
||||
markItDownResolved = await MarkItDown.resolveMarkItDown(require('child_process').execFile);
|
||||
}
|
||||
return markItDownResolved;
|
||||
}
|
||||
|
||||
/** IPC: availability probe for renderer hints (no version spam, cached). */
|
||||
ipcMain.handle('markitdown:available', async () => {
|
||||
const resolved = await getMarkItDown();
|
||||
return {
|
||||
available: Boolean(resolved),
|
||||
version: resolved?.version || null,
|
||||
// How the tool was found, e.g. "markitdown" or "python3 -m markitdown"
|
||||
via: resolved ? [resolved.command, ...resolved.argsPrefix].join(' ') : null,
|
||||
};
|
||||
});
|
||||
|
||||
/** IPC: convert one file to markdown (renderer-driven flows). */
|
||||
ipcMain.handle('markitdown:convert', async (_event, { path: inputPath } = {}) => {
|
||||
const validation = validatePath(inputPath);
|
||||
if (!validation.valid) throw new Error('Invalid file path');
|
||||
const stats = fs.statSync(validation.resolved);
|
||||
if (stats.size > MAX_FILE_SIZE) {
|
||||
throw new Error(`File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`);
|
||||
}
|
||||
const resolved = await getMarkItDown();
|
||||
return MarkItDown.convertToMarkdown(validation.resolved, { resolved });
|
||||
});
|
||||
|
||||
/**
|
||||
* Menu flow: pick any file, convert with markitdown, write <name>.md next to
|
||||
* the source (mirroring importDocument's UX), and open it in a new tab.
|
||||
* Existing outputs are never overwritten — a numeric suffix is appended.
|
||||
*/
|
||||
async function importWithMarkItDown() {
|
||||
const files = dialog.showOpenDialogSync(mainWindow, {
|
||||
properties: ['openFile'],
|
||||
title: 'Import with MarkItDown (any format)',
|
||||
});
|
||||
if (!files || !files[0]) return;
|
||||
const inputFile = files[0];
|
||||
|
||||
try {
|
||||
const stats = fs.statSync(inputFile);
|
||||
if (stats.size > MAX_FILE_SIZE) {
|
||||
dialog.showErrorBox('File Too Large', `File exceeds the ${MAX_FILE_SIZE_MB}MB size limit.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = await getMarkItDown();
|
||||
const { content } = await MarkItDown.convertToMarkdown(inputFile, { resolved });
|
||||
|
||||
// <name>.md, then <name>-1.md, <name>-2.md, … when it already exists
|
||||
const base = inputFile.replace(/\.[^/.]+$/, '');
|
||||
let outputFile = `${base}.md`;
|
||||
for (let i = 1; fs.existsSync(outputFile); i++) outputFile = `${base}-${i}.md`;
|
||||
fs.writeFileSync(outputFile, content, 'utf-8');
|
||||
|
||||
currentFile = outputFile;
|
||||
mainWindow.webContents.send('file-opened', { path: outputFile, content });
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: 'info',
|
||||
title: 'Import Complete',
|
||||
message: `Imported as ${path.basename(outputFile)} via MarkItDown\n\nOriginal: ${path.basename(inputFile)}`,
|
||||
buttons: ['OK'],
|
||||
});
|
||||
} catch (error) {
|
||||
dialog.showErrorBox(
|
||||
'MarkItDown Import',
|
||||
sanitizeErrorMessage(
|
||||
(error.code === 'not_installed'
|
||||
? error.message
|
||||
: `Import failed: ${error.message}`) +
|
||||
'\n\nMarkItDown is an optional Python tool from Microsoft (MIT):\n' +
|
||||
' pip install "markitdown[all]"'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
store.set('theme', theme);
|
||||
mainWindow.webContents.send('theme-changed', theme);
|
||||
|
||||
+31
-7
@@ -12,6 +12,10 @@
|
||||
* - `ollama` → http://localhost:11434/v1 (OpenAI-compatible)
|
||||
* - `lmstudio` → http://localhost:1234/v1 (OpenAI-compatible)
|
||||
* - `openai-compatible`→ any baseUrl speaking the OpenAI chat schema
|
||||
* - `anthropic-compatible` → any baseUrl speaking the Anthropic messages
|
||||
* schema (LiteLLM proxies, Bedrock gateways, local Claude-compatible
|
||||
* servers); baseUrl is required, the API key is optional because many
|
||||
* proxies are keyless or front their own auth
|
||||
*
|
||||
* The module takes an injectable `fetchImpl` (defaulting to global fetch) so
|
||||
* tests can stub the network without monkey-patching.
|
||||
@@ -33,6 +37,7 @@ const PROVIDER_DEFAULTS = {
|
||||
ollama: { baseUrl: 'http://localhost:11434/v1', defaultModel: 'llama3.1' },
|
||||
lmstudio: { baseUrl: 'http://localhost:1234/v1', defaultModel: 'local-model' },
|
||||
'openai-compatible': { baseUrl: '', defaultModel: '' },
|
||||
'anthropic-compatible': { baseUrl: '', defaultModel: 'claude-3-5-sonnet-latest' },
|
||||
};
|
||||
|
||||
/** Provider ids that speak the OpenAI chat-completions schema. */
|
||||
@@ -72,8 +77,13 @@ function resolveSettings({
|
||||
);
|
||||
}
|
||||
|
||||
// Local providers (ollama/lmstudio) don't need a key; remote ones do.
|
||||
const needsKey = requireKey && provider !== 'ollama' && provider !== 'lmstudio';
|
||||
// Local providers (ollama/lmstudio) and self-hosted *-compatible proxies
|
||||
// don't need a key; branded cloud endpoints do.
|
||||
const needsKey =
|
||||
requireKey &&
|
||||
provider !== 'ollama' &&
|
||||
provider !== 'lmstudio' &&
|
||||
provider !== 'anthropic-compatible';
|
||||
if (needsKey && !apiKey) {
|
||||
throw new AiProviderError(
|
||||
`The "${provider}" provider needs an API key. Add one in AI Assistant settings.`,
|
||||
@@ -158,13 +168,27 @@ async function callAnthropic(settings, { system, messages }, fetchImpl, timeoutM
|
||||
system: system || undefined,
|
||||
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
||||
};
|
||||
const response = await fetchImpl(`${settings.baseUrl}/v1/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'x-api-key': settings.apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
},
|
||||
};
|
||||
if (settings.apiKey) {
|
||||
// Official API keys travel via x-api-key; many compatible gateways
|
||||
// (LiteLLM proxies, Bedrock fronts) expect a Bearer token instead.
|
||||
// Sending both is harmless for the official endpoint and maximizes
|
||||
// proxy compatibility.
|
||||
headers['x-api-key'] = settings.apiKey;
|
||||
headers.Authorization = `Bearer ${settings.apiKey}`;
|
||||
}
|
||||
// Bases may or may not already carry the /v1 prefix — handle both so a
|
||||
// "http://host:4000" proxy base and an "http://host:4000/v1" style base
|
||||
// both land on a single /v1/messages path.
|
||||
const messagesUrl = settings.baseUrl.endsWith('/v1')
|
||||
? `${settings.baseUrl}/messages`
|
||||
: `${settings.baseUrl}/v1/messages`;
|
||||
const response = await fetchImpl(messagesUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
/**
|
||||
* MarkItDown bridge — "any file → Markdown" import via Microsoft's
|
||||
* markitdown Python tool (https://github.com/microsoft/markitdown, MIT).
|
||||
*
|
||||
* markitdown is a Python CLI, so like Pandoc/LibreOffice/FFmpeg it is used
|
||||
* when installed rather than bundled: the module probes for it once
|
||||
* (`markitdown` binary, then `python -m markitdown` / `python3 -m markitdown`)
|
||||
* and caches the resolved command. Every invocation goes through an argv
|
||||
* array via execFile — user-controlled paths are never passed through a
|
||||
* shell (same SEC-1 discipline as the Pandoc path).
|
||||
*
|
||||
* Supported by markitdown (core install): PDF, DOCX, PPTX, XLSX, Outlook
|
||||
* .msg/.eml, HTML, EPUB, images (EXIF), CSV/JSON/XML, ZIP archives, YouTube
|
||||
* URLs. Audio transcription and image OCR need the `[all]` extras:
|
||||
* pip install 'markitdown[all]'
|
||||
*
|
||||
* The execFile runner is injectable so tests can stub process spawning.
|
||||
*
|
||||
* @module MarkItDown
|
||||
*/
|
||||
|
||||
const CONVERT_TIMEOUT_MS = 120000;
|
||||
const MAX_OUTPUT_BUFFER = 20 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Path to the bundled markitdown binary (built by scripts/bundle-markitdown.js
|
||||
* via PyInstaller), when the app ships one. Layout matches getPandocPath:
|
||||
* dev: bin/<platform>/markitdown · packaged: <app dir>/bin/markitdown (next
|
||||
* to the executable on Linux/Windows, Contents/bin on macOS — the same
|
||||
* extraFiles destination). Returns null when absent (PATH/python fallbacks).
|
||||
*/
|
||||
function getBundledMarkItDownPath() {
|
||||
const pathUtil = require('path');
|
||||
const fs = require('fs');
|
||||
const exe = process.platform === 'win32' ? 'markitdown.exe' : 'markitdown';
|
||||
try {
|
||||
// Only meaningful under Electron; tests import this module under Node
|
||||
const electron = require('electron');
|
||||
if (electron.app?.isPackaged) {
|
||||
const base =
|
||||
process.platform === 'darwin'
|
||||
? pathUtil.dirname(electron.app.getPath('exe')) // …/Contents/MacOS
|
||||
: pathUtil.dirname(process.execPath);
|
||||
const packaged =
|
||||
process.platform === 'darwin'
|
||||
? pathUtil.join(base, '..', 'bin', exe) // …/Contents/bin
|
||||
: pathUtil.join(base, 'bin', exe);
|
||||
if (fs.existsSync(packaged)) return packaged;
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
/* not running under Electron (tests) — use the dev layout below */
|
||||
}
|
||||
const dev = pathUtil.join(__dirname, '..', '..', 'bin', process.platform, exe);
|
||||
return fs.existsSync(dev) ? dev : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidate command templates probed in order (bundled binary first). The
|
||||
* bundled candidate is verified with the same --version probe as the rest.
|
||||
*/
|
||||
function commandCandidates() {
|
||||
const bundled = getBundledMarkItDownPath();
|
||||
const candidates = [];
|
||||
if (bundled) candidates.push({ command: bundled, argsPrefix: [], bundled: true });
|
||||
candidates.push({ command: 'markitdown', argsPrefix: [] });
|
||||
candidates.push({
|
||||
command: process.platform === 'win32' ? 'python' : 'python3',
|
||||
argsPrefix: ['-m', 'markitdown'],
|
||||
});
|
||||
candidates.push({ command: 'python3', argsPrefix: ['-m', 'markitdown'] });
|
||||
return candidates;
|
||||
}
|
||||
|
||||
/** Run one probe: `--version` exits 0 when the tool is importable. */
|
||||
function probeCandidate(runner, candidate) {
|
||||
return new Promise((resolve) => {
|
||||
runner(
|
||||
candidate.command,
|
||||
[...candidate.argsPrefix, '--version'],
|
||||
{ timeout: 15000 },
|
||||
(error, stdout) => {
|
||||
if (error) return resolve(null);
|
||||
const match = /(\d+\.\d+(?:\.\d+)?)/.exec(String(stdout || ''));
|
||||
resolve({
|
||||
command: candidate.command,
|
||||
argsPrefix: candidate.argsPrefix,
|
||||
version: match ? match[1] : null,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the markitdown invocation. Probes candidates in order and returns
|
||||
* the first that answers `--version`, or null when none is installed.
|
||||
*
|
||||
* @param {Function} runner execFile-style (cmd, args, opts, cb)
|
||||
* @returns {Promise<{command: string, argsPrefix: string[], version: string|null}|null>}
|
||||
*/
|
||||
async function resolveMarkItDown(runner) {
|
||||
for (const candidate of commandCandidates()) {
|
||||
const resolved = await probeCandidate(runner, candidate);
|
||||
if (resolved) return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert any supported file to Markdown.
|
||||
*
|
||||
* @param {string} inputPath Absolute path to the source file
|
||||
* @param {object} [options]
|
||||
* @param {Function} [options.runner] injectable execFile (defaults to child_process)
|
||||
* @param {Function} [options.pathUtil] injected path module
|
||||
* @param {{command: string, argsPrefix: string[]}} [options.resolved] skip
|
||||
* probing when the caller already knows the command (main caches it)
|
||||
* @returns {Promise<{content: string}>} Markdown printed by markitdown on stdout
|
||||
* @throws {Error} with a user-safe message when the tool is missing or fails
|
||||
*/
|
||||
async function convertToMarkdown(inputPath, options = {}) {
|
||||
const runner = options.runner || require('child_process').execFile;
|
||||
const pathUtil = options.pathUtil || require('path');
|
||||
|
||||
// Validate the path BEFORE probing for the tool: a bad path is the user's
|
||||
// most actionable error and shouldn't be masked by an install hint
|
||||
if (typeof inputPath !== 'string' || !pathUtil.isAbsolute(inputPath)) {
|
||||
const err = new Error('MarkItDown import needs an absolute file path.');
|
||||
err.code = 'bad_path';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const resolved = options.resolved || (await resolveMarkItDown(runner));
|
||||
if (!resolved) {
|
||||
const err = new Error(
|
||||
'MarkItDown is not installed. Install it with:\n\npip install "markitdown[all]"\n\n' +
|
||||
'(or the lighter core: pip install markitdown)'
|
||||
);
|
||||
err.code = 'not_installed';
|
||||
throw err;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// markitdown prints the converted Markdown to stdout; keep everything in
|
||||
// argv so the path is never re-interpreted by a shell.
|
||||
runner(
|
||||
resolved.command,
|
||||
[...resolved.argsPrefix, inputPath],
|
||||
{ timeout: CONVERT_TIMEOUT_MS, maxBuffer: MAX_OUTPUT_BUFFER },
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
// Strip absolute paths, then keep the first informative line from
|
||||
// either the error or stderr — markitdown writes tracebacks there
|
||||
// (e.g. a missing optional dependency like pdfminer.six for PDFs).
|
||||
const safe = (text) =>
|
||||
String(text || '')
|
||||
.replace(/[A-Z]:\\[^\s"']+/gi, '(path)')
|
||||
.replace(/\/(?:home|Users|tmp|mnt)\/[^\s"']+/g, '(path)');
|
||||
// Prefer the LAST informative stderr line: markitdown ends its
|
||||
// output with the actionable install hint ("pip install
|
||||
// 'markitdown[pdf]'"), while the first lines are a traceback.
|
||||
const stderrLines = safe(stderr).split('\n').filter((l) => l.trim().length > 0);
|
||||
const detail =
|
||||
stderrLines[stderrLines.length - 1] ||
|
||||
safe(error.message).split('\n')[0] ||
|
||||
'unknown error';
|
||||
const err2 = new Error(`MarkItDown conversion failed: ${detail}`);
|
||||
err2.code = String(error.code || '').startsWith('ETIMEDOUT') ? 'timeout' : 'failed';
|
||||
err2.stderr = safe(stderr).slice(0, 2000);
|
||||
reject(err2);
|
||||
return;
|
||||
}
|
||||
const content = String(stdout || '');
|
||||
if (!content.trim()) {
|
||||
const err3 = new Error(
|
||||
'MarkItDown produced no output — the file type may be unsupported ' +
|
||||
'(core install covers PDF/DOCX/PPTX/XLSX/MSG/HTML/EPUB/images/CSV/JSON/XML/ZIP; ' +
|
||||
"audio/OCR need pip install 'markitdown[all]')."
|
||||
);
|
||||
err3.code = 'empty_output';
|
||||
reject(err3);
|
||||
return;
|
||||
}
|
||||
// Strip a UTF-8 BOM if present so downstream markdown tooling is happy
|
||||
resolve({ content: content.replace(/^\uFEFF/, '') });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
resolveMarkItDown,
|
||||
convertToMarkdown,
|
||||
commandCandidates,
|
||||
getBundledMarkItDownPath,
|
||||
};
|
||||
@@ -3,7 +3,10 @@
|
||||
*
|
||||
* Brings LLM assistance into the editor with multi-provider support:
|
||||
* - OpenAI / Anthropic (cloud, API key required)
|
||||
* - Ollama / LM Studio / any OpenAI-compatible endpoint (local, no key)
|
||||
* - Ollama / LM Studio (local, no key)
|
||||
* - Any OpenAI-compatible or Anthropic-compatible endpoint (custom base
|
||||
* URL; Anthropic-compatible gateways like LiteLLM proxies work with or
|
||||
* without a key)
|
||||
*
|
||||
* All provider traffic is proxied through the main process
|
||||
* (ai-assistant:complete / ai-assistant:status IPC) so API keys never enter
|
||||
@@ -231,11 +234,18 @@ class AiAssistantPlugin extends PluginAPI {
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="openai-compatible">OpenAI-compatible…</option>
|
||||
<option value="anthropic-compatible">Anthropic-compatible…</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>Model <input data-role="model" type="text" placeholder="e.g. llama3.1, gpt-4o-mini" /></label>
|
||||
<label>Base URL <input data-role="baseUrl" type="text" placeholder="http://localhost:11434/v1" /></label>
|
||||
<label>API Key <input data-role="apiKey" type="password" placeholder="(not needed for local)" /></label>
|
||||
<label>Base URL
|
||||
<input
|
||||
data-role="baseUrl"
|
||||
type="text"
|
||||
placeholder="Required for …-compatible providers, e.g. http://localhost:4000"
|
||||
/>
|
||||
</label>
|
||||
<label>API Key <input data-role="apiKey" type="password" placeholder="(not needed for local providers)" /></label>
|
||||
<label>Temperature <input data-role="temperature" type="number" min="0" max="2" step="0.1" /></label>
|
||||
<p class="ai-settings-note">Settings are stored locally in settings.json. API keys never leave this machine.</p>
|
||||
<div class="ai-settings-actions">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"key": "provider",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"label": "Provider (openai|anthropic|ollama|lmstudio|openai-compatible)"
|
||||
"label": "Provider (openai|anthropic|ollama|lmstudio|openai-compatible|anthropic-compatible)"
|
||||
},
|
||||
{ "key": "model", "type": "string", "default": "", "label": "Model name" },
|
||||
{
|
||||
|
||||
@@ -42,17 +42,20 @@ function newCommentId(now = Date.now()) {
|
||||
* Load all comments for a document. Missing file/corrupt JSON → [] (a fresh
|
||||
* document simply has no comments yet; a corrupt store must not crash the app).
|
||||
*
|
||||
* Async: `io.readFile`/`io.fileExists` are IPC-backed Promises in the app
|
||||
* (plain values in tests — `await` handles both).
|
||||
*
|
||||
* @param {string} docPath Document path
|
||||
* @param {object} io - { readFile: (path) => string|null, fileExists: (path) => bool }
|
||||
* @param {Function} pathUtil injected path module
|
||||
* @returns {Array<object>} comments sorted by line then createdAt
|
||||
* @returns {Promise<Array<object>>} comments sorted by line then createdAt
|
||||
*/
|
||||
function loadComments(docPath, io, pathUtil) {
|
||||
async function loadComments(docPath, io, pathUtil) {
|
||||
if (!docPath) return [];
|
||||
const file = commentsFilePathFor(docPath, pathUtil);
|
||||
try {
|
||||
if (!io.fileExists(file)) return [];
|
||||
const raw = io.readFile(file);
|
||||
if (!(await io.fileExists(file))) return [];
|
||||
const raw = await io.readFile(file);
|
||||
const data = JSON.parse(raw);
|
||||
if (!Array.isArray(data.comments)) return [];
|
||||
return normalizeComments(data.comments);
|
||||
@@ -69,10 +72,10 @@ function loadComments(docPath, io, pathUtil) {
|
||||
* @param {object} io - { writeFile, ensureDirectory }
|
||||
* @param {Function} pathUtil injected path module
|
||||
*/
|
||||
function saveComments(docPath, comments, io, pathUtil) {
|
||||
async function saveComments(docPath, comments, io, pathUtil) {
|
||||
const file = commentsFilePathFor(docPath, pathUtil);
|
||||
io.ensureDirectory(pathUtil.dirname(file));
|
||||
io.writeFile(
|
||||
await io.ensureDirectory(pathUtil.dirname(file));
|
||||
await io.writeFile(
|
||||
file,
|
||||
JSON.stringify({ version: 1, doc: pathUtil.basename(docPath), comments }, null, 2)
|
||||
);
|
||||
|
||||
@@ -31,7 +31,7 @@ function esc(text) {
|
||||
* @param {Function} deps.pathUtil injected path module
|
||||
* @param {string} [deps.author='me'] Display name for new comments
|
||||
*/
|
||||
function renderCommentsPanel(container, deps) {
|
||||
async function renderCommentsPanel(container, deps) {
|
||||
container.replaceChildren();
|
||||
const { editor, io, pathUtil } = deps;
|
||||
const docPath = editor.getCurrentFilePath();
|
||||
@@ -74,14 +74,14 @@ function renderCommentsPanel(container, deps) {
|
||||
const addBtn = document.createElement('button');
|
||||
addBtn.className = 'ws-btn ws-btn-primary';
|
||||
addBtn.textContent = 'Comment on line ' + editor.getCurrentLine();
|
||||
addBtn.addEventListener('click', () => {
|
||||
addBtn.addEventListener('click', async () => {
|
||||
const text = textarea.value.trim();
|
||||
if (!text) return;
|
||||
const line = editor.getCurrentLine();
|
||||
const anchorText = (getLines(editor)[line - 1] || '').trim();
|
||||
const comments = store.loadComments(docPath, io, pathUtil);
|
||||
const comments = await store.loadComments(docPath, io, pathUtil);
|
||||
store.addComment(comments, { line, anchorText, author: deps.author || 'me', text });
|
||||
store.saveComments(docPath, comments, io, pathUtil);
|
||||
await store.saveComments(docPath, comments, io, pathUtil);
|
||||
renderCommentsPanel(container, deps); // re-render with fresh list
|
||||
});
|
||||
composer.appendChild(textarea);
|
||||
@@ -89,7 +89,7 @@ function renderCommentsPanel(container, deps) {
|
||||
panel.appendChild(composer);
|
||||
|
||||
// --- List -----------------------------------------------------------------
|
||||
const comments = store.loadComments(docPath, io, pathUtil);
|
||||
const comments = await store.loadComments(docPath, io, pathUtil);
|
||||
const lines = getLines(editor);
|
||||
|
||||
const list = document.createElement('div');
|
||||
@@ -122,16 +122,16 @@ function renderCommentsPanel(container, deps) {
|
||||
item.querySelector('[data-action="jump"]').addEventListener('click', () => {
|
||||
editor.jumpToLine(comment.line);
|
||||
});
|
||||
item.querySelector('[data-action="resolve"]').addEventListener('click', () => {
|
||||
const updated = store.loadComments(docPath, io, pathUtil);
|
||||
item.querySelector('[data-action="resolve"]').addEventListener('click', async () => {
|
||||
const updated = await store.loadComments(docPath, io, pathUtil);
|
||||
store.toggleResolved(updated, comment.id);
|
||||
store.saveComments(docPath, updated, io, pathUtil);
|
||||
await store.saveComments(docPath, updated, io, pathUtil);
|
||||
renderCommentsPanel(container, deps);
|
||||
});
|
||||
item.querySelector('[data-action="delete"]').addEventListener('click', () => {
|
||||
const updated = store.loadComments(docPath, io, pathUtil);
|
||||
item.querySelector('[data-action="delete"]').addEventListener('click', async () => {
|
||||
const updated = await store.loadComments(docPath, io, pathUtil);
|
||||
store.deleteComment(updated, comment.id);
|
||||
store.saveComments(docPath, updated, io, pathUtil);
|
||||
await store.saveComments(docPath, updated, io, pathUtil);
|
||||
renderCommentsPanel(container, deps);
|
||||
});
|
||||
list.appendChild(item);
|
||||
|
||||
@@ -85,10 +85,10 @@ class CollaborationPlugin extends PluginAPI {
|
||||
document.addEventListener('keydown', this._f8Handler);
|
||||
}
|
||||
|
||||
_jumpToNextComment() {
|
||||
async _jumpToNextComment() {
|
||||
const docPath = this.context.editor.getCurrentFilePath();
|
||||
if (!docPath) return;
|
||||
const comments = store.loadComments(docPath, this._io, require('path'));
|
||||
const comments = await store.loadComments(docPath, this._io, require('path'));
|
||||
const fromLine = this.context.editor.getCurrentLine();
|
||||
const next = store.nextUnresolved(comments, fromLine);
|
||||
if (next) this.context.editor.jumpToLine(next.line);
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const HISTORY_KEY = 'plugins.writing-studio.history';
|
||||
|
||||
/**
|
||||
* Tracks daily word-count history for goals, streaks, and the heatmap.
|
||||
*
|
||||
* All methods are async: the settings backend in the real app is the
|
||||
* IPC-backed SettingsStore (get/set return Promises), while unit tests inject
|
||||
* a plain synchronous object — `await` transparently handles both.
|
||||
*/
|
||||
class GoalTracker {
|
||||
/**
|
||||
* @param {object} store - { get(key), set(key, value) } settings backend
|
||||
@@ -8,41 +15,41 @@ class GoalTracker {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
_getHistory() {
|
||||
const raw = this.store.get(HISTORY_KEY);
|
||||
async _getHistory() {
|
||||
const raw = await this.store.get(HISTORY_KEY);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
}
|
||||
|
||||
_setHistory(history) {
|
||||
this.store.set(HISTORY_KEY, JSON.stringify(history));
|
||||
async _setHistory(history) {
|
||||
await this.store.set(HISTORY_KEY, JSON.stringify(history));
|
||||
}
|
||||
|
||||
_setHistoryDay(dateStr, data) {
|
||||
const history = this._getHistory();
|
||||
async _setHistoryDay(dateStr, data) {
|
||||
const history = await this._getHistory();
|
||||
history[dateStr] = data;
|
||||
this._setHistory(history);
|
||||
await this._setHistory(history);
|
||||
}
|
||||
|
||||
addWords(count) {
|
||||
async addWords(count) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = this._getHistory();
|
||||
const history = await this._getHistory();
|
||||
if (!history[today]) {
|
||||
history[today] = { words: 0, sessions: 0 };
|
||||
}
|
||||
history[today].words += count;
|
||||
history[today].sessions += 1;
|
||||
this._setHistory(history);
|
||||
await this._setHistory(history);
|
||||
}
|
||||
|
||||
getDailyProgress(goal) {
|
||||
async getDailyProgress(goal) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = this._getHistory();
|
||||
const history = await this._getHistory();
|
||||
const written = history[today]?.words || 0;
|
||||
return { written, goal, pct: goal > 0 ? Math.min(100, Math.round((written / goal) * 100)) : 0 };
|
||||
}
|
||||
|
||||
getStreak(goal) {
|
||||
const history = this._getHistory();
|
||||
async getStreak(goal) {
|
||||
const history = await this._getHistory();
|
||||
let streak = 0;
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 365; i++) {
|
||||
@@ -58,8 +65,8 @@ class GoalTracker {
|
||||
return streak;
|
||||
}
|
||||
|
||||
getLast30Days() {
|
||||
const history = this._getHistory();
|
||||
async getLast30Days() {
|
||||
const history = await this._getHistory();
|
||||
const days = [];
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
@@ -71,8 +78,8 @@ class GoalTracker {
|
||||
return days.reverse();
|
||||
}
|
||||
|
||||
getWeeklyTotal() {
|
||||
const history = this._getHistory();
|
||||
async getWeeklyTotal() {
|
||||
const history = await this._getHistory();
|
||||
let total = 0;
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 7; i++) {
|
||||
|
||||
@@ -35,11 +35,14 @@ class WritingStudioPlugin extends PluginAPI {
|
||||
_registerCommands(context) {
|
||||
const { sprintEngine, snapshotManager, goalTracker } = this;
|
||||
|
||||
// NOTE: settings.get and all engine methods are async (IPC-backed
|
||||
// backend) — every handler below awaits what it reads.
|
||||
|
||||
context.commands.register(
|
||||
'start-sprint',
|
||||
'Studio: Start Sprint',
|
||||
() => {
|
||||
const duration = context.settings.get('sprintDuration') || 25;
|
||||
async () => {
|
||||
const duration = (await context.settings.get('sprintDuration')) || 25;
|
||||
const content = context.editor.getContent() || '';
|
||||
const words = content.split(/\s+/).filter(Boolean).length;
|
||||
sprintEngine.start(duration, words);
|
||||
@@ -50,12 +53,12 @@ class WritingStudioPlugin extends PluginAPI {
|
||||
context.commands.register(
|
||||
'stop-sprint',
|
||||
'Studio: Stop Sprint',
|
||||
() => {
|
||||
async () => {
|
||||
if (!sprintEngine.isActive()) return;
|
||||
const content = context.editor.getContent() || '';
|
||||
const words = content.split(/\s+/).filter(Boolean).length;
|
||||
const result = sprintEngine.stop(words);
|
||||
goalTracker.addWords(result.wordDelta);
|
||||
await goalTracker.addWords(result.wordDelta);
|
||||
context.events.emit('sprint:stopped', result);
|
||||
},
|
||||
'Ctrl+Alt+Shift+S'
|
||||
@@ -64,9 +67,9 @@ class WritingStudioPlugin extends PluginAPI {
|
||||
context.commands.register(
|
||||
'take-snapshot',
|
||||
'Studio: Take Snapshot',
|
||||
() => {
|
||||
async () => {
|
||||
const content = context.editor.getContent() || '';
|
||||
snapshotManager.create(content, 'manual');
|
||||
await snapshotManager.create(content, 'manual');
|
||||
context.events.emit('snapshot:created', {});
|
||||
},
|
||||
'Ctrl+Alt+N'
|
||||
@@ -75,10 +78,10 @@ class WritingStudioPlugin extends PluginAPI {
|
||||
context.commands.register(
|
||||
'restore-last-snapshot',
|
||||
'Studio: Restore Last Snapshot',
|
||||
() => {
|
||||
const snaps = snapshotManager.list();
|
||||
async () => {
|
||||
const snaps = await snapshotManager.list();
|
||||
if (snaps.length === 0) return;
|
||||
const content = snapshotManager.restore(snaps[0].id);
|
||||
const content = await snapshotManager.restore(snaps[0].id);
|
||||
context.editor.insertAtCursor(content);
|
||||
},
|
||||
'Ctrl+Alt+Z'
|
||||
@@ -134,9 +137,9 @@ class WritingStudioPlugin extends PluginAPI {
|
||||
extension: 'txt',
|
||||
handler: async (markdownContent, outputPath) => {
|
||||
const fs = require('fs');
|
||||
const goal = context.settings.get('dailyGoal') || 1000;
|
||||
const progress = goalTracker.getDailyProgress(goal);
|
||||
const streak = goalTracker.getStreak(goal);
|
||||
const goal = (await context.settings.get('dailyGoal')) || 1000;
|
||||
const progress = await goalTracker.getDailyProgress(goal);
|
||||
const streak = await goalTracker.getStreak(goal);
|
||||
const wordCount = (markdownContent || '').split(/\s+/).filter(Boolean).length;
|
||||
|
||||
const lines = [
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
function renderGoalsPanel(container, { engines, settings }) {
|
||||
const dailyGoal = settings.get('dailyGoal') || 1000;
|
||||
const progress = engines.goals.getDailyProgress(dailyGoal);
|
||||
const streak = engines.goals.getStreak(dailyGoal);
|
||||
const weekly = engines.goals.getWeeklyTotal();
|
||||
const last30 = engines.goals.getLast30Days();
|
||||
async function renderGoalsPanel(container, { engines, settings }) {
|
||||
// settings.get and the GoalTracker are async (IPC-backed store) — the whole
|
||||
// panel renders after its data resolves.
|
||||
const dailyGoal = (await settings.get('dailyGoal')) || 1000;
|
||||
const progress = await engines.goals.getDailyProgress(dailyGoal);
|
||||
const streak = await engines.goals.getStreak(dailyGoal);
|
||||
const weekly = await engines.goals.getWeeklyTotal();
|
||||
const last30 = await engines.goals.getLast30Days();
|
||||
|
||||
container.replaceChildren();
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
const projectDir = settings.get('projectDir');
|
||||
/**
|
||||
* Manuscript sidebar panel (writing-studio).
|
||||
*
|
||||
* All project/settings access is async (IPC-backed store), and the
|
||||
* "New Project" flow uses an inline dialog — Electron renderers have no
|
||||
* window.prompt, so the old prompt() call threw on click.
|
||||
*/
|
||||
async function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
const projectDir = await settings.get('projectDir');
|
||||
|
||||
container.replaceChildren();
|
||||
const panel = document.createElement('div');
|
||||
@@ -16,18 +23,22 @@ function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
btn.id = 'ws-new-project';
|
||||
btn.textContent = 'New Project';
|
||||
btn.addEventListener('click', () => {
|
||||
const name = prompt('Project name:');
|
||||
if (!name) return;
|
||||
settings.set('projectDir', name);
|
||||
// Inline name input (window.prompt is unavailable in Electron)
|
||||
const name = askForProjectName();
|
||||
name.then((result) => {
|
||||
if (!result) return;
|
||||
settings.set('projectDir', result).then(() => {
|
||||
renderManuscriptPanel(container, { engines, editor, settings });
|
||||
});
|
||||
});
|
||||
});
|
||||
empty.appendChild(btn);
|
||||
panel.appendChild(empty);
|
||||
container.appendChild(panel);
|
||||
return;
|
||||
}
|
||||
|
||||
const project = engines.projects.loadProject(projectDir);
|
||||
const project = await engines.projects.loadProject(projectDir);
|
||||
if (!project) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'ws-empty';
|
||||
@@ -39,16 +50,17 @@ function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
btn.id = 'ws-clear-project';
|
||||
btn.textContent = 'Clear Project';
|
||||
btn.addEventListener('click', () => {
|
||||
settings.set('projectDir', null);
|
||||
settings.set('projectDir', null).then(() => {
|
||||
renderManuscriptPanel(container, { engines, editor, settings });
|
||||
});
|
||||
});
|
||||
empty.appendChild(btn);
|
||||
panel.appendChild(empty);
|
||||
container.appendChild(panel);
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = engines.projects.getStats(projectDir);
|
||||
const stats = await engines.projects.getStats(projectDir);
|
||||
|
||||
// Project title + progress
|
||||
const section1 = document.createElement('div');
|
||||
@@ -112,8 +124,8 @@ function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
compileBtn.className = 'ws-btn ws-btn-primary';
|
||||
compileBtn.id = 'ws-compile';
|
||||
compileBtn.textContent = 'Compile Manuscript';
|
||||
compileBtn.addEventListener('click', () => {
|
||||
const compiled = engines.projects.compileManuscript(projectDir);
|
||||
compileBtn.addEventListener('click', async () => {
|
||||
const compiled = await engines.projects.compileManuscript(projectDir);
|
||||
editor.insertAtCursor(compiled);
|
||||
});
|
||||
section3.appendChild(compileBtn);
|
||||
@@ -122,4 +134,42 @@ function renderManuscriptPanel(container, { engines, editor, settings }) {
|
||||
container.appendChild(panel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal promise-based name input (Electron has no window.prompt).
|
||||
* @returns {Promise<string|null>} project name, or null when cancelled
|
||||
*/
|
||||
function askForProjectName() {
|
||||
return new Promise((resolve) => {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'ai-settings-overlay';
|
||||
overlay.setAttribute('role', 'dialog');
|
||||
overlay.setAttribute('aria-modal', 'true');
|
||||
overlay.innerHTML = `
|
||||
<div class="ai-settings-dialog">
|
||||
<h3>New manuscript project</h3>
|
||||
<input type="text" data-role="name" placeholder="Project name" aria-label="Project name" />
|
||||
<div class="ai-settings-actions">
|
||||
<button type="button" data-role="ok">Create</button>
|
||||
<button type="button" data-role="cancel">Cancel</button>
|
||||
</div>
|
||||
</div>`;
|
||||
const input = overlay.querySelector('[data-role="name"]');
|
||||
const done = (value) => {
|
||||
overlay.remove();
|
||||
resolve(value);
|
||||
};
|
||||
overlay.querySelector('[data-role="ok"]').addEventListener('click', () => done(input.value.trim() || null));
|
||||
overlay.querySelector('[data-role="cancel"]').addEventListener('click', () => done(null));
|
||||
overlay.addEventListener('click', (event) => {
|
||||
if (event.target === overlay) done(null);
|
||||
});
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter') done(input.value.trim() || null);
|
||||
if (event.key === 'Escape') done(null);
|
||||
});
|
||||
document.body.appendChild(overlay);
|
||||
input.focus();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { renderManuscriptPanel };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
function renderSnapshotsPanel(container, { engines, editor }) {
|
||||
const snapshots = engines.snapshots.list();
|
||||
async function renderSnapshotsPanel(container, { engines, editor }) {
|
||||
const snapshots = await engines.snapshots.list();
|
||||
|
||||
container.replaceChildren();
|
||||
|
||||
@@ -53,16 +53,16 @@ function renderSnapshotsPanel(container, { engines, editor }) {
|
||||
const actionBtn = document.createElement('button');
|
||||
actionBtn.className = 'ws-btn ws-btn-sm' + (cls ? ' ' + cls : '');
|
||||
actionBtn.textContent = text;
|
||||
actionBtn.addEventListener('click', () => {
|
||||
actionBtn.addEventListener('click', async () => {
|
||||
if (action === 'restore') {
|
||||
const content = engines.snapshots.restore(s.id);
|
||||
const content = await engines.snapshots.restore(s.id);
|
||||
editor.insertAtCursor(content);
|
||||
} else if (action === 'delete') {
|
||||
engines.snapshots.delete(s.id);
|
||||
await engines.snapshots.delete(s.id);
|
||||
renderSnapshotsPanel(container, { engines, editor });
|
||||
} else if (action === 'diff') {
|
||||
const current = editor.getContent() || '';
|
||||
const result = engines.snapshots.diff(s.id, current);
|
||||
const result = await engines.snapshots.diff(s.id, current);
|
||||
alert('+' + result.added + ' lines added, -' + result.removed + ' lines removed');
|
||||
}
|
||||
});
|
||||
@@ -75,9 +75,9 @@ function renderSnapshotsPanel(container, { engines, editor }) {
|
||||
container.appendChild(panel);
|
||||
|
||||
// Take snapshot button handler
|
||||
container.querySelector('#ws-take-snapshot').addEventListener('click', () => {
|
||||
container.querySelector('#ws-take-snapshot').addEventListener('click', async () => {
|
||||
const content = editor.getContent() || '';
|
||||
engines.snapshots.create(content, 'manual');
|
||||
await engines.snapshots.create(content, 'manual');
|
||||
renderSnapshotsPanel(container, { engines, editor });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* Manuscript project management (.project.json + chapter files).
|
||||
*
|
||||
* All methods are async: the fs backend injected by the plugin is the
|
||||
* IPC-backed file API (readFile/writeFile/fileExists/listDir return
|
||||
* Promises); `await` also works against the synchronous fakes used in unit
|
||||
* tests.
|
||||
*/
|
||||
class ProjectManager {
|
||||
/**
|
||||
* @param {object} fs - { readFile(path), writeFile(path, content), fileExists(path), listDir(path) }
|
||||
@@ -6,7 +14,7 @@ class ProjectManager {
|
||||
this.fs = fs;
|
||||
}
|
||||
|
||||
createProject(dir, opts) {
|
||||
async createProject(dir, opts) {
|
||||
const project = {
|
||||
title: opts.title,
|
||||
type: opts.type || 'manuscript',
|
||||
@@ -14,51 +22,53 @@ class ProjectManager {
|
||||
chapters: [],
|
||||
metadata: opts.metadata || {},
|
||||
};
|
||||
this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
||||
await this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
||||
return project;
|
||||
}
|
||||
|
||||
loadProject(dir) {
|
||||
const raw = this.fs.readFile(dir + '/.project.json');
|
||||
async loadProject(dir) {
|
||||
const exists = await this.fs.fileExists(dir + '/.project.json');
|
||||
if (!exists) return null;
|
||||
const raw = await this.fs.readFile(dir + '/.project.json');
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
_saveProject(dir, project) {
|
||||
this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
||||
async _saveProject(dir, project) {
|
||||
await this.fs.writeFile(dir + '/.project.json', JSON.stringify(project, null, 2));
|
||||
}
|
||||
|
||||
addChapter(dir, chapter) {
|
||||
const project = this.loadProject(dir);
|
||||
async addChapter(dir, chapter) {
|
||||
const project = await this.loadProject(dir);
|
||||
if (!project) throw new Error('Project not found');
|
||||
project.chapters.push(chapter);
|
||||
this._saveProject(dir, project);
|
||||
await this._saveProject(dir, project);
|
||||
}
|
||||
|
||||
updateChapter(dir, index, updates) {
|
||||
const project = this.loadProject(dir);
|
||||
async updateChapter(dir, index, updates) {
|
||||
const project = await this.loadProject(dir);
|
||||
if (!project) throw new Error('Project not found');
|
||||
Object.assign(project.chapters[index], updates);
|
||||
this._saveProject(dir, project);
|
||||
await this._saveProject(dir, project);
|
||||
}
|
||||
|
||||
compileManuscript(dir) {
|
||||
const project = this.loadProject(dir);
|
||||
async compileManuscript(dir) {
|
||||
const project = await this.loadProject(dir);
|
||||
if (!project) throw new Error('Project not found');
|
||||
const parts = [];
|
||||
for (const ch of project.chapters) {
|
||||
const content = this.fs.readFile(dir + '/' + ch.file);
|
||||
const content = await this.fs.readFile(dir + '/' + ch.file);
|
||||
if (content) parts.push(content);
|
||||
}
|
||||
return parts.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
getStats(dir) {
|
||||
const project = this.loadProject(dir);
|
||||
async getStats(dir) {
|
||||
const project = await this.loadProject(dir);
|
||||
if (!project) throw new Error('Project not found');
|
||||
let totalWords = 0;
|
||||
for (const ch of project.chapters) {
|
||||
const content = this.fs.readFile(dir + '/' + ch.file);
|
||||
const content = await this.fs.readFile(dir + '/' + ch.file);
|
||||
if (content) totalWords += content.split(/\s+/).filter(Boolean).length;
|
||||
}
|
||||
const target = project.target.words || 0;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/**
|
||||
* Content snapshots with a rolling window, stored via the plugin settings
|
||||
* backend. All methods are async because the real backend is the IPC-backed
|
||||
* SettingsStore (get/set return Promises); `await` also works against the
|
||||
* synchronous fakes used in unit tests.
|
||||
*/
|
||||
class SnapshotManager {
|
||||
/**
|
||||
* @param {object} store - { get(key), set(key, value) }
|
||||
@@ -8,17 +14,17 @@ class SnapshotManager {
|
||||
this.storeKey = storeKey;
|
||||
}
|
||||
|
||||
_getAll() {
|
||||
const raw = this.store.get(this.storeKey);
|
||||
async _getAll() {
|
||||
const raw = await this.store.get(this.storeKey);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
}
|
||||
|
||||
_saveAll(snaps) {
|
||||
this.store.set(this.storeKey, JSON.stringify(snaps));
|
||||
async _saveAll(snaps) {
|
||||
await this.store.set(this.storeKey, JSON.stringify(snaps));
|
||||
}
|
||||
|
||||
create(content, label = 'manual') {
|
||||
const snaps = this._getAll();
|
||||
async create(content, label = 'manual') {
|
||||
const snaps = await this._getAll();
|
||||
const snap = {
|
||||
id: 'snap-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8),
|
||||
timestamp: new Date().toISOString(),
|
||||
@@ -27,31 +33,31 @@ class SnapshotManager {
|
||||
label,
|
||||
};
|
||||
snaps.unshift(snap);
|
||||
this._saveAll(snaps);
|
||||
await this._saveAll(snaps);
|
||||
return snap;
|
||||
}
|
||||
|
||||
list() {
|
||||
async list() {
|
||||
return this._getAll();
|
||||
}
|
||||
|
||||
getById(id) {
|
||||
return this._getAll().find((s) => s.id === id) || null;
|
||||
async getById(id) {
|
||||
return (await this._getAll()).find((s) => s.id === id) || null;
|
||||
}
|
||||
|
||||
restore(id) {
|
||||
const snap = this.getById(id);
|
||||
async restore(id) {
|
||||
const snap = await this.getById(id);
|
||||
if (!snap) throw new Error('Snapshot not found');
|
||||
return snap.content;
|
||||
}
|
||||
|
||||
delete(id) {
|
||||
const snaps = this._getAll().filter((s) => s.id !== id);
|
||||
this._saveAll(snaps);
|
||||
async delete(id) {
|
||||
const snaps = (await this._getAll()).filter((s) => s.id !== id);
|
||||
await this._saveAll(snaps);
|
||||
}
|
||||
|
||||
diff(id, currentContent) {
|
||||
const snap = this.getById(id);
|
||||
async diff(id, currentContent) {
|
||||
const snap = await this.getById(id);
|
||||
if (!snap) throw new Error('Snapshot not found');
|
||||
const oldLines = snap.content.split('\n');
|
||||
const newLines = currentContent.split('\n');
|
||||
@@ -68,9 +74,9 @@ class SnapshotManager {
|
||||
return { added, removed };
|
||||
}
|
||||
|
||||
prune(keepCount) {
|
||||
const snaps = this._getAll();
|
||||
this._saveAll(snaps.slice(0, keepCount));
|
||||
async prune(keepCount) {
|
||||
const snaps = await this._getAll();
|
||||
await this._saveAll(snaps.slice(0, keepCount));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -168,6 +168,10 @@ const ALLOWED_SEND_CHANNELS = [
|
||||
'plantuml:available',
|
||||
'plantuml:render',
|
||||
|
||||
// MarkItDown import (optional Python CLI)
|
||||
'markitdown:available',
|
||||
'markitdown:convert',
|
||||
|
||||
// Quick Note scratchpad
|
||||
'quick-note:save',
|
||||
|
||||
|
||||
+13
-3
@@ -4184,7 +4184,12 @@ ipcRenderer.on('show-table-generator', () => {
|
||||
|
||||
// Show PDF Editor Dialog
|
||||
ipcRenderer.on('show-pdf-editor-dialog', (event, operation, openedFilePath) => {
|
||||
currentPDFOperation = operation;
|
||||
if (!operation) {
|
||||
// File > Open PDF sends null — legit, but log unexpected variants so
|
||||
// stray senders are traceable in the console.
|
||||
console.info('[pdf-editor] opening with default section; operation =', operation);
|
||||
}
|
||||
currentPDFOperation = operation || 'merge';
|
||||
showPDFEditorDialog(operation, openedFilePath);
|
||||
});
|
||||
|
||||
@@ -4373,8 +4378,13 @@ function showPDFEditorDialog(operation, openedFilePath = null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
title.textContent = titleText;
|
||||
document.getElementById(sectionId).classList.remove('hidden');
|
||||
|
||||
// Unknown/null operations (e.g. File > Open PDF sends null meaning "open
|
||||
// the editor with this file") fall back to the merge section instead of
|
||||
// crashing on a missing element.
|
||||
const section = document.getElementById(sectionId || 'pdf-merge-section');
|
||||
title.textContent = titleText || 'PDF Editor';
|
||||
if (section) section.classList.remove('hidden');
|
||||
window.modals.pdfEditorModal.open();
|
||||
}
|
||||
function hidePDFEditorDialog() {
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* @module BacklinksPanel
|
||||
*/
|
||||
|
||||
const { collectMarkdownFiles, findBacklinks } = require('../../utils/backlinks');
|
||||
const { docNameFor } = require('../../utils/wiki-links');
|
||||
const { collectMarkdownFiles, findBacklinks } = require('../utils/backlinks');
|
||||
const { docNameFor } = require('../utils/wiki-links');
|
||||
|
||||
/**
|
||||
* Render the backlinks panel.
|
||||
|
||||
@@ -26,3 +26,79 @@ describe('Bundled Pandoc binary', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bundled MarkItDown binary (optional)', () => {
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
const platform = process.platform;
|
||||
const binaryPath = path.join(rootDir, 'bin', platform, 'markitdown');
|
||||
const windowsBinaryPath = path.join(rootDir, 'bin', platform, 'markitdown.exe');
|
||||
|
||||
// The binary is built on demand (npm run bundle:markitdown), so CI machines
|
||||
// that haven't built it yet skip these — but when present it must be valid.
|
||||
const hasBinary = fs.existsSync(binaryPath) || fs.existsSync(windowsBinaryPath);
|
||||
const which = platform === 'win32' ? windowsBinaryPath : binaryPath;
|
||||
|
||||
(hasBinary ? describe : describe.skip)('binary present', () => {
|
||||
test('is executable', () => {
|
||||
const stats = fs.statSync(which);
|
||||
expect(stats.mode & 0o111).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('answers --version', (done) => {
|
||||
const { execFile } = require('child_process');
|
||||
execFile(which, ['--version'], { timeout: 30000 }, (error, stdout) => {
|
||||
expect(error).toBeNull();
|
||||
expect(stdout.toLowerCase()).toContain('markitdown');
|
||||
done();
|
||||
});
|
||||
}, 60000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Legal compliance artifacts', () => {
|
||||
const rootDir = path.resolve(__dirname, '..');
|
||||
|
||||
test('THIRD-PARTY-NOTICES.md and SOURCES.md ship at the repo root', () => {
|
||||
expect(fs.existsSync(path.join(rootDir, 'THIRD-PARTY-NOTICES.md'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(rootDir, 'SOURCES.md'))).toBe(true);
|
||||
});
|
||||
|
||||
test('canonical copyleft license texts are present', () => {
|
||||
for (const file of ['GPL-2.0.txt', 'LGPL-2.1.txt', 'MPL-2.0.txt', 'Apache-2.0.txt', 'OFL-1.1.txt']) {
|
||||
expect(fs.existsSync(path.join(rootDir, 'third-party-licenses', file))).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('notices mention every bundled external binary', () => {
|
||||
const notices = fs.readFileSync(path.join(rootDir, 'THIRD-PARTY-NOTICES.md'), 'utf-8');
|
||||
for (const component of ['Pandoc', 'FFmpeg', 'MarkItDown', 'libvips', 'KaTeX', 'JetBrains Mono']) {
|
||||
expect(notices).toContain(component);
|
||||
}
|
||||
// GPL source offer must reference the sources document
|
||||
expect(notices).toContain('SOURCES.md');
|
||||
});
|
||||
|
||||
test('SOURCES.md covers every GPL-licensed binary with a source link', () => {
|
||||
const sources = fs.readFileSync(path.join(rootDir, 'SOURCES.md'), 'utf-8');
|
||||
for (const section of ['Pandoc', 'FFmpeg', 'PyInstaller', 'libvips']) {
|
||||
expect(sources).toContain(section);
|
||||
}
|
||||
expect(sources).toMatch(/https:\/\/github\.com\/jgm\/pandoc/);
|
||||
expect(sources).toMatch(/https:\/\/ffmpeg\.org\/releases\//);
|
||||
});
|
||||
|
||||
test('packaging includes the legal documents in build files', () => {
|
||||
// The build config lives in electron-builder.config.js (dynamic — it
|
||||
// conditionally bundles the markitdown binary per platform)
|
||||
const configSrc = fs.readFileSync(
|
||||
path.join(rootDir, 'electron-builder.config.js'),
|
||||
'utf-8'
|
||||
);
|
||||
expect(configSrc).toContain("'THIRD-PARTY-NOTICES.md'");
|
||||
expect(configSrc).toContain("'SOURCES.md'");
|
||||
expect(configSrc).toContain('third-party-licenses');
|
||||
// And package.json must not carry a stale static build section anymore
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'));
|
||||
expect(pkg.build).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,11 +10,11 @@ function makeIo(initial = {}) {
|
||||
const files = new Map(Object.entries(initial));
|
||||
return {
|
||||
files,
|
||||
readFile: (p) => (files.has(p) ? files.get(p) : null),
|
||||
writeFile: (p, c) => {
|
||||
readFile: async (p) => (files.has(p) ? files.get(p) : null),
|
||||
writeFile: async (p, c) => {
|
||||
files.set(p, c);
|
||||
},
|
||||
fileExists: (p) => files.has(p),
|
||||
fileExists: async (p) => files.has(p),
|
||||
ensureDirectory: () => {},
|
||||
};
|
||||
}
|
||||
@@ -24,7 +24,7 @@ describe('comment-store', () => {
|
||||
const sidecar = path.join('/docs', '.comments', 'notes.md.json');
|
||||
|
||||
describe('commentsFilePathFor', () => {
|
||||
it('places the sidecar in .comments/ next to the document', () => {
|
||||
it('places the sidecar in .comments/ next to the document', async () => {
|
||||
expect(store.commentsFilePathFor(doc, path)).toBe(sidecar);
|
||||
expect(store.commentsFilePathFor('/a/b/c.md', path)).toBe(
|
||||
path.join('/a/b/.comments', 'c.md.json')
|
||||
@@ -33,29 +33,29 @@ describe('comment-store', () => {
|
||||
});
|
||||
|
||||
describe('load/save round-trip', () => {
|
||||
it('returns [] for documents with no sidecar yet', () => {
|
||||
expect(store.loadComments(doc, makeIo(), path)).toEqual([]);
|
||||
it('returns [] for documents with no sidecar yet', async () => {
|
||||
expect(await store.loadComments(doc, makeIo(), path)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns [] for corrupt JSON instead of throwing', () => {
|
||||
it('returns [] for corrupt JSON instead of throwing', async () => {
|
||||
const io = makeIo({ [sidecar]: '{oops' });
|
||||
expect(store.loadComments(doc, io, path)).toEqual([]);
|
||||
expect(await store.loadComments(doc, io, path)).toEqual([]);
|
||||
});
|
||||
|
||||
it('persists and reloads comments sorted by line', () => {
|
||||
it('persists and reloads comments sorted by line', async () => {
|
||||
const io = makeIo();
|
||||
const comments = [];
|
||||
store.addComment(comments, { line: 12, anchorText: 'para two', text: 'expand this' });
|
||||
store.addComment(comments, { line: 3, anchorText: 'intro', text: 'strong opener' });
|
||||
store.saveComments(doc, comments, io, path);
|
||||
await store.saveComments(doc, comments, io, path);
|
||||
|
||||
const loaded = store.loadComments(doc, io, path);
|
||||
const loaded = await store.loadComments(doc, io, path);
|
||||
expect(loaded.map((c) => c.line)).toEqual([3, 12]);
|
||||
expect(loaded[0].text).toBe('strong opener');
|
||||
expect(loaded[0].resolved).toBe(false);
|
||||
});
|
||||
|
||||
it('normalizes malformed entries on load', () => {
|
||||
it('normalizes malformed entries on load', async () => {
|
||||
const io = makeIo({
|
||||
[sidecar]: JSON.stringify({
|
||||
comments: [
|
||||
@@ -64,7 +64,7 @@ describe('comment-store', () => {
|
||||
],
|
||||
}),
|
||||
});
|
||||
const loaded = store.loadComments(doc, io, path);
|
||||
const loaded = await store.loadComments(doc, io, path);
|
||||
// '7' (string line) is dropped; the valid one survives normalization
|
||||
expect(loaded).toHaveLength(1);
|
||||
expect(loaded[0]).toMatchObject({ line: 2, author: 'ana' });
|
||||
@@ -72,7 +72,7 @@ describe('comment-store', () => {
|
||||
});
|
||||
|
||||
describe('mutators', () => {
|
||||
it('addComment clamps lines and caps field lengths', () => {
|
||||
it('addComment clamps lines and caps field lengths', async () => {
|
||||
const comments = [];
|
||||
const c = store.addComment(comments, {
|
||||
line: -5,
|
||||
@@ -86,7 +86,7 @@ describe('comment-store', () => {
|
||||
expect(c.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('toggleResolved flips state and returns null for unknown ids', () => {
|
||||
it('toggleResolved flips state and returns null for unknown ids', async () => {
|
||||
const comments = [];
|
||||
const c = store.addComment(comments, { line: 1, text: 'hi' });
|
||||
expect(store.toggleResolved(comments, c.id)).toBe(true);
|
||||
@@ -94,7 +94,7 @@ describe('comment-store', () => {
|
||||
expect(store.toggleResolved(comments, 'missing')).toBeNull();
|
||||
});
|
||||
|
||||
it('deleteComment reports whether anything was removed', () => {
|
||||
it('deleteComment reports whether anything was removed', async () => {
|
||||
const comments = [];
|
||||
const c = store.addComment(comments, { line: 1, text: 'hi' });
|
||||
expect(store.deleteComment(comments, c.id)).toBe(true);
|
||||
@@ -104,7 +104,7 @@ describe('comment-store', () => {
|
||||
});
|
||||
|
||||
describe('nextUnresolved (F8 navigation)', () => {
|
||||
it('returns the first open comment below the cursor line', () => {
|
||||
it('returns the first open comment below the cursor line', async () => {
|
||||
const comments = [
|
||||
{ id: 'a', line: 5, resolved: false },
|
||||
{ id: 'b', line: 20, resolved: false },
|
||||
@@ -112,7 +112,7 @@ describe('comment-store', () => {
|
||||
expect(store.nextUnresolved(comments, 7).id).toBe('b');
|
||||
});
|
||||
|
||||
it('wraps to the top when no open comments are below', () => {
|
||||
it('wraps to the top when no open comments are below', async () => {
|
||||
const comments = [
|
||||
{ id: 'a', line: 5, resolved: false },
|
||||
{ id: 'b', line: 20, resolved: true },
|
||||
@@ -120,7 +120,7 @@ describe('comment-store', () => {
|
||||
expect(store.nextUnresolved(comments, 10).id).toBe('a');
|
||||
});
|
||||
|
||||
it('returns null when everything is resolved or empty', () => {
|
||||
it('returns null when everything is resolved or empty', async () => {
|
||||
expect(store.nextUnresolved([{ id: 'a', line: 1, resolved: true }], 1)).toBeNull();
|
||||
expect(store.nextUnresolved([], 1)).toBeNull();
|
||||
});
|
||||
@@ -129,20 +129,20 @@ describe('comment-store', () => {
|
||||
describe('anchorStatus (drift detection)', () => {
|
||||
const base = { line: 2, anchorText: 'original text' };
|
||||
|
||||
it('reports ok when the line still matches', () => {
|
||||
it('reports ok when the line still matches', async () => {
|
||||
expect(store.anchorStatus(base, ['first', 'original text', 'third'])).toBe('ok');
|
||||
});
|
||||
|
||||
it('reports changed when the line text differs', () => {
|
||||
it('reports changed when the line text differs', async () => {
|
||||
expect(store.anchorStatus(base, ['first', 'edited text', 'third'])).toBe('changed');
|
||||
});
|
||||
|
||||
it('reports missing when the line is beyond the document', () => {
|
||||
it('reports missing when the line is beyond the document', async () => {
|
||||
expect(store.anchorStatus({ ...base, line: 99 }, ['only'])).toBe('missing');
|
||||
expect(store.anchorStatus(base, [])).toBe('missing');
|
||||
});
|
||||
|
||||
it('reports moved for comments without an anchor snippet', () => {
|
||||
it('reports moved for comments without an anchor snippet', async () => {
|
||||
expect(store.anchorStatus({ line: 1, anchorText: '' }, ['whatever'])).toBe('moved');
|
||||
});
|
||||
});
|
||||
|
||||
+46
-25
@@ -1,5 +1,8 @@
|
||||
const { GoalTracker } = require('../src/plugins/built-in/writing-studio/goal-tracker');
|
||||
|
||||
// All GoalTracker methods are async (the real settings backend is the
|
||||
// IPC-backed SettingsStore; these tests inject a synchronous fake, which
|
||||
// `await` handles transparently).
|
||||
describe('GoalTracker', () => {
|
||||
let tracker;
|
||||
let store;
|
||||
@@ -14,71 +17,89 @@ describe('GoalTracker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('addWords records words for today', () => {
|
||||
tracker.addWords(500);
|
||||
test('addWords records words for today', async () => {
|
||||
await tracker.addWords(500);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
expect(store['plugins.writing-studio.history']).toBeDefined();
|
||||
const history = JSON.parse(store['plugins.writing-studio.history']);
|
||||
expect(history[today].words).toBe(500);
|
||||
});
|
||||
|
||||
test('addWords accumulates across multiple calls', () => {
|
||||
tracker.addWords(300);
|
||||
tracker.addWords(200);
|
||||
test('addWords accumulates across multiple calls', async () => {
|
||||
await tracker.addWords(300);
|
||||
await tracker.addWords(200);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = JSON.parse(store['plugins.writing-studio.history']);
|
||||
expect(history[today].words).toBe(500);
|
||||
});
|
||||
|
||||
test('getDailyProgress returns 0 when no history', () => {
|
||||
expect(tracker.getDailyProgress(1000)).toEqual({ written: 0, goal: 1000, pct: 0 });
|
||||
test('works with a Promise-returning (IPC-style) backend', async () => {
|
||||
const asyncTracker = new GoalTracker({
|
||||
get: async (key) => store[key],
|
||||
set: async (key, value) => {
|
||||
store[key] = value;
|
||||
},
|
||||
});
|
||||
await asyncTracker.addWords(250);
|
||||
await expect(asyncTracker.getDailyProgress(1000)).resolves.toEqual({
|
||||
written: 250,
|
||||
goal: 1000,
|
||||
pct: 25,
|
||||
});
|
||||
});
|
||||
|
||||
test('getDailyProgress returns percentage', () => {
|
||||
tracker.addWords(500);
|
||||
const progress = tracker.getDailyProgress(1000);
|
||||
test('getDailyProgress returns 0 when no history', async () => {
|
||||
await expect(tracker.getDailyProgress(1000)).resolves.toEqual({
|
||||
written: 0,
|
||||
goal: 1000,
|
||||
pct: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('getDailyProgress returns percentage', async () => {
|
||||
await tracker.addWords(500);
|
||||
const progress = await tracker.getDailyProgress(1000);
|
||||
expect(progress.written).toBe(500);
|
||||
expect(progress.pct).toBe(50);
|
||||
});
|
||||
|
||||
test('getStreak counts consecutive days meeting goal', () => {
|
||||
test('getStreak counts consecutive days meeting goal', async () => {
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const key = d.toISOString().split('T')[0];
|
||||
tracker._setHistoryDay(key, { words: 1200 });
|
||||
await tracker._setHistoryDay(key, { words: 1200 });
|
||||
}
|
||||
const streak = tracker.getStreak(1000);
|
||||
expect(streak).toBe(3);
|
||||
await expect(tracker.getStreak(1000)).resolves.toBe(3);
|
||||
});
|
||||
|
||||
test('getStreak breaks on missed day', () => {
|
||||
test('getStreak breaks on missed day', async () => {
|
||||
const today = new Date();
|
||||
tracker._setHistoryDay(today.toISOString().split('T')[0], { words: 1200 });
|
||||
await tracker._setHistoryDay(today.toISOString().split('T')[0], { words: 1200 });
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
tracker._setHistoryDay(yesterday.toISOString().split('T')[0], { words: 500 });
|
||||
await tracker._setHistoryDay(yesterday.toISOString().split('T')[0], { words: 500 });
|
||||
const dayBefore = new Date(today);
|
||||
dayBefore.setDate(dayBefore.getDate() - 2);
|
||||
tracker._setHistoryDay(dayBefore.toISOString().split('T')[0], { words: 1200 });
|
||||
expect(tracker.getStreak(1000)).toBe(1);
|
||||
await tracker._setHistoryDay(dayBefore.toISOString().split('T')[0], { words: 1200 });
|
||||
await expect(tracker.getStreak(1000)).resolves.toBe(1);
|
||||
});
|
||||
|
||||
test('getLast30Days returns array of 30 entries', () => {
|
||||
tracker.addWords(100);
|
||||
const days = tracker.getLast30Days();
|
||||
test('getLast30Days returns array of 30 entries', async () => {
|
||||
await tracker.addWords(100);
|
||||
const days = await tracker.getLast30Days();
|
||||
expect(days.length).toBe(30);
|
||||
expect(days[29].words).toBe(100);
|
||||
});
|
||||
|
||||
test('getWeeklyTotal sums last 7 days', () => {
|
||||
test('getWeeklyTotal sums last 7 days', async () => {
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
tracker._setHistoryDay(d.toISOString().split('T')[0], { words: 200 });
|
||||
await tracker._setHistoryDay(d.toISOString().split('T')[0], { words: 200 });
|
||||
}
|
||||
expect(tracker.getWeeklyTotal()).toBe(1400);
|
||||
await expect(tracker.getWeeklyTotal()).resolves.toBe(1400);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,6 +33,10 @@ describe('AiProviders', () => {
|
||||
it('does not require an API key for local providers', () => {
|
||||
expect(() => resolveSettings({ provider: 'lmstudio' })).not.toThrow();
|
||||
expect(() => resolveSettings({ provider: 'ollama' })).not.toThrow();
|
||||
// Self-hosted Anthropic-compatible proxies may front their own auth
|
||||
expect(() =>
|
||||
resolveSettings({ provider: 'anthropic-compatible', baseUrl: 'http://localhost:4000' })
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('requires an API key for remote providers', () => {
|
||||
@@ -109,6 +113,52 @@ describe('AiProviders', () => {
|
||||
expect(body.system).toBe('be nice');
|
||||
});
|
||||
|
||||
it('routes anthropic-compatible to the custom base with Bearer + x-api-key auth', async () => {
|
||||
const fetchImpl = okFetch({ content: [{ type: 'text', text: 'proxy ok' }] });
|
||||
const result = await complete(
|
||||
{
|
||||
provider: 'anthropic-compatible',
|
||||
baseUrl: 'http://localhost:4000',
|
||||
apiKey: 'proxy-key',
|
||||
messages,
|
||||
},
|
||||
{ fetchImpl }
|
||||
);
|
||||
|
||||
expect(result.content).toBe('proxy ok');
|
||||
const [url, init] = fetchImpl.mock.calls[0];
|
||||
// Base without a /v1 suffix gets one appended
|
||||
expect(url).toBe('http://localhost:4000/v1/messages');
|
||||
// Both auth styles are sent so LiteLLM/Bedrock-style gateways accept it
|
||||
expect(init.headers['x-api-key']).toBe('proxy-key');
|
||||
expect(init.headers.Authorization).toBe('Bearer proxy-key');
|
||||
expect(init.headers['anthropic-version']).toBe('2023-06-01');
|
||||
});
|
||||
|
||||
it('does not double the /v1 segment when the base already ends with it', async () => {
|
||||
const fetchImpl = okFetch({ content: [{ type: 'text', text: 'ok' }] });
|
||||
await complete(
|
||||
{
|
||||
provider: 'anthropic-compatible',
|
||||
baseUrl: 'http://localhost:4000/v1/',
|
||||
messages,
|
||||
},
|
||||
{ fetchImpl }
|
||||
);
|
||||
// Trailing slash is stripped by resolveSettings, so the base ends in /v1
|
||||
const [url, init] = fetchImpl.mock.calls[0];
|
||||
expect(url).toBe('http://localhost:4000/v1/messages');
|
||||
// Keyless proxies must work without auth headers
|
||||
expect(init.headers.Authorization).toBeUndefined();
|
||||
expect(init.headers['x-api-key']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects anthropic-compatible without a base URL', async () => {
|
||||
await expect(
|
||||
complete({ provider: 'anthropic-compatible', messages }, { fetchImpl: okFetch({}) })
|
||||
).rejects.toThrow(/Invalid API base URL/);
|
||||
});
|
||||
|
||||
it('surfaces HTTP failures as user-safe errors without the body', async () => {
|
||||
const fetchImpl = jest.fn().mockResolvedValue({ ok: false, status: 401 });
|
||||
await expect(
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*
|
||||
* MarkItDown bridge tests with a stubbed execFile — no Python needed.
|
||||
* Covers command resolution order, argv-array invocation (SEC-1), stdout
|
||||
* capture, error surfacing, and path validation.
|
||||
*/
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { resolveMarkItDown, convertToMarkdown, commandCandidates } = require('../../src/main/MarkItDown');
|
||||
const { setImmediate } = require('timers');
|
||||
|
||||
/**
|
||||
* Build a runner stub. `script` maps "cmd argline" -> {error?, stdout?, stderr?}.
|
||||
* Unmatched invocations fail with ENOENT (an uninstalled command), which is
|
||||
* exactly what the real execFile does. Records every call for argv assertions.
|
||||
*/
|
||||
function makeRunner(script = {}) {
|
||||
const calls = [];
|
||||
const runner = (cmd, args, opts, cb) => {
|
||||
calls.push({ cmd, args, opts });
|
||||
const isVersionProbe = args.length > 0 && args[args.length - 1] === '--version';
|
||||
const key = isVersionProbe
|
||||
? [cmd, ...args.slice(0, -1), '--version'].join(' ')
|
||||
: [cmd, ...args].join(' ');
|
||||
const result = script[key];
|
||||
setImmediate(() =>
|
||||
cb(
|
||||
result ? result.error || null : Object.assign(new Error('spawn ENOENT'), { code: 'ENOENT' }),
|
||||
result?.stdout ?? '',
|
||||
result?.stderr ?? ''
|
||||
)
|
||||
);
|
||||
};
|
||||
runner.calls = calls;
|
||||
return runner;
|
||||
}
|
||||
|
||||
describe('MarkItDown', () => {
|
||||
describe('resolveMarkItDown', () => {
|
||||
it('prefers a direct markitdown binary when present', async () => {
|
||||
const runner = makeRunner({
|
||||
'markitdown --version': { stdout: 'markitdown 0.1.7\n' },
|
||||
});
|
||||
const resolved = await resolveMarkItDown(runner);
|
||||
expect(resolved).toEqual({ command: 'markitdown', argsPrefix: [], version: '0.1.7' });
|
||||
});
|
||||
|
||||
it('falls back to python -m markitdown', async () => {
|
||||
const py = process.platform === 'win32' ? 'python' : 'python3';
|
||||
const runner = makeRunner({
|
||||
[`${py} -m markitdown --version`]: { stdout: 'markitdown 0.1.6' },
|
||||
});
|
||||
const resolved = await resolveMarkItDown(runner);
|
||||
expect(resolved).toMatchObject({ command: py, argsPrefix: ['-m', 'markitdown'] });
|
||||
});
|
||||
|
||||
it('probes every candidate before giving up (null)', async () => {
|
||||
const runner = makeRunner({});
|
||||
expect(await resolveMarkItDown(runner)).toBeNull();
|
||||
// One probe per candidate (bundled binary included when present)
|
||||
expect(runner.calls).toHaveLength(commandCandidates().length);
|
||||
});
|
||||
|
||||
it('prefers the bundled binary when one ships with the app', async () => {
|
||||
const candidates = commandCandidates();
|
||||
if (!candidates[0].bundled) {
|
||||
// Machine has no bin/<platform>/markitdown — assert ordering of the
|
||||
// remaining candidates instead.
|
||||
expect(candidates.map((c) => c.command)).toContain('markitdown');
|
||||
return;
|
||||
}
|
||||
const runner = makeRunner({});
|
||||
const first = candidates[0];
|
||||
runner.calls.length = 0;
|
||||
// Stub the bundled path's --version probe
|
||||
const script = {};
|
||||
script[`${first.command} --version`] = { stdout: 'markitdown 0.1.7' };
|
||||
const stub = makeRunner(script);
|
||||
const resolved = await resolveMarkItDown(stub);
|
||||
expect(resolved.command).toBe(first.command);
|
||||
expect(stub.calls[0].cmd).toBe(first.command);
|
||||
});
|
||||
|
||||
it('treats a non-zero probe exit as unavailable', async () => {
|
||||
const runner = makeRunner({
|
||||
'markitdown --version': { error: Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) },
|
||||
});
|
||||
const resolved = await resolveMarkItDown(runner);
|
||||
// Falls through to python candidates which also fail here -> null
|
||||
expect(resolved).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertToMarkdown', () => {
|
||||
const inputPath = path.join(os.tmpdir(), 'any-file.docx');
|
||||
|
||||
it('passes the user path as a literal argv element (no shell)', async () => {
|
||||
const runner = makeRunner({
|
||||
[`markitdown ${inputPath}`]: { stdout: '# Converted\n' },
|
||||
});
|
||||
const { content } = await convertToMarkdown(inputPath, {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
});
|
||||
expect(content).toBe('# Converted\n');
|
||||
expect(runner.calls[0].cmd).toBe('markitdown');
|
||||
expect(runner.calls[0].args).toEqual([inputPath]);
|
||||
});
|
||||
|
||||
it('prepends the module prefix for python-style invocation', async () => {
|
||||
const calls = [];
|
||||
const fake = (cmd, args, opts, cb) => {
|
||||
calls.push({ cmd, args, opts });
|
||||
setImmediate(() => cb(null, 'ok', ''));
|
||||
};
|
||||
await convertToMarkdown(inputPath, {
|
||||
runner: fake,
|
||||
resolved: { command: 'python3', argsPrefix: ['-m', 'markitdown'], version: null },
|
||||
});
|
||||
expect(calls[0].cmd).toBe('python3');
|
||||
expect(calls[0].args).toEqual(['-m', 'markitdown', inputPath]);
|
||||
});
|
||||
|
||||
it('strips a UTF-8 BOM from the output', async () => {
|
||||
const runner = makeRunner({
|
||||
[`markitdown ${inputPath}`]: { stdout: '\uFEFF# Title\n' },
|
||||
});
|
||||
const { content } = await convertToMarkdown(inputPath, {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
});
|
||||
expect(content.startsWith('\uFEFF')).toBe(false);
|
||||
});
|
||||
|
||||
it('surfaces the actionable last stderr line and sanitizes paths', async () => {
|
||||
const runner = makeRunner({
|
||||
[`markitdown ${inputPath}`]: {
|
||||
error: Object.assign(new Error('Command failed'), { code: 1 }),
|
||||
stderr:
|
||||
'Traceback (most recent call last):\n File "/home/user/x.py", line 1\n' +
|
||||
"* pip install 'markitdown[pdf]'",
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
convertToMarkdown(inputPath, {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
})
|
||||
).rejects.toThrow(/pip install 'markitdown\[pdf\]'/);
|
||||
});
|
||||
|
||||
it('classifies timeouts', async () => {
|
||||
const runner = makeRunner({
|
||||
[`markitdown ${inputPath}`]: {
|
||||
error: Object.assign(new Error(' timeout'), { code: 'ETIMEDOUT' }),
|
||||
},
|
||||
});
|
||||
await expect(
|
||||
convertToMarkdown(inputPath, {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'timeout' });
|
||||
});
|
||||
|
||||
it('rejects empty output with the supported-formats hint', async () => {
|
||||
const runner = makeRunner({
|
||||
[`markitdown ${inputPath}`]: { stdout: ' ' },
|
||||
});
|
||||
await expect(
|
||||
convertToMarkdown(inputPath, {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'empty_output' });
|
||||
});
|
||||
|
||||
it('rejects non-absolute and non-string paths', async () => {
|
||||
const runner = makeRunner({});
|
||||
await expect(
|
||||
convertToMarkdown('relative.docx', {
|
||||
runner,
|
||||
resolved: { command: 'markitdown', argsPrefix: [], version: null },
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'bad_path' });
|
||||
await expect(convertToMarkdown(null, { runner })).rejects.toMatchObject({
|
||||
code: 'bad_path',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with install instructions when not installed', async () => {
|
||||
const runner = makeRunner({});
|
||||
await expect(convertToMarkdown(inputPath, { runner })).rejects.toMatchObject({
|
||||
code: 'not_installed',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -50,8 +50,11 @@ function listFilesRecursively(dir) {
|
||||
);
|
||||
|
||||
test('sharp-linux-x64 ships its native binding', () => {
|
||||
const binding = path.join(UNPACKED_IMG_DIR, 'sharp-linux-x64', 'lib', 'sharp-linux-x64.node');
|
||||
expect(fs.existsSync(binding)).toBe(true);
|
||||
// sharp 0.35 names the binding with a version suffix
|
||||
// (sharp-linux-x64-0.35.4.node); 0.34 used a bare name — accept both
|
||||
const libDir = path.join(UNPACKED_IMG_DIR, 'sharp-linux-x64', 'lib');
|
||||
const bindings = fs.readdirSync(libDir).filter((f) => /^sharp-linux-x64.*\.node$/.test(f));
|
||||
expect(bindings.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('sharp-libvips-linux-x64 ships the bundled libvips shared libraries', () => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const { ProjectManager } = require('../src/plugins/built-in/writing-studio/project-manager');
|
||||
|
||||
// All ProjectManager methods are async (the real fs backend is the IPC-backed
|
||||
// file API; these tests inject a synchronous fake, which `await` handles).
|
||||
describe('ProjectManager', () => {
|
||||
let pm;
|
||||
let files;
|
||||
@@ -19,8 +21,8 @@ describe('ProjectManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('createProject writes .project.json', () => {
|
||||
const project = pm.createProject('/manuscripts/novel', {
|
||||
test('createProject writes .project.json', async () => {
|
||||
const project = await pm.createProject('/manuscripts/novel', {
|
||||
title: 'My Novel',
|
||||
type: 'manuscript',
|
||||
targetWords: 80000,
|
||||
@@ -31,7 +33,7 @@ describe('ProjectManager', () => {
|
||||
expect(parsed.target.words).toBe(80000);
|
||||
});
|
||||
|
||||
test('loadProject reads and returns project data', () => {
|
||||
test('loadProject reads and returns project data', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -39,15 +41,15 @@ describe('ProjectManager', () => {
|
||||
chapters: [],
|
||||
metadata: {},
|
||||
});
|
||||
const project = pm.loadProject('/manuscripts/novel');
|
||||
const project = await pm.loadProject('/manuscripts/novel');
|
||||
expect(project.title).toBe('Test');
|
||||
});
|
||||
|
||||
test('loadProject returns null if no project file', () => {
|
||||
expect(pm.loadProject('/nonexistent')).toBeNull();
|
||||
test('loadProject returns null if no project file', async () => {
|
||||
await expect(pm.loadProject('/nonexistent')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('addChapter appends chapter and saves', () => {
|
||||
test('addChapter appends chapter and saves', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -55,7 +57,7 @@ describe('ProjectManager', () => {
|
||||
chapters: [],
|
||||
metadata: {},
|
||||
});
|
||||
pm.addChapter('/manuscripts/novel', {
|
||||
await pm.addChapter('/manuscripts/novel', {
|
||||
file: '01-chapter.md',
|
||||
title: 'Chapter One',
|
||||
status: 'draft',
|
||||
@@ -65,7 +67,7 @@ describe('ProjectManager', () => {
|
||||
expect(parsed.chapters[0].title).toBe('Chapter One');
|
||||
});
|
||||
|
||||
test('compileManuscript concatenates chapter files', () => {
|
||||
test('compileManuscript concatenates chapter files', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -78,11 +80,11 @@ describe('ProjectManager', () => {
|
||||
});
|
||||
files['/manuscripts/novel/01.md'] = 'First chapter content.';
|
||||
files['/manuscripts/novel/02.md'] = 'Second chapter content.';
|
||||
const result = pm.compileManuscript('/manuscripts/novel');
|
||||
const result = await pm.compileManuscript('/manuscripts/novel');
|
||||
expect(result).toBe('First chapter content.\n\n---\n\nSecond chapter content.');
|
||||
});
|
||||
|
||||
test('compileManuscript skips missing files', () => {
|
||||
test('compileManuscript skips missing files', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -94,11 +96,11 @@ describe('ProjectManager', () => {
|
||||
metadata: {},
|
||||
});
|
||||
files['/manuscripts/novel/01.md'] = 'Only chapter one.';
|
||||
const result = pm.compileManuscript('/manuscripts/novel');
|
||||
const result = await pm.compileManuscript('/manuscripts/novel');
|
||||
expect(result).toBe('Only chapter one.');
|
||||
});
|
||||
|
||||
test('getStats returns total word count across chapters', () => {
|
||||
test('getStats returns total word count across chapters', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -111,14 +113,14 @@ describe('ProjectManager', () => {
|
||||
});
|
||||
files['/manuscripts/novel/01.md'] = 'word '.repeat(100).trim();
|
||||
files['/manuscripts/novel/02.md'] = 'more '.repeat(50).trim();
|
||||
const stats = pm.getStats('/manuscripts/novel');
|
||||
const stats = await pm.getStats('/manuscripts/novel');
|
||||
expect(stats.totalWords).toBeGreaterThan(0);
|
||||
expect(stats.chapterCount).toBe(2);
|
||||
expect(stats.targetWords).toBe(50000);
|
||||
expect(stats.pctComplete).toBeDefined();
|
||||
});
|
||||
|
||||
test('updateChapter modifies a chapter by index', () => {
|
||||
test('updateChapter modifies a chapter by index', async () => {
|
||||
files['/manuscripts/novel/.project.json'] = JSON.stringify({
|
||||
title: 'Test',
|
||||
type: 'manuscript',
|
||||
@@ -126,7 +128,7 @@ describe('ProjectManager', () => {
|
||||
chapters: [{ file: '01.md', title: 'Old Title', status: 'draft' }],
|
||||
metadata: {},
|
||||
});
|
||||
pm.updateChapter('/manuscripts/novel', 0, { title: 'New Title', status: 'revised' });
|
||||
await pm.updateChapter('/manuscripts/novel', 0, { title: 'New Title', status: 'revised' });
|
||||
const parsed = JSON.parse(files['/manuscripts/novel/.project.json']);
|
||||
expect(parsed.chapters[0].title).toBe('New Title');
|
||||
expect(parsed.chapters[0].status).toBe('revised');
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const { SnapshotManager } = require('../src/plugins/built-in/writing-studio/snapshot-manager');
|
||||
|
||||
// All SnapshotManager methods are async (the real settings backend is the
|
||||
// IPC-backed SettingsStore; these tests inject a synchronous fake).
|
||||
describe('SnapshotManager', () => {
|
||||
let manager;
|
||||
let store;
|
||||
@@ -14,62 +16,63 @@ describe('SnapshotManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('create stores snapshot with timestamp, content, wordCount', () => {
|
||||
const snap = manager.create('Hello world this is a test', 'auto');
|
||||
test('create stores snapshot with timestamp, content, wordCount', async () => {
|
||||
const snap = await manager.create('Hello world this is a test', 'auto');
|
||||
expect(snap).toHaveProperty('id');
|
||||
expect(snap.content).toBe('Hello world this is a test');
|
||||
expect(snap.wordCount).toBe(6);
|
||||
expect(snap.label).toBe('auto');
|
||||
});
|
||||
|
||||
test('list returns snapshots ordered newest first', () => {
|
||||
manager.create('first', 'auto');
|
||||
manager.create('second', 'auto');
|
||||
const list = manager.list();
|
||||
test('list returns snapshots ordered newest first', async () => {
|
||||
await manager.create('first', 'auto');
|
||||
await manager.create('second', 'auto');
|
||||
const list = await manager.list();
|
||||
expect(list.length).toBe(2);
|
||||
expect(list[0].content).toBe('second');
|
||||
});
|
||||
|
||||
test('getById returns specific snapshot', () => {
|
||||
const snap = manager.create('find me', 'manual');
|
||||
const found = manager.getById(snap.id);
|
||||
test('getById returns specific snapshot', async () => {
|
||||
const snap = await manager.create('find me', 'manual');
|
||||
const found = await manager.getById(snap.id);
|
||||
expect(found.content).toBe('find me');
|
||||
});
|
||||
|
||||
test('getById returns null for missing id', () => {
|
||||
expect(manager.getById('nope')).toBeNull();
|
||||
test('getById returns null for missing id', async () => {
|
||||
await expect(manager.getById('nope')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('restore returns content of snapshot', () => {
|
||||
const snap = manager.create('restore this', 'manual');
|
||||
expect(manager.restore(snap.id)).toBe('restore this');
|
||||
test('restore returns content of snapshot', async () => {
|
||||
const snap = await manager.create('restore this', 'manual');
|
||||
await expect(manager.restore(snap.id)).resolves.toBe('restore this');
|
||||
});
|
||||
|
||||
test('restore throws for missing snapshot', () => {
|
||||
expect(() => manager.restore('nope')).toThrow('Snapshot not found');
|
||||
test('restore throws for missing snapshot', async () => {
|
||||
await expect(manager.restore('nope')).rejects.toThrow('Snapshot not found');
|
||||
});
|
||||
|
||||
test('delete removes a snapshot', () => {
|
||||
const snap = manager.create('delete me', 'auto');
|
||||
manager.delete(snap.id);
|
||||
expect(manager.getById(snap.id)).toBeNull();
|
||||
test('delete removes a snapshot', async () => {
|
||||
const snap = await manager.create('delete me', 'auto');
|
||||
await manager.delete(snap.id);
|
||||
await expect(manager.getById(snap.id)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
test('diff returns added/removed line counts', () => {
|
||||
const snap = manager.create('line one\nline two\nline three', 'auto');
|
||||
const result = manager.diff(snap.id, 'line one\nline modified\nline three\nline four');
|
||||
test('diff returns added/removed line counts', async () => {
|
||||
const snap = await manager.create('line one\nline two\nline three', 'auto');
|
||||
const result = await manager.diff(snap.id, 'line one\nline modified\nline three\nline four');
|
||||
expect(result.added).toBe(2);
|
||||
expect(result.removed).toBe(1);
|
||||
});
|
||||
|
||||
test('diff throws for missing snapshot', () => {
|
||||
expect(() => manager.diff('nope', 'new content')).toThrow('Snapshot not found');
|
||||
test('diff throws for missing snapshot', async () => {
|
||||
await expect(manager.diff('nope', 'new content')).rejects.toThrow('Snapshot not found');
|
||||
});
|
||||
|
||||
test('prune keeps only the N most recent snapshots', () => {
|
||||
for (let i = 0; i < 10; i++) manager.create('snap ' + i, 'auto');
|
||||
manager.prune(5);
|
||||
expect(manager.list().length).toBe(5);
|
||||
expect(manager.list()[0].content).toBe('snap 9');
|
||||
test('prune keeps only the N most recent snapshots', async () => {
|
||||
for (let i = 0; i < 10; i++) await manager.create('snap ' + i, 'auto');
|
||||
await manager.prune(5);
|
||||
const list = await manager.list();
|
||||
expect(list.length).toBe(5);
|
||||
expect(list[0].content).toBe('snap 9');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,338 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,501 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Moe Ghoul>, 1 April 1990
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,277 @@
|
||||
A. HISTORY OF THE SOFTWARE
|
||||
==========================
|
||||
|
||||
Python was created in the early 1990s by Guido van Rossum at Stichting
|
||||
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
|
||||
as a successor of a language called ABC. Guido remains Python's
|
||||
principal author, although it includes many contributions from others.
|
||||
|
||||
In 1995, Guido continued his work on Python at the Corporation for
|
||||
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
|
||||
in Reston, Virginia where he released several versions of the
|
||||
software.
|
||||
|
||||
In May 2000, Guido and the Python core development team moved to
|
||||
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
|
||||
year, the PythonLabs team moved to Digital Creations, which became
|
||||
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
|
||||
https://www.python.org/psf/) was formed, a non-profit organization
|
||||
created specifically to own Python-related Intellectual Property.
|
||||
Zope Corporation was a sponsoring member of the PSF.
|
||||
|
||||
All Python releases are Open Source (see https://opensource.org for
|
||||
the Open Source Definition). Historically, most, but not all, Python
|
||||
releases have also been GPL-compatible; the table below summarizes
|
||||
the various releases.
|
||||
|
||||
Release Derived Year Owner GPL-
|
||||
from compatible? (1)
|
||||
|
||||
0.9.0 thru 1.2 1991-1995 CWI yes
|
||||
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
|
||||
1.6 1.5.2 2000 CNRI no
|
||||
2.0 1.6 2000 BeOpen.com no
|
||||
1.6.1 1.6 2001 CNRI yes (2)
|
||||
2.1 2.0+1.6.1 2001 PSF no
|
||||
2.0.1 2.0+1.6.1 2001 PSF yes
|
||||
2.1.1 2.1+2.0.1 2001 PSF yes
|
||||
2.1.2 2.1.1 2002 PSF yes
|
||||
2.1.3 2.1.2 2002 PSF yes
|
||||
2.2 and above 2.1.1 2001-now PSF yes
|
||||
|
||||
Footnotes:
|
||||
|
||||
(1) GPL-compatible doesn't mean that we're distributing Python under
|
||||
the GPL. All Python licenses, unlike the GPL, let you distribute
|
||||
a modified version without making your changes open source. The
|
||||
GPL-compatible licenses make it possible to combine Python with
|
||||
other software that is released under the GPL; the others don't.
|
||||
|
||||
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
|
||||
because its license has a choice of law clause. According to
|
||||
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
|
||||
is "not incompatible" with the GPL.
|
||||
|
||||
Thanks to the many outside volunteers who have worked under Guido's
|
||||
direction to make these releases possible.
|
||||
|
||||
|
||||
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
|
||||
===============================================================
|
||||
|
||||
Python software and documentation are licensed under the
|
||||
Python Software Foundation License Version 2.
|
||||
|
||||
Starting with Python 3.8.6, examples, recipes, and other code in
|
||||
the documentation are dual licensed under the PSF License Version 2
|
||||
and the Zero-Clause BSD license.
|
||||
|
||||
Some software incorporated into Python is under different licenses.
|
||||
The licenses are listed with code falling under that license.
|
||||
|
||||
|
||||
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
|
||||
--------------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Python Software Foundation
|
||||
("PSF"), and the Individual or Organization ("Licensee") accessing and
|
||||
otherwise using this software ("Python") in source or binary form and
|
||||
its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, PSF hereby
|
||||
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
|
||||
analyze, test, perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use Python alone or in any derivative version,
|
||||
provided, however, that PSF's License Agreement and PSF's notice of copyright,
|
||||
i.e., "Copyright (c) 2001-2024 Python Software Foundation; All Rights Reserved"
|
||||
are retained in Python alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python.
|
||||
|
||||
4. PSF is making Python available to Licensee on an "AS IS"
|
||||
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between PSF and
|
||||
Licensee. This License Agreement does not grant permission to use PSF
|
||||
trademarks or trade name in a trademark sense to endorse or promote
|
||||
products or services of Licensee, or any third party.
|
||||
|
||||
8. By copying, installing or otherwise using Python, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
|
||||
-------------------------------------------
|
||||
|
||||
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
|
||||
|
||||
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
|
||||
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise using
|
||||
this software in source or binary form and its associated
|
||||
documentation ("the Software").
|
||||
|
||||
2. Subject to the terms and conditions of this BeOpen Python License
|
||||
Agreement, BeOpen hereby grants Licensee a non-exclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test, perform
|
||||
and/or display publicly, prepare derivative works, distribute, and
|
||||
otherwise use the Software alone or in any derivative version,
|
||||
provided, however, that the BeOpen Python License is retained in the
|
||||
Software, alone or in any derivative version prepared by Licensee.
|
||||
|
||||
3. BeOpen is making the Software available to Licensee on an "AS IS"
|
||||
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
|
||||
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
|
||||
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
|
||||
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
5. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
6. This License Agreement shall be governed by and interpreted in all
|
||||
respects by the law of the State of California, excluding conflict of
|
||||
law provisions. Nothing in this License Agreement shall be deemed to
|
||||
create any relationship of agency, partnership, or joint venture
|
||||
between BeOpen and Licensee. This License Agreement does not grant
|
||||
permission to use BeOpen trademarks or trade names in a trademark
|
||||
sense to endorse or promote products or services of Licensee, or any
|
||||
third party. As an exception, the "BeOpen Python" logos available at
|
||||
http://www.pythonlabs.com/logos.html may be used according to the
|
||||
permissions granted on that web page.
|
||||
|
||||
7. By copying, installing or otherwise using the software, Licensee
|
||||
agrees to be bound by the terms and conditions of this License
|
||||
Agreement.
|
||||
|
||||
|
||||
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
|
||||
---------------------------------------
|
||||
|
||||
1. This LICENSE AGREEMENT is between the Corporation for National
|
||||
Research Initiatives, having an office at 1895 Preston White Drive,
|
||||
Reston, VA 20191 ("CNRI"), and the Individual or Organization
|
||||
("Licensee") accessing and otherwise using Python 1.6.1 software in
|
||||
source or binary form and its associated documentation.
|
||||
|
||||
2. Subject to the terms and conditions of this License Agreement, CNRI
|
||||
hereby grants Licensee a nonexclusive, royalty-free, world-wide
|
||||
license to reproduce, analyze, test, perform and/or display publicly,
|
||||
prepare derivative works, distribute, and otherwise use Python 1.6.1
|
||||
alone or in any derivative version, provided, however, that CNRI's
|
||||
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
|
||||
1995-2001 Corporation for National Research Initiatives; All Rights
|
||||
Reserved" are retained in Python 1.6.1 alone or in any derivative
|
||||
version prepared by Licensee. Alternately, in lieu of CNRI's License
|
||||
Agreement, Licensee may substitute the following text (omitting the
|
||||
quotes): "Python 1.6.1 is made available subject to the terms and
|
||||
conditions in CNRI's License Agreement. This Agreement together with
|
||||
Python 1.6.1 may be located on the internet using the following
|
||||
unique, persistent identifier (known as a handle): 1895.22/1013. This
|
||||
Agreement may also be obtained from a proxy server on the internet
|
||||
using the following URL: http://hdl.handle.net/1895.22/1013".
|
||||
|
||||
3. In the event Licensee prepares a derivative work that is based on
|
||||
or incorporates Python 1.6.1 or any part thereof, and wants to make
|
||||
the derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary of
|
||||
the changes made to Python 1.6.1.
|
||||
|
||||
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
|
||||
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
|
||||
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
|
||||
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
|
||||
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
|
||||
INFRINGE ANY THIRD PARTY RIGHTS.
|
||||
|
||||
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
|
||||
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
|
||||
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
|
||||
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
|
||||
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
|
||||
7. This License Agreement shall be governed by the federal
|
||||
intellectual property law of the United States, including without
|
||||
limitation the federal copyright law, and, to the extent such
|
||||
U.S. federal law does not apply, by the law of the Commonwealth of
|
||||
Virginia, excluding Virginia's conflict of law provisions.
|
||||
Notwithstanding the foregoing, with regard to derivative works based
|
||||
on Python 1.6.1 that incorporate non-separable material that was
|
||||
previously distributed under the GNU General Public License (GPL), the
|
||||
law of the Commonwealth of Virginia shall govern this License
|
||||
Agreement only as to issues arising under or with respect to
|
||||
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
|
||||
License Agreement shall be deemed to create any relationship of
|
||||
agency, partnership, or joint venture between CNRI and Licensee. This
|
||||
License Agreement does not grant permission to use CNRI trademarks or
|
||||
trade name in a trademark sense to endorse or promote products or
|
||||
services of Licensee, or any third party.
|
||||
|
||||
8. By clicking on the "ACCEPT" button where indicated, or by copying,
|
||||
installing or otherwise using Python 1.6.1, Licensee agrees to be
|
||||
bound by the terms and conditions of this License Agreement.
|
||||
|
||||
ACCEPT
|
||||
|
||||
|
||||
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
|
||||
--------------------------------------------------
|
||||
|
||||
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
|
||||
The Netherlands. All rights reserved.
|
||||
|
||||
Permission to use, copy, modify, and distribute this software and its
|
||||
documentation for any purpose and without fee is hereby granted,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation, and that the name of Stichting Mathematisch
|
||||
Centrum or CWI not be used in advertising or publicity pertaining to
|
||||
distribution of the software without specific, written prior
|
||||
permission.
|
||||
|
||||
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
|
||||
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
|
||||
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
PERFORMANCE OF THIS SOFTWARE.
|
||||
Reference in New Issue
Block a user