407 lines
33 KiB
JavaScript
407 lines
33 KiB
JavaScript
|
'use strict';
|
||
|
|
||
|
var crypto = require('crypto');
|
||
|
var path = require('path');
|
||
|
var os = require('os');
|
||
|
var obsidian = require('obsidian');
|
||
|
var fs = require('fs');
|
||
|
|
||
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
||
|
|
||
|
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
||
|
|
||
|
/*! *****************************************************************************
|
||
|
Copyright (c) Microsoft Corporation.
|
||
|
|
||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||
|
purpose with or without fee is hereby granted.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||
|
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||
|
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||
|
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||
|
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||
|
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||
|
PERFORMANCE OF THIS SOFTWARE.
|
||
|
***************************************************************************** */
|
||
|
|
||
|
function __awaiter(thisArg, _arguments, P, generator) {
|
||
|
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||
|
return new (P || (P = Promise))(function (resolve, reject) {
|
||
|
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||
|
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||
|
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||
|
});
|
||
|
}
|
||
|
|
||
|
const FIELD_DEFAULTS = {
|
||
|
width: 600,
|
||
|
height: 400,
|
||
|
boundary_left: -10,
|
||
|
boundary_right: 10,
|
||
|
boundary_bottom: -7,
|
||
|
boundary_top: 7,
|
||
|
};
|
||
|
class Dsl {
|
||
|
constructor(equations, fields) {
|
||
|
this.equations = equations;
|
||
|
this.fields = Object.assign(Object.assign({}, FIELD_DEFAULTS), fields);
|
||
|
Dsl.assert_sanity(this.fields);
|
||
|
this.hash = crypto.createHash("sha256")
|
||
|
.update(JSON.stringify(this))
|
||
|
.digest("hex");
|
||
|
}
|
||
|
/** Check if the fields are sane, throws a `SyntaxError` if they aren't */
|
||
|
static assert_sanity(fields) {
|
||
|
// Ensure boundaries are complete and in order
|
||
|
if (fields.boundary_left >= fields.boundary_right) {
|
||
|
throw new SyntaxError(`Right boundary (${fields.boundary_right}) must be greater than left boundary (${fields.boundary_left})`);
|
||
|
}
|
||
|
if (fields.boundary_bottom >= fields.boundary_top) {
|
||
|
throw new SyntaxError(`
|
||
|
Top boundary (${fields.boundary_top}) must be greater than bottom boundary (${fields.boundary_bottom})
|
||
|
`);
|
||
|
}
|
||
|
}
|
||
|
static parse(source) {
|
||
|
const split = source.split("---");
|
||
|
let equations;
|
||
|
let fields;
|
||
|
switch (split.length) {
|
||
|
case 0: {
|
||
|
equations = [];
|
||
|
break;
|
||
|
}
|
||
|
case 1: {
|
||
|
equations = split[0].split("\n").filter(Boolean);
|
||
|
break;
|
||
|
}
|
||
|
case 2: {
|
||
|
// If there are two segments then we know the first one must contain the settings
|
||
|
fields = split[0]
|
||
|
// Allow either a newline or semicolon as a delimiter
|
||
|
.split(/[;\n]+/)
|
||
|
.map((setting) => setting.trim())
|
||
|
// Remove any empty elements
|
||
|
.filter(Boolean)
|
||
|
// Split each field on the first equals sign to create the key=value pair
|
||
|
.map((setting) => {
|
||
|
const [key, ...value] = setting.split("=");
|
||
|
return [key, value.join("=")];
|
||
|
})
|
||
|
.reduce((settings, [key, value]) => {
|
||
|
if (FIELD_DEFAULTS.hasOwnProperty(key)) {
|
||
|
if (!value) {
|
||
|
throw new SyntaxError(`Field '${key}' must have a value`);
|
||
|
}
|
||
|
// We can use the defaults to determine the type of each field
|
||
|
const field_v = FIELD_DEFAULTS[key];
|
||
|
const field_t = typeof field_v;
|
||
|
switch (field_t) {
|
||
|
case "number": {
|
||
|
const s = parseInt(value);
|
||
|
if (Number.isNaN(s)) {
|
||
|
throw new SyntaxError(`Field '${key}' must have an integer value`);
|
||
|
}
|
||
|
settings[key] = s;
|
||
|
break;
|
||
|
}
|
||
|
case "string": {
|
||
|
settings[key] = value;
|
||
|
break;
|
||
|
}
|
||
|
case "object": {
|
||
|
const val = JSON.parse(value);
|
||
|
if (val.constructor === field_v.constructor) {
|
||
|
settings[key] = val;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
throw new SyntaxError(`Unrecognised field: ${key}`);
|
||
|
}
|
||
|
return settings;
|
||
|
}, {});
|
||
|
equations = split[1].split("\n").filter(Boolean);
|
||
|
break;
|
||
|
}
|
||
|
default: {
|
||
|
fields = {};
|
||
|
}
|
||
|
}
|
||
|
if (!equations) {
|
||
|
throw new SyntaxError("Too many segments");
|
||
|
}
|
||
|
return new Dsl(equations, fields);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function renderError(err, el) {
|
||
|
el.innerHTML = `
|
||
|
<div style="padding: 20px; background-color: #f44336; color: white;">
|
||
|
<strong>Desmos Graph Error:</strong> ${err}
|
||
|
</div>`;
|
||
|
}
|
||
|
|
||
|
class Renderer {
|
||
|
static render(args, settings, el, plugin) {
|
||
|
const { fields, equations, hash } = args;
|
||
|
// Calculate cache info for filesystem caching
|
||
|
const vault_root = plugin.app.vault.adapter.basePath;
|
||
|
const cache_dir = settings.cache_directory
|
||
|
? path__default['default'].isAbsolute(settings.cache_directory)
|
||
|
? settings.cache_directory
|
||
|
: path__default['default'].join(vault_root, settings.cache_directory)
|
||
|
: os.tmpdir();
|
||
|
const cache_target = path__default['default'].join(cache_dir, `desmos-graph-${hash}.png`);
|
||
|
// If this graph is in the cache then fetch it
|
||
|
if (settings.cache) {
|
||
|
if (settings.cache_location == "memory" &&
|
||
|
hash in plugin.graph_cache) {
|
||
|
const data = plugin.graph_cache[hash];
|
||
|
const img = document.createElement("img");
|
||
|
img.src = data;
|
||
|
el.appendChild(img);
|
||
|
return;
|
||
|
}
|
||
|
else if (settings.cache_location == "filesystem" &&
|
||
|
fs.existsSync(cache_target)) {
|
||
|
fs.promises.readFile(cache_target).then((data) => {
|
||
|
const b64 = "data:image/png;base64," +
|
||
|
Buffer.from(data).toString("base64");
|
||
|
const img = document.createElement("img");
|
||
|
img.src = b64;
|
||
|
el.appendChild(img);
|
||
|
});
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
const expressions = equations.map((equation) => {
|
||
|
var _a;
|
||
|
return `calculator.setExpression({
|
||
|
latex: "${equation.split("|")[0].replace("\\", "\\\\")}${((_a = equation.split("|")[1]) !== null && _a !== void 0 ? _a : "")
|
||
|
.replace("{", "\\\\{")
|
||
|
.replace("}", "\\\\}")
|
||
|
.replace("<=", "\\\\leq ")
|
||
|
.replace(">=", "\\\\geq ")
|
||
|
.replace("<", "\\\\le ")
|
||
|
.replace(">", "\\\\ge ")}",
|
||
|
|
||
|
${(() => {
|
||
|
const mode = equation.split("|")[2];
|
||
|
if (mode) {
|
||
|
if (["solid", "dashed", "dotted"].contains(mode.toLowerCase())) {
|
||
|
return `lineStyle: Desmos.Styles.${mode.toUpperCase()}`;
|
||
|
}
|
||
|
else if (["point", "open", "cross"].contains(mode.toLowerCase())) {
|
||
|
return `pointStyle: Desmos.Styles.${mode.toUpperCase()}`;
|
||
|
}
|
||
|
}
|
||
|
return "";
|
||
|
})()}
|
||
|
});`;
|
||
|
});
|
||
|
// Because of the electron sandboxing we have to do this inside an iframe,
|
||
|
// otherwise we can't include the desmos API (although it would be nice if they had a REST API of some sort)
|
||
|
const html_src_head = `<script src="https://www.desmos.com/api/v1.6/calculator.js?apiKey=dcb31709b452b1cf9dc26972add0fda6"></script>`;
|
||
|
const html_src_body = `
|
||
|
<div id="calculator" style="width: ${fields.width}px; height: ${fields.height}px;"></div>
|
||
|
<script>
|
||
|
const options = {
|
||
|
settingsMenu: false,
|
||
|
expressions: false,
|
||
|
lockViewPort: true,
|
||
|
zoomButtons: false,
|
||
|
trace: false,
|
||
|
};
|
||
|
|
||
|
const calculator = Desmos.GraphingCalculator(document.getElementById("calculator"), options);
|
||
|
calculator.setMathBounds({
|
||
|
left: ${fields.boundary_left},
|
||
|
right: ${fields.boundary_right},
|
||
|
top: ${fields.boundary_top},
|
||
|
bottom: ${fields.boundary_bottom},
|
||
|
});
|
||
|
|
||
|
${expressions.join("")}
|
||
|
|
||
|
calculator.observe("expressionAnalysis", () => {
|
||
|
for (const id in calculator.expressionAnalysis) {
|
||
|
const analysis = calculator.expressionAnalysis[id];
|
||
|
if (analysis.isError) {
|
||
|
parent.postMessage({ t: "desmos-graph", d: "error", data: analysis.errorMessage, hash: "${hash}" });
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
calculator.asyncScreenshot({ showLabels: true, format: "png" }, (data) => {
|
||
|
document.body.innerHTML = "";
|
||
|
parent.postMessage({ t: "desmos-graph", d: "render", data, hash: "${hash}" }, "app://obsidian.md");
|
||
|
});
|
||
|
</script>
|
||
|
`;
|
||
|
const html_src = `<html><head>${html_src_head}</head><body>${html_src_body}</body>`;
|
||
|
const iframe = document.createElement("iframe");
|
||
|
iframe.width = fields.width.toString();
|
||
|
iframe.height = fields.height.toString();
|
||
|
iframe.style.border = "none";
|
||
|
iframe.scrolling = "no"; // fixme use a non-depreciated function
|
||
|
iframe.srcdoc = html_src;
|
||
|
// iframe.style.display = "none"; //fixme hiding the iframe breaks the positioning
|
||
|
el.appendChild(iframe);
|
||
|
const handler = (message) => {
|
||
|
if (message.origin === "app://obsidian.md" &&
|
||
|
message.data.t === "desmos-graph" &&
|
||
|
message.data.hash === hash) {
|
||
|
el.empty();
|
||
|
if (message.data.d === "error") {
|
||
|
renderError(message.data.data, el);
|
||
|
}
|
||
|
if (message.data.d === "render") {
|
||
|
const { data } = message.data;
|
||
|
window.removeEventListener("message", handler);
|
||
|
const img = document.createElement("img");
|
||
|
img.src = data;
|
||
|
el.appendChild(img);
|
||
|
if (settings.cache) {
|
||
|
if (settings.cache_location == "memory") {
|
||
|
plugin.graph_cache[hash] = data;
|
||
|
}
|
||
|
else if (settings.cache_location == "filesystem") {
|
||
|
if (fs.existsSync(cache_dir)) {
|
||
|
fs.promises.writeFile(cache_target, data.replace(/^data:image\/png;base64,/, ""), "base64").catch((err) => new obsidian.Notice(`desmos-graph: unexpected error when trying to cache graph: ${err}`, 10000));
|
||
|
}
|
||
|
else {
|
||
|
new obsidian.Notice(`desmos-graph: cache directory not found: '${cache_dir}'`, 10000);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
window.addEventListener("message", handler);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const DEFAULT_SETTINGS = {
|
||
|
debounce: 500,
|
||
|
cache: true,
|
||
|
cache_location: "memory",
|
||
|
cache_directory: null,
|
||
|
};
|
||
|
class SettingsTab extends obsidian.PluginSettingTab {
|
||
|
constructor(app, plugin) {
|
||
|
super(app, plugin);
|
||
|
this.plugin = plugin;
|
||
|
}
|
||
|
display() {
|
||
|
let { containerEl } = this;
|
||
|
containerEl.empty();
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Debounce Time (ms)")
|
||
|
.setDesc("How long to wait after a keypress to render the graph (requires restart to take effect)")
|
||
|
.addText((text) => text
|
||
|
.setValue(this.plugin.settings.debounce.toString())
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
const val = parseInt(value);
|
||
|
this.plugin.settings.debounce =
|
||
|
val === NaN ? DEFAULT_SETTINGS.debounce : val;
|
||
|
yield this.plugin.saveSettings();
|
||
|
})));
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Cache")
|
||
|
.setDesc("Whether to cache the rendered graphs")
|
||
|
.addToggle((toggle) => toggle
|
||
|
.setValue(this.plugin.settings.cache)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.settings.cache = value;
|
||
|
yield this.plugin.saveSettings();
|
||
|
// Reset the display so the new state can render
|
||
|
this.display();
|
||
|
})));
|
||
|
if (this.plugin.settings.cache) {
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Cache in memory (alternate: filesystem)")
|
||
|
.setDesc("Cache rendered graphs in memory or on the filesystem (note that memory caching is not persistent).")
|
||
|
.addToggle((toggle) => toggle
|
||
|
.setValue(this.plugin.settings.cache_location === "memory"
|
||
|
? true
|
||
|
: false)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.settings.cache_location = value
|
||
|
? "memory"
|
||
|
: "filesystem";
|
||
|
yield this.plugin.saveSettings();
|
||
|
// Reset the display so the new state can render
|
||
|
this.display();
|
||
|
})));
|
||
|
if (this.plugin.settings.cache_location == "filesystem") {
|
||
|
new obsidian.Setting(containerEl)
|
||
|
.setName("Cache Directory")
|
||
|
.setDesc("The directory to save cached graphs in (technical note: the graphs will be saved as `desmos-graph-<hash>.png` where the name is a SHA-256 hash of the graph source). The default directory is the system tempdir for your current operating system, and this value may be either a path relative to the root of your vault or an absolute path. Also note that a lot of junk will be saved to this folder, you have been warned.")
|
||
|
.addText((text) => text
|
||
|
.setPlaceholder(os.tmpdir())
|
||
|
.setValue(this.plugin.settings.cache_directory)
|
||
|
.onChange((value) => __awaiter(this, void 0, void 0, function* () {
|
||
|
this.plugin.settings.cache_directory = value;
|
||
|
yield this.plugin.saveSettings();
|
||
|
})));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Desmos extends obsidian.Plugin {
|
||
|
onload() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.graph_cache = {};
|
||
|
yield this.loadSettings();
|
||
|
this.addSettingTab(new SettingsTab(this.app, this));
|
||
|
// Keep track of the total number of graphs in each file
|
||
|
// This allows us to skip the debounce on recently opened files to make it feel snappier to use
|
||
|
let total = 0;
|
||
|
this.app.workspace.on("file-open", (file) => __awaiter(this, void 0, void 0, function* () {
|
||
|
const contents = yield this.app.vault.cachedRead(file);
|
||
|
// Attempt to figure out the number of graphs there are in this file
|
||
|
// In this case it is fine if we overestimate because we only need a general idea since this just makes it skip the debounce
|
||
|
total = (contents.match(/```desmos-graph/g) || []).length;
|
||
|
}));
|
||
|
const render = (source, el) => {
|
||
|
try {
|
||
|
Renderer.render(Dsl.parse(source), this.settings, el, this);
|
||
|
}
|
||
|
catch (err) {
|
||
|
renderError(err.message, el);
|
||
|
}
|
||
|
};
|
||
|
const debounce_render = obsidian.debounce((source, el) => render(source, el), this.settings.debounce);
|
||
|
this.registerMarkdownCodeBlockProcessor("desmos-graph", (source, el) => {
|
||
|
if (total > 0) {
|
||
|
total--;
|
||
|
// Skip the debounce on initial render
|
||
|
render(source, el);
|
||
|
}
|
||
|
else {
|
||
|
debounce_render(source, el);
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
loadSettings() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, yield this.loadData());
|
||
|
});
|
||
|
}
|
||
|
saveSettings() {
|
||
|
return __awaiter(this, void 0, void 0, function* () {
|
||
|
yield this.saveData(this.settings);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Desmos;
|
||
|
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5qcyIsInNvdXJjZXMiOlsibm9kZV9tb2R1bGVzL3RzbGliL3RzbGliLmVzNi5qcyIsInNyYy9kc2wudHMiLCJzcmMvZXJyb3IudHMiLCJzcmMvcmVuZGVyZXIudHMiLCJzcmMvc2V0dGluZ3MudHMiLCJzcmMvbWFpbi50cyJdLCJzb3VyY2VzQ29udGVudCI6bnVsbCwibmFtZXMiOlsiY3JlYXRlSGFzaCIsInBhdGgiLCJ0bXBkaXIiLCJleGlzdHNTeW5jIiwiZnMiLCJOb3RpY2UiLCJQbHVnaW5TZXR0aW5nVGFiIiwiU2V0dGluZyIsIlBsdWdpbiIsImRlYm91bmNlIl0sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7QUFBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBdURBO0FBQ08sU0FBUyxTQUFTLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxDQUFDLEVBQUUsU0FBUyxFQUFFO0FBQzdELElBQUksU0FBUyxLQUFLLENBQUMsS0FBSyxFQUFFLEVBQUUsT0FBTyxLQUFLLFlBQVksQ0FBQyxHQUFHLEtBQUssR0FBRyxJQUFJLENBQUMsQ0FBQyxVQUFVLE9BQU8sRUFBRSxFQUFFLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFO0FBQ2hILElBQUksT0FBTyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsT0FBTyxDQUFDLEVBQUUsVUFBVSxPQUFPLEVBQUUsTUFBTSxFQUFFO0FBQy9ELFFBQVEsU0FBUyxTQUFTLENBQUMsS0FBSyxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxFQUFFLEVBQUUsTUFBTSxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsRUFBRTtBQUNuRyxRQUFRLFNBQVMsUUFBUSxDQUFDLEtBQUssRUFBRSxFQUFFLElBQUks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
|