More DIY MS Word Add-ins — Python and R code execution using Pyodide and WebR

Picking up on the pattern I used in DIY Microsoft Office Add-Ins – Postgres WASM in the Task Pane, it’s easy enough to do the same thing for Python code execution using pyodide WASM and R code using WebR WASM.

As in the postgres/SQL demo, this lets you select code in the Word doc, then edit and execute it in the task pane. If you modify the code in the task pane, you can use it to replace the highlighted text in the Word doc. (I haven’t yet looked to see what else the Word API supports…) The result of the code execution can be pasted back to the end of the Word doc.

The Pyodide and WebR environments persist between code execution steps, so you can build up state. I added a button to reset the state of the Pyodide environment back to an initial state, but haven’t done that for the WebR environment yet.

I’m not sure how useful this is? It’s a scratchpad thing as much as anything for lightly checking whether the code fragment in a Word doc are valid and run as expected. It can be used to bring back the results of the code execution into the Word doc, which may be useful. The coupling is tighter than if you are copying and pasting code and results to/from a code editor, but it’s still weaker than the integration you get from a reproducible document type such as a Jupyter notebook or an executable MyST markdown doc.

One thing that might be interesting to explore is whether I can style the code in the Word doc, then extract and run the code from the Task Pane to check it works, maybe even checking the output against some sort of particularly style output in the Word doc. But again, that feels a bit clunky compared to authoring in a notebook or Myst and then generating a Word doc, or whatever format doc, with actual code execution generating the reported outputs etc.

Here’s the code:

<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. -->
<!-- This file shows how to design a first-run page that provides a welcome screen to the user about the features of the add-in. -->

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Contoso Task Pane Add-in</title>

    <!-- Office JavaScript API -->
    https://appsforoffice.microsoft.com/lib/1.1/hosted/office.js

    <!-- Load pyodide -->
    https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js
    http://./pyodide.js
    <!-- For more information on Fluent UI, visit https://developer.microsoft.com/fluentui#/. -->
    <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/>

    <!-- Template styles -->
    <link href="taskpane.css" rel="stylesheet" type="text/css" />
</head>

<body class="ms-font-m ms-welcome ms-Fabric">
    <header class="ms-welcome__header ms-bgColor-neutralLighter">
        <h1 class="ms-font-su">pyodide & WebR demo</h1>
    </header>
    <section id="sideload-msg" class="ms-welcome__main">
        <h2 class="ms-font-xl">Please <a target="_blank" href="https://learn.microsoft.com/office/dev/add-ins/testing/test-debug-office-add-ins#sideload-an-office-add-in-for-testing">sideload</a> your add-in to see app body.</h2>
    </section>
    <main id="app-body" class="ms-welcome__main" style="display: none;">
        <h2 class="ms-font-xl"> Pyodide & WebR demo </h2>
        <div>Execute Python or R code using Pyodide and WebR WASM powered code execution environments.</div>
        <textarea id="query" rows="4" cols="30"></textarea>
    <div><button id="getsel">Get Selection</button><button id="execute-py">Execute Py</button><button id="execute-r">Execute R</button><button id="exepaste">Paste Result</button><button id="replacesel">Replace selection</button><button id="reset">Reset Py</button></div>
     <div id="output-type"></div>
    <div id="output"></div>

    </main>
</body>

</html>

import { WebR } from "https://webr.r-wasm.org/latest/webr.mjs";

window.addEventListener("DOMContentLoaded", async function () {
  const buttonpy = /** @type {HTMLButtonElement} */ (document.getElementById("execute-py"));
  const buttonr = /** @type {HTMLButtonElement} */ (document.getElementById("execute-r"));
 
  let pyodide = await loadPyodide();

  const webR = new WebR();
  await webR.init();

  const resetpybutton = /** @type {HTMLButtonElement} */ (document.getElementById("reset"));
  resetpybutton.addEventListener("click", async function () {
    pyodide = await loadPyodide();
  });

  // Execute py on button click.
  buttonpy.addEventListener("click", async function () {
    buttonpy.disabled = true;

    // Get SQL from editor.
    const queries = document.getElementById("query").value;

    // Clear any previous output on the page.
    const output = document.getElementById("output");
    while (output.firstChild) output.removeChild(output.lastChild);

    //const timestamp = document.getElementById("timestamp");
    //timestamp.textContent = new Date().toLocaleTimeString();

    let time = Date.now();
    console.log(`${queries}`);
    document.getElementById("output-type").innerHTML = "Executing Py code...";
    try {
      const queries = document.getElementById("query").value;
      let output_txt = pyodide.runPython(queries);
      output.innerHTML = output_txt;
    } catch (e) {
      // Adjust for browser differences in Error.stack().
      const report = (window["chrome"] ? "" : `${e.message}\n`) + e.stack;
      output.innerHTML = `<pre>${report}</pre>`;
    } finally {
      //timestamp.textContent += ` ${(Date.now() - time) / 1000} seconds`;
      buttonpy.disabled = false;
      document.getElementById("output-type").innerHTML = "Py code result:";
    }
  });

  // Execute R on button click.
  buttonr.addEventListener("click", async function () {
    buttonr.disabled = true;

    // Get SQL from editor.
    const queries = document.getElementById("query").value;

    // Clear any previous output on the page.
    const output = document.getElementById("output");
    while (output.firstChild) output.removeChild(output.lastChild);

    //const timestamp = document.getElementById("timestamp");
    //timestamp.textContent = new Date().toLocaleTimeString();

    let time = Date.now();
    console.log(`${queries}`);
    document.getElementById("output-type").innerHTML = "Executing R code...";
    try {
      const queries = document.getElementById("query").value;
      let output_r = await webR.evalR(queries);
      let output_json = await output_r.toJs();
      output.innerHTML = JSON.stringify(output_json);
    } catch (e) {
      // Adjust for browser differences in Error.stack().
      const report = (window["chrome"] ? "" : `${e.message}\n`) + e.stack;
      output.innerHTML = `<pre>${report}</pre>`;
    } finally {
        document.getElementById("output-type").innerHTML = "R code result:";
      //timestamp.textContent += ` ${(Date.now() - time) / 1000} seconds`;
      buttonr.disabled = false;
    }
  });

});

/*
 * Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
 * See LICENSE in the project root for license information.
 */

/* global document, Office, Word */

Office.onReady((info) => {
  if (info.host === Office.HostType.Word) {
    document.getElementById("sideload-msg").style.display = "none";
    document.getElementById("app-body").style.display = "flex";
    document.getElementById("exepaste").onclick = exePaste;
    document.getElementById("getsel").onclick = getSel;
    document.getElementById("replacesel").onclick = replaceSel;
  }
});

async function getSel() {
  await Word.run(async (context) => {
    // Get code from selection
    const selected = context.document.getSelection();
    selected.load("text");
    await context.sync();
    document.getElementById("query").value = selected.text;
  });
}

async function replaceSel() {
  await Word.run(async (context) => {
    // Replace selected code
    const selected = context.document.getSelection();
    const replace_text = document.getElementById("query").value;
    selected.insertText(replace_text, Word.InsertLocation.replace);
    await context.sync();
  });
}

async function exePaste() {
  await Word.run(async (context) => {
    var output = document.getElementById("output").innerHTML;
  const docBody = context.document.body;
  docBody.insertParagraph(
    output,
    Word.InsertLocation.end
  );
    await context.sync();
  });
}

Author: Tony Hirst

I'm a Senior Lecturer at The Open University, with an interest in #opendata policy and practice, as well as general web tinkering...

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.