The editing engine¶
Schreibzeug edits documents with sdkjs, the JavaScript engine of Euro-Office (a fork of ONLYOFFICE).
It runs entirely in the browser; there is no Document Server. Only packages/editor-core talks to
it.
Where it comes from¶
engine/engine.json pins one official Euro-Office Document Server release.
bun run engine:prepare (engine/scripts/prepare.ts, also run by bun run dev) downloads that
package, checks its SHA-256, and builds engine/dist, showing each step and the download's
progress:
| Path | What it is |
|---|---|
sdkjs/{common,word,cell,slide,pdf} |
the built engine, as shipped in the release (pdf holds the PDF renderer drawingfile.wasm; the PDF editor's code is part of word) |
sdkjs/common/AllFonts.js, fonts/ |
the font index and web fonts, generated by the release's allfontsgen from its open-source fonts (core-fonts) |
sdkjs/slide/themes/ |
the presentation themes (themes.js and one folder per theme), generated by the release's allthemesgen from the theme files it ships, as its install script does (documentserver-generate-allfonts.sh) |
vendor/xregexp/, vendor/jquery/ |
globals sdkjs uses without loading them (jQuery: the text editor's page layout) |
x2t/ |
the converter: ONLYOFFICE core's x2t compiled to WebAssembly (cryptpad/onlyoffice-x2t-wasm), pinned in engine.json |
frame/x2t/worker.js |
the worker that runs x2t (from engine/frame) |
frame/editor/<editor>/main/index.html |
the pages that host the engine (from engine/frame) |
dictionaries/<lang>/ |
Hunspell dictionaries for the spell checker, with their licenses (below) |
licenses/ |
engine and font licenses |
allfontsgen is a Linux x86_64 binary, so preparing the engine needs Linux (CI does it and caches
the result by the hash of the spec, the scripts and the frame pages).
How it is loaded¶
sdkjs resolves its own files relative to the page as ../../../../sdkjs/… and ../../../../fonts/
(common/libfont/loader.js, common/GlobalLoaders.js, common/zlib/zlib.js). The host page
therefore sits four folders below the engine root: <engine>/frame/editor/<editor>/main/index.html
(<engine> is /engine/<id> in the build, see below),
with word for text documents, cell for spreadsheets, slide for presentations and pdf for
PDFs (PDFs).
mountEngineFrame loads that page in a same-origin iframe and checks that the editor's API
constructor is there (asc_docs_api for text and presentations, spreadsheet_api for spreadsheets). The iframe keeps the engine's globals
(Asc, AscCommon, AscFonts, …) out of the app and gives each document its own engine.
The iframe has its own Object, Array and so on. sdkjs sometimes checks values with
instanceof (CNumInfo.Parse takes an object only if value instanceof Object), which fails for
objects made in the app's window. So everything handed to the engine is a primitive, JSON text, or
an object made with the frame's own constructors (new win.Asc.asc_CColor(…)).
Opening fast, on a phone too¶
The engine is large: for a text document the browser loads about 68 MB (x2t.wasm 35 MB,
word/sdk-all.js 18 MB, fonts.wasm, the fonts the document uses). Four things keep that from
happening on every open:
- Kept for good. The build puts the engine in a folder named after the prepared engine,
/engine/<id>/(the start of the stampengine:preparewrites toengine/dist/.version, a hash of the release, the scripts and the frame pages;apps/web/engine-plugin.ts), and the app loads it from there (import.meta.env.ENGINE_PATH,apps/web/src/editor/engine.ts). A new engine gets a new name, so the server sends the engine and the app's own scripts (/assets/, named by their content) withCache-Control: immutablefor a year; everything else,index.htmlfirst, withno-cacheand anETag, which the browser asks about again and gets 304 while it is unchanged (apps/server/src/web.ts). Opening a document a second time downloads nothing of the engine. Dev and preview serve the engine at/engine/with or without the name. - Compressed. The server compresses what shrinks (scripts, JSON, WebAssembly, the engine's
fonts) with Brotli where the browser takes it, else gzip, once per file, and keeps the result in
memory (
apps/server/src/compression.ts): about 15 MB instead of 68 on the first open. Brotli at quality 5: x2t.wasm takes about a second, quality 11 would take two minutes. The desktop app's local server sends files as they are; they never leave the computer. - At the same time. The file is converted while the frame loads:
convertForEnginestarts x2t as soon as the editor mounts, so x2t downloads, compiles and converts in its worker while the frame parses sdkjs, and the editor's open function takes the result asconverted. - Compiled while it downloads. The worker hands x2t.wasm to emscripten through
instantiateWasmwithWebAssembly.instantiateStreaming(engine/frame/x2t/worker.js, the hook in x2t.jscreateWasm), not as bytes (wasmBinary): it compiles while it arrives, and Chromium keeps the compiled code beside the cached file, so the next open does not compile 35 MB again.
Code in documents does not run¶
Being same-origin, the frame acts with the signed-in user's session. sdkjs can run code a document
carries, in closures that hide window and document but not parent, top, fetch or
location (common/macros.js _safe_eval_closure, pdf/src/forms/formActions.js EvalScript).
Schreibzeug has no macro editor and no plugins, so src/document-scripts.ts switches all of it off:
- JS macros (text, spreadsheets, presentations):
createDocInfosetsput_IsEnabledMacroses(false), whichasc_runMacros(a click on a shape or button bound to a macro) andasc_runAutostartMacrosescheck (common/apiBase.js). - Custom functions of a spreadsheet: the engine evaluates those a file brings on open
(
cell/api.js_openDocumentEndCallback→registerCustomFunctionsLibrary,cell/api_plugins.js) without asking the macro switch, and a co-editor's change can set new ones (SetCustomFunctions).withoutCustomFunctionsreplaces that method on the API object. - JavaScript actions of a PDF (a field's format, keystroke and calculation scripts, link
actions):
CActionRunScriptruns them with no switch at all.silencePdfScriptslets each action run as an empty script, right before the engine reads the PDF (beforeOpeningwrapsopenDocument: the PDF code is insdk-all.js, whichsdk-all-min.jsloads only after the API exists,common/editorscommon.jsloadSdk). The action queue goes on as before and the script stays in the file; a field formatted by a script (AFNumber_Format) shows the plain value.
sdk-all.js cannot be left out of engine/dist for this: every editor loads it. Instead
engine/scripts/prepare.ts (checkScriptHooks) counts the places these switches rely on in the
built bundles and stops when an engine update moves one.
Why a converter¶
The engine opens and saves its own binary format ("DOCY", "XLSY", "PPTY"). Reading and writing
OOXML directly is an optional addon (Asc.Addons.ooxml, CDocument.fromZip) that is not part of
the Euro-Office build: setting the flag only moves the failure to fromZip is not a function.
So every open converts the .docx to the binary format with x2t, and every save converts back, as
CryptPad does. x2t runs in a worker (createConverter), so its large heap stays out of the page.
How a document opens without a server¶
Opening is the same for every editor (src/document.ts openDocument); each editor adds its part
as an EditorBinding: the format, how its API is made, its state and events, its commands, what it
sets once the document is shown. openWordDocument (src/word.ts), openSheetDocument
(src/cell.ts) and openPresentation (src/slide.ts) are the bindings. The flow converts the
file (docx → bin, xlsx → bin, pptx → bin) and
then follows the mobile editors of web-apps (apps/*editor/mobile/src/controller/Main.jsx):
- The editor's API inside the frame:
new Asc.asc_docs_api({"id-view": "editor_sdk"}), ornew Asc.spreadsheet_api({"id-view": "editor_sdk", "id-input": "ce-cell-content"}). - Register the callbacks (state events,
asc_onGetEditorPermissions,asc_onDocumentContentReady,asc_onError). asc_setDocInfo(info)with the URLAscCommon.offlineMode. That switches off the server connection:_coAuthoringInitkeeps the co-authoring URL empty,CDocsCoApi.initthen answers with a local "first connect" and an empty license instead of opening a socket (common/apiBase.js,common/docscoapi.js). In a shared session the URL is a blob URL of the converted document instead, andwindow.ioin the frame is a socket.io stand-in that talks to the Schreibzeug relay; see collaboration.asc_getEditorPermissions(); the permissions callback callsasc_LoadDocument().- In offline mode sdkjs asks for the file through
_openChartOrLocalDocument, which would open an empty document. Schreibzeug replaces it on the API instance withasc_openDocumentFromBytes(binary). asc_onDocumentContentReady→ the editor is ready.
How it saves¶
asc_nativeGetFile() (word/api.js) returns the document in the binary format as
DOCY;v5;<size>;<base64>; x2t converts it to .docx (bin → docx). The web app stores the result
on the server as a new version (PUT /api/files/:id/content, see files) and then calls
markSaved(saved), which is SetUnchangedDocument(): "modified" turns false until the next change.
In a shared session it also tells the relay how much of the change log the stored copy holds; see
collaboration.
Mail merge¶
"Serienbrief" in the text editor: a letter with merge fields and a table of recipients become one
new document with a letter per recipient, each after a page break, stored beside the letter as
"apps/web/src/editor/use-mail-merge.tsx).
- Recipients: "Empfänger wählen …" takes a table from the user's files or from the computer:
.xlsx (its first sheet, read by
packages/ooxmlxlsxRows, dates and numbers as German text), .ods and .xls (turned into an .xlsx by x2t first) or .csv (decodeCsvinsrc/csv.ts; UTF-8 or Windows-1252, as German Excel writes it). The first row names the fields; a column without a name or a table without recipients is refused with the reason. The engine gets them as a list (word/api.jsasc_StartMailMergeByList); they live in the session, not in the document. - Fields: the bar above the letter puts a field at the cursor (
asc_AddMailMergeField, a MERGEFIELD showing «Name», which Word reads too) and names the fields of the letter that the table lacks. Word writes merge fields as complex fields (w:fldChar…MERGEFIELD Name…w:fldChar); the engine reads those but merges only simple ones (word/Editor/Document.jsRegister_Field→m_oMailMergeFields), so a .docx has its complex merge fields made simple (w:fldSimple, same instruction and result) before it opens (packages/ooxmlsimplifyMergeFields). - Preview: shows the letter for one recipient (
asc_PreviewMailMergeResult); the engine does not let the letter be edited meanwhile. - The letters:
CDocument.Get_MailMergedDocumentcopies the letter once per recipient with the fields replaced. The engine's own download sends that to the Document Server; here it is written in the binary format (AscCommonWord.BinaryFileWriter, asasc_nativeGetFile) and turned into a .docx by x2t with the letter's images (src/word-mail-merge.ts).
e2e/mail-merge.spec.ts merges test-files/einladung.docx (Word's merge fields) with
test-files/empfaenger.xlsx, adds a field, steps through the preview and reads the stored letters;
a second test takes a Windows-1252 CSV from the computer and checks the warning for fields the table
lacks.
Other formats¶
x2t reads more than the Office formats, and the file list opens those files too:
- OpenDocument (.odt, .ods, .odp, as LibreOffice writes them) opens in the editor of its kind and
is saved as what it is: x2t reads it into the binary format (
odt → bin) and writes it back (bin → odt), as with a .docx (OpenOptions.openDocument,OPEN_DOCUMENT_FORMATinsrc/converter.ts). "Herunterladen" gives the .odt; a spreadsheet can also be downloaded as CSV. - Older formats (Word, Excel and PowerPoint 97–2003, RTF) x2t reads but does not write; reading
a .doc into the binary format stops with error 88, while
doc → docxworks. So Schreibzeug does what Word does with "Konvertieren": the page for such a file (apps/web/src/editor/convert-page.tsx) makes a copy in the Office format (OFFICE_FORMAT_OF_LEGACY: .doc and .rtf to .docx, .xls to .xlsx, .ppt to .pptx) beside it, under a free name ("Angebot.docx", "Angebot (2).docx"), and opens that; the file itself stays as it is. Someone the file is only shared with gets the copy in their own files.
e2e/other-formats.spec.ts edits and saves an .odt, an .ods and an .odp and checks the stored files
(their mimetype part and their text), and opens a .doc, an .rtf, an .xls and a .ppt through their
copy. LibreOffice reads the stored .odt back with its text. The test files are Schreibzeug's own,
converted with LibreOffice (test-files/README.md).
Commands and state¶
The UI never calls the engine. editor-core translates engine events into EditorState
(src/events.ts, src/state.ts) and offers commands (src/commands.ts):
| Command | Engine call | Event back |
|---|---|---|
| bold, italic, underline | put_TextPrBold/Italic/Underline(value) |
asc_onBold/Italic/Underline |
| font size | put_TextPrFontSize(size) |
asc_onFontSize |
| alignment | put_PrAlign(AscCommon.align_*) |
asc_onPrAlign |
| undo, redo | Undo(), Redo() |
asc_onCanUndo, asc_onCanRedo |
| strikethrough | put_TextPrStrikeout(value) |
asc_onStrikeout |
| font | put_TextPrFontName(name) |
asc_onFontFamily; the list: asc_onInitEditorFonts |
| text colour | put_TextColor(asc_CColor), automatic: Auto = true |
asc_onTextColor |
| paragraph style | put_Style(name) |
asc_onParaStyleName; the list: asc_onInitEditorStyles |
| bulleted, numbered list | put_ListTypeCustom(preset), end: { Type: "remove" } |
asc_onFocusObject, then asc_GetCurrentNumberingId / asc_GetNumberingPr |
| insert table | put_Table(columns, rows) |
asc_onFocusObject with a Table object |
| rows, columns | addRowAbove/Below(), addColumnLeft/Right(), remRow(), remColumn(), remTable() |
– |
| select all | asc_EditSelectAll() |
– |
| superscript, subscript | put_TextPrBaseline(AscCommon.vertalign_*) |
asc_onVerticalAlign |
| clear formatting | ClearFormating() |
– |
| page break | put_AddPageBreak() |
– |
| cut, copy, paste | Cut(), Copy(), Paste(); false: the browser refused |
– |
| zoom | zoomIn(), zoomOut(), zoom(percent), zoomFitToWidth(), zoomFitToPage() |
asc_onZoomChange(percent): document.zoom |
| mark saved | SetUnchangedDocument() |
asc_onDocumentModifiedChanged |
| – | – | asc_onCountPages |
| – | – | asc_onStartAction / asc_onEndAction with c_oAscAsyncAction.Disconnect: document.connected |
Builder scripts¶
OpenedDocument.runBuilder runs a script against the engine's builder API (word/apiBuilder.js,
cell/apiBuilder.js, slide/apiBuilder.js) through callCommand (common/apiBase.js), as plugins
do: one action of the history, recalculated once at the end, with the co-editing lock check. The
MCP server's jobs for an open document use it (src/agent.ts, see the MCP server).
The script is evaluated as it is, so it is wrapped in a function to return a value, and only plain
data comes back (Asc.checkReturnCommand).
Comments¶
src/comments.ts reads and writes asc_CCommentDataWord (word/api.js) the way web-apps does
(common/mobile/lib/controller/collaboration/Comments.jsx). A comment carries two times: Time,
the local time written as if it were UTC, and OOTime, real milliseconds; Schreibzeug keeps one time
in milliseconds and writes both.
| Command | Engine call | Event back |
|---|---|---|
| add (on the selection) | asc_addComment(data) |
asc_onAddComment(id, data) |
| reply, resolve | asc_changeComment(id, data) with the whole comment and all replies |
asc_onChangeCommentData |
| delete | asc_removeComment(id) |
asc_onRemoveComment |
| go to | asc_selectComment(id) |
– |
| – | asc_showComments(false) on content ready marks commented text |
asc_onShowComment(ids), asc_onHideComment |
On open, the engine reports every comment of the file with asc_onAddComment; a comment carries
the name of the account that wrote it.
The presentation engine speaks the same calls and events (slide/api.js, asc_CCommentDataSlide,
exported as Asc.asc_CCommentData); a new comment belongs to the current slide, and
asc_showComments() on content ready shows the comment marks on the slides.
The spreadsheet engine has the same comment fields (cell/model/CellComment.js
asc_CCommentData) but puts a new comment on the whole workbook unless its document flag is false,
so the editor sets it false and the comment goes on the active cell, whose reference the comment
reports as its quote. asc_addComment returns nothing there; the id comes with
asc_onAddComment. Besides the single events it reports lists: a sheet's comments on open and
when it is shown (asc_onAddComments), moved ones (asc_onChangeComments) and removed ones
(asc_onRemoveComments). A comment's id starts with sheet<id>_ for its sheet
(asc_getWorksheetId), and the panel shows the comments of the sheet shown, as web-apps filters
them. asc_showComments(false) marks commented cells.
All three editors share the commands (src/comments.ts), the state (src/comment-state.ts) and
the panel (apps/web/src/editor/use-comments-panel.tsx).
asc_onDocumentModifiedChanged carries no value (word/api.js SetDocumentModified sends it
bare); like web-apps (documenteditor/mobile Main.jsx), editor-core reads
isDocumentModified() when it arrives.
Style names are the engine's English ones (Normal, Heading 1), as stored in the .docx; the
toolbar shows the built-in ones in German (apps/web/src/toolbar/style-labels.ts). The list
presets are the first bullet and number presets of web-apps (documenteditor/mobile
store/textSettings.js), in src/lists.ts; the colour palette is Word's ten standard colours.
The engine takes target values, not toggles, so a toggle command reads the current state first.
Menus and the context menu¶
The editor's title row carries a menu bar as in a desktop office suite (apps/web/src/ui/menu-bar.tsx).
"Datei" and "Hilfe" are the same in every editor (src/editor/file-menu.ts); the editor adds its
own menus between them (src/editor/word-menus.ts), built from the state and the commands above.
The entries follow the desktop editors of web-apps (documenteditor/main Toolbar.js,
DocumentHolder.js); the shortcuts shown are the engine's own (word/Editor/Shortcuts.js). On a phone
the document gets the room: the header is one row (back, name, the save state as an icon, the
editor's own buttons, "Werkzeuge" and "Speichern"), and the menus, the file's buttons and the
toolbar show only after a tap on "Werkzeuge" (src/editor/editor-header.tsx, e2e/phone.spec.ts).
Inside Kontext, Kontext leaves out its tab bar under an open file.
A text document opens on a phone in the engine's mobile view, as web-apps' mobile editor opens
it (documenteditor/mobile Main.jsx, "mobile-view"): ChangeReaderMode lays the text out in
the width of the screen at a size to read, still editable, without rulers
(packages/editor-core/src/word-mobile-view.ts, apps/web/src/editor/use-mobile-view.ts). It is a
layout only (word/Editor/Layout/ReadView.js, set without history), so the saved .docx stays as
it is. The PDF and printing need the pages: for them the engine goes back to the pages, lays them
out at once (RecalculateAllAtOnce, as asc_Print does) and returns to the mobile view. "Ansicht →
Mobilansicht" switches it on any screen. A presentation on a phone has its slide list under the slide
(thumbnails-position: bottom) and its notes hidden until "Ansicht → Notizen", as web-apps'
mobile presentation editor has it (presentationeditor/mobile Main.jsx).
A right click in the document makes the engine send asc_onContextMenu with a CContextMenuData
(word/api.js sync_ContextMenuCallback): the point relative to its view element and whether it
was in a header or footer. editor-core adds the view element's place in the frame
(src/view.ts toContextMenuRequest) and hands the request to onContextMenu listeners; the web
app adds the frame's place in the window and shows its own menu there, as web-apps does
(DocumentHolder.js showPopupMenu). Someone who may only look gets no context menu.
"Hilfe" has the keyboard shortcuts of the editor, the switch for the engine's screen reader
support (accessibility) and "Über Schreibzeug" with the license and the
link to the running source code, which the AGPL asks for (src/editor/help-dialogs.tsx).
Cut, copy and paste from a menu go through the engine (common/clipboard_base.js Button_Copy,
Button_Cut, Button_Paste), which uses the async clipboard API; the frame allows it
(allow="clipboard-read; clipboard-write"). When the browser refuses, the entry answers false
and the app says which keys to use, like web-apps.
Page layout and spacing¶
The "Layout" menu sets the orientation, the paper size and the margins of the section at the
cursor; "Format" and the toolbar set the line spacing and the indent of the paragraphs
(src/layout.ts). The choices are web-apps' (documenteditor main view/Toolbar.js: A4, A5,
Letter, Legal; margins normal, narrow, moderate, wide; spacing 1 to 3 lines), and so are the
calls (controller/Toolbar.js): change_PageOrient(isPortrait), change_DocSize(width, height)
with the upright size (the engine turns it for a landscape page), asc_SetSectionProps with an
Asc.CDocumentSectionProps that has only the margins set (CDocument.Set_SectionProps leaves
what is undefined alone), put_PrLineSpacing(Asc.linerule_Auto, multiple), and
IncreaseIndent / DecreaseIndent. What the cursor's section and paragraph have comes from
asc_onSectionProps (CDocument.UpdateInterface) and asc_onParaSpacingLine; a preset is
checked when every value is within a tenth of a millimetre, as web-apps checks it.
Headers, footers and page numbers¶
"Einfügen" opens the header or footer of the current page (GoToHeader(page),
GoToFooter(page) with getCurrentPage(), as web-apps' HeaderFooterTab.js) and, inside one,
closes it (asc_CancelHdrFtrEditing) or removes it (asc_RemoveHeader, asc_RemoveFooter).
Page numbers go in with put_PageNum(part, align), the numbers of web-apps' c_pageNumPosition
(DocumentHolder.js: top 1, bottom 2; right 0, left 1, center 2), or put_PageNum(-1) at the
cursor; asc_AddPageCount adds the number of pages (src/header-footer.ts). The menu is outside
the frame, so going into the header or back out hands the keys to the engine again. Whether the
cursor is in a header or footer comes from asc_onFocusObject (a Header element).
Table of contents, notes, breaks and text boxes¶
"Einfügen" puts in what web-apps' "Verweise" tab and toolbar offer (src/word-insert.ts, after
documenteditor/mobile controller/add/AddTableContents.jsx, controller/add/AddOther.jsx,
controller/add/AddShape.jsx):
- Table of contents:
asc_GetTableOfContentsPr()for the one the document has, or a newAsc.CTableOfContentsPr, set to headings 1 to 9 (put_OutlineRange(1, 9)), linked entries, page numbers right-aligned behind dots (c_oAscTabLeader.Dot), thenasc_AddTableOfContents(null, props); with one there already that changes it instead of adding a second. "Inhaltsverzeichnis aktualisieren" isasc_UpdateTableOfContents(false)(entries and page numbers); without a table of contents the editor says so rather than letting the engine reportComplexFieldNoTOC. - Notes:
asc_AddFootnote()andasc_AddEndnote()put the mark at the cursor and the cursor into the note. Strg+Alt+F is the engine's own shortcut (InsertFootnoteNow). - Breaks:
put_AddPageBreak()(Strg+Enter),put_AddColumnBreak(), andadd_SectionBreak(c_oAscSectionBreakType.NextPage | Continuous), which starts a section with its own page setup. - Text box:
AddShapeOnCurrentPage("textRect")draws a rectangle that holds text in the middle of the page shown (GraphicObjects.addShapeOnPage), selected so the next keys go into it. It is saved aswps:txbx.
The menu is outside the frame, so after each of these the keys go back to the engine, as for the header and footer: the next thing typed lands in the note, the text box or the page.
Links¶
"Link …" (Strg+K, the toolbar, the context menu) works as web-apps' link dialog
(controller/Links.js, view/HyperlinkSettingsDialog.js): when can_AddHyperlink() answers the
selected text, a new link goes in with add_Hyperlink; otherwise the cursor is in a link, whose
Asc.CHyperlinkProperty from getSelectedElements() is changed or removed with
change_Hyperlink and remove_Hyperlink (src/links.ts). An address without a scheme gets
http:// (an e-mail address mailto:), and an unchanged text is passed as null so the linked
text keeps its formatting. Strg+K reaches the app as asc_onDialogAddHyperlink; the engine's
shortcut table is empty until it is reset to the defaults (asc_resetAllShortcutTypes, as
web-apps' Shortcuts.js does), which the text editor does once the document shows. Strg+click on
a link sends asc_onHyperlinkClick; the app opens http, https and mailto addresses in a new tab.
Highlight and format painter¶
"Hervorheben" highlights the selection in one of the first colours of web-apps' palette, or takes
the highlight away: SetMarkerFormat(true, isColor, r, g, b) (CDocument.SetParagraphHighlight),
then SetMarkerFormat(false), so the mouse does not stay a highlighter as in web-apps, where it
does until Escape. The colour at the cursor is read from get_TextProps().get_TextPr().get_HighLight()
when asc_onTextHighLight comes, as web-apps' onApiHighlightColor does, since the event itself
reports "none" (-1) as black. "Format übertragen" is SetPaintFormat(1); the engine applies the
copied formatting to the next selection made with the mouse and reports the painter off
(asc_onPaintFormatChanged), but not on, so the button keeps that state itself (src/marking.ts).
Tracked changes¶
src/review.ts, after web-apps' review (common/main controller/ReviewChanges.js,
common/mobile controller/collaboration/Review.jsx). The menu "Überprüfen" switches tracking,
moves through the changes, accepts and rejects them and picks how they show; a balloon next to the
change at the cursor names who made it and when, with accept and reject.
| Command | Engine call (word/api.js) |
|---|---|
| track changes | asc_SetLocalTrackRevisions(null), asc_SetGlobalTrackRevisions(on): for the document, stored as w:trackRevisions in settings.xml, as web-apps' "for everyone"; state from asc_onOnTrackRevisionsChange(local, global) (local null: the document's setting holds) |
| accept, reject at the cursor | asc_AcceptChangesBySelection(true), asc_RejectChangesBySelection(true): the change at the cursor or in the selection, then on to the next |
| previous, next | asc_GetPrevRevisionsChange(), asc_GetNextRevisionsChange() |
| all | asc_AcceptAllChanges(), asc_RejectAllChanges() |
| display | asc_SetDisplayModeInReview(c_oAscDisplayModeInReview.Edit \| Final \| Original); there is no event, so the choice is kept by the editor |
The changes at the cursor come with asc_onShowRevisionsChange(changes, isShow), each a
CRevisionsChange (word/Editor/RevisionsChange.js): get_UserName, get_DateTime, get_Type
(c_oAscRevisionsChangeType: text or paragraph added or removed, formatting), get_Value (the
text, or a list of text and drawings) and get_X/get_Y relative to the view element
(#editor_sdk). As web-apps, the balloon shows only for the cursor (isShow), not for a selection,
and only while changes are marked. The author is the name the relay gives the session (see
collaboration).
Spell checking¶
src/spelling.ts. Without a spell checker address the engine checks in a worker of its own
(common/apiBase.js _coSpellCheckInit → CSpellchecker, common/spell/spell.js: Hunspell as
WebAssembly) and loads <lang>/<lang>.aff and .dic from engine/dist/dictionaries, which
engine/scripts/prepare.ts fills from the release package with the languages in engine.json
(de_DE, de_AT, de_CH, en_US, en_GB) and their licenses.
- Only shipped languages. The engine checks every language it lists
(
AscCommon.spellcheckGetLanguages); a language without its dictionary marks every word, which is why it was switched off before. The list is cut down to the dictionaries we ship before the editor starts (limitSpellLanguages); text in other languages stays unmarked. - A worker of the frame's own. From the editor's frame the shared worker (
SharedWorker, namedonlyoffice-spellchecker) never answered in Chromium, while a dedicated worker does; spell.js already does without the shared one for WebKit's WKWebView, so the frame hidesSharedWorkerbefore the editor starts (useDedicatedSpellWorker). - Started however fast it loads. The worker (
common/spell/spell/spell.js) setsModule.onRuntimeInitializedonly when the page's "init" message comes. Once the browser has the WebAssembly cached it is ready before that, the callback never comes and the build's assertions abort the module ("Attempt to setModule.onRuntimeInitializedafter it has already been processed"): no spell checking in that document.engine/scripts/prepare.ts(patchSpellWorker) makes the worker callself.onEngineInit()at once whenModule.calledRunis set; it stops with an error should an engine update change that line. - On or off with
asc_setSpellCheck(on)once the document is ready, and from "Überprüfen → Rechtschreibprüfung"; the choice is kept in this browser (apps/websettings/spell-check.ts, as web-apps keeps "settings-spellcheck"). Someone who may only look gets no underlines. - Suggestions. With the cursor in a misspelled word,
asc_onFocusObjectlists ac_oAscTypeSelectElement.SpellCheckelement (asc_CSpellCheckProperty:get_Word,get_Checkedfalse,get_Variants). The context menu offers the first five variants (asc_replaceMisspelledWord(variant, element)), "Ignorieren" and "Alle ignorieren" (asc_ignoreMisspelledWord(element, all)), as web-apps' (documenteditor/mainDocumentHolderExt.jsspellProps). The same calls work in the presentation editor.
The format-fidelity pictures and the screenshots are taken with spell checking off, so a dictionary update does not change them.
Find and replace¶
src/search.ts fills a CSearchSettings (common/api/searchSettings.js: text, match case,
whole words) in the engine's frame and calls asc_findText(settings, isNext),
asc_replaceText(settings, replacement, isReplaceAll) and asc_endFindText(), as web-apps does
(documenteditor/main controller/Search.js). The engine answers with
asc_onSetSearchCurrent(current, total) and, after replacing all, asc_onReplaceAll(total,
replaced); both go into state.search. When nothing matches it sends nothing, so the command
reports that itself.
Strg+F and Strg+H are not handled by the engine (web-apps opens its own panel for them); a
keydown listener in the frame (document.ts onKey with shortcutOf, handed out as onShortcut) and one in the app window open
the search bar instead of the browser's search. Someone who may only look can search but not
replace.
PDF and printing¶
This x2t build does not turn a document into a PDF by itself; it writes a PDF from the engine's
drawing commands, as the Document Server does for printing. exportPdf (src/document.ts) hands
the converter three things, the way CryptPad does (www/common/outer/x2t.js
x2tConvertDataInternal):
- the document in the engine's binary format (
asc_nativeGetFile()), asinput.bin; - every page as drawing commands, as
pdf.binnext to it:WordControl.m_oDrawingDocument .ToRendererPart(true, true), whatword/api.js_downloadAssends for a PDF to print; x2t reads that file by this name (X2tConverter/src/lib/pdf_image.hdoct_bin2pdf, CryptPad's patch); - the fonts the document was drawn with, in
m_sFontDir: the font files the engine has loaded (AscCommon.g_font_loader, status 0 or 1), fetched again from its font folder (the browser has them cached) and with the obfuscation of their first 32 bytes undone, as the engine does when it loads them (common/Drawings/Externals.js,guidOdttf);src/print.ts.
"Als PDF/A herunterladen" asks x2t for the same PDF as PDF/A-2 (m_bIsPDFA in the task;
X2tConverter/src/cextracttools.h → PdfFile.cpp CreatePdf(isPDFA)): XMP metadata with
pdfaid:part 2, an sRGB output intent, every font embedded. For a PDF itself see PDFs.
"Drucken" prints that PDF through the browser's dialog from a hidden frame (apps/web/src/editor/pdf.ts).
Strg+P is another key the engine leaves to its UI; it goes to the same place as Strg+F
(onShortcut), and so does Strg+S, which saves through the app (the engine's own asc_Save would
talk to the Document Server). The document's images go along as media/, as below.
Images¶
The converter keeps a document's images apart from its binary form: opening a .docx, x2t writes
them into media/ next to the output, and saving or printing it reads them from there
(engine/frame/x2t/worker.js; CryptPad does the same in www/common/outer/x2t.js). The engine
itself never sees files; it looks every image up by its name in AscCommon.g_oDocumentUrls
(common/editorscommon.js DocumentUrls), which the Document Server fills with download links.
src/media.ts fills it with blob URLs made in the frame (provideMedia) after the converter
answers, and on saving or printing fetches every media/ entry back into bytes (collectMedia),
including images added while editing.
"Einfügen → Bild …" puts an image from the computer in at the cursor (insertImage): the app
stores it on the server, which answers its name, and the image goes into DocumentUrls under that
name with its server URL; then AddImageUrl([url]) (common/apiBase.js) finds the URL there and
hands it to the editor's _addImageUrl (word/api.js, cell/api.js, slide/api.js), as after an upload
to the Document Server (UploadImageFiles). Saving maps the URL back to the name
(Shapes/SerializeWriter.js getImageLocal), so the image lands in the file's media/. How the
others in a session get it is in collaboration.
Keyboard focus¶
The engine takes keys through a hidden text area in its frame (common/text_input2.js, id
area_id) and cancels mousedown on its canvas. A click into the page therefore does not move
focus out of the app: after a toolbar button had the focus, typing went nowhere. editor-core
listens for pointerdown in the frame and, when nothing in the frame holds the keys, focuses the
engine's input (api.getFocusElement(), common/apiBase.js); the engine's own focus handler
then turns its key events back on (keepFocusOnClick in src/frame.ts). It checks again when the
main button is released: a click into the presentation's notes leaves the focus on the page's body
afterwards. A right click is left alone there, since the app's context menu then has the focus.
The way out again is F6: in the document Tab is the engine's (a tab, the next cell, the next form
field), and Escape and Umschalt+Tab stay there too. shortcutOf (src/view.ts) takes F6 and
Umschalt+F6 in the frame, before the engine, as it takes Strg+F; the web app moves to the menu bar,
the toolbar or back into the document (focus(), the engine's input again; src/editor/regions.ts).
Screen reader¶
The engine's screen reader support (common/workerspeech.js) writes what the cursor passed and what
got selected into div#area_id_screen_reader (role="region", aria-live="assertive") beside its
input. src/screen-reader.ts switches it with setSpeechEnabled (common/apiBase.js), from the
start when OpenOptions.screenReader says so; off, EditorActionSpeaker.stop has removed its
callbacks. Strg+Alt+Z switches it in the engine itself (word/Editor/Shortcuts.js SpeechWorker,
cell/view/EventsController.js, slide/Editor/Format/Presentation.js); a keyup listener reads
EditorActionSpeaker.isLanched afterwards, so the store's screenReader and the menu follow. Its
words come from the translate map of the API (OpenOptions.texts, which the app fills from its
messages in apps/web/src/editor/engine-texts.ts; looked up by translateManager.getValue). Right after the content is ready the engine makes its input
(word/api.js OpenDocument → InitControl); a microtask then names it after the file and replaces
its autocomplete="extremely_off" (common/text_input2.js), which is no valid value.
Spreadsheets¶
src/cell.ts binds the spreadsheet engine (cell/api.js), following web-apps'
spreadsheeteditor/mobile:
- Formula bar. The engine types into an input it finds by id in its own document
(
config["id-input"],document.getElementById), so the formula bar is part of the frame page (engine/frame/editor/cell/main/index.html), with the name box beside it. The name box showsasc_onSelectionNameChangedand jumps withasc_findCellon Enter. - German regional format.
asc_setLocale(0x0407)before loading: decimal comma,;between function arguments. Function names stay English (SUM), as the engine has no German names without a translation file. - State.
asc_onSelectionChangedbrings the active cell (asc_CCellInfo→CellXfs: font, colours, alignment, wrap, number format; merge state),asc_onSheetsChangedandasc_onActiveSheetChangedthe sheet tabs, read withasc_getWorksheetsCount,asc_getWorksheetName,asc_isWorksheetHidden,asc_getActiveWorksheetIndex. - The handler list. The engine keeps one function per event as is and only several as a list.
While opening it unregisters a callback of its own for
asc_onSelectionChanged, which deletes the whole event when only one function is there (cell/view/HandlerList.jsremove). An empty first callback keeps ours. It also calls only what itsasc_typeOfcalls "function", so callbacks are neverasyncfunctions.
| Command | Engine call (cell/api.js) |
|---|---|
| bold, italic, underline, strikethrough | asc_setCellBold/Italic/Underline/Strikeout(value) |
| font, size | asc_setCellFontName(name), asc_setCellFontSize(size) (at most 409) |
| text colour, fill | asc_setCellTextColor(asc_CColor), asc_setCellBackgroundColor(asc_CColor \| null) |
| alignment, wrap | asc_setCellAlign(AscCommon.align_*), asc_setCellTextWrap(value) |
| number format | asc_setCellFormat(code): General, #,##0.00, #,##0.00\ [$€-407], 0.00%, dd.mm.yyyy, @ |
| merge | asc_mergeCells(c_oAscMergeOptions.Merge \| None) |
| rows, columns | asc_insertCells(c_oAscInsertOptions.InsertRows \| InsertColumns), asc_deleteCells(…) |
| undo, redo | asc_Undo(), asc_Redo(); state from asc_onCanUndoChanged, asc_onCanRedoChanged |
| sheets | asc_showWorksheet(i), asc_addWorksheet(name), asc_renameWorksheet(name), asc_deleteWorksheet() |
| cut, copy, paste | asc_Cut(), asc_Copy(), asc_Paste() |
| zoom | asc_setZoom(factor), asc_getZoom(); asc_onZoomChanged(factor) |
| find, replace | asc_findText(asc_CFindOptions) (the count), asc_replaceText(options); asc_onSetSearchCurrent, asc_onRenameCellTextEnd(found, replaced) |
wb.calcPagesPrint(), wb.printSheets(pages, null).DocumentRenderer.Memory.GetData(), as cell/api.js _downloadAs |
The menus and the context menu work as in the text editor (apps/web/src/editor/sheet-menus.ts).
The spreadsheet's right click hands over the mouse event itself (WorkbookView.js onContextMenu),
whose point is already in the frame's coordinates. Search follows web-apps
(spreadsheeteditor/main controller/Search.js): values in the active sheet, "whole words" as
"whole cell"; matches are marked with asc_selectSearchingResults, because the spreadsheet's
asc_endFindText fails in this build (it calls a _cleanFindResults that WorkbookView.js lacks).
The spreadsheet has no SetUnchangedDocument; its "modified" flag drops once the relay has the
changes (asc_isDocumentModified, History.Have_Changes).
Sorting, filters and frozen panes¶
src/cell-data.ts, after web-apps' desktop editor (spreadsheeteditor/main controller/Toolbar.js
onSortType, onAutoFilter, onClearFilter; controller/ViewTab.js onFreeze) and its mobile
column filter (spreadsheeteditor/mobile controller/FilterOptions.jsx):
| Command | Engine call (cell/api.js) |
|---|---|
| sort | asc_sortCellsRangeExpand(), then asc_sortColFilter(c_oAscSortOptions.Ascending \| Descending, "", undefined, undefined, expand) |
| filter on, off | asc_addAutoFilter(); asc_changeAutoFilter(tableName, c_oAscChangeFilterOptions.filter, value) where buttons are or a formatted table is |
| clear filter | asc_clearFilter() |
| freeze | asc_freezePane() (at the active cell, or unfreeze), asc_freezePane(c_oAscFrozenPaneAddType.firstRow \| firstCol) |
| a column's filter | asc_onSetAFDialog(asc_CAutoFilterObject): values with asc_getText/asc_getVisible; asc_setVisible on each, asc_getFilterObj().asc_setType(c_oAscAutoFilterTypes.Filters), asc_applyAutoFilter(object); sort by it with asc_sortColFilter(order, asc_getCellId(), asc_getDisplayName()) |
- Sorting one column asks Excel's question in web-apps when more data is next to it
(
c_oAscSelectionSortExpand.showExpandMessage). Schreibzeug takes that question's default and sorts the rows along, the whole region around the selection; when someone else has those rows (showLockMessage) it sorts the selection alone, which is what "yes" there does. - The filter state comes with the selection:
asc_getAutoFilterInfo()istruewith buttons,falsewhere they can be added andnullwhere not;asc_getIsApplyAutoFilter()tells whether a filter hides rows. A formatted table's name isasc_getFormatTableInfo().asc_getTableName(). - Frozen panes have no event: freezing waits for the pane's lock (
WorksheetView.js_isLockedFrozenPane), the relay's answer in a shared session.asc_getSheetViewSettings() .asc_getIsFreezePane()is read again shortly after and whenever the selection moves; the menu reads "Fixierung aufheben" while something is frozen, as web-apps' does. - A column's filter opens under its button. The button's place is relative to the grid's
canvas, inside
#ws-canvas-outer; we add that element's place in the frame and the frame's in the window. "OK" applies the marked values; at least one stays shown.
Charts¶
src/cell-chart.ts, after web-apps' mobile spreadsheet editor (controller/add/AddChart.jsx
onInsertChart, controller/edit/EditChart.jsx onType). "Einfügen" and the toolbar offer
columns, bars, lines, a pie and areas (c_oAscChartTypeSettings barNormal, hBarNormal,
lineNormal, pie, areaNormal):
- A new chart:
asc_getChartSettings(true)gives the settings for the selection (truewithout opening the chart editor's frame),putType(type), thenasc_checkDataRange( c_oAscSelectionDialogType.Chart, getRange(), true, !getInColumns(), getType()); withc_oAscError.ID.Nothe chart goes in withasc_addChartDrawingObject(settings), too many series (MaxDataSeriesError) or no range are said in a message. - The selected chart: with
asc_getSelectionType()RangeChart(orRangeChartText) the chart is amongasc_getGraphicObjectProps(), its settingsget_ObjectValue().get_ChartProperties();changeType(type)changes it at once. The menu then checks the selected chart's kind.
Dropdown lists and conditional formats¶
src/cell-rules.ts, after web-apps' desktop spreadsheet editor (controller/DataTab.js
onDataValidation with view/DataValidationDialog.js; controller/Toolbar.js for the
conditional-format pickers):
- A dropdown list ("Daten → Dropdown-Liste …") is a data validation of type list.
asc_getDataValidationProps()gives the selection's rule; where the selection holds several rules it answersc_oAscError.ID.MoreOneTypeDataValidateand is asked again withnull(start over), otherwise withtrue(extend the rule). The values go in asCDataFormulawith the entries joined by ";" (the German argument separator), withasc_setType(List),asc_setAllowBlank(true),asc_setShowErrorMessage(true), thenasc_setDataValidation(props); typeNonetakes the list away. The .xlsx gets it in thex14extension form. - Conditional formats ("Format"): a data bar and a green–yellow–red colour scale, the first
preset of web-apps' pickers:
asc_setCF([], [], [c_oAscCFRuleTypeSettings.dataBar | colorScale, 0]).asc_clearCF(c_oAscSelectionForCFType.selection)takes them off the selected cells.
Pivot tables¶
src/cell-pivot.ts, after web-apps' desktop spreadsheet editor (controller/PivotTable.js,
view/PivotSettings.js, view/ValueFieldSettingsDialog.js); the engine keeps and computes the table
(cell/model/PivotTables.js CT_pivotTableDefinition), and x2t writes it to the .xlsx with its
cache (xl/pivotTables/, xl/pivotCache/).
- A new one ("Einfügen → Pivot-Tabelle …"): the dialog starts with the table around the active
cell (
asc_getAddPivotTableOptions().asc_getRange()), checked withasc_checkDataRange( c_oAscSelectionDialogType.PivotTableData, …);asc_insertPivotNewWorksheet(source, "Pivot<n>")puts it on a new sheet with the first free name (createSheetName). It answers nothing where the range has no headings; the dialog says so. - The table at the active cell comes with the selection:
asc_getCellInfo().asc_getPivotTableInfo(). Its fields areasc_getPivotFields()(or the source'sasc_getCacheFields()names), and the four areasasc_getPageFields(),asc_getColumnFields(),asc_getRowFields()(field indexes;-2is the "Werte" entry once there are two values) andasc_getDataFields()(CT_DataFieldwith the source column, name andc_oAscDataConsolidateFunction). The field list beside the grid shows them while the active cell is in a pivot table. - Arranging: a ticked field goes in with
asc_addField(numbers to the values, others to the rows,CT_CacheField.isSumSubtotal), an unticked one out withasc_removeField;asc_moveToRowField,asc_moveToColField,asc_moveToPageField,asc_moveToDataFieldmove one (a value with its place among the values). Instead of dragging, each entry offers where it may go. - How a value is summed up: a
CT_DataFieldwithasc_setSubtotalandasc_setName, applied withvalue.asc_set(api, pivot, index, change). Values are named as German Excel names them ("Summe von Betrag", "Anzahl von …"): the engine fills its name pattern (%1 of %2) with the English function name, so the app renames each new value at once. - Refreshing after the source changed:
asc_refresh(api)on the table, from "Daten", the context menu or the field list. - Captions in the interface's language: "Gesamtergebnis", "Zeilenbeschriftungen",
"Spaltenbeschriftungen", "Werte", "(Alle)", "(Leer)" in German, through the
translatemap the API is created with (OpenOptions.texts, filled byapps/web/src/editor/engine-texts.ts; the keys arePivotTables.js's captions, as web-apps'Main.jspasses them). - The engine changes the table without a selection change, so each command reads the state again.
CSV files¶
A .csv opens in the spreadsheet editor and is saved as a CSV again, with the delimiter it came
with (src/csv.ts detectDelimiter: the one most of the first lines use outside quotes, a quoted
field running on over a line break included, a quote inside a field such as 27" quoting nothing, ";" on a tie). The editor says once that only values are kept.
- Reading: x2t reads a CSV into an .xlsx, not into the binary format (that stops with error
89), and only with both formats in the task (
m_nFormatFrom260,m_nFormatTo257) plusm_nCsvTxtEncoding46 (UTF-8) andm_nCsvDelimiter(c_oAscCsvDelimiter: tab 1, ";" 2, "," 4);m_nLcid1031 makes it read "-4,20" and "01.10.2026" as a number and a date (coreX2tConverter/src/cextracttools.h,OOXML/Binary/Sheets/Reader/CSVReader.cpp). The .xlsx then opens as usual. - Writing happens in the page, not in x2t: x2t's CSV writer encodes text through ICU, which
has no converter data in the WebAssembly build, so every character past ASCII came out as a
single byte (
Writer/CSVWriter.cppWriteFile,UnicodeConverter.cppfromUnicode). The values are the active sheet's as its cells show them, through the Document Builder API (api.getJsApi().GetActiveSheet().GetUsedRange().GetText(),cell/apiBuilder.js), andencodeCsvwrites them as UTF-8 with a byte order mark and CR LF, quoting what holds the delimiter, a quote or a line break. "Als CSV herunterladen" on an .xlsx does the same with ";".
Presentations¶
src/slide.ts binds the presentation engine (slide/api.js), set up as web-apps'
presentationeditor/mobile does: SetDrawingFreeze(true) and SetThemesPath before loading,
SetDrawingFreeze(false), zoomFitToPage() once the document is shown. The engine draws the slide
list and the notes field inside its frame; the notes show once the document is shown
(asc_ShowNotes(true), as web-apps' desktop editor does unless they were hidden), and "Ansicht →
Notizen" hides and shows them. The engine does not report whether they show, so the command keeps
that in the state. Its own texts (empty placeholders, "click to add
notes") come from the translate map the API is created with (OpenOptions.texts): the app passes
them in the interface's language (apps/web/src/editor/engine-texts.ts); without it the engine shows
its own English (common/editorscommon.js CTranslateManager.getValue returns the key).
Character formatting is named and reported the same as in the text editor, so both share
createCharacterCommands and textEvents (put_TextPrBold, asc_onBold, …).
| Command | Engine call (slide/api.js) | Event back |
|---|---|---|
| new slide | AddSlide(layoutIndex) |
asc_onCountPages, asc_onCurrentPage |
| layout | ChangeLayout(layoutIndex); the layouts with their pictures: asc_onUpdateLayout (CLayoutThumbnail) |
– |
| duplicate, delete | DublicateSlide() (the engine's spelling), DeleteSlide() |
asc_onCountPages |
| bullets, numbering | put_ListType(0 \| 1, 1), end: put_ListType(…, -1) (CommonController.js fFillBullet) |
asc_onListType |
| character formatting, alignment, undo | as in the text editor | as in the text editor |
| clipboard, zoom, find and replace | as in the text editor (Copy, zoom, asc_findText, …) |
as in the text editor |
WordControl.m_oDrawingDocument.ToRendererPart(true, false): all slides, as slide/api.js _downloadAs |
– |
Shapes, designs and transitions¶
src/slide-design.ts, after web-apps (presentationeditor/mobile controller/add/AddShape.jsx,
controller/edit/EditSlide.jsx, lib/editor.jsx; main controller/Transitions.js):
- Shapes ("Einfügen"):
AddShapeOnCurrentPage(preset)puts a shape in the middle of the slide, selected: a text box (textRect, saved withtxBox="1"), a rectangle (rect), a rounded one (roundRect), an ellipse, an arrow (rightArrow) and a line. The keys go back to the engine afterwards, so typing fills the text box. - Designs ("Design"): the engine loads the list of its designs from
themes.jsin the themes path (SetThemesPath,g_defaultThemes) and sends it withasc_onInitEditorStyles([editor's, document's],CAscThemeInfowithget_Index,get_Name); the menu names them in German.ChangeTheme(index)loadstheme<index + 1>/theme.binand applies it to every slide. The design in use is reported only by its master (asc_onUpdateThemeIndexwith-master - 1), so none is checked. - Transitions ("Übergänge"):
SetSlideProps(props)with aCAscSlideTransitionofc_oAscSlideTransitionTypesand the first option of each, as web-apps'fillEffectTypespicks it (c_oAscSlideTransitionParams), on the selected slides;SlideTransitionApplyToAll()gives the current slide's to every slide. The selected slide's transition comes withasc_onFocusObject(the object of typeSlide,get_transition().get_TransitionType()) and is checked.
The slide show is in "Ansicht" (from the start or from the current slide), beside the "Präsentieren" button.
A right click sends CContextMenuData as in the text editor; its type tells the slide
(c_oAscContextMenuTypes.Main) from the slide list (Thumbnails), and the context menu offers the
slides first in the list (apps/web/src/editor/slide-menus.ts).
New presentations come from packages/ooxml (pptx.ts): one master with the layouts
"Titelfolie", "Titel und Inhalt", "Nur Titel" and "Leer", a theme with Office's default colours
and Calibri, 16:9.
Slide show¶
"Präsentieren" (and "Ansicht → Bildschirmpräsentation …", from the start or from the current
slide) shows the slides full screen, as web-apps' preview does (controller/Viewport.js
onPreviewStart, mobile controller/Preview.jsx). The engine draws the show into an element of
its own page, #presentation-preview (CDemonstrationManager.Start in
slide/Drawing/Transitions.js), and handles the arrow keys, space, clicks and Escape there itself,
but only while nothing in its page holds the keys (wrapKeyboard), so src/slide-show.ts gives
the frame's window the focus and blurs the engine's input first. The app makes the frame full
screen and starts the show once it has the screen's size (StartDemonstrationFromBeginning,
StartDemonstrationFromCurrentSlide). Leaving full screen ends the show (EndDemonstration), and
the end of the show (asc_onEndDemonstration) leaves full screen. Someone who may only look can
present too.
PDFs¶
The PDF editor of the engine (pdf/api.js PDFEditorApi) is built into the text-document bundle,
word/sdk-all-min.js; sdkjs/pdf/src/engine/ adds its renderer, drawingfile.wasm, which reads
and draws the PDF itself. So engine.json lists pdf among the editors, and its host page
(frame/editor/pdf/main/index.html) loads the word bundle and is checked for PDFEditorApi.
Unlike the Office editors, a PDF needs no conversion to open and keeps its format when it is saved:
- Open.
asc_openDocumentFromBytes(pdf)with the PDF's own bytes; the editor hands them todrawingfile.wasm(PDFEditorApi.openDocument→DocumentRenderer.open). The document info sayspdfas its format; the rest of the load chain is the one in How a document opens. - Page thumbnails. When the host page has an element
thumbnails-list, the editor fills it with the page thumbnails on content ready (onDocumentContentReady,pdf/src/thumbnails.js). Rotating, removing and moving pages act on the pages selected there, so the page has one. - Save. The editor's
DocumentRenderer.Save()(pdf/src/viewer.js) returns a change file, not a PDF: "%PDF" and the pages, annotations, form values and page operations that changed. x2t writes it into the original: the worker puts the original beside the change file (changes/changes0.bin) and setsm_bFromChangeswith the PDF format (513) both ways, and x2t applies it (X2tConverter/src/lib/pdf_image.hfromCrossPlatform→applyChangesPdf→applyCompiledChangesPdf). This is the Document Server'ssavefromorigin(word/api.js_downloadAs), done in the browser.createConverter().applyPdfChanges(original, changes, media)asks for it.
e2e/pdf-engine.spec.ts checks this contract below the app: it opens test-files/formular.pdf in
the host page, rotates a page, removes one, fills a field, saves through the worker, opens the
result again and reads the file back with pdf-lib.
The PDF view and its tools build on this; see PDFs.
Tests¶
packages/editor-core/test checks the binding against a stand-in for the engine
(test/fake-engine.ts): what each command calls, and how engine events become state. The E2E
specs run the real engine in Chromium:
| Spec | What it covers |
|---|---|
e2e/word-roundtrip.spec.ts |
a .docx opened and saved back with its content, images, comments and formatting |
e2e/word-formatting.spec.ts, e2e/word-menus.spec.ts |
the toolbar and the menus of the text editor: formatting, lists, layout, headers, notes, breaks |
e2e/comments.spec.ts, e2e/review.spec.ts |
comments with answers, resolving and deleting; tracked changes |
e2e/spelling.spec.ts |
the spell checker's marks and suggestions, by language |
e2e/spreadsheet.spec.ts, e2e/pivot.spec.ts |
the spreadsheet editor, and pivot tables |
e2e/presentation.spec.ts |
the presentation editor and the slide show |
e2e/formats.fidelity.ts |
the format-fidelity pictures (architecture) |