How the HTML Rendering Engine Works for Document Visualization in OfficeCLI

The OfficeCLI HTML rendering engine visualizes Word, Excel, and PowerPoint documents in the browser through a two-layer JavaScript architecture: Layer 1 handles Server-Sent Events (SSE) streaming and DOM mutations via watch-sse-core.js, while Layer 2 manages selection overlays and interactive decorations via watch-overlay.js.

OfficeCLI transforms Office documents into live, interactive web visualizations without requiring desktop applications. This article explains the complete rendering pipeline based on the iOfficeAI/OfficeCLI source code, walking through how document updates flow from server to screen and how user interactions sync back in real time.

Two-Layer Rendering Architecture

The HTML rendering engine splits responsibilities between tightly coupled but functionally distinct layers:

Layer Responsibility Core File
Layer 1 – Document Rendering & Navigation SSE stream management, full document swaps, diff/patch updates, scroll commands, and re-apply hook infrastructure [watch-sse-core.js](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Resources/watch-sse-core.js)
Layer 2 – Overlay & Decoration Selection tracking, rectangular overlay drawing, CSS class injection, mouse-driven interactions, and inline marks [watch-overlay.js](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Resources/watch-overlay.js)

The coupling contract between layers operates through two global window properties: window._watchEs exposes the SSE connection for Layer 2 to listen on, and window._watchReapplyHook allows Layer 1 to trigger visual refreshes after any DOM mutation.

Layer 1: Event-Driven Document Rendering

SSE Connection Initialization

When the visualization page loads, watch-sse-core.js establishes a persistent Server-Sent Events connection to /events and stores it globally:

// In watch-sse-core.js (auto-executed)
(function () {
  const es = new EventSource('/events');   // ← opens SSE stream
  window._watchEs = es;                    // ← exported for overlay layer
  // …register listeners for 'update', 'doc-switched', etc.
})();

This EventSource listens continuously for server-side update events that carry JSON payloads with an action field determining how to process the message.

Message Dispatch and Routing

The main event listener in watch-sse-core.js parses incoming SSE data and routes to specialized handlers based on the action field:

  • full – Complete document body replacement
  • word-diff – Incremental diff for Word documents
  • word-patch – Granular DOM patch operations
  • scroll – Viewport positioning without DOM changes
// Simplified dispatch pattern from watch-sse-core.js#L17-L33
function onMessage(event) {
  const msg = JSON.parse(event.data);
  switch (msg.action) {
    case 'full':           _replaceDocumentBody(msg); break;
    case 'word-diff':      wordDiffUpdate(msg);       break;
    case 'word-patch':     wordPatchUpdate(msg);      break;
    case 'scroll':         scrollToSlide(msg);        break;
  }
  _callReapplyHook();  // notify Layer 2 to repaint overlays
}

Full Document Swaps

For non-Word documents (Excel, PowerPoint), the _replaceDocumentBody function in watch-sse-core.js handles complete refreshes. This function:

  1. Fetches current HTML from the root endpoint
  2. Parses with DOMParser to extract <head> styles
  3. Preserves scroll position before mutation
  4. Re-injects the SSE script into the new body
  5. Invokes _callReapplyHook() for overlay synchronization
function _replaceDocumentBody(msg) {
  fetch('/').then(r => r.text()).then(html => {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    // Copy head styles, preserve scroll offset
    const scrollY = window.scrollY;
    document.head.innerHTML = doc.head.innerHTML;
    document.body.innerHTML = doc.body.innerHTML;
    window.scrollTo(0, scrollY);
    // Re-inject SSE script and notify overlay layer
    _callReapplyHook();
  });
}

Word Document: Diff Algorithm

Word documents use wordDiffUpdate for incremental updates. This lightweight algorithm de-paginates content, merges old and new sections, and replaces only changed node ranges. If the version gap exceeds a threshold, it falls back to full replacement.

Word Document: Granular Patching

The wordPatchUpdate function applies surgical DOM operations using marker elements (.wb / .we classes) that delimit content blocks. Supported operations include add, replace, remove, and style. After each patch, it re-paginates via window._wordPaginate() and triggers the re-apply hook.

