Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 254 additions & 114 deletions DocDiff.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ python3 scripts/build.py # writes site/index.html, site/en/index.html and DocD
node scripts/check.mjs # invariants and tests
```

`scripts/check.mjs` verifies the Content Security Policy, the absence of external URLs and storage APIs, matching German and English strings, the search metadata of both language pages (title, canonical, hreflang, JSON-LD that matches the visible FAQ, sitemap, `llms.txt`), and runs the unit tests for the diff core (`scripts/test-core.mjs`) and the export builders (`scripts/test-export.mjs`). CI runs the same and also checks that the committed bundle is up to date.
`scripts/check.mjs` verifies the Content Security Policy, the absence of external URLs and storage APIs, matching German and English strings, the search metadata and web app manifests of both language pages (title, canonical, hreflang, JSON-LD that matches the visible FAQ, sitemap, `llms.txt`, `manifest.webmanifest`), and runs the unit tests for the diff core (`scripts/test-core.mjs`), the export builders (`scripts/test-export.mjs`), and app state guards (`scripts/test-app-state.mjs`). CI runs the same and also checks that the committed bundle is up to date.

Bundled libraries: [PDF.js](https://github.com/mozilla/pdf.js), [Mammoth.js](https://github.com/mwilliamson/mammoth.js), [jsdiff](https://github.com/kpdecker/jsdiff), and the [Geist](https://vercel.com/font) fonts. Versions and licenses are listed in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).

Expand Down
10 changes: 10 additions & 0 deletions docs/releases/v1.7.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# v1.7.0 — Accept all, numbers filter, two-file import and safer DOCX handling

- **Review workflow.** "Accept all open" accepts every open change in the comparison after a confirmation that names the count and scope (including filtered entries and every page of grouped headers and footers), and it is one undo step. A new "Numbers, currencies & percentages" filter isolates changes to digits, amounts and percentages, and once everything is reviewed the action offers the final Word export. Review tooltips explain accepting, restoring and undoing, and the stepper and floating review controls support the reading shortcuts.
- **Import.** Both file pickers accept two files at once: the first is the old version, the second the new one. Replacing a reviewed pair asks once instead of twice, an explicit load starts a clean baseline, and cancelling the prompt lets you pick the same file again. A swap button in the top bar exchanges the two versions.
- **Safer DOCX and PDF handling.** DOCX archives are read with fixed limits (4,096 parts, 64 MiB per part, 256 MiB in total) before import and source export. Original-format Word export refuses ambiguous table edits and offers a newly typeset document (`_Generated.docx`) instead. PDF pages and loading tasks are cleaned up after extraction.
- **Fixes.** Rejected header/footer changes keep their numeric differences and still stay grouped across pages; the `hidden` attribute always hides elements; dark-mode links use the readable ink colour; narrower laptop widths get tighter padding; the English pages ship an English web app manifest.

## Offline version

Download `DocDiff.html` below and open it from disk. It works without an internet connection.
57 changes: 57 additions & 0 deletions docs/review-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Review assessment and implementation

The previous review mixed reproducible defects, product proposals and unverified claims. This assessment records what was changed and what remains open. No numeric quality score or search-ranking improvement has been established.

## Implemented

- **DOCX safety:** ambiguous table-cell boundaries and edits to cells containing several nonempty paragraphs stop original-format rewriting. The user chooses whether to download a generated document before downloading it; its filename ends in `_Generated.docx`. Unchanged content keeps the existing source-preservation path.
- **Bounded DOCX archives:** a shared reader limits archives to 4,096 entries, 64 MiB per entry and 256 MiB total decompressed bytes. Import validates the archive before handing it to Mammoth; source export uses the same limits. These bounds reduce decompression risk; they do not guarantee a fixed peak memory usage.
- **PDF cleanup:** each extracted page is cleaned up and the loading task is destroyed after success or failure.
- **Review correctness:** rejected header/footer entries retain substantive numeric differences. They recompute their changed words from the rejected text, so a rejected group whose pages differ only in the page number stays one entry. Live modified entries already included changed-token evidence, so the previous blanket grouping diagnosis was too broad.
- **Session continuity:** switching language preserves edited/reviewed demos. Result exports no longer mark the session as saved. Oversized session downloads are blocked without clearing dirty state or allowing a pending destructive action.
- **Review workflow:** stepper and floating review controls support reading shortcuts. Batch acceptance explicitly names the whole-comparison scope, confirms the count, revalidates the selection and creates one undo step. After all entries are reviewed, the action offers final Word export.
- **Numeric filter:** changed digits, currency symbols and percentages can be isolated. This is a text filter, not a legal-risk classifier; dates or amounts spelled without digits may not match.
- **Import workflow:** both document pickers allow two files and use the existing drop order: first file is the old version, second is the new version. No age or filename heuristic silently reorders them. Cancelling the unsaved-work prompt clears the pickers, so the same file can be chosen again. Browsers without `DecompressionStream` keep importing DOCX text through Mammoth and only skip list numbering, as before.
- **UI:** direct toolbar swap reuses the existing guarded action; dark-mode links use the readable ink token; smaller laptop widths receive narrower sidebar and pane padding; decorative list markers are hidden from supported screen readers. Review tooltips explain acceptance, restoration and undoing a review.
- **Localized manifest:** English pages use an English manifest with `/en/` as its start URL and scope. Both manifests, their descriptions and icon files are checked. This corrects installation metadata; no SEO or AEO benefit is claimed.

## Rejected recommendations

- **Mark final Word export as a saved session:** it loses source files, review state and history. Only a `.docdiff` save establishes the saved point. Session history remains subject to its existing serialization budget.
- **Automatically infer old/new from timestamps or filenames:** such evidence can be misleading. Keep explicit ordering and make swapping easy.
- **Update sitemap timestamps on every build:** rebuilds alone do not establish that page content changed. Keep content dates intentional.
- **Delete assets solely because the application does not reference them:** external consumers may use published paths. No deletion was necessary for this task.
- **Add another landing preview:** the hero already demonstrates deleted and inserted text. Extra content needs a demonstrated usability benefit.
- **Promise ranking gains from keywords or `llms.txt`:** no ranking evidence was provided.

## Deferred proposals and limitations

- Moved-and-edited matching, paragraph split/merge alignment and expanded numbering grammar need representative fixtures and reconstruction checks before changing matching decisions.
- Structured cell editing and table rendering need persistent cell identities. The current safety fix refuses ambiguous source edits rather than claiming full table fidelity.
- Batch rejection needs a replacement model covering deleted rows and grouped changes; only batch acceptance is implemented.
- Notes, scoped search and suppressed-content inspection remain product proposals, not verified defects.
- Lazy mounting, virtualization and separate hosted assets need render-time/memory measurements and accessibility checks. Existing comparison-time figures do not establish DOM rendering performance.
- OCR, formatting comparison and reliable multi-column PDF reading remain outside the current feature set. Identical moves are detected; moved-and-edited content can appear as deletion plus insertion.

## Verification

Run `python3 scripts/build.py` and `node scripts/check.mjs`. Checks cover security/privacy invariants, both languages and manifests, worker parity, core diff behavior, export builders and application-state guards. New regression cases cover table refusal, fallback cancellation/naming, numeric filtering, session continuity and batch acceptance.

## Browser and office verification on 2026-11-03

Additional interactive checks used locally generated DOCX files with delivery and price changes, plus PDFs rendered from those files by LibreOffice.

- Two DOCX files selected in one picker loaded both versions and produced the expected two changes. This exposed a live `FileList` bug: clearing the picker after the first upload removed the second file. The intake now snapshots the selection before awaiting either upload.
- PDF/PDF import produced the expected two changed passages. Mixed PDF/DOCX import succeeded, but the compact PDF fixture merged several paragraphs, increasing alignment noise. Cross-format paragraph reconstruction remains a limitation.
- Batch acceptance confirmed the count and whole-comparison scope. Twenty demo entries were accepted with one undo snapshot; one keyboard undo restored all twenty open entries.
- The numeric filter reduced the demo list to fifteen entries and updated navigation counts. Accepting via the keyboard after clicking the stepper worked.
- Language switching kept review decisions. Saving and reopening a `.docdiff` file restored review counts, undo history and the original DOCX source.
- Replacing a saved pair revealed a second bug: review resets could make the second upload prompt again. Intake now guards the pair as one operation. Explicit loads establish a clean baseline even when the right text is unchanged. Reloading a reviewed, saved pair was rechecked without an intermediate dialog or stale dirty state.
- Direct toolbar swap reversed the document names without creating a dirty state on a fresh comparison. Replacing either document and changing either paste field now guards pending review work.
- Ambiguous table redline export displayed the fallback confirmation. Cancellation produced no new download; confirmation downloaded a file ending in `_Generated.docx`.
- Original-format final and tracked DOCX exports, plus the generated fallback, opened and rendered successfully in LibreOffice. Rendered pages were visually inspected: text, table columns and revision markings were present without clipping or overlap in these fixtures.
- Layout was checked at 1280×800 and 390×844 CSS pixels. The interactive comparison had no horizontal overflow. Because the preview screenshot endpoint timed out, visual inspection used local Chrome renders of captured DOM with the built CSS and fonts; the mobile capture used a 390-pixel iframe. These captures verify layout, not touch-device behavior.

The additional checks found and fixed the import and baseline issues above. Regression coverage now totals 15 app-state groups, alongside 426 export groups, 51 core tests and 18 worker comparisons. Build, checks and whitespace validation pass.

Microsoft Word, Safari, Firefox, physical mobile devices and interactive assistive technologies were not tested. These fixture checks do not establish fidelity for every Word feature or every PDF layout. The deferred proposals above remain separate work; this verification does not turn them into completed features.
6 changes: 5 additions & 1 deletion scripts/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
]
},
"asset_base": "",
"manifest_href": "manifest.webmanifest",
"og_locale": "de_DE",
"og_locale_alt": "en_US",
"og_image": SITE_URL + "og-image.png?v=3",
Expand All @@ -59,6 +60,7 @@
"url": SITE_URL + "en/",
"html_attrs": 'lang="en" data-page-lang="en"',
"asset_base": "../",
"manifest_href": "manifest.webmanifest",
"og_locale": "en_US",
"og_locale_alt": "de_DE",
"og_image": SITE_URL + "og-image-en.png?v=1",
Expand Down Expand Up @@ -222,8 +224,10 @@ def render_page(template: str, lang: str, i18n: dict, version: str) -> str:
)
alt_lang = "en" if lang == "de" else "de"
alt_href = (SITE_URL + "en/") if lang == "de" else SITE_URL
manifest_href = page.get("manifest_href", "manifest.webmanifest")
out = (
out.replace("__HTML_ATTRS__", page["html_attrs"])
out.replace('href="__ASSET_BASE__manifest.webmanifest"', f'href="{manifest_href}"')
.replace("__HTML_ATTRS__", page["html_attrs"])
.replace("__PAGE_URL__", page["url"])
.replace("__ASSET_BASE__", page["asset_base"])
.replace("__OG_LOCALE__", page["og_locale"])
Expand Down
35 changes: 29 additions & 6 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ for (const siteFile of ['CNAME', 'og-image.png', 'og-image-en.png', 'impressum.h
assert.equal(fs.readFileSync(path.join(root, 'site', 'CNAME'), 'utf8').trim(), 'docdiff.app', 'CNAME must be docdiff.app');

// 7. Search engines and assistants: both language pages, head metadata, structured data, crawl files
for (const siteFile of ['en/index.html', 'llms.txt', 'llms-full.txt', 'manifest.webmanifest', 'favicon.ico', 'apple-touch-icon.png']) {
for (const siteFile of ['en/index.html', 'llms.txt', 'llms-full.txt', 'manifest.webmanifest', 'en/manifest.webmanifest', 'favicon.ico', 'apple-touch-icon.png']) {
assert.ok(fs.existsSync(path.join(root, 'site', siteFile)), `Missing required site file: site/${siteFile}`);
}
const enHtmlPath = path.join(root, 'site', 'en', 'index.html');
Expand Down Expand Up @@ -207,12 +207,19 @@ for (const page of [
assert.equal(metaContent(head, 'name', 'application-name'), 'Doc Diff');
assert.ok(head.includes('<meta name="theme-color"'), `${where}: theme-color missing`);

// Same-site assets resolve from the page's folder (e.g. ../favicon.ico from /en/)
for (const asset of ['favicon.ico', 'apple-touch-icon.png', 'manifest.webmanifest']) {
// Same-site icon assets resolve from the page's folder (e.g. ../favicon.ico from /en/)
for (const asset of ['favicon.ico', 'apple-touch-icon.png']) {
const href = head.match(new RegExp(`href="([^"]*${asset.replace('.', '\\.')})"`))[1];
assert.equal(href, page.assetBase + asset, `${where}: ${asset} href`);
assert.ok(fs.existsSync(path.resolve(path.dirname(page.file), href)), `${where}: ${href} does not exist`);
}
// Manifest link: localized to each language folder (or valid same-site manifest)
const manifestHref = head.match(/href="([^"]*manifest\.webmanifest)"/)[1];
assert.ok(
manifestHref === 'manifest.webmanifest',
`${where}: unexpected manifest href ${manifestHref}`
);
assert.ok(fs.existsSync(path.resolve(path.dirname(page.file), manifestHref)), `${where}: ${manifestHref} does not exist`);

