Last active
February 26, 2023 10:57
-
-
Save IvanaGyro/6e4eea78d24760211d127e9c67b2f335 to your computer and use it in GitHub Desktop.
Insert math equations into the online version of PowerPoint.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Formulas for PowerPoint | |
description: Insert math equations into the online version of PowerPoint. | |
host: POWERPOINT | |
api_set: {} | |
script: | |
content: | | |
var editor; | |
const MathJax = window["MathJax"]; | |
const ace = window["ace"]; | |
const toastr = window["toastr"]; | |
/** | |
* The new task passed to `PowerPoint.run` will interrupt the previous one. | |
* Lock this flag to prevent the process of changing name after inserting | |
* from being interrupted. | |
*/ | |
var insertionLocked = false; | |
init(); | |
$("#insert").click(() => tryCatch(insertEquation)); | |
subscribe(); | |
/** Default helper for invoking an action and handling errors. */ | |
async function tryCatch(callback) { | |
try { | |
await callback(); | |
} catch (error) { | |
// Note: In a production add-in, you'd want to notify the user through your add-in's UI. | |
console.error(error); | |
} | |
} | |
async function wait(ms) { | |
return new Promise((resolve) => { | |
setTimeout(resolve, ms); | |
}); | |
} | |
async function waitUntilInternal(condition, deadline, intervalMs) { | |
if (Date.now() >= deadline) { | |
console.log("timeout"); | |
return; | |
} | |
if (await condition()) { | |
return; | |
} | |
await wait(intervalMs); | |
return waitUntilInternal(condition, deadline, intervalMs); | |
} | |
async function waitUntil(condition, timeoutMs, intervalMs = 100) { | |
return waitUntilInternal(condition, Date.now() + timeoutMs, intervalMs); | |
} | |
function init() { | |
editor = ace.edit("editor"); | |
editor.setTheme("ace/theme/textmate"); | |
editor.getSession().setMode("ace/mode/latex"); | |
editor.$blockScrolling = Infinity; | |
editor.getSession().setUseWrapMode(true); | |
// editor.setTheme("ace/theme/chrome"); | |
editor.setOption("minLines", 10); | |
// editor.setOption("maxLines", Infinity); | |
// editor.setOption("maxLines", 15); | |
editor.setAutoScrollEditorIntoView(true); | |
editor.setShowPrintMargin(false); | |
// editor.setPrintMarginColumn(100);\ | |
editor.on("change", () => { | |
let backup = editor.getValue(); | |
typeset(() => { | |
const math = document.getElementById("equation-output"); | |
math.innerHTML = `\$\$${backup}\$\$`; | |
return [math]; | |
}); | |
}); | |
toastr.options.positionClass = "toast-bottom-right"; | |
} | |
let promise = Promise.resolve(); // Used to hold chain of typesetting calls | |
function typeset(code) { | |
promise = promise | |
.then(() => MathJax.typesetPromise(code())) | |
.catch((err) => { | |
console.log(err); | |
}); | |
return promise; | |
} | |
async function insertEquation() { | |
// Inserts an image anchored to the last paragraph. | |
const latex = editor.getValue().replace(/ +(?= )/g, ""); | |
insertionLocked = true; | |
const svg = document.getElementById("equation-output").getElementsByTagName("svg")[0]; | |
newImage(svg.outerHTML); | |
// await wait(500); // Wait for the inserted image to be selected. | |
toastr.warning("DO NOT click anything until the LaTex is stored.", "Processing..."); | |
await setSelectedShapeName(`\$\$${latex}\$\$`); | |
toastr.success("", "Successfully insert the equation!"); | |
insertionLocked = false; | |
} | |
function newImage(svgImage) { | |
Office.context.document.setSelectedDataAsync( | |
svgImage, | |
{ | |
coercionType: Office.CoercionType.XmlSvg, | |
imageLeft: 50, | |
imageTop: 50 | |
}, | |
function(asyncResult) { | |
if (asyncResult.status === Office.AsyncResultStatus.Failed) { | |
toastr.error("asyncResult.error.message", "Fail to insert the equation."); | |
console.error(asyncResult); | |
} | |
} | |
); | |
} | |
async function setSelectedShapeName(name) { | |
await PowerPoint.run(async (context) => { | |
let shapes; | |
let shapeCount; | |
const isOneShapeSelected = async () => { | |
shapes = context.presentation.getSelectedShapes(); | |
shapeCount = shapes.getCount(); | |
await context.sync(); | |
return shapeCount.value === 1; | |
}; | |
await waitUntil(isOneShapeSelected, 50000); | |
if (shapeCount.value !== 1) { | |
return; | |
} | |
shapes.load("items"); | |
await context.sync(); | |
shapes.items[0].name = name; | |
await context.sync(); | |
}); | |
} | |
async function getSelectedShapeName() { | |
let name = undefined; | |
await PowerPoint.run(async (context) => { | |
const shapes = context.presentation.getSelectedShapes(); | |
const shapeCount = shapes.getCount(); | |
await context.sync(); | |
if (shapeCount.value !== 1) { | |
return; | |
} | |
shapes.load("items"); | |
await context.sync(); | |
const selectedShape = shapes.items[0]; | |
selectedShape.load("name"); | |
await context.sync(); | |
name = selectedShape.name; | |
}); | |
return name; | |
} | |
async function subscribe() { | |
// Some old handlers may not be removed after reloading. | |
await Office.context.document.removeHandlerAsync(Office.EventType.DocumentSelectionChanged); | |
Office.context.document.addHandlerAsync(Office.EventType.DocumentSelectionChanged, handler); | |
} | |
async function handler(something) { | |
if (insertionLocked) { | |
return; | |
} | |
const selectedShapeName = await getSelectedShapeName(); | |
console.debug(`selectedShapeName: ${selectedShapeName}`); | |
if (selectedShapeName != null && selectedShapeName.startsWith("$$") && selectedShapeName.endsWith("$$")) { | |
editor.setValue(selectedShapeName.slice(2, -2)); | |
toastr.success("Successfully load the LaTex!"); | |
} | |
} | |
language: typescript | |
template: | |
content: "<div class=\"equation-container\">\n\t<div id=\"editor\"></div>\n\t<div id=\"equation-output\"></div>\n</div>\n<div class=\"button-container\">\n\t<button id=\"insert\" class=\"ms-Button\">\n <span class=\"ms-Button-label\">Insert</span>\n </button>\n</div>\n\n<script>\n\t// To use physics and colorv2 package, braket and color have to be removed.\n\t// Refer to https://docs.mathjax.org/en/latest/web/components/input.html#tex-component\n\t\tMathJax = {\n\t\t\ttex: {\n\t\t\t\tpackages: {\n\t\t\t\t\t'[+]': ['physics', 'colorv2'],\n\t\t\t\t\t'[-]': ['braket', 'color']\n\t\t\t\t}\n\t\t\t}\n\t };\n</script>\n<script id=\"MathJax-script\" src=\"https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg-full.js\"></script>" | |
language: html | |
style: | |
content: | | |
body { | |
display: flex; | |
justify-content: space-between; | |
flex-flow: column nowrap; | |
margin: 0; | |
height: 100vh; | |
gap: 8px 0px; | |
align-content: stretch; | |
} | |
.equation-container { | |
display: flex; | |
flex-flow: column nowrap; | |
align-self: stretch; | |
flex-grow: 1; | |
gap: 8px 0px; | |
} | |
.button-container { | |
display: flex; | |
flex-flow: row nowrap; | |
padding: 0 8px 5px 8px; | |
gap: 0px 8px; | |
} | |
#editor { | |
height: 70vh; | |
} | |
#equation-output { | |
flex-grow: 1; | |
margin: 0px 8px; | |
background: rgb(244, 244, 244); | |
display: flex; | |
flex-flow: column nowrap; | |
justify-content: center; | |
} | |
.ms-Button { | |
display: block; | |
min-width: 80px; | |
} | |
language: css | |
libraries: | | |
https://appsforoffice.microsoft.com/lib/1/hosted/office.js | |
@types/office-js | |
[email protected]/dist/css/fabric.min.css | |
[email protected]/dist/css/fabric.components.min.css | |
[email protected]/client/core.min.js | |
@types/core-js | |
[email protected] | |
@types/[email protected] | |
https://cdnjs.cloudflare.com/ajax/libs/ace/1.15.2/ace.js | |
https://cdn.jsdelivr.net/npm/[email protected]/css/ace.min.css | |
https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css | |
https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment