Merge commit '0af3fc8ba67d852dd98d4867e110e0de72f9e94c' as '.obsidian/plugins/obsidian-desmos'

This commit is contained in:
max_richter 2022-03-10 17:54:12 +01:00
commit 14e28d89cc
21 changed files with 1454 additions and 0 deletions

View File

@ -0,0 +1 @@
/main.js linguist-generated

View File

@ -0,0 +1,2 @@
/data.json
/node_modules/

View File

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View File

@ -0,0 +1,95 @@
# Obsidian Desmos
Render [Desmos](https://www.desmos.com/calculator) graphs right inside your notes.
# Installation
## Using Git
If your vault is hosted using git then congratulations, you can use the easy method. Run
`git subtree add --prefix .obsidian/plugins/obsidian-desmos https://github.com/Nigecat/obsidian-desmos master --squash`
to add the plugin to the vault in the current working directory of your terminal.
You can then run
`git subtree pull --prefix .obsidian/plugins/obsidian-desmos https://github.com/Nigecat/obsidian-desmos master --squash`
to update the plugin to the latest version (from the same working directory).
## Using anything else
Alternatively, if you do not use git or are not comfortable using the terminal, you can manually install the plugin. Download [manifest.json](manifest.json), [versions.json](versions.json), and [main.js](main.js) and place them in `<vault>/.obsidian/plugins/obsidian-desmos` (you may have to create any missing folders).
This process must be repeated to update the application.
# Usage
The most basic usage of this plugin involves creating a codeblock with the tag `desmos-graph` and placing the equations you wish to graph in the body:
````
```desmos-graph
y=x
```
````
Equations use the [LaTeX math](https://en.wikibooks.org/wiki/LaTeX/Mathematics) format and you can graph multiple equations by placing each one on a seperate line:
````
```desmos-graph
y=\sin(x)
y=\frac{1}{x}
```
````
You can restrict the bounds of the graph and apply other settings by placing a `---` seperator before your equations. The content before it must be a set of `key=value` pairs seperated by either **newlines or semicolons** (or both):
````
```desmos-graph
boundary_left=0; boundary_right=100;
boundary_top=10; boundary_bottom=-10;
---
y=\sin(x)
```
````
You can set the dimensions of the rendered image by using the `height` and `width` fields.
#### Restrictions
Note that graph restrictions follow the same format as desmos itself (except we use a `|` to denote the beginning of the restrictions):
````
```desmos-graph
y=\sin(x)|{y > 0}
```
````
### Style
We support six different types of (case-insensitive) styles:
Line: `SOLID` `DASHED` `DOTTED`
Point: `POINT` `OPEN` `CROSS`
These are placed after the graph restrictions, following another `|`:
````
```desmos-graph
y=\sin(x)|{y > 0}|DASHED
```
````
If you do not wish to apply any restrictions, the center field can be left blank:
````
```desmos-graph
y=\sin(x)||DASHED
(1,2)||OPEN
```
````
## Important
Note that to be able to render these graphs into a PDF the following conditions must be fulfilled
1. Memory caching **must** be enabled
2. Obsidian **must** have been restarted since you initially created the graph
3. You **must** have viewed the rendered graph in the preview since the restart
After these are complete, a standard PDF export should work fine.
In the future these steps will be removed and you will be able to directly export them.

406
.obsidian/plugins/obsidian-desmos/main.js generated vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
{
"id": "obsidian-desmos",
"name": "Desmos",
"version": "0.0.1",
"minAppVersion": "0.9.12",
"description": "Embed Desmos graphs into your notes",
"author": "Nigecat",
"isDesktopOnly": true
}

343
.obsidian/plugins/obsidian-desmos/package-lock.json generated vendored Normal file
View File

@ -0,0 +1,343 @@
{
"name": "obsidian-desmos",
"version": "0.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@rollup/plugin-commonjs": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-19.0.0.tgz",
"integrity": "sha512-adTpD6ATGbehdaQoZQ6ipDFhdjqsTgpOAhFiPwl+dzre4pPshsecptDPyEFb61JMJ1+mGljktaC4jI8ARMSNyw==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"commondir": "^1.0.1",
"estree-walker": "^2.0.1",
"glob": "^7.1.6",
"is-reference": "^1.2.1",
"magic-string": "^0.25.7",
"resolve": "^1.17.0"
}
},
"@rollup/plugin-node-resolve": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-13.0.0.tgz",
"integrity": "sha512-41X411HJ3oikIDivT5OKe9EZ6ud6DXudtfNrGbC4nniaxx2esiWjkLOzgnZsWq1IM8YIeL2rzRGLZLBjlhnZtQ==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"@types/resolve": "1.17.1",
"builtin-modules": "^3.1.0",
"deepmerge": "^4.2.2",
"is-module": "^1.0.0",
"resolve": "^1.19.0"
}
},
"@rollup/plugin-typescript": {
"version": "8.2.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-typescript/-/plugin-typescript-8.2.1.tgz",
"integrity": "sha512-Qd2E1pleDR4bwyFxqbjt4eJf+wB0UKVMLc7/BAFDGVdAXQMCsD4DUv5/7/ww47BZCYxWtJqe1Lo0KVNswBJlRw==",
"dev": true,
"requires": {
"@rollup/pluginutils": "^3.1.0",
"resolve": "^1.17.0"
}
},
"@rollup/pluginutils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz",
"integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==",
"dev": true,
"requires": {
"@types/estree": "0.0.39",
"estree-walker": "^1.0.1",
"picomatch": "^2.2.2"
},
"dependencies": {
"estree-walker": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz",
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==",
"dev": true
}
}
},
"@types/codemirror": {
"version": "0.0.108",
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.108.tgz",
"integrity": "sha512-3FGFcus0P7C2UOGCNUVENqObEb4SFk+S8Dnxq7K6aIsLVs/vDtlangl3PEO0ykaKXyK56swVF6Nho7VsA44uhw==",
"dev": true,
"requires": {
"@types/tern": "*"
}
},
"@types/debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==",
"dev": true
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"dev": true
},
"@types/node": {
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==",
"dev": true
},
"@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
"integrity": "sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/tern": {
"version": "0.23.3",
"resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz",
"integrity": "sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==",
"dev": true,
"requires": {
"@types/estree": "*"
}
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"builtin-modules": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
"integrity": "sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA==",
"dev": true
},
"commondir": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
},
"deepmerge": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz",
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
"estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"dev": true
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"function-bind": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
"dev": true
},
"glob": {
"version": "7.1.7",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
"integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
"dev": true,
"requires": {
"function-bind": "^1.1.1"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"dev": true,
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true
},
"is-core-module": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz",
"integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==",
"dev": true,
"requires": {
"has": "^1.0.3"
}
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"is-reference": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
"integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
"dev": true,
"requires": {
"@types/estree": "*"
}
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
"integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==",
"dev": true,
"requires": {
"sourcemap-codec": "^1.4.4"
}
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==",
"dev": true
},
"obsidian": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/obsidian/-/obsidian-0.12.5.tgz",
"integrity": "sha512-dQkcHWVgPzVlCxvkeu02Co8P1t4Ii95dC2NsFJV5bKK04J4//YxEdTTI/TFKr77CtYcH78LlUmOFeY5rHwyU/Q==",
"dev": true,
"requires": {
"@types/codemirror": "0.0.108",
"moment": "2.29.1"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"dev": true,
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
"integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
"dev": true
},
"resolve": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"dev": true,
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "2.51.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.51.2.tgz",
"integrity": "sha512-ReV2eGEadA7hmXSzjxdDKs10neqH2QURf2RxJ6ayAlq93ugy6qIvXMmbc5cWMGCDh1h5T4thuWO1e2VNbMq8FA==",
"dev": true,
"requires": {
"fsevents": "~2.3.1"
}
},
"sourcemap-codec": {
"version": "1.4.8",
"resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true
},
"typescript": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz",
"integrity": "sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==",
"dev": true
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
}
}
}

