Squashed '.obsidian/plugins/obsidian-desmos/' content from commit d760d65

git-subtree-dir: .obsidian/plugins/obsidian-desmos
git-subtree-split: d760d65452fdf4629227e7945b90a004a4c57b60
This commit is contained in:
max_richter 2022-03-10 17:54:12 +01:00
commit 0af3fc8ba6
21 changed files with 1454 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

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

2
.gitignore vendored Normal file
View File

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

3
.prettierrc Normal file
View File

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

95
README.md Normal file
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
main.js generated Normal file

File diff suppressed because one or more lines are too long

9
manifest.json Normal file
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
package-lock.json generated 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
}
}
}

22
package.json Normal file
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"
}
}

18
rollup.config.js Normal file
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()],
};

141
src/dsl.ts Normal file
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);
}
}

6
src/error.ts Normal file
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>`;
}

64
src/main.ts Normal file
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);
}
}

205
src/renderer.ts Normal file
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);
}
}

105
src/settings.ts Normal file
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();
})
);
}
}
}
}

3
test/.obsidian/.gitignore vendored Normal file
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

9
test/test.md Normal file
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
```

16
tsconfig.json Normal file
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"]
}

3
versions.json Normal file
View File

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