// From watch-sse-core.js#L221-L259
function wordPatchUpdate(msg) {
  msg.patches.forEach(patch => {
    const startMarker = findMarker(patch.start, 'wb');
    const endMarker   = findMarker(patch.end,   'we');
    const range = document.createRange();
    range.setStartAfter(startMarker);
    range.setEndBefore(endMarker);
    
    switch (patch.op) {
      case 'replace': range.deleteContents(); range.insertNode(parseHtml(patch.html)); break;
      case 'remove':  range.deleteContents();  break;
      case 'style':   applyStyles(range, patch.css); break;
    }
  });
  window._wordPaginate?.();
  _callReapplyHook();
}

Scroll Commands

Pure navigation without DOM mutation: the scroll action accepts a selector or slide number and adjusts viewport position directly.

Layer 2: Selection Overlay and Interactive Decoration

Coupling Contract with Layer 1

watch-overlay.js reads the shared SSE connection and registers its refresh callback:

// From watch-overlay.js#L5-L9
(function () {
  const es = window._watchEs;  // ← consume Layer 1's EventSource
  window._watchReapplyHook = reapplyDecorations;
  
  es.addEventListener('selection-update', onSelectionUpdate);
  es.addEventListener('marks-update', onMarksUpdate);
})();

Selection State Synchronization

The client maintains _selection as a local mirror of the server's currentSelection. Server updates arrive via selection-update events, replace the local array, and immediately trigger applySelectionToDom().

// From watch-overlay.js#L41-L50
function onSelectionUpdate(event) {
  const data = JSON.parse(event.data);
  _selection.length = 0;
  _selection.push(...data.paths);
  applySelectionToDom();
}

Rectangular Overlay Positioning

For contiguous Excel-style ranges, _detectRect computes bounding sheet coordinates. The _selOverlay div positions absolutely within the nearest <table> to maintain alignment during scrolling:

function _positionSelOverlay(cellEls) {
  const container = cellEls[0].closest('table') || document.body;
  const ov = _getSelOverlayEl();
  
  // Compute union rectangle of selected cells
  const rects = cellEls.map(el => el.getBoundingClientRect());
  const minL = Math.min(...rects.map(r => r.left));
  const minT = Math.min(...rects.map(r => r.top));
  const maxR = Math.max(...rects.map(r => r.right));
  const maxB = Math.max(...rects.map(r => r.bottom));
  
  const cRect = container.getBoundingClientRect();
  ov.style.left   = `${minL - cRect.left + container.scrollLeft}px`;
  ov.style.top    = `${minT - cRect.top  + container.scrollTop}px`;
  ov.style.width  = `${maxR - minL}px`;
  ov.style.height = `${maxB - minT}px`;
  ov.style.display = 'block';
}

CSS Injection for Highlights

A self-executing block at module startup injects all highlight classes into the document head:

// From watch-overlay.js#L27-L71
(function injectStyles() {
  const style = document.createElement('style');
  style.textContent = `
    .officecli-range-fill { background: rgba(66, 133, 244, 0.16); }
    .officecli-row-header { background: #e8f0fe; }
    .officecli-col-header { background: #e8f0fe; }
    .officecli-handle      { border: 2px solid #4285f4; }
    .officecli-mark        { background: yellow; }
    /* …additional classes */
  `;
  document.head.appendChild(style);
})();

Mouse-Driven Interactions

watch-overlay.js handles click selection (with Shift/Ctrl modifiers), rubber-band dragging, header dragging, and chart dragging. Each interaction:

  1. Mutates _selection or _marks locally
  2. Calls applySelectionToDom() for immediate visual feedback
  3. Posts changes back to server endpoints
// Simplified interaction handler from watch-overlay.js#L140-L148
function onMouseUp(event) {
  const newSelection = computeSelectionFromDrag();
  _selection.length = 0;
  _selection.push(...newSelection);
  applySelectionToDom();
  
  fetch('/api/selection', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paths: _selection })
  });
}

Inline Marks (Search Highlights)

The applyMarks() function processes search results and annotations. It distinguishes block-level marks from inline patterns (plain strings or regular expressions), builds text node maps for target elements, and wraps matching ranges in <span class="officecli-mark…"> elements.