View File

@ -0,0 +1,22 @@
{
"name": "obsidian-desmos",
"version": "0.0.1",
"description": "Embed Desmos graphs into your notes",
"main": "main.js",
"author": "Nigecat",
"scripts": {
"build": "rollup --config rollup.config.js --environment BUILD:production",
"build:dev": "rollup --config rollup.config.js -w"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^19.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/debounce": "^1.2.0",
"@types/node": "^15.12.2",
"obsidian": "^0.12.5",
"rollup": "^2.51.2",
"tslib": "^2.3.0",
"typescript": "^4.3.2"
}
}

View File

@ -0,0 +1,18 @@
import typescript from "@rollup/plugin-typescript";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
const isProd = process.env.BUILD === "production";
export default {
input: "src/main.ts",
output: {
dir: ".",
sourcemap: "inline",
sourcemapExcludeSources: isProd,
format: "cjs",
exports: "default",
},
external: ["obsidian"],
plugins: [typescript(), nodeResolve({ browser: true }), commonjs()],
};

View File

@ -0,0 +1,141 @@
import { createHash } from "crypto";
export interface Fields {
width: number;
height: number;
boundary_left: number;
boundary_right: number;
boundary_bottom: number;
boundary_top: number;
}
const FIELD_DEFAULTS: Fields = {
width: 600,
height: 400,
boundary_left: -10,
boundary_right: 10,
boundary_bottom: -7,
boundary_top: 7,
};
export class Dsl {
/** A (hex) SHA-256 hash of the fields of this object */
public readonly hash: string;
public readonly equations: string[];
public readonly fields: Fields;
private constructor(equations: string[], fields: Partial<Fields>) {
this.equations = equations;
this.fields = { ...FIELD_DEFAULTS, ...fields };
Dsl.assert_sanity(this.fields);
this.hash = createHash("sha256")
.update(JSON.stringify(this))
.digest("hex");
}
/** Check if the fields are sane, throws a `SyntaxError` if they aren't */
private static assert_sanity(fields: 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})
`);
}
}
public static parse(source: string): Dsl {
const split = source.split("---");
let equations: string[];
let fields: Partial<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 as any)[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 as any)[key] = s;
break;
}
case "string": {
(settings as any)[key] = value;
break;
}
case "object": {
const val = JSON.parse(value);
if (
val.constructor === field_v.constructor
) {
(settings as any)[key] = val;
}
break;
}
}
} else {
throw new SyntaxError(`Unrecognised field: ${key}`);
}
return settings;
}, {} as Partial<Fields>);
equations = split[1].split("\n").filter(Boolean);
break;
}
default: {
fields = {};
}
}
if (!equations) {
throw new SyntaxError("Too many segments");
}
return new Dsl(equations, fields);
}
}

View File

@ -0,0 +1,6 @@
export function renderError(err: string, el: HTMLElement) {
el.innerHTML = `
<div style="padding: 20px; background-color: #f44336; color: white;">
<strong>Desmos Graph Error:</strong> ${err}
</div>`;
}

View File

@ -0,0 +1,64 @@
import { Dsl } from "./dsl";
import { Renderer } from "./renderer";
import { renderError } from "./error";
import { debounce, Plugin } from "obsidian";
import { Settings, SettingsTab, DEFAULT_SETTINGS } from "./settings";
export default class Desmos extends Plugin {
settings: Settings;
/** Helper for in-memory graph caching */
graph_cache: Record<string, string>;
async onload() {
this.graph_cache = {};
await 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", async (file) => {
const contents = await 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: string, el: HTMLElement) => {
try {
Renderer.render(Dsl.parse(source), this.settings, el, this);
} catch (err) {
renderError(err.message, el);
}
};
const debounce_render = debounce(
(source: string, el: HTMLElement) => 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);
}
}
);
}
async loadSettings() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
}

View File

@ -0,0 +1,205 @@
import path from "path";
import Desmos from "./main";
import { Dsl } from "./dsl";
import { tmpdir } from "os";
import { Notice } from "obsidian";
import { Settings } from "./settings";
import { renderError } from "./error";
import { existsSync, promises as fs } from "fs";
export class Renderer {
static render(
args: Dsl,
settings: Settings,
el: HTMLElement,
plugin: Desmos
) {
const { fields, equations, hash } = args;
// Calculate cache info for filesystem caching
const vault_root = (plugin.app.vault.adapter as any).basePath;
const cache_dir = settings.cache_directory
? path.isAbsolute(settings.cache_directory)
? settings.cache_directory
: path.join(vault_root, settings.cache_directory)
: tmpdir();
const cache_target = path.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" &&
existsSync(cache_target)
) {
fs.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) =>
`calculator.setExpression({
latex: "${equation.split("|")[0].replace("\\", "\\\\")}${(
equation.split("|")[1] ?? ""
)
.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: MessageEvent<{
t: string;
d: string;
data: string;
hash: string;
}>
) => {
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 (existsSync(cache_dir)) {
fs.writeFile(
cache_target,
data.replace(
/^data:image\/png;base64,/,
""
),
"base64"
).catch(
(err) =>
new Notice(
`desmos-graph: unexpected error when trying to cache graph: ${err}`,
10000
)
);
} else {
new Notice(
`desmos-graph: cache directory not found: '${cache_dir}'`,
10000
);
}
}
}
}
}
};
window.addEventListener("message", handler);
}
}