// Performance: the landing markup comes before the ~2.5 MB of inline scripts
// The one exception is the tiny theme switch that sets dark mode before the first paint.
Expand Down Expand Up @@ -294,9 +301,25 @@ for (const file of ['llms.txt', 'llms-full.txt']) {
assert.ok(fs.existsSync(target), `${file}: link ${m[0]} has no file in site/`);
}
}
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'site', 'manifest.webmanifest'), 'utf8'));
for (const icon of manifest.icons) {
assert.ok(fs.existsSync(path.join(root, 'site', icon.src)), `manifest icon ${icon.src} missing`);
for (const [lang, relPath] of [['de', 'manifest.webmanifest'], ['en', 'en/manifest.webmanifest']]) {
const manifestPath = path.join(root, 'site', relPath);
assert.ok(fs.existsSync(manifestPath), `Missing manifest: ${relPath}`);
const manifestData = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const route = lang === 'en' ? '/en/' : '/';
assert.equal(manifestData.start_url, route, `${relPath}: start_url`);
assert.equal(manifestData.scope, route, `${relPath}: scope`);
assert.equal(manifestData.lang, lang, `${relPath}: manifest.lang must be ${lang}`);
assert.equal(manifestData.name, 'Doc Diff', `${relPath}: manifest.name`);
assert.equal(manifestData.short_name, 'DocDiff', `${relPath}: manifest.short_name`);
assert.equal(manifestData.description, I18N[lang].metaDescription, `${relPath}: manifest.description must match i18n metaDescription`);
assert.equal(manifestData.display, 'standalone', `${relPath}: manifest.display`);
assert.equal(manifestData.theme_color, '#060606', `${relPath}: manifest.theme_color`);
assert.equal(manifestData.background_color, '#060606', `${relPath}: manifest.background_color`);
assert.ok(Array.isArray(manifestData.icons) && manifestData.icons.length >= 3, `${relPath}: manifest icons`);
for (const icon of manifestData.icons) {
const iconRel = icon.src.startsWith('/') ? icon.src.slice(1) : icon.src;
assert.ok(fs.existsSync(path.join(root, 'site', iconRel)), `${relPath}: manifest icon ${icon.src} missing`);
}
}
console.log('✓ SEO checks passed (de + en pages, metadata, JSON-LD FAQ, robots, sitemap, llms.txt).');

Expand Down
Loading
Loading