How to Perform Element-Level Manipulation in OfficeCLI Using DOM Operations

OfficeCLI uses a two-layer JavaScript architecture that receives Server-Sent Events (SSE) to perform targeted DOM mutations—such as adding Word paragraphs or replacing PowerPoint slides—then re-applies UI decorations via window._watchReapplyHook().

The iOfficeAI/OfficeCLI repository renders Microsoft Office documents as a live browser DOM, enabling real-time document editing through programmatic element-level manipulation. By leveraging the patch system in watch-sse-core.js and the decoration layer in watch-overlay.js, developers can insert, update, or delete specific document fragments without full page reloads.

Understanding the Two-Layer Architecture

OfficeCLI’s browser preview relies on two distinct JavaScript layers that separate data mutation from UI decoration.

Layer 1: Core Rendering and SSE Listener

Located in [src/officecli/Resources/watch-sse-core.js](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Resources/watch-sse-core.js), this layer opens an EventSource connection to /events and listens for update messages from the server. It parses incoming HTML and performs direct DOM mutations including full-body swaps, block-level patches, and slide add/replace/remove operations.

Layer 2: Overlay and Decoration

Implemented in [src/officecli/Resources/watch-overlay.js](https://github.com/iOfficeAI/OfficeCLI/blob/main/src/officecli/Resources/watch-overlay.js), this layer registers window._watchReapplyHook() to re-apply selection highlights, text marks, and UI decorations immediately after Layer 1 completes any DOM mutation.

SSE Update Message Format

The server streams JSON-encoded SSE events containing an action field. For element-level manipulation, the relevant actions include:

  • replace: Replace an entire slide or document block. Payload includes slide (number) and html (new markup).
  • add: Insert a new slide or block. Requires slide and html.
  • remove: Delete a slide or block. Requires slide only.
  • full: Replace the entire <body> as a fallback when the client has missed messages. Requires html.
  • word-patch / excel-patch: Apply granular block patches. Payload contains patches: [{op, block, html?, scrollTo?}] where op can be add, replace, remove, or style.

The core message handler resides in watch-sse-core.js at lines 17-48, with per-action logic beginning at line 317.

Performing DOM Mutations

Full-Body Replacement

When the client receives a full action or detects a missed message, Layer 1 executes _replaceDocumentBody():

function _replaceDocumentBody(msg) {
    fetch('/').then(r => r.text()).then(html => {
        const doc = new DOMParser().parseFromString(html, 'text/html');
        document.body.innerHTML = doc.body.innerHTML;
        _callReapplyHook();
    });
}

This approach creates a fresh document using DOMParser, swaps the existing <body> content in a single operation to preserve scroll position, then triggers the overlay hook to refresh decorations.

Block-Level Patches for Word and Excel

For word-patch or excel-patch actions, the wordPatchUpdate() function manipulates content between invisible marker spans (<span class="wb" data-block="N"> for begin, <span class="we" data-block="N"> for end):

function wordPatchUpdate(msg) {
    msg.patches.forEach(patch => {
        const start = document.querySelector(`.wb[data-block="${patch.block}"]`);
        const end = document.querySelector(`.we[data-block="${patch.block}"]`);
        
        if (patch.op === 'remove') {
            /* remove nodes between start and end */
        } else if (patch.op === 'replace') {
            /* replace inner range */
        } else if (patch.op === 'add') {
            const tmp = document.createElement('div');
            tmp.innerHTML = '<span class="wb" data-block="' + patch.block + '"></span>' 
                          + patch.html 
                          + '<span class="we" data-block="' + patch.block + '"></span>';
            prevEnd.parentNode.insertBefore(tmp.firstChild, prevEnd.nextSibling);
        } else if (patch.op === 'style') {
            /* replace <style> tags */
        }
    });
    _callReapplyHook();
}

This implementation (lines 48-90) allows surgical updates to specific document regions without affecting surrounding content.

Slide-Specific Actions for PowerPoint

For PowerPoint previews, Layer 1 locates slide containers via data-slide attributes:

if (msg.action === 'replace') {
    const el = document.querySelector(`.slide-container[data-slide="${msg.slide}"]`);
    if (el) {
        const tmp = document.createElement('div');
        tmp.innerHTML = msg.html;
        el.parentNode.replaceChild(tmp.firstElementChild, el);
        _executeScripts(tmp.firstElementChild);
        _callReapplyHook();
    }
}

The _executeScripts() function (lines 20-38) re-creates <script> elements to ensure module scripts execute correctly after DOM insertion.

Re-applying Decorations After Mutations

After any mutation, Layer 1 invokes window._watchReapplyHook(). watch-overlay.js defines this hook at lines 31-33:

function reapplyDecorations() {
    applySelectionToDom();
    applyMarks();
}
window._watchReapplyHook = reapplyDecorations;

This mechanism keeps selection states and text marks synchronized with the mutated DOM. The mark handling logic resides in lines 300-500 of watch-overlay.js, using pure DOM APIs like classList.add and document.createRange().surroundContents.

Client-Side API for Direct Manipulation

OfficeCLI exposes several global methods for programmatic control (defined at lines 34-38 of watch-overlay.js):

  • window._officecliSetMarks(arr): Replace the current marks array and render them immediately.
  • window._officecliApplyMarks(): Re-apply existing marks after manual DOM changes.
  • window._officecliReapplyDecorations(): Force a full refresh of selections and marks.
  • window._watchEs: The EventSource object for custom event listening or debugging.

Practical Code Examples

Insert a New Paragraph into Word

const patch = {
  op: 'add',
  block: 42,
  html: '<p data-path="/doc/body/para[123]">New paragraph</p>'
};

fetch('/api/patch', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ action: 'word-patch', patches: [patch] })
});

