diff --git a/bin/build.sh b/bin/build.sh index 6681f6c..8d9fb80 100755 --- a/bin/build.sh +++ b/bin/build.sh @@ -114,7 +114,7 @@ else echo "Warning: $TOKENS_DIST not found, skipping token fingerprint." fi -node ./bin/offline-assets.mjs "$DIST_DIR" +# EditAllFile has no assistant/PWA; its host owns the asset lifecycle. # Inject the version stamps into sw.js. # diff --git a/index.ts b/index.ts index 135c32f..eef33c7 100644 --- a/index.ts +++ b/index.ts @@ -21,7 +21,6 @@ import { createControlPanel, hideControlPanel, hideLanding, showControlPanel } f import 'ranui/button'; import 'ranui/card'; import { initWebMcp } from './lib/web-mcp'; -import { createAgentEntry } from './lib/agent-entry'; import { hasUnsavedChanges, installUnsavedChangesGuard } from './lib/unsaved-guard'; import { initDocumentHistory } from './lib/history'; import './styles/base.css'; @@ -113,15 +112,7 @@ const documentUrl = file || src; // Pure preview mode: ?readonly=true (also accepts ?readonly=1 or bare ?readonly). // Opens the document with editing/download disabled (#25, #85, #87). const isReadonly = parseReadonly(readonly); -// A discoverable product entry; shared URL flags never opt a user in. -const agentEntry = createAgentEntry(); -const toggleAgentPanelLazy = (): void => agentEntry.toggle(); -(window as unknown as { __toggleAgentPanel?: () => void }).__toggleAgentPanel = toggleAgentPanelLazy; -window.addEventListener('message', (event: MessageEvent) => { - const frame = document.querySelector('#app iframe'); - if (event.origin === location.origin && event.source === frame?.contentWindow && event.data?.type === 'agent:toggle') - toggleAgentPanelLazy(); -}); +// EditAllFile ships the Office editors without the optional AI assistant. // Deep-link to a blank document: ?new=docx|xlsx|pptx opens the editor straight // into a new file (skipping the landing hero). The localized homepages use it — // e.g. /zh-CN/ links to `/?locale=zh-CN&new=docx`, so i18n (which reads ?locale) @@ -224,7 +215,8 @@ void (async () => { // "continue last time" line (public/history-recent.js) and /history. // Register Service Worker for PWA -if ('serviceWorker' in navigator) { +// EditAllFile owns the hosting lifecycle; embedded editors never register a PWA. +if ('serviceWorker' in navigator && window.parent === window) { // Update policy lives in lib/sw-update.ts: a new build's worker waits until // no document is open, then takes over and the page reloads once. const controllerAtBoot = navigator.serviceWorker.controller; diff --git a/lib/embed-api.ts b/lib/embed-api.ts index d17a808..e30eb48 100644 --- a/lib/embed-api.ts +++ b/lib/embed-api.ts @@ -3,12 +3,14 @@ import { clearDocumentSession } from './history/autosave'; import { beginDocumentOpen, commitDocumentOpen, isMountedDocumentOpen } from './document-open'; import { getDocmentObj, setDocmentObj } from '@ranuts/shared/store'; import { handleDocumentOperation, loadEditorApi } from './converter'; -import { openDocumentFromUrl } from './document'; +import { openDocumentFromUrl, onCreateNew } from './document'; import { getReadonlyMode, requestSaveDocument, setReadonlyMode } from './onlyoffice-editor'; import { captureSavePoint, commitSavePoint } from './onlyoffice/save-point'; +import { requireDocumentContentReady } from './onlyoffice/open-state'; import { hasUnsavedChanges, markDocumentSaved, onUnsavedChangesChange } from './unsaved-guard'; type EmbedMessageType = + | 'document:create' | 'document:open' | 'document:open-url' | 'document:open-file' @@ -181,11 +183,24 @@ async function handleMessage(event: MessageEvent): Promise { try { switch (message.type) { + // EditAllFile integration: create through the same origin-checked API. + case 'document:create': { + const ext = String(payload.extension || '').toLowerCase(); + if (!['docx', 'xlsx', 'pptx'].includes(ext)) throw new Error('Unsupported new document type'); + await onCreateNew('.' + ext); + postToParent('document:loading', {}, message.id); + await requireDocumentContentReady(); + postToParent('document:opened', { readonly: false }, message.id); + break; + } + case 'document:open': case 'document:open-url': case 'document:open-file': case 'document:open-buffer': await handleOpen(payload); + postToParent('document:loading', {}, message.id); + await requireDocumentContentReady(); postToParent('document:opened', { readonly: getReadonlyMode() }, message.id); break; @@ -276,7 +291,7 @@ export function initEmbedApi(): void { void handleMessage(event); }); - window.addEventListener('load', () => { - postToParent('document:ready'); - }); + // Locale modules may finish after load; always complete the host handshake. + if (document.readyState === 'complete') postToParent('document:ready'); + else window.addEventListener('load', () => postToParent('document:ready'), { once: true }); } diff --git a/lib/onlyoffice-editor.ts b/lib/onlyoffice-editor.ts index 223b986..7ef4fb7 100644 --- a/lib/onlyoffice-editor.ts +++ b/lib/onlyoffice-editor.ts @@ -195,7 +195,7 @@ function createPersonalEditorInstance(config: { : {}), help: false, // Keep About reachable for copyright, version, license and source - // information. Product marks are hidden separately; see NOTICE. + // information. Original ONLYOFFICE product marks remain visible. hideRightMenu: true, uiTheme: resolveUiTheme(), ...(isCompactViewport() ? compactViewportCustomization() : {}), @@ -425,7 +425,7 @@ export function createEditorInstance(config: { } export async function loadEditorApi(): Promise { - if (!window.DocsAPI && /^\/editor(?:\.html)?\/?$/.test(location.pathname)) await waitForAppWorkerControl(); + if (!isEmbedMode() && !window.DocsAPI && /^\/editor(?:\.html)?\/?$/.test(location.pathname)) await waitForAppWorkerControl(); return new Promise((resolve, reject) => { // Check if already loaded if (window.DocsAPI) { diff --git a/lib/onlyoffice/font-system.ts b/lib/onlyoffice/font-system.ts index bb5ebac..e303fdd 100644 --- a/lib/onlyoffice/font-system.ts +++ b/lib/onlyoffice/font-system.ts @@ -72,9 +72,10 @@ export const FONT_WAIT_PROBE = '__ooFontWaitMs'; */ export async function waitForFontSystem( win: FontSystemWindow, - options: { timeoutMs?: number; intervalMs?: number } = {}, + options: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal } = {}, ): Promise { const { timeoutMs = FONT_SYSTEM_WAIT_MS, intervalMs = FONT_SYSTEM_POLL_MS } = options; + options.signal?.throwIfAborted(); const record = (waited: number): void => { (win as Record)[FONT_WAIT_PROBE] = waited; }; @@ -85,6 +86,7 @@ export async function waitForFontSystem( let waited = 0; while (waited < timeoutMs) { await new Promise((resolve) => setTimeout(resolve, intervalMs)); + options.signal?.throwIfAborted(); waited += intervalMs; let ready: boolean; try { @@ -101,10 +103,20 @@ export async function waitForFontSystem( } } record(waited); - console.warn(`[OO] font system still not ready after ${timeoutMs} ms; importing without fonts`); + console.warn(`[OO] font system still not ready after ${timeoutMs} ms`); return false; } +/** Modern worker conversions must not silently discard a document's fonts. */ +export async function requireFontSystem( + win: FontSystemWindow, + options: { timeoutMs?: number; intervalMs?: number; signal?: AbortSignal } = {}, +): Promise { + if (!(await waitForFontSystem(win, { timeoutMs: 300_000, ...options }))) { + throw new Error('X2T module initialization timeout: font system did not become ready before conversion'); + } +} + /** * The dependency the vendor never declared. `fetchFonts` is awaited by the * open conversion (x2t_helper `_convertDocument`, shared with the export diff --git a/lib/onlyoffice/guards/about-source.ts b/lib/onlyoffice/guards/about-source.ts index e2c6529..e7c3dfe 100644 --- a/lib/onlyoffice/guards/about-source.ts +++ b/lib/onlyoffice/guards/about-source.ts @@ -1,18 +1,6 @@ -/** - * Guard 12: legal attribution for the modified editor, independent of product marks. - * The vendor's copyright and version remain; this adds the modified-version - * notice, warranty disclaimer, license and source links. See NOTICE for the - * decision to omit product branding. - * - * The pane is populated lazily -- `#about-menu-panel` exists from boot but is - * empty until the user opens it -- so this watches for the content to arrive - * instead of writing once. Appending inside the observed node re-enters the - * callback; the id check is what stops that after one pass. - * - * Legal text is additive; guards/chrome.ts hides only product branding. - */ +/** Add the matching project source and legal notices without hiding product marks. */ const NOTICE_ID = 'oo-source-notice'; -const SOURCE_URL = 'https://github.com/ranuts/document'; +const SOURCE_URL = 'https://editdocfree.com/source.html'; const WATCHED = '__ranSourceNoticeWatched'; function renderNotice(doc: Document, panel: HTMLElement): void { @@ -45,6 +33,7 @@ function renderNotice(doc: Document, panel: HTMLElement): void { const legal = doc.createElement('div'); for (const [label, href] of [ + ['Corresponding source and licenses', '/source.html'], ['License (AGPL-3.0)', '/LICENSE'], ['Copyright and modification notices', '/NOTICE'], ]) { diff --git a/lib/onlyoffice/guards/chrome.ts b/lib/onlyoffice/guards/chrome.ts index 42b6cce..0420bf3 100644 --- a/lib/onlyoffice/guards/chrome.ts +++ b/lib/onlyoffice/guards/chrome.ts @@ -6,9 +6,7 @@ import { COMPACT_VIEWPORT_MAX_WIDTH } from '../viewport'; * session this build cannot have -- and hide the right panel on phone-sized * viewports. There is no DocEditor config switch for either in this build. * - * Product marks are hidden independently of legal attribution. About remains - * reachable with the upstream copyright, version, license and source links. - * NOTICE records the branding decision and the FSF's interpretation of AGPLv3. + * Original product marks and the About legal notices remain visible. */ export function injectLocalChromeCss(doc: Document): void { if (!doc.getElementById('oo-local-chrome-css')) { @@ -21,8 +19,6 @@ export function injectLocalChromeCss(doc: Document): void { // cannot: the thumbnails panel and the SDK's own canvas geometry. style.textContent = [ '.btn-current-user, #tlb-box-users { display: none !important; }', - '#header-logo, .brand-logo, .asc-about-office, #id-about-company-logo { display: none !important; }', - '#id-about-licensor-logo .asc-about-version:not(#id-about-licensor-version-name) { display: none !important; }', `@media (max-width: ${COMPACT_VIEWPORT_MAX_WIDTH}px), (pointer: coarse) and (max-height: ${COMPACT_VIEWPORT_MAX_WIDTH}px) {`, ' [data-layout-name="rightMenu"] { display: none !important; }', '}', diff --git a/lib/onlyoffice/guards/font-loading.ts b/lib/onlyoffice/guards/font-loading.ts index b81eee9..6c23669 100644 --- a/lib/onlyoffice/guards/font-loading.ts +++ b/lib/onlyoffice/guards/font-loading.ts @@ -16,6 +16,7 @@ */ export function installFontLoadAcceleration(win: Window): boolean { const fontWin = win as unknown as { + AscCommon?: { baseEditorsApi?: { prototype?: { IsNeedDefaultFonts?: () => boolean } } }; Asc?: { editor?: { IsNeedDefaultFonts?: () => boolean; @@ -40,6 +41,10 @@ export function installFontLoadAcceleration(win: Window): boolean { }; __ooFontLoadAccelerated?: boolean; }; + // The base API lands before the full SDK creates the editor/FontLoader. + // Patch it now so the first document also skips unused default families. + const prototype = fontWin.AscCommon?.baseEditorsApi?.prototype; + if (typeof prototype?.IsNeedDefaultFonts === 'function') prototype.IsNeedDefaultFonts = () => false; const fontApi = fontWin.Asc?.editor; const loader = fontApi?.FontLoader; if (fontApi && loader && typeof loader.LoadDocumentFonts === 'function' && !fontWin.__ooFontLoadAccelerated) { diff --git a/lib/onlyoffice/guards/x2t-worker.ts b/lib/onlyoffice/guards/x2t-worker.ts index b95e2ad..e68dada 100644 --- a/lib/onlyoffice/guards/x2t-worker.ts +++ b/lib/onlyoffice/guards/x2t-worker.ts @@ -39,7 +39,8 @@ * same URLs off the same HTTP and Service Worker cache. */ -import { waitForFontSystem, type FontSystemWindow } from '../font-system'; +import { requireFontSystem, type FontSystemWindow } from '../font-system'; +import { getDocumentLifetimeSignal } from '../open-state'; /** How long an idle worker is kept before its heap is handed back. */ const IDLE_TERMINATE_MS = 30_000; @@ -171,7 +172,7 @@ class WorkerChannel { * is worse, because the document opens silently with no fonts at all (#146). */ async function fontSourcesWhenReady(win: FrameScope): Promise { - await waitForFontSystem(win as unknown as FontSystemWindow); + await requireFontSystem(win as unknown as FontSystemWindow, { signal: getDocumentLifetimeSignal() }); return fontSources(win); } @@ -337,6 +338,9 @@ export function installX2tWorkerProxy(win: Window): boolean { // Whatever could not be resolved here crosses as it came: the worker runs // the same handleFileData, so a form this does not know is still the // vendor's to interpret rather than ours to reject. + // Start the required download/compilation while the font catalog initializes. + // A rejected warmup is observed again by the real conversion's initialize(). + void channel.send('initialize', {}, []).catch(() => {}); const payload = (await toBytes(data)) ?? data; const result = (await channel.send( 'convertToBin', diff --git a/lib/onlyoffice/iframe-guards.ts b/lib/onlyoffice/iframe-guards.ts index 7c8de39..667abea 100644 --- a/lib/onlyoffice/iframe-guards.ts +++ b/lib/onlyoffice/iframe-guards.ts @@ -54,7 +54,7 @@ export function prepareEditorIframe(): boolean { const longActionGuarded = installLongActionLeakGuard(win); installSeriesSettingsGuard(win); - installFontLoadAcceleration(win); + const fontsAccelerated = installFontLoadAcceleration(win); installCommentSelectionGuard(win); installCanvasLossGuard(win, doc); installSingleUnloadPrompt(win); @@ -75,7 +75,8 @@ export function prepareEditorIframe(): boolean { longActionGuarded && x2tWorkerProxied && wasmBinaryHandled && - assistantFocusGuarded + assistantFocusGuarded && + fontsAccelerated ) { fullyApplied = true; } diff --git a/lib/onlyoffice/open-state.ts b/lib/onlyoffice/open-state.ts index 918506b..cacc59c 100644 --- a/lib/onlyoffice/open-state.ts +++ b/lib/onlyoffice/open-state.ts @@ -141,3 +141,11 @@ export function getDocumentOpenError(): string | null { export function getLastFrameError(): string | null { return lastFrameError; } + +/** A mounted editor is not an opened document. Embedded hosts need strict readiness. */ +export async function requireDocumentContentReady(timeoutMs = 330_000): Promise { + if (documentOpenError) throw new Error(documentOpenError); + await waitForDocumentContentReady(timeoutMs, getDocumentLifetimeSignal()); + if (documentOpenError) throw new Error(documentOpenError); + if (!documentContentReady) throw new Error('Document content did not become ready before the deadline'); +} diff --git a/packages/converter/src/document-converter.ts b/packages/converter/src/document-converter.ts index acc36d2..3c40b1b 100644 --- a/packages/converter/src/document-converter.ts +++ b/packages/converter/src/document-converter.ts @@ -56,7 +56,7 @@ export class X2TConverter { private readonly WORKING_DIRS = ['/working', '/working/media', '/working/fonts', '/working/themes']; private readonly SCRIPT_PATH = `${BASE_PATH}wasm/x2t/x2t.js`; - private readonly WASM_PATH = `${BASE_PATH}wasm/x2t/x2t.wasm.br`; + private readonly WASM_PATH = `${BASE_PATH}wasm/x2t/x2t.aee5fc0dcf0d.wasm`; private readonly INIT_TIMEOUT = 300000; /** @@ -126,14 +126,13 @@ export class X2TConverter { }; if (globalScope.Module?.instantiateWasm) return; - const wasmPath = this.WASM_PATH; const instantiateWasm = ( imports: WebAssembly.Imports, successCallback: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void, ): Record => { void (async () => { try { - const response = await fetchWasmResponse(wasmPath); + const response = await fetchWasmResponse(this.WASM_PATH); if (!response.body) throw new Error('x2t WASM response has no body to stream'); // Re-wrapped only to state the Content-Type: the body is already the // decoded module, but instantiateStreaming rejects anything not diff --git a/public/sdkjs/common/AllFonts.js b/public/sdkjs/common/AllFonts.js index 8bdbd68..6417ae1 100644 --- a/public/sdkjs/common/AllFonts.js +++ b/public/sdkjs/common/AllFonts.js @@ -299,7 +299,7 @@ window["__fonts_infos"] = [ ["Asana Math",107,1,-1,-1,-1,-1,-1,-1], ["ASCW3",103,0,-1,-1,-1,-1,-1,-1], ["Caladea",111,0,110,0,108,0,109,0], -["Calibri",115,0,-1,-1,-1,-1,-1,-1], +["Calibri",115,0,114,0,112,0,113,0], ["Carlito",115,0,114,0,112,0,113,0], ["Comic Sans MS",51,0,-1,-1,50,0,-1,-1], ["Courier New",61,0,60,0,58,0,59,0], diff --git a/public/sdkjs/common/wasm/x2t/x2t.worker.js b/public/sdkjs/common/wasm/x2t/x2t.worker.js index eb66c9a..9a7e1af 100644 --- a/public/sdkjs/common/wasm/x2t/x2t.worker.js +++ b/public/sdkjs/common/wasm/x2t/x2t.worker.js @@ -29,7 +29,7 @@ importScripts('x2t_helper.js'); // document, which is not where this script lives. var converter = AscCommon.x2t; converter.SCRIPT_PATH = new URL('x2t.js', self.location.href).href; -converter.WASM_PATH = new URL('x2t.wasm.br', self.location.href).href; +converter.WASM_PATH = new URL('x2t.aee5fc0dcf0d.wasm', self.location.href).href; /** * Media crosses as bytes in both directions. Blob URLs are realm-bound in @@ -56,6 +56,9 @@ function transferablesOf(result) { } var OPERATIONS = { + initialize: function () { + return converter.initialize().then(function () {}); + }, convertToBin: function (payload) { return converter.convertToBin(payload.data, payload.fileName, payload.fileExt); }, diff --git a/public/sdkjs/common/wasm/x2t/x2t_helper.js b/public/sdkjs/common/wasm/x2t/x2t_helper.js index db22ea3..5daf2bf 100644 --- a/public/sdkjs/common/wasm/x2t/x2t_helper.js +++ b/public/sdkjs/common/wasm/x2t/x2t_helper.js @@ -386,7 +386,7 @@ // Docker re-encodes the same URL as gzip for ordinary HTTP (#267). // Each host declares the matching Content-Encoding, so the browser // decodes it at the network layer; nothing here decompresses anything. - this.WASM_PATH = '../../../../sdkjs/common/wasm/x2t/x2t.wasm.br' + this.WASM_PATH = '../../../../sdkjs/common/wasm/x2t/x2t.aee5fc0dcf0d.wasm' // Total tries for that fetch, and the step of the linear backoff // between them (0.5 s, then 1 s). Small on purpose: a CDN blip is over // in a moment, and anything longer is time the user spends watching a @@ -418,7 +418,8 @@ '/working/themes' ]; - this.INIT_TIMEOUT = 60000; + // Allow a cold converter download and compilation on slower connections. + this.INIT_TIMEOUT = 300000; } X2TConverter.prototype.initialize = function () { @@ -466,7 +467,10 @@ }; return fetch(wasmPath).then( function (response) { - if (response.ok) return response; + if (response.ok) { + // HTTP Content-Encoding is decoded natively before streaming compilation. + return response; + } var failure = new Error( "Failed to fetch x2t WASM at '" + wasmPath + "' (" + response.status + ')', ); diff --git a/public/web-apps/apps/documenteditor/main/index.html b/public/web-apps/apps/documenteditor/main/index.html index 656ea96..5668482 100644 --- a/public/web-apps/apps/documenteditor/main/index.html +++ b/public/web-apps/apps/documenteditor/main/index.html @@ -1,12 +1,8 @@ - + - Document Editor diff --git a/public/web-apps/apps/pdfeditor/main/index.html b/public/web-apps/apps/pdfeditor/main/index.html index 8cd6beb..85e7407 100644 --- a/public/web-apps/apps/pdfeditor/main/index.html +++ b/public/web-apps/apps/pdfeditor/main/index.html @@ -1,12 +1,8 @@ - + - PDF Editor diff --git a/public/web-apps/apps/presentationeditor/main/index.html b/public/web-apps/apps/presentationeditor/main/index.html index a6dc76d..1b26564 100644 --- a/public/web-apps/apps/presentationeditor/main/index.html +++ b/public/web-apps/apps/presentationeditor/main/index.html @@ -1,12 +1,8 @@ - + - Presentation Editor diff --git a/public/web-apps/apps/spreadsheeteditor/main/index.html b/public/web-apps/apps/spreadsheeteditor/main/index.html index 5be0aa0..072086a 100644 --- a/public/web-apps/apps/spreadsheeteditor/main/index.html +++ b/public/web-apps/apps/spreadsheeteditor/main/index.html @@ -1,12 +1,8 @@ - + - Document Editor diff --git a/public/web-apps/apps/visioeditor/main/index.html b/public/web-apps/apps/visioeditor/main/index.html index 5163f17..01124ac 100644 --- a/public/web-apps/apps/visioeditor/main/index.html +++ b/public/web-apps/apps/visioeditor/main/index.html @@ -1,12 +1,8 @@ - + - Visio Editor