Inline Calculator (REPL)

demo:
Screen Recording 2023-09-23 at 7.00.17 PM

full integration code for Keyboard Maestro:

function TaskPaperContextScript(editor, options) {
  const outline = editor.outline;
  const selection = editor.selection;
  let childTexts = [];

  // dont support multiselect for now
  if (editor.selection.selectedItems.length !== 1) return;

  const hasChildren = item => item.children?.length > 0;
  const firstSelectedItem = editor.selection.selectedItems[0];
  const selectedItem = hasChildren(firstSelectedItem)?firstSelectedItem:firstSelectedItem.parent;
  selectedItem.children.forEach(child => {
    childTexts.push(child.bodyContentString);
  });

  // TODO: fix bug where context (previous variables) seem to still be in memory
  const evaluateEachLineInBlob = inputBlob => {
    const lines = inputBlob.split('\n');

    // sanitize lines
    const sanitizedLines = lines.map(line => line.split('==>')[0].trim());

    // convert to script
    const scriptLines = sanitizedLines.map((line, lineI) => {
      if (!line.trim()) {  // empty
        return line;
      } else if (line.includes('=>')) {  // function
        return line;
      } else if (line.includes('=')) {  // equation
        return line;
      } else if (line.startsWith('//')) {  // comment
        return line;
      } else {  // unknown
        return `// ${line}`;
      }
    });

    // execute each line in script
    let didError = false;
    const resultOfEachLine = scriptLines.map((scriptLine, scriptLineI) => {
      if (didError) return scriptLine;

      const prevLines = scriptLines.slice(0, scriptLineI);

      const isEquation = !scriptLine.includes('=>') && scriptLine.includes('=');
      if (isEquation) {
        const [lhs, rhs] = scriptLine.split('=');

        const script = `${prevLines.join('\n') +'\n'+ scriptLine}\n${lhs.trim()}`;

        let result;
        try {
          result = eval(script);
        } catch(e) {
          didError = true;

          let errHint = '';
          const errRawFirstLine = e.toString().split('\n')[0];
          if (errRawFirstLine.startsWith('ReferenceError: ')) {
            errHint = errRawFirstLine
              .replace('ReferenceError: ', '')
              .replace('is not defined', '')
              .replace('Can\'t find variable: ', '')
              .trim();
          }

          result = ` // ERROR(${errHint})`;  //! add error var
        }

        return result.toLocaleString();
      } else {
        return '';
      }
    });

    const output = scriptLines.map((scriptLine, i) => `${scriptLine}${resultOfEachLine[i]?(' ==> '+resultOfEachLine[i]):''}`);

    return output.join('\n');
  };

  const inputMathBlob = childTexts.join('\n');
  const outputMathBlob = evaluateEachLineInBlob(inputMathBlob);
  const outputMathBlobLines = outputMathBlob.split('\n');

  const setRealAns = () => {
    outline.groupUndoAndChanges(() => {
      selectedItem.children.forEach((child, i) => {
        const curContent = child.bodyContentString;
        const newContent = outputMathBlobLines[i];
        if (curContent === newContent) return;

        child.bodyContentString = newContent;
      });
    });
    if (editor.selection.selectedItems[0] !== firstSelectedItem) {
      editor.moveSelectionToItems(firstSelectedItem);
    }
  };
  const setRefreshingAnimationFrame = () => {
    outline.groupUndoAndChanges(() => {
      selectedItem.children.forEach((child, i) => {
        const curText = child.bodyContentString;        
        const newText = childTexts[i].split('==>')[0];
        if (curText === newText) return;        

        child.bodyContentString = newText;
      });
    });
  };
  setRefreshingAnimationFrame();
  setTimeout(setRealAns, 250);
}

Application("TaskPaper").documents[0].evaluate({script: TaskPaperContextScript.toString()});

disclaimer: mvp prototype, use at your own risk

2 Likes