View File

@ -0,0 +1,105 @@
import { tmpdir } from "os";
import Desmos from "./main";
import { PluginSettingTab, App, Setting } from "obsidian";
export interface Settings {
debounce: number;
cache: boolean;
cache_location: "memory" | "filesystem";
cache_directory: string | null;
}
export const DEFAULT_SETTINGS: Settings = {
debounce: 500,
cache: true,
cache_location: "memory",
cache_directory: null,
};
export class SettingsTab extends PluginSettingTab {
plugin: Desmos;
constructor(app: App, plugin: Desmos) {
super(app, plugin);
this.plugin = plugin;
}
display() {
let { containerEl } = this;
containerEl.empty();
new 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(async (value) => {
const val = parseInt(value);
this.plugin.settings.debounce =
val === NaN ? DEFAULT_SETTINGS.debounce : val;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Cache")
.setDesc("Whether to cache the rendered graphs")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.cache)
.onChange(async (value) => {
this.plugin.settings.cache = value;
await this.plugin.saveSettings();
// Reset the display so the new state can render
this.display();
})
);
if (this.plugin.settings.cache) {
new 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(async (value) => {
this.plugin.settings.cache_location = value
? "memory"
: "filesystem";
await this.plugin.saveSettings();
// Reset the display so the new state can render
this.display();
})
);
if (this.plugin.settings.cache_location == "filesystem") {
new 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(tmpdir())
.setValue(this.plugin.settings.cache_directory)
.onChange(async (value) => {
this.plugin.settings.cache_directory = value;
await this.plugin.saveSettings();
})
);
}
}
}
}

View File

@ -0,0 +1,3 @@
*
!.gitignore
!/plugins/**/*

View File

@ -0,0 +1 @@
../../../../main.js

View File

@ -0,0 +1 @@
../../../../manifest.json

View File

@ -0,0 +1 @@
../../../../versions.json

View File

@ -0,0 +1,9 @@
```desmos-graph
boundary_left=-2; boundary_right=2;
boundary_bottom=-1; boundary_top=3;
---
y=x^2|{x<0}|DASHED
y=x||DOTTED
(1,2)||OPEN
(-1,2)||CROSS
```

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "es6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"importHelpers": true,
"lib": ["dom", "es5", "scripthost", "es2015"]
},
"include": ["**/*.ts"]
}

View File

@ -0,0 +1,3 @@
{
"0.0.1": "0.9.12"
}