When the server broadcasts this as an SSE message, Layer 1 creates the marker spans and inserts the paragraph, then calls _callReapplyHook() to refresh the overlay.

Replace an Existing PowerPoint Slide

const slideNum = 5;
const newHtml = `<div class="slide-container" data-slide="${slideNum}">
  <h2>Updated Title</h2>
</div>`;

const el = document.querySelector(`.slide-container[data-slide="${slideNum}"]`);
if (el) {
  const tmp = document.createElement('div');
  tmp.innerHTML = newHtml;
  el.parentNode.replaceChild(tmp.firstElementChild, el);
  _executeScripts(tmp.firstElementChild);
  window._watchReapplyHook();
}

Remove a Block from Word

fetch('/api/patch', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: 'word-patch',
    patches: [{ op: 'remove', block: 17 }]
  })
});

Layer 1 finds the .wb and .we markers for block 17 and removes all sibling nodes between them (lines 50-57).

Apply Temporary CSS Styles

fetch('/api/patch', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    action: 'word-patch',
    patches: [{ 
      op: 'style', 
      html: '<style>.officecli-selected{background:#e0f7fa;}</style>' 
    }]
  })
});

The style operation (lines 40-46) updates the document's <style> blocks without touching content markup.

Summary

  • OfficeCLI uses Server-Sent Events to drive DOM updates through two layers: core mutation (watch-sse-core.js) and decoration (watch-overlay.js).
  • Block-level patches rely on invisible marker spans (.wb/.we) to delimit update regions in Word and Excel documents.
  • Slide-specific actions target containers via data-slide attributes for PowerPoint manipulation.
  • After every mutation, window._watchReapplyHook() restores selection highlights and text marks.
  • Client-side APIs like window._officecliSetMarks() provide direct access to the decoration pipeline for custom automation.

Frequently Asked Questions

How does OfficeCLI track document blocks for patching?

According to the source code in WordHandler.cs, the server generates invisible marker spans with classes .wb (word begin) and .we (word end) that wrap each content block. These markers carry data-block attributes containing numeric identifiers, allowing watch-sse-core.js to precisely locate insertion points and boundaries for add, replace, and remove operations.

What happens if I miss an SSE update message?

If the client detects a sequence gap or connection interruption, watch-sse-core.js falls back to a full body replacement. The _replaceDocumentBody() function fetches the complete document HTML from the root endpoint, parses it with DOMParser, and replaces the entire <body> content to ensure synchronization before re-applying decorations.

Can I manipulate the DOM directly without using SSE patches?

Yes. You can manipulate the DOM directly using standard browser APIs, but you must call window._officecliReapplyDecorations() or window._watchReapplyHook() afterward to synchronize selection highlights and marks. For programmatic mark management, use window._officecliSetMarks() to update the internal state and trigger a re-render.

How do I preserve PowerPoint slide scripts after DOM replacement?

When replacing slide content, use the _executeScripts() helper function from watch-sse-core.js (lines 20-38). This utility re-creates <script> elements from the new markup to ensure that module scripts and event handlers execute correctly in the replaced DOM subtree.

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 →