Word (.docx) documents as sites, via docx-preview

A site whose path is a local .docx renders as pages in the kiosk instead
of Chromium offering a download. kiosk-app/docview.js maps the file URL
to docview.html (alongside slideshow.js, through main.js's new
resolveLocalSite) and pushes the file's bytes into the page from the main
process; the page renders them with docx-preview + jszip, both pure-JS
npm dependencies of kiosk-app (so online installs, Advanced -> Upgrade
and the offline bundle all pick them up - no LibreOffice).

- Scaled to the screen width (capped at 160%), scrolls normally.
- Re-checked every minute; a changed file re-renders from the top.
- Word's Symbol/Wingdings private-use bullet glyphs (U+F0B7 etc.), which
  render blank without Microsoft fonts, are mapped to Unicode (•, ▪, ...).
- Missing/unreadable/oversized (>50 MB) files and a missing library show
  a message instead of a blank page. .doc/.odt aren't supported.

Tested with the real main.js on Electron 44.5.1 under Xvfb + Openbox:
heading, coloured bold run, image, bordered table, bullets and a page
break rendered; wheel scrolling works; replacing the file while running
re-rendered the new document within the refresh. Offline bundle rebuilt
and contains docx-preview and jszip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cjz8a58eB5P9sUweqd3NHm
This commit is contained in:
Claude
2026-10-03 14:57:10 +00:00
parent d4b75ca1b9
commit c54ae352a1
6 changed files with 209 additions and 7 deletions
+7 -3
View File
@@ -4,8 +4,11 @@ const fs=require('fs');
const path=require('path');
const os=require('os');
const crypto=require('crypto');
// Sites that are a local folder show as an image slideshow (slideshow.js).
// Local-file sites with their own viewer pages: a folder plays as an image
// slideshow (slideshow.js), a .docx renders as a Word document (docview.js).
const slideshow=require('./slideshow');
const docview=require('./docview');
function resolveLocalSite(url){return docview.resolveSiteUrl(slideshow.resolveSiteUrl(url));}
// Suppress EPIPE errors (happen when no terminal attached)
process.stdout.on('error',(e)=>{if(e.code!=='EPIPE')throw e;});
@@ -784,7 +787,7 @@ function attachView(i){
const tabIdx=viewIndexToTabIndex(i);
if(tabIdx>=0&&tabs[tabIdx]){
const configuredUrl=slideshow.resolveSiteUrl(tabs[tabIdx].url);
const configuredUrl=resolveLocalSite(tabs[tabIdx].url);
const currentUrl=views[i].webContents.getURL();
if(currentUrl&&!currentUrl.startsWith(configuredUrl)){
@@ -1336,7 +1339,7 @@ async function createWindow(){
url=u.toString();
}catch(e){}
}
url=slideshow.resolveSiteUrl(url);
url=resolveLocalSite(url);
const initialOrigin=new URL(t.url).origin;
@@ -1371,6 +1374,7 @@ async function createWindow(){
view.webContents.setAudioMuted(false);
slideshow.attach(view.webContents);
docview.attach(view.webContents);
view.webContents.loadURL(url);
view.webContents.on('did-finish-load',()=>{