Crucially, Layer 2 never mutates the underlying document structure—only CSS classes and transient wrappers—ensuring that Layer 1's full or diff updates can safely replace content without corrupting visual state.

Complete Data Flow


┌─────────┐     SSE (/events)      ┌─────────────────────┐
│ Server  │ ─────────────────────→ │ watch-sse-core.js   │
│         │                        │ (Layer 1)             │
│         │  update {action,…}     │  ├─► full swap      │
│         │───────────────────────→│  ├─► diff           │
│         │                        │  ├─► patch          │
│         │                        │  └─► scroll         │
│         │                        │                       │
│         │  after DOM mutation    │  _callReapplyHook()  │
│         │←───────────────────────│        ↓             │
│         │                        ├─────────────────────┤
│         │                        │ watch-overlay.js    │
│         │  selection-update      │ (Layer 2)           │
│         │───────────────────────→│  ├─► overlay draw   │
│         │  marks-update          │  ├─► CSS injection  │
│         │───────────────────────→│  └─► mouse handling │
│         │                        │                       │
│         │  POST /api/selection   │  user interaction ──┐│
│         │←───────────────────────│  POST /api/send     ││
└─────────┘                        └─────────────────────┘

Key Source Files

File Purpose
src/officecli/Resources/watch-sse-core.js Layer 1: SSE connection, DOM swapping, Word diff/patch logic, scroll handling, re-apply hook infrastructure
src/officecli/Resources/watch-overlay.js Layer 2: Selection state, overlay rendering, CSS injection, mouse interactions, marks
npm/officecli.js CLI entry point that boots the web server for HTML serving and SSE streaming
sdk/node/index.js Node SDK for server-side HTML payload generation

Summary

  • Two-layer architecture separates document mutation (watch-sse-core.js) from visual decoration (watch-overlay.js), connected via window._watchEs and window._watchReapplyHook
  • Server-Sent Events drive all rendering through a single /events stream carrying update, selection, and mark messages
  • Three update strategies: full body replacement for Excel/PPT, diff merging for Word, and granular patch operations for Word blocks
  • Overlay system computes absolute-positioned rectangles for Excel ranges and injects self-contained CSS without touching document structure
  • Bidirectional synchronization: server pushes state changes, client posts interaction results to /api/selection and /api/send

Frequently Asked Questions

What protocol does OfficeCLI use for real-time document updates?

OfficeCLI uses Server-Sent Events (SSE) via the /events endpoint. The EventSource API in watch-sse-core.js maintains a persistent connection that streams JSON messages with action fields (full, word-diff, word-patch, scroll) for document updates and selection-update/marks-update for overlay state. This unidirectional server-push protocol was chosen over WebSockets for simpler reconnection handling and HTTP compatibility.

How does OfficeCLI preserve user selections during document refreshes?

The re-apply hook pattern preserves selections across DOM mutations. watch-sse-core.js exposes window._watchReapplyHook, which watch-overlay.js sets to its reapplyDecorations function. After every full swap, diff, or patch operation, Layer 1 calls this hook, causing Layer 2 to re-query _selection state and redraw overlays at the new DOM positions. Since Layer 2 only adds CSS classes and transient wrappers—not structural changes—Layer 1 can safely replace content without losing selection state.

Why does Word document rendering use both diff and patch strategies?

Word documents require granular update strategies because pagination makes full replacements visually disruptive. The wordDiffUpdate function handles section-level merging for moderate changes, while wordPatchUpdate applies surgical DOM operations (add, replace, remove, style) using .wb/.we marker elements. If version gaps are detected, the engine falls back from patch to diff, ensuring consistency while minimizing layout shifts.

Can the rendering engine handle multiple users editing simultaneously?

The source code shows single-user architecture with server-authoritative selection state. While the SSE stream broadcasts updates to all connected clients, there's no operational transform or conflict resolution logic visible in watch-sse-core.js or watch-overlay.js. The /api/selection endpoint posts single-user changes; concurrent modifications would likely exhibit last-write-wins behavior based on server-side implementation in npm/officecli.js or sdk/node/index.js.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →