diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b95506f..8120586 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -18,7 +18,7 @@ jobs:
fail-fast: false
max-parallel: 2
matrix:
- java: ['21', '22', '23', '24', '25', '26', '27']
+ java: ['21', '22', '23', '24', '25', '26']
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v6
@@ -36,6 +36,12 @@ jobs:
if: matrix.java == '21'
run: node --test src/test/js/*.test.cjs
- - name: Build static browser demo
+ - name: Build approved homepage and static browser demo
if: matrix.java == '21'
- run: node scripts/build-web-demo.mjs
+ run: |
+ node scripts/build-website.mjs
+ test -f website-dist/index.html
+ test -f website-dist/styles.css
+ test -f website-dist/demo/index.html
+ test -f website-dist/demo/retro.css
+ node scripts/verify-website-build.mjs
diff --git a/.gitignore b/.gitignore
index 845489f..03558a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,4 @@ Thumbs.db
# Generated browser demo (built from tracked sources)
/demo-dist/
+/website-dist/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f0dc685..9819086 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,9 @@ All notable Doc2FAQ changes are recorded here. Version numbers follow semantic v
### Added
- Browser PDF/Word (.docx) import and paginated export with local processing, PDF page/file/text limits, bounded DOCX XML decompression, optional references, and clear unsupported-file/font errors.
+- Browser PDF exports now embed bundled DejaVu Sans regular/bold font subsets instead of standard Helvetica, fixing Turkish and extended-Unicode text copied from web pages; unchanged Word exports continue to preserve the source characters.
- Browser-only website demo built from the shared frontend, with simplified text drafting, explicit PDF/Word limitations, homepage navigation, and text exports.
+- Approved Doc2FAQ landing page and matching browser/Java workbench theme, with original document mascot, responsive illustrated cards, accessible focus styles, reduced-motion support, and a reproducible `website-dist/` build. The homepage includes a "What's new" release-notes preview, clearly labeled as unreleased.
- Source & Review user guide and current documentation for development builds, schema compatibility, and v0.5.0 release preparation.
- Read-only source viewer highlighting the selected FAQ's exact original passages, with a stacked layout on narrow screens.
- Per-FAQ Needs review / Approved status, persisted in saved projects and optional browser autosave.
@@ -16,6 +18,10 @@ All notable Doc2FAQ changes are recorded here. Version numbers follow semantic v
- Editing questions or answers and merging FAQs reset approval; incomplete FAQs cannot be approved.
- Project schema 2 stores review status and upgrades schema-1 projects with Needs review defaults. Older application versions reject schema-2 files instead of silently discarding approval state.
- Filters affect only the review list; saving, export preview, and all export formats retain every FAQ in draft order.
+- Export formats now open with PDF first and Word (.docx) second; the default download name uses `.pdf`.
+- **Clear saved data** is now a prominent, high-contrast action with a confirmation prompt. Cancel leaves the browser's saved draft intact; confirming removes only browser storage and keeps the current workspace open.
+- GitHub Actions now uses Node 24-based checkout/setup-java actions and pins Ubuntu 24.04 to avoid runner-label migration notices.
+- The v0.5.0 release gate validates **Java 21–26** with Java 21-compatible bytecode. Java 27 formal support is deferred, not declared incompatible: a transient JDK-download HTTP 504 caused an initial CI failure, and a later rerun **passed Java 27 build/tests and packaged-JAR smoke checks**.
- Release-candidate source version is 0.5.0; v0.4.0 remains the published release until the new artifacts are validated and released.
## 0.4.0 — 2026-10-29
diff --git a/README.md b/README.md
index b3c9f1d..9a4357d 100644
--- a/README.md
+++ b/README.md
@@ -10,6 +10,8 @@ Created by **Okan Tiktas** · [OKAN-SOFTWARE](https://github.com/OKAN-SOFTWARE).
## Current source: v0.5.0 release candidate — Source & Review
+The **approved Doc2FAQ design** introduces a playful, illustrated homepage and a matching responsive FAQ workbench. Visit the [product homepage](https://okantiktas.com/projects/doc2faq/) for the overview or open the [interactive browser demo](https://okantiktas.com/projects/doc2faq/demo/) to try local document import, review, and export. The new design and demo are staged for deployment; the URLs may still show the earlier version until the website changes are reviewed and published. See [the v0.5.0 release notes](CHANGELOG.md#050--unreleased-release-candidate) for the complete changes and [the design system](docs/DESIGN-SYSTEM.md) for colors, components, and accessibility choices.
+
- **Source beside answer:** select **View source beside answer** on an FAQ to highlight all of its original passages in a read-only document viewer beside the review editor on desktop (stacked on smaller screens). References still use extracted-text lines, not PDF page numbers. Manual FAQs have no automatic reference.
- **Review status:** FAQs start as **Needs review**. Mark a complete answer **Approved** after checking it. Editing its question or answer, or merging FAQs, returns it to **Needs review**. Moving an item preserves approval; Undo restores the status from before a removal or merge.
- **Search and review filters:** combine a case-insensitive question/answer search with **All statuses**, **Needs review**, or **Approved**. The summary shows visible, approved, and pending counts. **Clear filters** restores the full list. Filters never change what is saved, previewed, or exported: all FAQs remain included, and incomplete entries still block export.
@@ -20,7 +22,7 @@ The release-candidate branch uses `0.5.0` in `pom.xml` (`target/doc2faq-0.5.0.ja
## New in v0.4.0
- **Save work / Open saved work:** a versioned JSON file preserves source text, title, mode, draft order, edits, original excerpts, and duplicate-review choices. Incomplete FAQs can be saved too. Project files are limited to 4 MB and validated before replacing the workspace.
-- **Optional browser autosave:** off by default. Enabling it stores the complete source and draft on this browser's origin and restores them on reload. Disabling autosave or using **Clear saved data** removes Doc2FAQ's saved data without clearing your open workspace. Storage failures leave the draft open; use **Save work** as a backup.
+- **Optional browser autosave:** off by default. Enabling it stores the complete source and draft on this browser's origin and restores them on reload. The highlighted **Clear saved data** button asks for confirmation before removing the browser copy; canceling preserves it, and confirming leaves your current workspace open. Disabling autosave also removes the stored copy. Storage failures leave the draft open; use **Save work** as a backup.
- **Source references:** expand **Show source** to see the original section, extracted-text line range, and exact passage. Edited questions and answers are marked for rechecking. References use text lines, not PDF page numbers; manually added FAQs have no automatic reference.
- **Duplicate review:** distinct passages are retained even if their questions cover the same topic. Similar questions are flagged using a word-overlap heuristic. Choose **Keep both** or **Merge into this FAQ**. Merging keeps both answers and all source references; **Undo removal / merge** restores the previous items until another FAQ edit, addition, reorder, or review choice. Exact duplicate question/answer pairs are still filtered. The maximum remains 30 FAQs.
- **PDF and Word export:** edited content, a centered document title, readable page layout, metadata, and page numbers. Optionally include section/line references and edited-answer notices. PDF embeds Lato Regular and Bold (see `src/main/resources/fonts/LICENSE-Lato.txt`); if text contains a glyph that font cannot represent, export returns an error and suggests DOCX.
@@ -52,15 +54,15 @@ The document placeholder now uses neutral Q&A prompts, and Generate Mode's sampl
## Try it in your browser
-[Open the interactive browser demo](https://okantiktas.com/projects/doc2faq/) · [Source & Review user guide](docs/USER-GUIDE.md) · [Demo capabilities and deployment](docs/WEB-DEMO.md)
+[Doc2FAQ homepage](https://okantiktas.com/projects/doc2faq/) · [Try the interactive browser demo](https://okantiktas.com/projects/doc2faq/demo/) · [Source & Review user guide](docs/USER-GUIDE.md) · [Demo capabilities and deployment](docs/WEB-DEMO.md) · [Approved design system](docs/DESIGN-SYSTEM.md)
-The demo processes pasted text and TXT/Markdown in your browser, using simplified rules. Try both drafting modes, source highlighting, editing, approval status, duplicate review, search/filters, saved projects, and Markdown/JSON/HTML exports. PDF/Word (.docx) import and export also run locally in the browser; PDF uploads are limited to 5 MB and 50 pages, and DOCX to 5 MB. Scanned PDFs need external OCR. The demo does not run a Java backend and its drafting rules remain simplified. Document libraries load from a version-pinned CDN when needed; document content is not uploaded. Browser PDF export uses standard Latin fonts and suggests Word when a character cannot be represented. The development demo may include features newer than the latest release download.
+The demo processes pasted text and TXT/Markdown in your browser, using simplified rules. Try both drafting modes, source highlighting, editing, approval status, duplicate review, search/filters, saved projects, and Markdown/JSON/HTML exports. PDF/Word (.docx) import and export also run locally in the browser; PDF uploads are limited to 5 MB and 50 pages, and DOCX to 5 MB. Scanned PDFs need external OCR. The demo does not run a Java backend and its drafting rules remain simplified. Document libraries load from a version-pinned CDN when needed; document content is not uploaded. Browser PDF export now embeds bundled DejaVu Sans fonts for Turkish and a wider range of Unicode characters; Word remains the fallback for unsupported scripts. The development demo may include features newer than the latest release download.
For detailed changes, see [CHANGELOG.md](CHANGELOG.md). For the release checklist, see [docs/RELEASING.md](docs/RELEASING.md).
## Requirements
-- To run a release download: **Java 21 or newer**. The JAR targets Java 21 bytecode; GitHub Actions checks the released JDK versions 21–27 before v0.5.0 publication. Newer future Java versions are not automatically guaranteed compatible. The JAR works on Windows, macOS, and Linux; the Windows ZIP includes `Start-Doc2FAQ.cmd` and a quick-start guide. Extract the ZIP into a new folder, stop any older Doc2FAQ process, run the launcher, and open **http://localhost:8080**. Maven and Node.js are only needed to build and test from source.
+- To run a release download: **Java 21 or newer**. The JAR targets Java 21 bytecode; v0.5.0's **required automated compatibility matrix is Java 21–26**. Java 27 **passed an exploratory CI build and packaged-JAR smoke test** on November 8, 2026, but is not a committed v0.5.0 support target; formal support can follow in a later release. Newer Java versions are not automatically guaranteed compatible. The JAR works on Windows, macOS, and Linux; the Windows ZIP includes `Start-Doc2FAQ.cmd` and a quick-start guide. Extract the ZIP into a new folder, stop any older Doc2FAQ process, run the launcher, and open **http://localhost:8080**. Maven and Node.js are only needed to build and test from source.
To build from source:
diff --git a/docs/DESIGN-SYSTEM.md b/docs/DESIGN-SYSTEM.md
new file mode 100644
index 0000000..100aaad
--- /dev/null
+++ b/docs/DESIGN-SYSTEM.md
@@ -0,0 +1,57 @@
+# Doc2FAQ design system — approved November 2026
+
+Doc2FAQ uses the light, playful **paper-and-answers** visual identity for its
+product homepage, hosted browser demo, and local Java workspace. This direction
+was approved by project owner Okan Tiktas for future versions of Doc2FAQ.
+
+## Principles
+
+1. A friendly original document mascot (not another project's copyrighted logo).
+2. Warm cream and cool off-white backgrounds, ink-dark outlines, offset shadows.
+3. Coral for primary actions, yellow for highlights, mint for positive states.
+4. Oversized, confident headings; concise, accessible helper text.
+5. Direct workflow: import → generate/extract → review → export.
+6. Real functionality first: responsive controls, visible keyboard focus,
+ sufficient text contrast, and reduced-motion support.
+7. A visible **What's new** area may preview unreleased changes, but must
+ distinguish the release candidate from the latest published download.
+8. Preserve the native right-click menu, text selection, keyboard shortcuts,
+ and browser developer tools. Hiding them does not protect application code
+ or document data; security checks belong on the server, where applicable.
+
+## Palette
+
+| Token | Hex | Usage |
+| --- | --- | --- |
+| Ink | `#202b34` | Text, outlines and offset shadows |
+| Cool paper | `#f0f3f1` | Page background |
+| Warm paper | `#fffefa` | Panels and inputs |
+| Coral | `#f7a088` | Main actions and emphasis |
+| Yellow | `#f8d77b` | Badges and hero art |
+| Mint | `#a8d6c8` | Review and success accents |
+
+The homepage source is `website/`; the Java app's theme override is
+`src/main/resources/static/retro.css`. The generated browser demo reuses the
+same stylesheet. Keep the HTML fields/IDs and JavaScript adapters aligned so
+styling changes cannot break document import, FAQ review or PDF/DOCX export.
+
+Do not copy Shepherd.js logos, illustration assets, text, or code; Doc2FAQ's
+visuals and mascot are original work inspired only by the broad illustrated,
+outlined presentation style.
+
+## Local build
+
+```sh
+node scripts/build-website.mjs
+node scripts/verify-website-build.mjs
+```
+
+This produces ignored `website-dist/` with:
+
+- `index.html` — marketing/product homepage
+- `styles.css` — homepage design tokens and components
+- `demo/` — fully interactive browser demo generated from shared app sources
+
+Upload **the contents** of `website-dist/` to `/projects/doc2faq/` on the
+portfolio host. Leave the rest of the portfolio intact. The root homepage and
+demo use relative URLs to work under the `/projects/doc2faq/` subdirectory.
diff --git a/docs/RELEASING.md b/docs/RELEASING.md
index 03623c9..8214350 100644
--- a/docs/RELEASING.md
+++ b/docs/RELEASING.md
@@ -4,12 +4,12 @@
## Validate the release candidate
-Use JDK 21+, Maven 3.6.3+, and Node.js. CI compiles the application for Java 21 and runs Java tests plus a packaged-JAR smoke check on JDKs 21 through 27. All matrix jobs must succeed before publication:
+Use JDK 21+, Maven 3.6.3+, and Node.js. CI compiles the application for Java 21 and runs Java tests plus a packaged-JAR smoke check on **JDKs 21 through 26**. All six required matrix jobs must succeed before publication. Java 27 is outside this **required** release matrix, but an exploratory [Java 27 CI rerun on November 8, 2026](https://github.com/OKAN-SOFTWARE/Doc2FAQ/actions/runs/42477058195) **passed the Maven build, tests, and packaged-JAR smoke check**. The earlier attempt failed only while downloading the JDK (HTTP 504). Consider formal Java 27 support separately in a later release:
```cmd
mvn -B -ntp clean verify
node --test src/test/js/*.test.cjs
-node scripts/build-web-demo.mjs
+node scripts/build-website.mjs
java -jar target/doc2faq-0.5.0.jar
```
@@ -34,6 +34,7 @@ GitHub Actions runs this smoke check on every push/PR. On Windows without Bash,
9. Import `TEst Data.pdf`, check the suggested `testdata-Doc2FAQ.pdf`, rename it, and verify the download. Check centered titles, PDF Lato fonts, page numbers, long answers/tokens, Unicode, and page breaks. Unsupported PDF glyphs must suggest Word. HTML must escape document markup.
10. Change source/mode, clear the document, or open another project during pending imports/drafting/exports. Old results must not replace or download current work. Check keyboard focus, mobile widths, and reduced motion.
11. Follow [WEB-DEMO.md](WEB-DEMO.md) for the separate static browser demo; verify its feature disclosure and website links. The demo cannot substitute for packaged-JAR testing.
+12. Review the [approved homepage design](DESIGN-SYSTEM.md), including responsive layouts, readable focus states, reduced-motion behavior, and the **What's new / v0.5.0 preview** release-notes area. Confirm that an unreleased candidate is not presented as a published download. Keep browser context menus and developer tools available; browser UI restrictions are not an application security control.
## Prepare v0.5.0
@@ -48,7 +49,7 @@ GitHub Actions runs this smoke check on every push/PR. On Windows without Bash,
git rev-parse HEAD
mvn -B -ntp clean verify
node --test src/test/js/*.test.cjs
- node scripts/build-web-demo.mjs
+ node scripts/build-website.mjs
java -jar target/doc2faq-0.5.0.jar
```
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index f87bc42..9436c31 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -44,7 +44,7 @@ Similar-question warnings use word overlap. Keep both retains distinct passages;
The source build reads schemas 1 and 2, and saves schema 2. Older v0.4 apps reject schema-2 files. Keep an original project copy if you also use an older release. The limit remains 4 MB.
-Optional browser autosave is off by default. Enabling it stores source and drafts on that browser origin. Clear saved data removes the stored copy while leaving the open workspace intact. Clear document removes source, drafts, pending results, and the saved copy. Download a project when browser storage is unavailable.
+Optional browser autosave is off by default. Enabling it stores source and drafts on that browser origin. The prominently highlighted **Clear saved data** button asks for confirmation; cancel keeps the browser copy, while confirmation removes it and leaves the open workspace intact. Clear document removes source, drafts, pending results, and the saved copy. Download a project when browser storage is unavailable.
## Export
diff --git a/docs/WEB-DEMO.md b/docs/WEB-DEMO.md
index 57d8522..f7878fc 100644
--- a/docs/WEB-DEMO.md
+++ b/docs/WEB-DEMO.md
@@ -1,6 +1,6 @@
# Browser demo and website deployment
-The public-facing route is `https://okantiktas.com/projects/doc2faq/`. The portfolio's existing singular convention, `/project/doc2faq/`, redirects to it. The homepage's Doc2FAQ card links to the interactive demo.
+The public-facing homepage route is `https://okantiktas.com/projects/doc2faq/`. Its **interactive demo** lives at `https://okantiktas.com/projects/doc2faq/demo/`. The portfolio's existing singular convention, `/project/doc2faq/`, redirects to the product homepage. The approved light, outlined, illustrated design is documented in [DESIGN-SYSTEM.md](DESIGN-SYSTEM.md).
## What visitors can try
@@ -16,9 +16,9 @@ The public-facing route is `https://okantiktas.com/projects/doc2faq/`. The portf
| PDF/DOCX import and export | Yes; browser PDF.js / Word XML extraction and PDF-LIB / docx exports | Yes; PDFBox / Apache POI |
| OCR, external AI, accounts/billing | No | Not implemented |
-PDF and Word files are processed on the visitor's device. PDF import is limited to 5 MB, 50 pages and 50,000 extracted characters; DOCX import is limited to 5 MB, 50,000 extracted characters, 1,000 archive entries and an 8 MB expanded main XML part. Legacy `.doc`, password-protected PDFs and image-only/scanned PDFs are not supported. Word extraction reads main-document paragraphs and tables, mapping standard Heading 1–6 styles to Markdown section headings (levels 2–6) so the generator treats Heading 1 as a section rather than a document title. Headers/footers, images and complex layouts may not be retained. Browser PDF export uses Helvetica with Latin character coverage; unsupported characters produce an error recommending Word, never silently dropped text. Word exports preserve Unicode. Both exports include every FAQ regardless of filters and optionally include source references.
+PDF and Word files are processed on the visitor's device. PDF import is limited to 5 MB, 50 pages and 50,000 extracted characters; DOCX import is limited to 5 MB, 50,000 extracted characters, 1,000 archive entries and an 8 MB expanded main XML part. Legacy `.doc`, password-protected PDFs and image-only/scanned PDFs are not supported. Word extraction reads main-document paragraphs and tables, mapping standard Heading 1–6 styles to Markdown section headings (levels 2–6) so the generator treats Heading 1 as a section rather than a document title. Headers/footers, images and complex layouts may not be retained. Browser PDF export **embeds bundled DejaVu Sans regular/bold font subsets** for Turkish, extended Latin, IPA and many mathematical symbols. If a font still cannot display a character, the app recommends Word rather than silently dropping it. Word exports preserve Unicode. Both exports include every FAQ regardless of filters and optionally include source references.
-The demo demonstrates the review workflow; it is not a hosted Java backend and does not claim identical generation results. Its development features may be newer than the latest published JAR. No pasted text is sent to an FAQ server. Optional autosave and downloaded project files contain source text; clear saved data on shared devices. Normal website hosting still serves the page and assets. The document adapter lazily loads version-pinned PDF.js, PDF-LIB, JSZip and docx code from jsDelivr. An internet connection is needed on first use; load failures leave the workspace available for retry. See [third-party libraries](../web-demo/THIRD-PARTY.md).
+The demo demonstrates the review workflow; it is not a hosted Java backend and does not claim identical generation results. Its development features may be newer than the latest published JAR. No pasted text is sent to an FAQ server. Optional autosave and downloaded project files contain source text; clear saved data on shared devices. Normal website hosting still serves the page and assets. The document adapter lazily loads version-pinned PDF.js, PDF-LIB, fontkit, JSZip and docx code from jsDelivr. DejaVu fonts are **served by the same website**, not downloaded from a third-party font service. An internet connection is needed on first use; load failures leave the workspace available for retry. See [third-party libraries](../web-demo/THIRD-PARTY.md).
## Build and keep it synchronized
@@ -26,18 +26,20 @@ Run from the repository root with Node.js:
```sh
node --test src/test/js/*.test.cjs
-node scripts/build-web-demo.mjs
+node scripts/build-website.mjs
```
-The builder creates ignored `demo-dist/` from the actual app frontend, shared project validator, stylesheet, and background. It replaces backend requests with the browser-only adapter in `web-demo/engine.js`, routes document import and binary export through `web-demo/documents.js`, keeps PDF/Word options available, uses relative asset paths with content-hash cache versions, fills the source-build version, and adds demo disclosure and homepage/source/download links. It does not modify the Java application's served UI.
+The builder creates ignored `demo-dist/` from the actual app frontend, shared project validator, stylesheet, and background. It replaces backend requests with the browser-only adapter in `web-demo/engine.js`, routes document import and binary export through `web-demo/documents.js`, keeps PDF/Word options available, uses relative asset paths with content-hash cache versions, fills the source-build version, and adds demo disclosure and homepage/source/download links. The approved retro theme also styles the Java application's UI without changing its JavaScript behavior.
-Copy the complete contents of `demo-dist/` into `projects/doc2faq/` in the private `OKAN-SOFTWARE/okantiktas.com` repository. Preserve relative filenames. Update the Doc2FAQ homepage card to the demo route. Keep `/project/doc2faq/index.html` as a redirect with a visible fallback link. Do not replace the rest of the portfolio or its activity scripts.
+The `build-website.mjs` step then creates ignored `website-dist/` containing the approved **Doc2FAQ marketing homepage** and the working, equally styled `demo/` subdirectory. It rewrites navigation to relative URLs that are valid under `/projects/doc2faq/`.
+
+Copy the complete contents of **`website-dist/`** into `projects/doc2faq/` in the private `OKAN-SOFTWARE/okantiktas.com` repository. Preserve the `demo/` subdirectory and relative filenames. The Doc2FAQ homepage card may link to the new product homepage. Keep `/project/doc2faq/index.html` as a redirect with a visible fallback link. Do not replace the rest of the portfolio or its activity scripts.
The portfolio is hosted through its existing GitHub-main → Hostinger deployment. Pushing source and observing a live deployment are separate steps: verify the actual custom-domain route after deployment before reporting it live. Do not deploy the full Java app to static hosting or change DNS to add this subdirectory.
## Verify before publishing
-- Both demo routes resolve; homepage, source, and download links work.
+- Both homepage routes and the `/projects/doc2faq/demo/` application resolve; source, download and return links work.
- Examples and pasted TXT/Markdown work in both modes; text source references match exactly.
- Source highlights remain stable while typing, including on mobile.
- Approvals persist after save/open and optional autosave/reload; edits/merges reset approval.
diff --git a/scripts/build-web-demo.mjs b/scripts/build-web-demo.mjs
index 43f5756..79bab07 100644
--- a/scripts/build-web-demo.mjs
+++ b/scripts/build-web-demo.mjs
@@ -1,4 +1,4 @@
-import {readFile, writeFile, mkdir, copyFile} from 'node:fs/promises';
+import {readFile, writeFile, mkdir, copyFile, cp} from 'node:fs/promises';
import {fileURLToPath} from 'node:url';
import path from 'node:path';
import {createHash} from 'node:crypto';
@@ -12,21 +12,22 @@ let html = await readFile(path.join(source, 'index.html'), 'utf8');
html = html.replaceAll('@project.version@', version)
.replace('content="Doc2FAQ extracts existing Q&A or builds source-grounded FAQ suggestions from TXT, Markdown, PDF, and DOCX. Review, edit, and export."', 'content="Try Doc2FAQ in your browser: draft FAQs from text, inspect sources, edit, approve, and export."')
.replaceAll('href="/styles.css"', 'href="./styles.css"')
+ .replaceAll('href="/retro.css"', 'href="./retro.css"')
.replaceAll('src="/project.js"', 'src="./project.js"')
.replace('src="/app.js"', 'src="./app.js"')
.replace('src="/background.js"', 'src="./background.js"')
.replace('\n \n
@@ -19,14 +23,15 @@
D✳Doc2FAQ
- PREVIEW · LOCAL WORKSPACE
+
+ LOCAL JAVA WORKSPACE
-
DOCUMENT INTELLIGENCE / PREVIEW @project.version@
-
Turn long documents into clear answers.
+
✳ WELCOME TO DOC2FAQ / @project.version@
+
LET’S TURN PAGES INTO ANSWERS.
Extract questions already in your document or turn ordinary sections into source-grounded FAQ suggestions. Review, edit, and export your draft.
✦ No account✦ No AI calls✦ Review before sharing
@@ -58,7 +63,7 @@
Turn long documents into clear answers.
Save work keeps your source and edits for later. Use Download for the finished FAQ.
-
+
Autosave off
diff --git a/src/main/resources/static/retro.css b/src/main/resources/static/retro.css
new file mode 100644
index 0000000..fc9fc1a
--- /dev/null
+++ b/src/main/resources/static/retro.css
@@ -0,0 +1,83 @@
+/* Concept skin. The existing Doc2FAQ browser engine and workflow stay intact. */
+:root{color-scheme:light;--bg:#f0f3f1;--surface:#fffefa;--surface2:#fff7e6;--line:#26333b;--muted:#5e6a72;--text:#202b34;--mint:#ec9179;--gold:#b76549;--danger:#a43232;font-family:'DM Sans',system-ui,sans-serif}
+html{scroll-behavior:smooth}body{background:#f0f3f1;color:var(--text)}body::before,#ambient-background{display:none!important}
+button,input,select,textarea{font-family:'DM Sans',system-ui,sans-serif}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,a:focus-visible{outline:3px solid #dc7054;outline-offset:3px}
+.app-shell{max-width:1330px;padding:0 34px}.site-header{height:92px;border-bottom:3px solid var(--line);gap:20px}.brand{font-family:'Space Grotesk',sans-serif;font-size:27px;font-weight:700;letter-spacing:-.075em;color:var(--text)}.brand-mark{background:#f9d77b;color:var(--text);border:2px solid var(--line);border-radius:8px;box-shadow:3px 3px 0 var(--line);transform:rotate(-5deg)}.brand-accent{color:#e9866d}
+.retro-nav{margin-left:auto;display:flex;gap:24px;align-items:center;font-size:12px;font-weight:900;letter-spacing:.02em}.retro-nav a{text-decoration:none}.retro-nav a:hover{text-decoration:underline;text-underline-offset:4px}.preview-pill{border:2px solid var(--line);border-radius:7px;background:#f8d77b;color:var(--text);font-size:10px;font-weight:900;box-shadow:3px 3px 0 var(--line)}.signal-dot{background:#6dc59b;box-shadow:none;border:1px solid var(--line)}
+.hero{padding:66px 0 76px;gap:70px;grid-template-columns:1.15fr .85fr}.hero .eyebrow{color:#ad634f;font-weight:900;letter-spacing:.12em;margin-bottom:17px}.hero h1{font-family:'DM Sans',sans-serif;font-size:clamp(49px,5.5vw,80px);font-weight:900;letter-spacing:-.08em;line-height:1.06;max-width:740px}.hero h1 em{color:#df806b;font-weight:900}.hero-copy{color:#54636b;font-size:16px;line-height:1.7;margin:22px 0}.hero-chips{color:#3d4e55;font-size:12px;font-weight:700}.hero-chips span{display:inline-flex;align-items:center;background:#e0e9e4;border:1px solid #b5c4bf;padding:7px 11px;border-radius:20px}
+.hero-art{border:3px solid var(--line);border-radius:12px;background:#f9d777;box-shadow:11px 12px 0 var(--line);transform:rotate(3deg);padding:25px}.art-top,.art-bottom{color:#344851}.art-skeleton{background:#d9b85f}.art-question,.art-answer{border:2px solid var(--line);border-radius:7px;background:#fffefa;color:var(--text);font-weight:700;box-shadow:3px 3px 0 #202b3423}.art-answer{background:#b4dbd0;color:var(--text)}.art-question span{background:#f9b19d;color:var(--text);border:1px solid var(--line)}.art-answer span{background:#f8e2a3;color:var(--text);border:1px solid var(--line)}.art-bottom{border-color:#9e8345}.art-bottom span:last-child{color:#344851}
+.demo-links{gap:12px;margin:0 0 18px}.demo-disclosure{color:#68737c;font-size:12px}.demo-disclosure a{color:#995f4e;text-decoration:underline}
+.workspace{border-top:3px solid var(--line);padding-top:44px}.workspace-heading{margin-bottom:27px}.workspace-heading .eyebrow{color:#a85e4a}.workspace h2{font-family:'DM Sans',sans-serif;font-size:35px;font-weight:900;letter-spacing:-.07em}.step-number{color:#67747a;font-weight:900}
+.project-toolbar{border:2px solid var(--line);border-radius:10px;background:#fbe7b9;padding:21px;gap:12px;box-shadow:4px 5px 0 var(--line)}.project-name input,.mode-select,.export-select,.result-actions input,.review-filters select,.review-status-select,.faq-edit-question,.faq-edit-answer,.download-name-field,input#search-input,textarea{background:#fffefa;color:var(--text);border:2px solid #374751;border-radius:7px}.project-name input{min-height:42px}.project-toolbar .field-label{color:#34434c}.save-work-help,.autosave-controls,.field-help{color:#66727b}.autosave-controls input,.export-options input{accent-color:#e68b73}
+.panel{border:3px solid var(--line);border-radius:12px;background:#fffefa;box-shadow:7px 8px 0 var(--line);padding:27px}.workspace-grid{gap:29px;margin-bottom:32px}.step-label{display:inline-block;background:#f5d97b;padding:6px 10px;border:2px solid var(--line);border-radius:5px;color:var(--text);font-weight:900}.panel h3{font-size:23px;font-weight:900;letter-spacing:-.055em;margin-top:14px}.panel-intro,.mode-description{color:#53636c;font-size:12px}.panel-intro code{color:#8d4f3f;background:#fcead6}.file-symbol{color:#df806b}.field-label{color:#37454d;font-weight:900}.input-meta,.source-filename{color:#66737b}textarea::placeholder{color:#8a969b}
+.button{border:2px solid var(--line);border-radius:7px;padding:11px 14px;font-weight:900;box-shadow:2px 3px 0 var(--line);transition:transform .18s,box-shadow .18s}.button:not(:disabled):hover{transform:translate(1px,1px);box-shadow:1px 2px 0 var(--line)}.button-primary{background:#f69e84;color:var(--text)}.button-primary:hover{background:#ffa98e}.button-secondary{background:#fffefa;color:var(--text)}.button-secondary:hover{background:#f4e5c5}.button-quiet{color:#7f493d;box-shadow:none;border:0;background:transparent;padding:7px 8px}.button:disabled{box-shadow:none;opacity:.44}.status{color:#59666d}.status.success{color:#247554}.status.error{color:#a43232}
+/* Give autosave and helper text the requested high-contrast treatment. */
+.autosave-controls,.field-help{color:#000}
+/* This is a destructive browser-storage action, not a subtle text link. */
+#clear-saved-btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;min-height:43px;padding:5px 5px;border:2px solid #8d3b32;border-radius:8px;background:#ffe0d9;color:#131313;font-size:12px;font-weight:900;box-shadow:3px 3px 0 #7d2d24;white-space:nowrap}
+#clear-saved-btn::before{content:"✕";display:inline-grid;place-items:center;width:22px;height:22px;border-radius:50%;background:#a53932;color:#fff;font-size:13px;font-weight:900}
+#clear-saved-btn:not(:disabled):hover{background:#ffc9bd;box-shadow:2px 2px 0 #8d3b32;transform:translate(1px,1px)}
+.count{color:var(--text);background:#add8ca;border:2px solid var(--line);border-radius:6px;font-weight:900}.result-actions input{color:var(--text);background:#fffefa}.result-actions input:disabled{opacity:.5}.review-toolbar .button{color:#874b3f}.export-options{color:#5e6a72}.empty-state{border:2px dashed #8c9c9d;border-radius:8px;background:#f9f8f0}.empty-icon{background:#f9d779;border:2px solid var(--line);color:var(--text);box-shadow:3px 3px 0 var(--line)}.empty-state p{color:#647179}
+.faq-editor,.faq-item{border:2px solid var(--line);border-radius:8px;background:#f5f8f5}.faq-editor-index{color:#925643;font-weight:900}.faq-editor .field-label{color:#3e4b52}.editor-action{background:#fffefa;color:var(--text);border:1px solid #374751;border-radius:5px}.editor-action:hover{background:#fce5bd}.editor-action.editor-remove{color:#ad3434}.faq-edit-question{font-weight:750}.faq-edit-answer{color:var(--text)}.faq-item p{color:#52616a;border-color:#bdc9c6}.faq-item summary::after{color:#e08b74}
+.review-filters .field-label{color:#303e47}.source-badge{color:#8d4e33}.source-details,.similar-review{border-color:#cbd4d1}.source-details summary,.similar-review summary{color:#89513f}.source-details pre,.source-viewer-content{color:#3d4c54;background:#f2f2eb}.source-viewer{background:#f4f5ec;border:2px solid var(--line)}.source-line-number{color:#78868b}.source-line-selected{background:#bdebdc;border-color:#619c86}.source-line mark{color:#244538}.download-name-field{background:#fffefa;color:var(--text);border:2px solid var(--line)}.download-name-field input{color:var(--text)}.download-name-field span{color:#925440;border-color:#a9b5b1}
+.footnote{border:2px solid var(--line);background:#e0eae3;border-radius:10px;margin:44px 0 60px}.footnote-icon{color:var(--text);border:2px solid var(--line);font-weight:900}.footnote p{color:#4f5d63}.footnote strong{color:var(--text)}.site-footer{border-top:3px solid var(--line);color:#647078;font-weight:800;padding-bottom:45px}.site-footer a{text-decoration:underline;text-underline-offset:3px}
+#export-preview{background:#fffefa;color:var(--text);border:3px solid var(--line);border-radius:12px;box-shadow:9px 10px 0 var(--line)}#export-preview::backdrop{background:#23313ab3}.preview-note{color:#526069}#preview-content{border:2px solid #d6deda}
+@media(max-width:1100px){.retro-nav{gap:12px}.preview-pill{font-size:9px}.hero{gap:35px}.panel{padding:22px}.workspace-grid{gap:21px}}
+@media(max-width:800px){.app-shell{padding:0 22px}.site-header{height:auto;min-height:89px;flex-wrap:wrap;padding:15px 0}.preview-pill{order:3;width:100%;justify-content:center}.hero{grid-template-columns:1fr;gap:40px}.hero-art{max-width:520px}.workspace-grid{grid-template-columns:1fr}.results-list{max-height:none}}
+@media(max-width:520px){.app-shell{padding:0 16px}.brand{font-size:24px}.retro-nav{font-size:10px;gap:10px}.hero{padding:52px 0}.hero h1{font-size:47px}.hero-copy{font-size:14px}.hero-art{display:none}.workspace h2{font-size:28px}.panel{padding:17px;box-shadow:4px 5px 0 var(--line)}.result-actions{display:grid;grid-template-columns:1fr 1fr}.result-actions input{grid-column:1/-1}.result-actions .export-select{grid-column:1/-1}.result-actions button{white-space:normal}.field-label{letter-spacing:.08em}.review-filters{align-items:flex-start}.review-toolbar{gap:6px}}
+@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.button{transition:none}}
+/* Mobile workspace: prevent horizontal scrolling and keep every control thumb-friendly. */
+@media(max-width:650px){
+ .site-header{display:grid;grid-template-columns:minmax(0,1fr);gap:12px;height:auto;padding:17px 0}
+ .retro-nav{margin-left:0;display:flex;justify-content:flex-start;flex-wrap:wrap;gap:12px 20px;line-height:1.5}
+ .retro-nav a{min-height:34px;display:inline-flex;align-items:center}
+ .preview-pill{order:initial;width:100%;min-height:38px;justify-content:center}
+ .hero h1{font-size:clamp(38px,10vw,53px);overflow-wrap:anywhere}
+ .hero-chips{gap:8px}
+ .workspace-heading{align-items:start;gap:9px}
+ .workspace-heading .step-number{flex-shrink:0}
+ .project-toolbar{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px;padding:17px}
+ .project-name,.save-work-help,.autosave-controls{grid-column:1/-1;min-width:0}
+ #save-project-btn,#open-project-btn{width:100%;min-width:0;white-space:normal}
+ .autosave-controls{display:grid;grid-template-columns:minmax(0,1fr);gap:11px}
+ .autosave-controls label{min-width:0}
+ #clear-saved-btn{width:100%;min-height:44px;white-space:normal}
+ #save-status{overflow-wrap:anywhere}
+ .panel,.project-toolbar,.workspace-grid{min-width:0}
+ .button,.editor-action,.mode-select,.export-select,.review-filters select,.review-status-select{min-height:44px}
+ .actions{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:10px}
+ .actions #generate-btn{grid-column:1/-1;min-width:0}
+ .actions #choose-file-btn,.actions #sample-btn{min-width:0;white-space:normal}
+ .result-actions{grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:9px}
+ .result-actions input,.result-actions .export-select{grid-column:1/-1;width:100%;min-width:0}
+ .result-actions button{min-width:0;white-space:normal;padding:10px}
+ .review-filters{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px}
+ .review-filters .field-label{grid-column:1/-1}
+ .review-filters select{width:100%;min-width:0}
+ .review-filters .button{white-space:normal}
+ .review-toolbar{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:8px}
+ .review-toolbar .button{min-width:0;white-space:normal}
+ .export-options{align-items:stretch;flex-direction:column;gap:10px}
+ .export-options .button{width:100%}
+ .faq-editor{padding:12px}
+ .faq-editor-heading{display:grid;grid-template-columns:minmax(0,1fr);gap:10px}
+ .faq-editor-actions{flex-wrap:wrap;gap:7px}
+ .editor-action{min-height:42px}
+ .source-viewer{min-width:0}
+ .input-meta{gap:8px;flex-wrap:wrap}
+ #export-preview{width:calc(100% - 20px);padding:15px}
+ #preview-content{padding:14px}
+ input[type=text],input[type=search],select,textarea{font-size:16px}
+}
+@media(max-width:370px){
+ .app-shell{padding:0 12px}
+ .brand{font-size:22px}
+ .panel{padding:14px}
+ .panel-top{gap:8px}
+ .hero h1{font-size:36px}
+ .workspace h2{font-size:clamp(25px,8vw,28px)}
+ .review-toolbar{grid-template-columns:minmax(0,1fr)}
+ .review-toolbar .button{width:100%}
+ .input-meta{font-size:10px}
+}
+
diff --git a/src/test/js/app-state.test.cjs b/src/test/js/app-state.test.cjs
index 12a2b3c..512a9d9 100644
--- a/src/test/js/app-state.test.cjs
+++ b/src/test/js/app-state.test.cjs
@@ -6,7 +6,7 @@ const assert = require("node:assert/strict");
const tools = require("../../main/resources/static/project.js");
const script = readFileSync(path.resolve(__dirname, "../../main/resources/static/app.js"), "utf8");
-function harness(saved = new Map(), blocked = false, fetcher = null) {
+function harness(saved = new Map(), blocked = false, fetcher = null, confirmer = () => true) {
const elements = new Map();
function node(tag = "") {
return {tag, value: "", checked: false, disabled: false, children: [], handlers: {}, textContent: "",
@@ -26,7 +26,7 @@ function harness(saved = new Map(), blocked = false, fetcher = null) {
removeItem(key) { if (blocked) throw new Error("Storage blocked"); saved.delete(key); }
};
const context = {Doc2FaqProject: tools, document: {getElementById: element, createElement: node},
- FormData: class { append() {} }, localStorage: storage, setTimeout(fn) { fn(); return 1; }, clearTimeout() {}, confirm: () => true,
+ FormData: class { append() {} }, localStorage: storage, setTimeout(fn) { fn(); return 1; }, clearTimeout() {}, confirm: confirmer,
fetch: fetcher || (async () => ({ok: true, json: async () => ({items: [{question: "Where is support?", answer: "Ask the team.",
source: {section: "Support", startLine: 1, endLine: 2, excerpt: "Q: Where is support?\nA: Ask the team."}}]})}))};
vm.runInNewContext(script, context);
@@ -75,6 +75,18 @@ test("disabling autosave removes stored text and the preference", async () => {
assert.equal(app.element("faq-count").textContent, "1 ITEM");
});
+test("canceling Clear saved data preserves the browser copy and current work", async () => {
+ const app = harness(new Map(), false, null, () => false);
+ await makeDraft(app);
+ app.element("autosave-toggle").checked = true;
+ app.element("autosave-toggle").handlers.change();
+ assert.equal(app.saved.has("doc2faq.project.v1"), true);
+ app.element("clear-saved-btn").handlers.click();
+ assert.equal(app.saved.has("doc2faq.project.v1"), true);
+ assert.equal(app.saved.get("doc2faq.autosave"), "true");
+ assert.equal(app.element("faq-count").textContent, "1 ITEM");
+});
+
test("blocked storage and an invalid saved project leave the app usable", async () => {
const app = harness(new Map(), true); await makeDraft(app);
app.element("autosave-toggle").checked = true; app.element("autosave-toggle").handlers.change();
diff --git a/src/test/js/browser-documents.test.cjs b/src/test/js/browser-documents.test.cjs
index ef071fe..8878a9d 100644
--- a/src/test/js/browser-documents.test.cjs
+++ b/src/test/js/browser-documents.test.cjs
@@ -55,6 +55,38 @@ test('PDF line wrapping preserves blank lines and splits long tokens within the
assert.equal(lines.slice(0,3).join(''),'a'.repeat(51));
});
+test('PDF export embeds custom Unicode fonts and retains Turkish and phonetic characters', async () => {
+ const drawn = [], embedded = [];
+ const font = {widthOfTextAtSize: text => [...text].length * 5};
+ const page = {drawText: text => drawn.push(text)};
+ const lib = {
+ rgb: () => ({}),
+ PDFDocument: {create: async () => ({
+ setTitle() {}, setCreator() {}, setAuthor() {},
+ registerFontkit(value) { assert.equal(typeof value.create, 'function'); },
+ async embedFont(bytes, options) { embedded.push({bytes, options}); return font; },
+ addPage() { return page; }, getPages() { return [page]; },
+ async save() { return new Uint8Array([37, 80, 68, 70]); }
+ })}
+ };
+ const fonts = {fontkit: {create: () => ({hasGlyphForCodePoint: () => true})}, regular: new Uint8Array([1]), bold: new Uint8Array([2])};
+ const pdf = await tools.exportPdf({title: 'İğdır', items: [{question: 'Türkçe?', answer: 'ışık, ş, ğ, ı, →, ŋ, ʒ'}]}, lib, fonts);
+ assert.equal(pdf.type, 'application/pdf');
+ assert.equal(embedded.length, 2);
+ assert.deepEqual(embedded.map(item => [...item.bytes]), [[1], [2]]);
+ assert.ok(embedded.every(item => item.options.subset === true));
+ assert.ok(drawn.join(' ').includes('İğdır'));
+ assert.ok(drawn.join(' ').includes('ışık, ş, ğ, ı, →, ŋ, ʒ'));
+ assert.match(tools.libraries.fontkit, /fontkit@1\.1\.1/);
+});
+
+test('PDF export reports an unsupported glyph instead of silently replacing it', () => {
+ const fonts = {fontkit: {create: () => ({hasGlyphForCodePoint: point => point !== 0x6c49})}, regular: new Uint8Array([1]), bold: new Uint8Array([2])};
+ const faq = {title: 'FAQ', items: [{question: 'What?', answer: 'The character 汉 must not disappear.'}]};
+ assert.throws(() => tools.checkPdfGlyphs(faq, fonts), /“汉” \(U\+6C49\).*Choose Word/);
+ assert.doesNotThrow(() => tools.checkPdfGlyphs({title: 'FAQ', items: [{question: 'Türkçe?', answer: 'İğdır → şı'}]}, fonts));
+});
+
test('Word Heading 1 becomes a section the browser generator can draft from', () => {
const {draft} = require('../../../web-demo/engine.js');
const text = tools.headingPrefix('Heading1') + 'Services\nOnboarding guides.\n' + tools.headingPrefix('Heading2') + 'Support\nAsk the team.';
diff --git a/web-demo/THIRD-PARTY.md b/web-demo/THIRD-PARTY.md
index b0cd69f..8452cf5 100644
--- a/web-demo/THIRD-PARTY.md
+++ b/web-demo/THIRD-PARTY.md
@@ -5,7 +5,9 @@ The website lazily downloads these version-pinned scripts from jsDelivr. It send
| Library | Version | Purpose | License |
| --- | --- | --- | --- |
| [Mozilla PDF.js](https://github.com/mozilla/pdf.js) | 5.6.205 | PDF text extraction and worker | Apache-2.0 |
-| [PDF-LIB](https://github.com/Hopding/pdf-lib) | 1.17.1 | Paginated PDF creation using standard Helvetica fonts | MIT |
+| [PDF-LIB](https://github.com/Hopding/pdf-lib) | 1.17.1 | Paginated PDF creation with embedded Unicode fonts | MIT |
+| [@pdf-lib/fontkit](https://github.com/Hopding/fontkit) | 1.1.1 | Embed custom PDF fonts (loaded lazily for PDF export) | MIT |
+| [DejaVu Sans](https://dejavu-fonts.github.io/) | Distribution-provided TTF | Bundled regular and bold Unicode PDF fonts; see `fonts/LICENSE-DejaVu.txt` | Bitstream Vera / DejaVu license |
| [JSZip](https://github.com/Stuk/jszip) | 3.10.1 | Read DOCX archive entries through bounded decompression | MIT (also offered under GPLv3) |
| [docx](https://github.com/dolanmiu/docx) | 9.6.1 | Generate editable Word documents | MIT |
diff --git a/web-demo/documents.js b/web-demo/documents.js
index 16b6b13..4741564 100644
--- a/web-demo/documents.js
+++ b/web-demo/documents.js
@@ -6,6 +6,9 @@
pdf: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@5.6.205/build/pdf.mjs',
worker: 'https://cdn.jsdelivr.net/npm/pdfjs-dist@5.6.205/build/pdf.worker.min.mjs',
pdfLib: 'https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js',
+ fontkit: 'https://cdn.jsdelivr.net/npm/@pdf-lib/fontkit@1.1.1/dist/fontkit.umd.min.js',
+ pdfRegular: './fonts/DejaVuSans.ttf',
+ pdfBold: './fonts/DejaVuSans-Bold.ttf',
zip: 'https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js',
docx: 'https://cdn.jsdelivr.net/npm/docx@9.6.1/dist/index.iife.js'
};
@@ -22,6 +25,16 @@
}));
return pending.get(url);
}
+ function loadFont(url) {
+ if (!pending.has(url)) pending.set(url, root.fetch(url).then(async response => {
+ if (!response.ok) throw new Error('PDF fonts could not load. Check your connection and try again.');
+ return new Uint8Array(await response.arrayBuffer());
+ }).catch(error => {
+ pending.delete(url);
+ throw new Error('PDF fonts could not load. Check your connection and try again.', {cause: error});
+ }));
+ return pending.get(url);
+ }
function validateInput(file) {
if (!file || !/\.(pdf|docx)$/i.test(file.name)) throw new Error('Choose a PDF or Word (.docx) file. Legacy .doc files are not supported.');
if (!file.size || file.size > MAX_FILE) throw new Error('PDF and DOCX files must be nonempty and no larger than 5 MB.');
@@ -159,10 +172,12 @@
}
function wrap(text, font, size, width) {
const result = [];
+ const advances = new Map();
for (const paragraph of text.replace(/\r\n?/g, '\n').split('\n')) {
let line = '', used = 0, lastSpace = -1;
for (const char of paragraph) {
- const advance = font.widthOfTextAtSize(char, size);
+ if (!advances.has(char)) advances.set(char, font.widthOfTextAtSize(char, size));
+ const advance = advances.get(char);
if (used + advance > width && line) {
if (lastSpace >= 0) { result.push(line.slice(0, lastSpace)); line = line.slice(lastSpace + 1); }
else { result.push(line); line = ''; }
@@ -174,11 +189,33 @@
}
return result;
}
- async function exportPdf(data, lib) {
+ function checkPdfGlyphs(data, fonts) {
+ const regular = fonts.fontkit.create(fonts.regular);
+ const bold = fonts.fontkit.create(fonts.bold);
+ const examined = [
+ [data.title || 'FAQ draft', bold],
+ ...data.items.flatMap(item => [[item.question, bold], [item.answer, regular], ...(data.includeSources ? [[references(item), regular]] : [])])
+ ];
+ for (const [text, face] of examined) {
+ for (const char of new Set(text)) {
+ const point = char.codePointAt(0);
+ if (point <= 32 || point === 127) continue;
+ if (!face.hasGlyphForCodePoint(point)) {
+ throw new Error(`PDF export cannot display “${char}” (U+${point.toString(16).toUpperCase()}) with the bundled font. Choose Word (.docx) to preserve it.`);
+ }
+ }
+ }
+ }
+ async function exportPdf(data, lib, fonts) {
validateExport(data);
+ checkPdfGlyphs(data, fonts);
const pdf = await lib.PDFDocument.create();
pdf.setTitle(data.title); pdf.setCreator('Doc2FAQ browser demo'); pdf.setAuthor('Doc2FAQ');
- const regular = await pdf.embedFont(lib.StandardFonts.Helvetica), bold = await pdf.embedFont(lib.StandardFonts.HelveticaBold);
+ pdf.registerFontkit(fonts.fontkit);
+ // DejaVu Sans covers Turkish, Latin Extended, IPA, symbols and quotation marks
+ // that standard PDF Helvetica/WinAnsi cannot encode. Embed only used glyphs.
+ const regular = await pdf.embedFont(fonts.regular, {subset: true});
+ const bold = await pdf.embedFont(fonts.bold, {subset: true});
const width = 595.28, height = 841.89, margin = 50, bottom = 58;
let page, y;
function newPage() { page = pdf.addPage([width, height]); y = height - margin; }
@@ -205,7 +242,7 @@
}
return new Blob([await pdf.save()], {type: 'application/pdf'});
} catch (error) {
- if (/WinAnsi|encode/i.test(error.message)) throw new Error('This PDF font cannot display a character in your text. Choose Word (.docx) to preserve it.');
+ if (/WinAnsi|encode|No glyph/i.test(error.message)) throw new Error('A character in your text is not supported by the PDF font. Choose Word (.docx) to preserve it.');
throw error;
}
}
@@ -239,13 +276,19 @@
}
if (url === '/api/faqs/export/pdf' || url === '/api/faqs/export/docx') {
const data = JSON.parse(options.body); validateExport(data);
- const blob = url.endsWith('/pdf') ? await exportPdf(data, await loadScript(libraries.pdfLib, 'PDFLib')) : await exportDocx(data, await loadScript(libraries.docx, 'docx'));
+ const blob = url.endsWith('/pdf') ? await (async () => {
+ const [lib, fontkit, regular, bold] = await Promise.all([
+ loadScript(libraries.pdfLib, 'PDFLib'), loadScript(libraries.fontkit, 'fontkit'),
+ loadFont(libraries.pdfRegular), loadFont(libraries.pdfBold)
+ ]);
+ return exportPdf(data, lib, {fontkit, regular, bold});
+ })() : await exportDocx(data, await loadScript(libraries.docx, 'docx'));
return {ok: true, blob: async () => blob};
}
throw new Error('Unsupported document operation.');
} catch (error) { return {ok: false, json: async () => ({message: error.message || 'Could not process this document.'})}; }
}
- const api = {request, validateInput, checkedText, validateZip, boundedEntry, headingPrefix, wordXmlText, extractDocx, extractPdf, validateExport, references, wrap, exportPdf, exportDocx, libraries};
+ const api = {request, validateInput, checkedText, validateZip, boundedEntry, headingPrefix, wordXmlText, extractDocx, extractPdf, validateExport, references, wrap, checkPdfGlyphs, exportPdf, exportDocx, libraries};
if (typeof module !== 'undefined' && module.exports) module.exports = api;
else root.Doc2FaqDocuments = api;
})(globalThis);
diff --git a/web-demo/fonts/DejaVuSans-Bold.ttf b/web-demo/fonts/DejaVuSans-Bold.ttf
new file mode 100644
index 0000000..426812b
Binary files /dev/null and b/web-demo/fonts/DejaVuSans-Bold.ttf differ
diff --git a/web-demo/fonts/DejaVuSans.ttf b/web-demo/fonts/DejaVuSans.ttf
new file mode 100644
index 0000000..fb0bd94
Binary files /dev/null and b/web-demo/fonts/DejaVuSans.ttf differ
diff --git a/web-demo/fonts/LICENSE-DejaVu.txt b/web-demo/fonts/LICENSE-DejaVu.txt
new file mode 100644
index 0000000..f17c558
--- /dev/null
+++ b/web-demo/fonts/LICENSE-DejaVu.txt
@@ -0,0 +1,45 @@
+DejaVu Fonts
+https://dejavu-fonts.github.io/
+
+Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved.
+Bitstream Vera is a trademark of Bitstream, Inc.
+DejaVu changes are in public domain.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of the fonts accompanying this license ("Fonts") and associated documentation
+files (the "Font Software"), to reproduce and distribute the Font Software,
+including without limitation the rights to use, copy, merge, publish,
+distribute, and/or sell copies of the Font Software, and to permit persons to
+whom the Font Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright and trademark notices and this permission notice shall
+be included in all copies of one or more of the Font Software typefaces.
+
+The Font Software may be modified, altered, or added to, and in particular
+the designs of glyphs or characters in the Fonts may be modified and
+additional glyphs or characters may be added to the Fonts, only if the fonts
+are renamed to names not containing either the words "Bitstream" or the word
+"Vera".
+
+This License becomes null and void to the extent applicable to Fonts or Font
+Software that has been modified and is distributed under the "Bitstream Vera"
+names.
+
+The Font Software may be sold as part of a larger software package but no
+copy of one or more of the Font Software typefaces may be sold by itself.
+
+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 BITSTREAM OR
+THE GNOME FOUNDATION 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 OR IN CONNECTION WITH THE USE OR OTHER DEALINGS IN THE FONT SOFTWARE.
+
+Except as contained in this notice, the names of Gnome, the Gnome Foundation,
+and Bitstream Inc., shall not be used in advertising or otherwise to promote
+the sale, use or other dealings in this Font Software without prior written
+authorization from the Gnome Foundation or Bitstream Inc., respectively. For
+further information, contact: fonts at gnome dot org.
diff --git a/website/index.html b/website/index.html
new file mode 100644
index 0000000..d4b407a
--- /dev/null
+++ b/website/index.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+ Doc2FAQ — Make sense of every document
+
+
+
+
+
+
+
A little less reading, a lot more understanding. TRY DOC2FAQ ✳
v0.5.0 PREVIEWRelease candidate — not published yet
+
+
+ ✦
HELLO, NEW LOOK.
A cheerful illustrated homepage and a matching, mobile-friendly workbench that keeps your document front and center.
+ ✓
REVIEW WITH CONFIDENCE.
See source highlights, approve answers, search your draft, and reopen saved projects for another pass.
+ ↓
TAKE IT WITH YOU.
Import PDF and Word files in the browser demo, then export as PDF, DOCX, Markdown, JSON, or HTML.
+
+
Java 21–26 is the required v0.5.0 test matrix. Java 27 passed a separate CI run; formal support can follow later. v0.4.0 is still the latest published download.