├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
├── FUNDING.yml
└── workflows
│ └── release.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── docs
├── clever-referencing.html
├── commands.html
├── credits.html
├── equations.html
├── extending-obsidian's-math-rendering-functionalities
│ ├── multi-line-equation-support-inside-blockquotes.html
│ └── rendering-equations-inside-callouts.html
├── index.html
├── lib
│ ├── scripts
│ │ ├── generated.js
│ │ ├── graph-render-worker.js
│ │ ├── graph_view.js
│ │ ├── graph_wasm.js
│ │ ├── graph_wasm.wasm
│ │ ├── tinycolor.js
│ │ └── webpage.js
│ └── styles
│ │ ├── generated-styles.css
│ │ ├── obsidian-styles.css
│ │ ├── plugin-styles.css
│ │ ├── snippets.css
│ │ └── theme.css
├── migration-from-math-booster-version-1.html
├── proof-environment.html
├── search-&-link-auto-completion
│ ├── assets
│ │ └── enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp
│ ├── auto-completion.html
│ ├── custom-link-autocomplete.html
│ ├── custom-link-completion.html
│ ├── editor-auto-completion.html
│ ├── enhancing-obsidian's-built-in-link-autocomplete.html
│ ├── enhancing-obsidian's-built-in-link-completion.html
│ ├── search-&-link-auto-completion.html
│ ├── search-&-link-autocomplete.html
│ └── search-modal.html
├── search-&-link-autocomplete
│ ├── assets
│ │ └── enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp
│ ├── custom-link-autocomplete.html
│ ├── enhancing-obsidian's-built-in-link-autocomplete.html
│ ├── search-&-link-autocomplete.html
│ └── search-modal.html
├── settings
│ ├── local-settings.html
│ ├── prefix-inference
│ │ ├── 5.6-sample-note.html
│ │ ├── a-sample-note.html
│ │ └── prefix-inference.html
│ ├── profiles.html
│ └── settings.html
└── theorem-callouts
│ ├── prefix-inference.html
│ ├── style-your-theorems.html
│ ├── styling.html
│ └── theorem-callouts.html
├── esbuild.config.mjs
├── manifest-beta.json
├── manifest.json
├── package-lock.json
├── package.json
├── src
├── cleveref.ts
├── env.ts
├── equations
│ ├── common.ts
│ ├── live-preview.ts
│ └── reading-view.ts
├── file-io.ts
├── index
│ ├── README.md
│ ├── expression
│ │ ├── link.ts
│ │ └── literal.ts
│ ├── import
│ │ └── markdown.ts
│ ├── manager.ts
│ ├── math-index.ts
│ ├── storage
│ │ └── inverted.ts
│ ├── typings
│ │ ├── indexable.ts
│ │ ├── json.ts
│ │ └── markdown.ts
│ ├── utils
│ │ ├── deferred.ts
│ │ └── normalizers.ts
│ └── web-worker
│ │ ├── importer.ts
│ │ ├── importer.worker.ts
│ │ ├── message.ts
│ │ └── transferable.ts
├── main.ts
├── notice.ts
├── patches
│ ├── link-completion.ts
│ └── page-preview.ts
├── proof
│ ├── common.ts
│ ├── live-preview.ts
│ └── reading-view.ts
├── search
│ ├── core.ts
│ ├── editor-suggest.ts
│ └── modal.ts
├── settings
│ ├── helper.ts
│ ├── modals.ts
│ ├── profile.ts
│ ├── settings.ts
│ └── tab.ts
├── theorem-callouts
│ ├── renderer.ts
│ ├── state-field.ts
│ └── view-plugin.ts
├── typings
│ ├── type.d.ts
│ └── workers.d.ts
└── utils
│ ├── editor.ts
│ ├── format.ts
│ ├── general.ts
│ ├── obsidian.ts
│ ├── parse.ts
│ ├── plugin.ts
│ └── render.ts
├── styles.scss
├── styles
├── framed.scss
├── main.css
├── mathwiki.scss
├── plain.scss
└── vivid.scss
├── tsconfig.json
├── version-bump.mjs
└── versions.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: RyotaUshio
2 | custom: ['https://www.buymeacoffee.com/ryotaushio']
3 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian plugin
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Use Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: "18.x"
19 |
20 | - name: Build plugin
21 | run: |
22 | npm install -g sass
23 | npm install
24 | npm run build
25 |
26 | - name: Create release
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 | run: |
30 | tag="${GITHUB_REF#refs/tags/}"
31 |
32 | gh release create "$tag" \
33 | --title="$tag" \
34 | --draft \
35 | main.js manifest.json styles.css
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # This is auto-generated by the css files in the styles directory.
16 | styles.css
17 |
18 | # Exclude sourcemaps
19 | *.map
20 |
21 | # obsidian
22 | data.json
23 |
24 | # Exclude macOS Finder (System Explorer) View States
25 | .DS_Store
26 |
27 | # Docs
28 | docs/_site/
29 |
30 | # Unused
31 | src/callout_decoration.ts
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Ryota Ushio
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LaTeX-like Theorem & Equation Referencer for Obsidian
2 |
3 | > [!important]
4 | > This plugin had been called **Math Booster** until version 2.1.4, but has been renamed for better clarity and discoverability. A big thank you to those who shared their thoughts [here](https://github.com/RyotaUshio/obsidian-math-booster/issues/210).
5 |
6 | **LaTeX-like Theorem & Equation Referencer** is an [Obsidian.md](https://obsidian.md/) plugin that provides a powerful indexing & referencing system for theorems & equations in your vault, bringing $\LaTeX$-like workflow into Obsidian.
7 |
8 | 
9 |
10 | (The theorem in the screenshot is cited from [Tao, Terence, ed. An introduction to measure theory. Vol. 126. American Mathematical Soc., 2011.](https://terrytao.files.wordpress.com/2012/12/gsm-126-tao5-measure-book.pdf))
11 |
12 | ## Docs
13 |
14 | https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/
15 |
16 | ## Features
17 |
18 | - [Theorem environments](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/theorem-callouts/theorem-callouts.html)
19 | - [Automatic equation numbering](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/equations.html)
20 | - [Clever referencing](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/clever-referencing.html)
21 | - [Search & link autocomplete](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/search-&-link-autocomplete/search-&-link-autocomplete.html)
22 | - [Custom link autocomplete](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/search-&-link-autocomplete/custom-link-autocomplete.html)
23 | - Easily find & insert link to theorems & equations.
24 | - Filter theorems & equations based on their locations (*entire vault/recent notes/active note*)
25 | - [Search modal](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/search-&-link-autocomplete/search-modal.html): more control & flexibility than editor autocomplete, including *Dataview queries*
26 | - [Proof environment (experimental)](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/proof-environment.html)
27 |
28 | > [!note]
29 | > For more modular and focused enhancements, some features are planned to be transitioned from this plugin to dedicated, specialized plugins in the near future. Below are the upcoming changes:
30 | >
31 | > #### Transitioning to [**Better Math in Callouts & Blockquotes**](https://github.com/RyotaUshio/obsidian-math-in-callout)
32 | >
33 | > - Rendering equations inside callouts
34 | > - Multi-line equation support inside blockquotes
35 | >
36 | > #### Transitioning to [**Rendered Block Link Suggestions**](https://github.com/RyotaUshio/obsidian-rendered-block-link-suggestions)
37 | >
38 | > - [Render equations in Obsidian's built-in link suggestions](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/search-&-link-autocomplete/enhancing-obsidian's-built-in-link-autocomplete.html)
39 |
40 | Theorems & equations can be **dynamically/automatically numbered**, while you can statically/manually number them if you want.
41 | The number prefix can be either explicitly specified or automatically inferred from the note title.
42 |
43 | Thanks to the integration with [MathLinks](https://github.com/zhaoshenzhai/obsidian-mathlinks), links to theorems/equations are displayed with their title or number, similarly to the `cleveref` package in LaTeX. (No need for manually typing aliases!)
44 |
45 | You can also customize the appearance of theorem callouts using CSS snippets; see [here](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/theorem-callouts/styling.html).
46 |
47 | ## Companion plugins
48 |
49 | Here's a list of other math-related plugins I've developed:
50 |
51 | - [No More Flickering Inline Math](https://github.com/RyotaUshio/obsidian-inline-math)
52 | - [Better Math in Callouts & Blockquotes](https://github.com/RyotaUshio/obsidian-math-in-callout)
53 | - [MathJax Preamble Manager](https://github.com/RyotaUshio/obsidian-mathjax-preamble-manager)
54 | - [Auto-\\displaystyle Inline Math](https://github.com/RyotaUshio/obsidian-auto-displaystyle-inline-math)
55 |
56 | ## Installation
57 |
58 | You can install this plugin via Obsidian's community plugin browser (see [here](https://help.obsidian.md/Extending+Obsidian/Community+plugins#Install+a+community+plugin) for instructions).
59 |
60 | Also, you can test the latest beta release using [BRAT](https://github.com/TfTHacker/obsidian42-brat):
61 |
62 | 1. Install BRAT and enable it.
63 | 2. Go to **Options**. In the **Beta Plugin List** section, click on the **Add Beta plugin** button.
64 | 3. Copy and paste `RyotaUshio/obsidian-latex-theorem-equation-referencer` in the pop-up prompt and click on **Add Plugin**.
65 | 4. _(Optional)_ Turn on **Auto-update plugins at startup** at the top of the page.
66 | 5. Go to **Community plugins > Installed plugins**. You will find "LaTeX-like Theorem & Equation Referencer" in the list. Click on the toggle button to enable it.
67 | Since version 2 is still beta, it's not on the community plugin browser yet.
68 |
69 | ## Dependencies
70 |
71 | ### Obsidian plugins
72 |
73 | This plugin requires [MathLinks](https://github.com/zhaoshenzhai/obsidian-mathlinks) version 0.5.3 or higher installed to work properly ([Clever referencing](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/clever-referencing.html)).
74 |
75 | In version 2, [Dataview](https://github.com/blacksmithgu/obsidian-dataview) is no longer required. But I strongly recommend installing it because it enhances this plugin's [search](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/search-&-link-auto-completion/search-modal.html) functionality significantly.
76 |
77 | ### Fonts
78 |
79 | You have to install [CMU Serif](https://www.cufonfonts.com/font/cmu-serif) to get some of the [preset styles for theorem callouts](https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/theorem-callouts/styling.html) displayed properly.
80 |
81 | Additionally, [Noto Sans JP](https://fonts.google.com/noto/specimen/Noto+Sans+JP) is required for render the preset styles properly in Japanese.
82 |
83 | ## Contributing
84 |
85 | - Feel free to create a new issue if something is not working well. Questions are also welcomed.
86 | - Please send a pull request if you have any ideas to improve this plugin and our experience!
87 | - Contribution to the docs is also highly appreciated: see [here](https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer-docs).
88 |
89 | ## Roadmaps
90 |
91 | - Import from LaTeX: ArXiv papers, research/literature notes written in LaTeX, ...
92 | - Export to LaTeX: Write research notes in Obsidian, and then export them into LaTeX.
93 |
94 | ## Support development
95 |
96 | If you find this plugin useful, please support my work by buying me a coffee!
97 |
98 |
99 |
--------------------------------------------------------------------------------
/docs/lib/scripts/generated.js:
--------------------------------------------------------------------------------
1 | let nodes={paths:["extending-obsidian's-math-rendering-functionalities/multi-line-equation-support-inside-blockquotes.html","extending-obsidian's-math-rendering-functionalities/rendering-equations-inside-callouts.html","search-&-link-autocomplete/custom-link-autocomplete.html","search-&-link-autocomplete/enhancing-obsidian's-built-in-link-autocomplete.html","search-&-link-autocomplete/search-&-link-autocomplete.html","search-&-link-autocomplete/search-modal.html","settings/prefix-inference/5.6-sample-note.html","settings/prefix-inference/prefix-inference.html","settings/local-settings.html","settings/profiles.html","settings/settings.html","theorem-callouts/styling.html","theorem-callouts/theorem-callouts.html","clever-referencing.html","commands.html","credits.html","equations.html","index.html","migration-from-math-booster-version-1.html","proof-environment.html"],nodeCount:20,linkSources:[4,4,4,5,7,9,9,9,10,10,11,12,12,12,12,13,13,13,14,14,14,14,14,14,15,15,16,16,17,17,17,17,17,17,17,17,17,17,18,18,18,18,18,19,19],linkTargets:[3,2,5,2,6,8,12,19,8,9,9,13,9,16,11,12,16,4,5,19,8,12,18,16,5,11,12,13,18,12,16,13,4,3,2,5,19,15,12,3,2,5,16,12,9],labels:["Multi-line equation support inside blockquotes","Rendering equations inside callouts","Custom link autocomplete","Enhancing Obsidian's built-in link autocomplete","Search & link autocomplete","Search modal","5.6. Sample note","Prefix inference","Local settings","Profiles","Settings","Styling","Theorem callouts","Clever referencing","Commands","Credits","Equations","index","Migration from Math Booster version 1","Proof environment"],radii:[3,3,5.809917355371901,5.2623966942148765,6.2541322314049586,6.595041322314049,3.857438016528926,3.857438016528926,5.2623966942148765,6.832644628099173,4.6115702479338845,5.2623966942148765,7,6.595041322314049,6.595041322314049,5.2623966942148765,6.832644628099173,7,6.832644628099173,6.2541322314049586],linkCount:45},attractionForce=1,linkLength=10,repulsionForce=150,centralForce=3,edgePruning=100
--------------------------------------------------------------------------------
/docs/lib/scripts/graph-render-worker.js:
--------------------------------------------------------------------------------
1 | if("function"==typeof importScripts){let e,t,o;importScripts("https://d157l7jdn8e5sf.cloudfront.net/v7.2.0/webworker.js","./tinycolor.js"),addEventListener("message",onMessage),isDrawing=!1;let n=0,a=[],r=[],i=0,l=[],c=[],d=[],s=[],u=[],g={x:0,y:0},p=new Float32Array(0),h=0,f=0,y={background:2302755,link:11184810,node:13421772,outline:11184810,text:16777215,accent:4203434},S=-1,x=-1,v=-1,m=!1,w=[],b=-1,C=1,k=1;function toScreenSpace(e,t,o=!0){return o?{x:Math.floor(e*C+g.x),y:Math.floor(t*C+g.y)}:{x:e*C+g.x,y:t*C+g.y}}function vecToScreenSpace({x:e,y:t},o=!0){return toScreenSpace(e,t,o)}function toWorldspace(e,t){return{x:(e-g.x)/C,y:(t-g.y)/C}}function vecToWorldspace({x:e,y:t}){return toWorldspace(e,t)}function setCameraCenterWorldspace({x:e,y:t}){g.x=canvas.width/2-e*C,g.y=canvas.height/2-t*C}function getCameraCenterWorldspace(){return toWorldspace(canvas.width/2,canvas.height/2)}function getNodeScreenRadius(e){return e*k}function getNodeWorldspaceRadius(e){return e/k}function getPosition(e){return{x:p[2*e],y:p[2*e+1]}}function mixColors(e,t,o){return tinycolor.mix(tinycolor(e.toString(16)),tinycolor(t.toString(16)),o).toHexNumber()}function darkenColor(e,t){return tinycolor(e.toString(16)).darken(t).toHexNumber()}function lightenColor(e,t){return tinycolor(e.toString(16)).lighten(t).toHexNumber()}function invertColor(e,t){if(0===(e=e.toString(16)).indexOf("#")&&(e=e.slice(1)),3===e.length&&(e=e[0]+e[0]+e[1]+e[1]+e[2]+e[2]),6!==e.length)throw new Error("Invalid HEX color.");var o=parseInt(e.slice(0,2),16),n=parseInt(e.slice(2,4),16),a=parseInt(e.slice(4,6),16);return t?.299*o+.587*n+.114*a>186?"#000000":"#FFFFFF":(o=(255-o).toString(16),n=(255-n).toString(16),a=(255-a).toString(16),"#"+padZero(o)+padZero(n)+padZero(a))}function clamp(e,t,o){return Math.min(Math.max(e,t),o)}function lerp(e,t,o){return e+(t-e)*o}let N=0,T=.2,F=15,M=12,P=F/M;function showLabel(e,t,o=!1){let n=u[e];if(d[e]=t,!(t>.01))return void hideLabel(e);n.visible=!0,n.style.fontSize=o?F:M;let a=vecToScreenSpace(getPosition(e)),r=s[e]*(o?P:1)/2;n.x=a.x-r,n.y=a.y+getNodeScreenRadius(l[e])+9,n.alpha=t}function hideLabel(e){u[e].visible=!1}function draw(){o.clear();let e=[];m&&(w=[]),N=-1!=S||-1!=v?Math.min(1,N+T):Math.max(0,N-T),o.lineStyle(1,mixColors(y.link,y.background,50*N),.7);for(let t=0;t2){showLabel(e,lerp(0,(t-4)/10-1/k/6*.9,Math.max(1-N,.2)))}else hideLabel(e);if(S==e||x==e&&0!=N||-1!=S&&w.includes(e))continue;let n=vecToScreenSpace(getPosition(e));o.drawCircle(n.x,n.y,t)}o.endFill(),t=.7*N,o.lineStyle(1,mixColors(mixColors(y.link,y.accent,100*N),y.background,20),t);for(let t=0;t{throw n},ENVIRONMENT_IS_WEB="object"==typeof window,ENVIRONMENT_IS_WORKER="function"==typeof importScripts,ENVIRONMENT_IS_NODE="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,scriptDirectory="";function locateFile(e){return Module.locateFile?Module.locateFile(e,scriptDirectory):scriptDirectory+e}if(ENVIRONMENT_IS_NODE){var fs=require("fs"),nodePath=require("path");scriptDirectory=ENVIRONMENT_IS_WORKER?nodePath.dirname(scriptDirectory)+"/":__dirname+"/",read_=(e,n)=>(e=isFileURI(e)?new URL(e):nodePath.normalize(e),fs.readFileSync(e,n?void 0:"utf8")),readBinary=e=>{var n=read_(e,!0);return n.buffer||(n=new Uint8Array(n)),n},readAsync=(e,n,t)=>{e=isFileURI(e)?new URL(e):nodePath.normalize(e),fs.readFile(e,(function(e,r){e?t(e):n(r.buffer)}))},!Module.thisProgram&&process.argv.length>1&&(thisProgram=process.argv[1].replace(/\\/g,"/")),arguments_=process.argv.slice(2),"undefined"!=typeof module&&(module.exports=Module),process.on("uncaughtException",(function(e){if(!("unwind"===e||e instanceof ExitStatus||e.context instanceof ExitStatus))throw e}));var nodeMajor=process.versions.node.split(".")[0];nodeMajor<15&&process.on("unhandledRejection",(function(e){throw e})),quit_=(e,n)=>{throw process.exitCode=e,n},Module.inspect=function(){return"[Emscripten Module object]"}}else(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)&&(ENVIRONMENT_IS_WORKER?scriptDirectory=self.location.href:"undefined"!=typeof document&&document.currentScript&&(scriptDirectory=document.currentScript.src),scriptDirectory=0!==scriptDirectory.indexOf("blob:")?scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1):"",read_=e=>{var n=new XMLHttpRequest;return n.open("GET",e,!1),n.send(null),n.responseText},ENVIRONMENT_IS_WORKER&&(readBinary=e=>{var n=new XMLHttpRequest;return n.open("GET",e,!1),n.responseType="arraybuffer",n.send(null),new Uint8Array(n.response)}),readAsync=(e,n,t)=>{var r=new XMLHttpRequest;r.open("GET",e,!0),r.responseType="arraybuffer",r.onload=()=>{200==r.status||0==r.status&&r.response?n(r.response):t()},r.onerror=t,r.send(null)},setWindowTitle=e=>document.title=e);var wasmBinary,out=Module.print||console.log.bind(console),err=Module.printErr||console.warn.bind(console);Object.assign(Module,moduleOverrides),moduleOverrides=null,Module.arguments&&(arguments_=Module.arguments),Module.thisProgram&&(thisProgram=Module.thisProgram),Module.quit&&(quit_=Module.quit),Module.wasmBinary&&(wasmBinary=Module.wasmBinary);var wasmMemory,noExitRuntime=Module.noExitRuntime||!0;"object"!=typeof WebAssembly&&abort("no native wasm support detected");var EXITSTATUS,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64,wasmTable,ABORT=!1;function updateMemoryViews(){var e=wasmMemory.buffer;Module.HEAP8=HEAP8=new Int8Array(e),Module.HEAP16=HEAP16=new Int16Array(e),Module.HEAP32=HEAP32=new Int32Array(e),Module.HEAPU8=HEAPU8=new Uint8Array(e),Module.HEAPU16=HEAPU16=new Uint16Array(e),Module.HEAPU32=HEAPU32=new Uint32Array(e),Module.HEAPF32=HEAPF32=new Float32Array(e),Module.HEAPF64=HEAPF64=new Float64Array(e)}var __ATPRERUN__=[],__ATINIT__=[],__ATPOSTRUN__=[],runtimeInitialized=!1;function preRun(){if(Module.preRun)for("function"==typeof Module.preRun&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=!0,callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module.postRun)for("function"==typeof Module.postRun&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(e){__ATPRERUN__.unshift(e)}function addOnInit(e){__ATINIT__.unshift(e)}function addOnPostRun(e){__ATPOSTRUN__.unshift(e)}var runDependencies=0,runDependencyWatcher=null,dependenciesFulfilled=null;function addRunDependency(e){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies)}function removeRunDependency(e){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),0==runDependencies&&(null!==runDependencyWatcher&&(clearInterval(runDependencyWatcher),runDependencyWatcher=null),dependenciesFulfilled)){var n=dependenciesFulfilled;dependenciesFulfilled=null,n()}}function abort(e){throw Module.onAbort&&Module.onAbort(e),err(e="Aborted("+e+")"),ABORT=!0,EXITSTATUS=1,e+=". Build with -sASSERTIONS for more info.",new WebAssembly.RuntimeError(e)}var wasmBinaryFile,tempDouble,tempI64,dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(e){return e.startsWith(dataURIPrefix)}function isFileURI(e){return e.startsWith("file://")}function getBinary(e){try{if(e==wasmBinaryFile&&wasmBinary)return new Uint8Array(wasmBinary);if(readBinary)return readBinary(e);throw"both async and sync fetching of the wasm failed"}catch(e){abort(e)}}function getBinaryPromise(e){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if("function"==typeof fetch&&!isFileURI(e))return fetch(e,{credentials:"same-origin"}).then((function(n){if(!n.ok)throw"failed to load wasm binary file at '"+e+"'";return n.arrayBuffer()})).catch((function(){return getBinary(e)}));if(readAsync)return new Promise((function(n,t){readAsync(e,(function(e){n(new Uint8Array(e))}),t)}))}return Promise.resolve().then((function(){return getBinary(e)}))}function instantiateArrayBuffer(e,n,t){return getBinaryPromise(e).then((function(e){return WebAssembly.instantiate(e,n)})).then((function(e){return e})).then(t,(function(e){err("failed to asynchronously prepare wasm: "+e),abort(e)}))}function instantiateAsync(e,n,t,r){return e||"function"!=typeof WebAssembly.instantiateStreaming||isDataURI(n)||isFileURI(n)||ENVIRONMENT_IS_NODE||"function"!=typeof fetch?instantiateArrayBuffer(n,t,r):fetch(n,{credentials:"same-origin"}).then((function(e){return WebAssembly.instantiateStreaming(e,t).then(r,(function(e){return err("wasm streaming compile failed: "+e),err("falling back to ArrayBuffer instantiation"),instantiateArrayBuffer(n,t,r)}))}))}function createWasm(){var e={a:wasmImports};function n(e,n){var t=e.exports;return Module.asm=t,wasmMemory=Module.asm.f,updateMemoryViews(),wasmTable=Module.asm.r,addOnInit(Module.asm.g),removeRunDependency("wasm-instantiate"),t}if(addRunDependency("wasm-instantiate"),Module.instantiateWasm)try{return Module.instantiateWasm(e,n)}catch(e){return err("Module.instantiateWasm callback failed with error: "+e),!1}return instantiateAsync(wasmBinary,wasmBinaryFile,e,(function(e){n(e.instance)})),{}}isDataURI(wasmBinaryFile="graph_wasm.wasm")||(wasmBinaryFile=locateFile(wasmBinaryFile));var ASM_CONSTS={2304:e=>{console.log(UTF8ToString(e))}};function ExitStatus(e){this.name="ExitStatus",this.message="Program terminated with exit("+e+")",this.status=e}function callRuntimeCallbacks(e){for(;e.length>0;)e.shift()(Module)}function getValue(e,n="i8"){switch(n.endsWith("*")&&(n="*"),n){case"i1":case"i8":return HEAP8[e>>0];case"i16":return HEAP16[e>>1];case"i32":case"i64":return HEAP32[e>>2];case"float":return HEAPF32[e>>2];case"double":return HEAPF64[e>>3];case"*":return HEAPU32[e>>2];default:abort("invalid type for getValue: "+n)}}function setValue(e,n,t="i8"){switch(t.endsWith("*")&&(t="*"),t){case"i1":case"i8":HEAP8[e>>0]=n;break;case"i16":HEAP16[e>>1]=n;break;case"i32":HEAP32[e>>2]=n;break;case"i64":tempI64=[n>>>0,(tempDouble=n,+Math.abs(tempDouble)>=1?tempDouble>0?(0|Math.min(+Math.floor(tempDouble/4294967296),4294967295))>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[e>>2]=tempI64[0],HEAP32[e+4>>2]=tempI64[1];break;case"float":HEAPF32[e>>2]=n;break;case"double":HEAPF64[e>>3]=n;break;case"*":HEAPU32[e>>2]=n;break;default:abort("invalid type for setValue: "+t)}}function _abort(){abort("")}var readEmAsmArgsArray=[];function readEmAsmArgs(e,n){var t;for(readEmAsmArgsArray.length=0,n>>=2;t=HEAPU8[e++];)n+=105!=t&n,readEmAsmArgsArray.push(105==t?HEAP32[n]:HEAPF64[n++>>1]),++n;return readEmAsmArgsArray}function runEmAsmFunction(e,n,t){var r=readEmAsmArgs(n,t);return ASM_CONSTS[e].apply(null,r)}function _emscripten_asm_const_int(e,n,t){return runEmAsmFunction(e,n,t)}function _emscripten_date_now(){return Date.now()}function _emscripten_memcpy_big(e,n,t){HEAPU8.copyWithin(e,n,n+t)}function getHeapMax(){return 2147483648}function emscripten_realloc_buffer(e){var n=wasmMemory.buffer;try{return wasmMemory.grow(e-n.byteLength+65535>>>16),updateMemoryViews(),1}catch(e){}}function _emscripten_resize_heap(e){var n=HEAPU8.length;e>>>=0;var t=getHeapMax();if(e>t)return!1;for(var r=1;r<=4;r*=2){var o=n*(1+.2/r);if(o=Math.min(o,e+100663296),emscripten_realloc_buffer(Math.min(t,(a=Math.max(e,o))+((i=65536)-a%i)%i)))return!0}var a,i;return!1}function getCFunc(e){return Module["_"+e]}function writeArrayToMemory(e,n){HEAP8.set(e,n)}function lengthBytesUTF8(e){for(var n=0,t=0;t=55296&&r<=57343?(n+=4,++t):n+=3}return n}function stringToUTF8Array(e,n,t,r){if(!(r>0))return 0;for(var o=t,a=t+r-1,i=0;i=55296&&u<=57343)u=65536+((1023&u)<<10)|1023&e.charCodeAt(++i);if(u<=127){if(t>=a)break;n[t++]=u}else if(u<=2047){if(t+1>=a)break;n[t++]=192|u>>6,n[t++]=128|63&u}else if(u<=65535){if(t+2>=a)break;n[t++]=224|u>>12,n[t++]=128|u>>6&63,n[t++]=128|63&u}else{if(t+3>=a)break;n[t++]=240|u>>18,n[t++]=128|u>>12&63,n[t++]=128|u>>6&63,n[t++]=128|63&u}}return n[t]=0,t-o}function stringToUTF8(e,n,t){return stringToUTF8Array(e,HEAPU8,n,t)}function stringToUTF8OnStack(e){var n=lengthBytesUTF8(e)+1,t=stackAlloc(n);return stringToUTF8(e,t,n),t}var UTF8Decoder="undefined"!=typeof TextDecoder?new TextDecoder("utf8"):void 0;function UTF8ArrayToString(e,n,t){for(var r=n+t,o=n;e[o]&&!(o>=r);)++o;if(o-n>16&&e.buffer&&UTF8Decoder)return UTF8Decoder.decode(e.subarray(n,o));for(var a="";n>10,56320|1023&s)}}else a+=String.fromCharCode((31&i)<<6|u)}else a+=String.fromCharCode(i)}return a}function UTF8ToString(e,n){return e?UTF8ArrayToString(HEAPU8,e,n):""}function ccall(e,n,t,r,o){var a={string:e=>{var n=0;return null!=e&&0!==e&&(n=stringToUTF8OnStack(e)),n},array:e=>{var n=stackAlloc(e.length);return writeArrayToMemory(e,n),n}};var i=getCFunc(e),u=[],l=0;if(r)for(var s=0;s"number"===e||"boolean"===e));return"string"!==n&&o&&!r?getCFunc(e):function(){return ccall(e,n,t,arguments,r)}}var calledRun,wasmImports={b:_abort,e:_emscripten_asm_const_int,d:_emscripten_date_now,c:_emscripten_memcpy_big,a:_emscripten_resize_heap},asm=createWasm(),___wasm_call_ctors=function(){return(___wasm_call_ctors=Module.asm.g).apply(null,arguments)},_SetBatchFractionSize=Module._SetBatchFractionSize=function(){return(_SetBatchFractionSize=Module._SetBatchFractionSize=Module.asm.h).apply(null,arguments)},_SetAttractionForce=Module._SetAttractionForce=function(){return(_SetAttractionForce=Module._SetAttractionForce=Module.asm.i).apply(null,arguments)},_SetLinkLength=Module._SetLinkLength=function(){return(_SetLinkLength=Module._SetLinkLength=Module.asm.j).apply(null,arguments)},_SetRepulsionForce=Module._SetRepulsionForce=function(){return(_SetRepulsionForce=Module._SetRepulsionForce=Module.asm.k).apply(null,arguments)},_SetCentralForce=Module._SetCentralForce=function(){return(_SetCentralForce=Module._SetCentralForce=Module.asm.l).apply(null,arguments)},_SetDt=Module._SetDt=function(){return(_SetDt=Module._SetDt=Module.asm.m).apply(null,arguments)},_Init=Module._Init=function(){return(_Init=Module._Init=Module.asm.n).apply(null,arguments)},_Update=Module._Update=function(){return(_Update=Module._Update=Module.asm.o).apply(null,arguments)},_SetPosition=Module._SetPosition=function(){return(_SetPosition=Module._SetPosition=Module.asm.p).apply(null,arguments)},_FreeMemory=Module._FreeMemory=function(){return(_FreeMemory=Module._FreeMemory=Module.asm.q).apply(null,arguments)},___errno_location=function(){return(___errno_location=Module.asm.__errno_location).apply(null,arguments)},_malloc=Module._malloc=function(){return(_malloc=Module._malloc=Module.asm.s).apply(null,arguments)},_free=Module._free=function(){return(_free=Module._free=Module.asm.t).apply(null,arguments)},stackSave=function(){return(stackSave=Module.asm.u).apply(null,arguments)},stackRestore=function(){return(stackRestore=Module.asm.v).apply(null,arguments)},stackAlloc=function(){return(stackAlloc=Module.asm.w).apply(null,arguments)},___cxa_is_pointer_type=function(){return(___cxa_is_pointer_type=Module.asm.__cxa_is_pointer_type).apply(null,arguments)};function run(){function e(){calledRun||(calledRun=!0,Module.calledRun=!0,ABORT||(initRuntime(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),postRun()))}runDependencies>0||(preRun(),runDependencies>0||(Module.setStatus?(Module.setStatus("Running..."),setTimeout((function(){setTimeout((function(){Module.setStatus("")}),1),e()}),1)):e()))}if(Module.cwrap=cwrap,Module.setValue=setValue,Module.getValue=getValue,dependenciesFulfilled=function e(){calledRun||run(),calledRun||(dependenciesFulfilled=e)},Module.preInit)for("function"==typeof Module.preInit&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();run()
--------------------------------------------------------------------------------
/docs/lib/scripts/graph_wasm.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/978f8dc28459fb8c96bf5b732c0f0fa301e2eeea/docs/lib/scripts/graph_wasm.wasm
--------------------------------------------------------------------------------
/docs/lib/styles/generated-styles.css:
--------------------------------------------------------------------------------
1 | body{--line-width:50em;--line-width-adaptive:50em;--file-line-width:50em;--content-width:500em;--sidebar-width:calc(min(22em, 80vw));--collapse-arrow-size:0.35em;--tree-horizontal-spacing:0.6em;--tree-vertical-spacing:0.6em;--sidebar-margin:24px}body{--zoom-factor:1!important;--font-text-override:'CMU Serif','Noto Serif JP'!important;--font-print-override:'CMU Serif','Noto Serif JP'!important;--font-monospace-override:'CMU Typewriter Text'!important;--font-text-size:16px}
--------------------------------------------------------------------------------
/docs/lib/styles/snippets.css:
--------------------------------------------------------------------------------
1 | .cm-blockid{opacity:.2}.cm-active .cm-blockid{opacity:.7}.markdown-source-view.mod-cm6 div.edit-block-button{display:none}.better-search-views-file-match{font-size:var(--nav-item-size)!important}.theme-dark{filter:brightness(1.3)}.HyperMD-codeblock,code{line-height:1.3!important}.HyperMD-footnote,li[data-footnote-id]{color:var(--text-muted)}.cm-comment{font-family:var(--font-monospace)!important;color:var(--code-comment)!important}.theorem-callout{--callout-color:248,248,255;background-color:rgb(255,255,255,.03);border:solid;border-radius:6px}.theorem-callout .callout-icon{display:none}.theorem-callout .callout-title-inner{font-style:bold}.theorem-callout-subtitle{font-weight:400}.webpage-container{--h1-size:1.8em;--h2-size:1.5em;--h3-size:1.324em;--h4-size:1.266em;--h5-size:1.266em;--h6-size:1em}.callout .callout-content>*{margin-block-start:0.7rem;margin-block-end:0.7rem}.file-tree>.tree-scroll-area{top:3em}
--------------------------------------------------------------------------------
/docs/search-&-link-auto-completion/assets/enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/978f8dc28459fb8c96bf5b732c0f0fa301e2eeea/docs/search-&-link-auto-completion/assets/enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp
--------------------------------------------------------------------------------
/docs/search-&-link-autocomplete/assets/enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/978f8dc28459fb8c96bf5b732c0f0fa301e2eeea/docs/search-&-link-autocomplete/assets/enhancing-obsidian's-built-in-link-autocomplete-20231130210116619.webp
--------------------------------------------------------------------------------
/esbuild.config.mjs:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 | import process from "process";
3 | import builtins from "builtin-modules";
4 | import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
5 |
6 | const banner =
7 | `/*
8 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
9 | if you want to view the source, please visit the github repository of this plugin
10 | */
11 | `;
12 |
13 | const prod = (process.argv[2] === "production");
14 |
15 | const context = await esbuild.context({
16 | banner: {
17 | js: banner,
18 | },
19 | entryPoints: ["src/main.ts"],
20 | bundle: true,
21 | plugins: [inlineWorkerPlugin()],
22 | external: [
23 | "obsidian",
24 | "electron",
25 | "@codemirror/autocomplete",
26 | "@codemirror/collab",
27 | "@codemirror/commands",
28 | "@codemirror/language",
29 | "@codemirror/lint",
30 | "@codemirror/search",
31 | "@codemirror/state",
32 | "@codemirror/view",
33 | "@lezer/common",
34 | "@lezer/highlight",
35 | "@lezer/lr",
36 | ...builtins],
37 | format: "cjs",
38 | target: "es2018",
39 | logLevel: "info",
40 | sourcemap: prod ? false : "inline",
41 | treeShaking: true,
42 | outfile: "main.js",
43 | });
44 |
45 | if (prod) {
46 | await context.rebuild();
47 | process.exit(0);
48 | } else {
49 | await context.watch();
50 | }
--------------------------------------------------------------------------------
/manifest-beta.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "math-booster",
3 | "name": "LaTeX-like Theorem & Equation Referencer",
4 | "version": "2.2.0",
5 | "minAppVersion": "1.3.5",
6 | "description": "A powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
7 | "author": "Ryota Ushio",
8 | "authorUrl": "https://github.com/RyotaUshio",
9 | "fundingUrl": "https://www.buymeacoffee.com/ryotaushio",
10 | "helpUrl": "https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/",
11 | "isDesktopOnly": false
12 | }
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "math-booster",
3 | "name": "LaTeX-like Theorem & Equation Referencer",
4 | "version": "2.2.0",
5 | "minAppVersion": "1.3.5",
6 | "description": "A powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
7 | "author": "Ryota Ushio",
8 | "authorUrl": "https://github.com/RyotaUshio",
9 | "fundingUrl": "https://www.buymeacoffee.com/ryotaushio",
10 | "helpUrl": "https://ryotaushio.github.io/obsidian-latex-theorem-equation-referencer/",
11 | "isDesktopOnly": false
12 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-math-booster",
3 | "version": "2.2.0",
4 | "description": "An Obsidian.md plugin that provides a powerful indexing & referencing system for theorems & equations in your vault. Bring LaTeX-like workflow into Obsidian with theorem environments, automatic equation numbering, and more.",
5 | "scripts": {
6 | "dev": "node esbuild.config.mjs",
7 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production && sass styles.scss styles.css",
8 | "dev-style": "sass --watch styles.scss styles.css",
9 | "build-style": "sass --watch styles.scss styles.css",
10 | "version": "node version-bump.mjs && git add manifest.json versions.json"
11 | },
12 | "keywords": [],
13 | "author": "Ryota Ushio",
14 | "license": "MIT",
15 | "devDependencies": {
16 | "@types/node": "^16.11.6",
17 | "@typescript-eslint/eslint-plugin": "5.29.0",
18 | "@typescript-eslint/parser": "5.29.0",
19 | "builtin-modules": "3.3.0",
20 | "esbuild": "^0.19.4",
21 | "obsidian": "latest",
22 | "obsidian-mathlinks": "^0.5.1",
23 | "obsidian-quick-preview": "latest",
24 | "tslib": "2.4.0",
25 | "typescript": "4.7.4"
26 | },
27 | "dependencies": {
28 | "@codemirror/language": "^6.0.0",
29 | "@lezer/common": "^1.0.3",
30 | "esbuild-plugin-inline-worker": "^0.1.1",
31 | "flatqueue": "^2.0.3",
32 | "monkey-around": "^2.3.0",
33 | "sorted-btree": "^1.8.1"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/cleveref.ts:
--------------------------------------------------------------------------------
1 | import { EquationBlock } from 'index/typings/markdown';
2 | import { TFile, HeadingSubpathResult, BlockSubpathResult, App } from 'obsidian';
3 | import * as MathLinks from 'obsidian-mathlinks';
4 |
5 | import LatexReferencer from 'main';
6 | import { MathIndex } from 'index/math-index';
7 | import { MarkdownPage, MathBlock, TheoremCalloutBlock } from 'index/typings/markdown';
8 |
9 |
10 | export class CleverefProvider extends MathLinks.Provider {
11 | app: App;
12 | index: MathIndex;
13 |
14 | constructor(mathLinks: any, public plugin: LatexReferencer) {
15 | super(mathLinks);
16 | this.app = plugin.app;
17 | this.index = plugin.indexManager.index;
18 | }
19 |
20 | provide(
21 | parsedLinktext: { path: string; subpath: string; },
22 | targetFile: TFile | null,
23 | targetSubpathResult: HeadingSubpathResult | BlockSubpathResult | null,
24 | ): string | null {
25 | const { path, subpath } = parsedLinktext;
26 | if (targetFile === null) return null;
27 | const page = this.index.load(targetFile.path);
28 | if (!MarkdownPage.isMarkdownPage(page)) return null
29 |
30 | // only path, no subpath: return page.$refName if it exists, otherwise there's nothing to do
31 | if (!subpath) return page.$refName ?? null;
32 |
33 | const processedPath = path ? page.$refName ?? path : '';
34 |
35 | // subpath resolution failed, do nothing
36 | if (targetSubpathResult === null) return null;
37 |
38 | // subpath resolution succeeded
39 | if (targetSubpathResult.type === 'block') {
40 | // handle block links
41 |
42 | // get the target block
43 | const block = page.$blocks.get(targetSubpathResult.block.id);
44 |
45 | if (MathBlock.isMathBlock(block)) {
46 | // display text set manually: higher priority
47 | if (block.$display) return path && this.shouldShowNoteTitle(block) ? processedPath + ' > ' + block.$display : block.$display;
48 | // display text computed automatically: lower priority
49 | if (block.$refName) return path && this.shouldShowNoteTitle(block) ? processedPath + ' > ' + block.$refName : block.$refName;
50 | }
51 | } else {
52 | // handle heading links
53 | // just ignore (return null) if we don't need to perform any particular processing
54 | if (path && page.$refName) {
55 | return processedPath + ' > ' + subpath;
56 | }
57 | }
58 |
59 | return null;
60 | }
61 |
62 | shouldShowNoteTitle(block: MathBlock): boolean {
63 | if (TheoremCalloutBlock.isTheoremCalloutBlock(block)) return this.plugin.extraSettings.noteTitleInTheoremLink;
64 | if (EquationBlock.isEquationBlock(block)) return this.plugin.extraSettings.noteTitleInEquationLink;
65 | return true;
66 | }
67 | }
--------------------------------------------------------------------------------
/src/env.ts:
--------------------------------------------------------------------------------
1 | // length must be >= 5
2 | export const THEOREM_LIKE_ENV_IDs = [
3 | "axiom",
4 | "definition",
5 | "lemma",
6 | "proposition",
7 | "theorem",
8 | "corollary",
9 | "claim",
10 | "assumption",
11 | "example",
12 | "exercise",
13 | "conjecture",
14 | "hypothesis",
15 | "remark",
16 | ] as const;
17 |
18 | export const PROOF_LIKE_ENV_IDs = [
19 | "proof",
20 | "solution",
21 | ] as const;
22 |
23 | export const ENV_IDs = [...THEOREM_LIKE_ENV_IDs, ...PROOF_LIKE_ENV_IDs,] as const;
24 |
25 | // length must be <= 4
26 | export const THEOREM_LIKE_ENV_PREFIXES = [
27 | "axm",
28 | "def",
29 | "lem",
30 | "prp",
31 | "thm",
32 | "cor",
33 | "clm",
34 | "asm",
35 | "exm",
36 | "exr",
37 | "cnj",
38 | "hyp",
39 | "rmk",
40 | ] as const;
41 |
42 | export type TheoremLikeEnvID = typeof THEOREM_LIKE_ENV_IDs[number];
43 | export type TheoremLikeEnvPrefix = typeof THEOREM_LIKE_ENV_PREFIXES[number];
44 |
45 | export interface TheoremLikeEnv {
46 | id: TheoremLikeEnvID,
47 | prefix: TheoremLikeEnvPrefix,
48 | }
49 |
50 | export const THEOREM_LIKE_ENVs = {} as Record;
51 | THEOREM_LIKE_ENV_IDs.forEach((id, index) => {
52 | THEOREM_LIKE_ENVs[id] = {id, prefix: THEOREM_LIKE_ENV_PREFIXES[index]};
53 | });
54 |
55 | export const THEOREM_LIKE_ENV_PREFIX_ID_MAP = {} as Record;
56 | THEOREM_LIKE_ENV_PREFIXES.forEach((prefix, index) => {
57 | THEOREM_LIKE_ENV_PREFIX_ID_MAP[prefix] = THEOREM_LIKE_ENV_IDs[index];
58 | });
59 |
60 | export const THEOREM_LIKE_ENV_ID_PREFIX_MAP = {} as Record;
61 | THEOREM_LIKE_ENV_IDs.forEach((id, index) => {
62 | THEOREM_LIKE_ENV_ID_PREFIX_MAP[id] = THEOREM_LIKE_ENV_PREFIXES[index];
63 | });
64 |
--------------------------------------------------------------------------------
/src/equations/common.ts:
--------------------------------------------------------------------------------
1 | import { EquationBlock } from "index/typings/markdown";
2 | import { finishRenderMath, renderMath } from "obsidian";
3 | import { MathContextSettings } from "settings/settings";
4 | import { parseLatexComment } from "utils/parse";
5 |
6 |
7 |
8 | export function replaceMathTag(displayMathEl: HTMLElement, equation: EquationBlock, settings: Required) {
9 | if (equation.$manualTag) return; // respect a tag (\tag{...}) manually set by the user
10 |
11 | const taggedText = getMathTextWithTag(equation, settings.lineByLine);
12 | if (taggedText) {
13 | const mjxContainerEl = renderMath(taggedText, true);
14 | if (equation.$printName !== null) {
15 | displayMathEl.setAttribute('width', 'full');
16 | displayMathEl.style.cssText = mjxContainerEl.style.cssText;
17 | } else {
18 | displayMathEl.removeAttribute('width');
19 | displayMathEl.removeAttribute('style');
20 | }
21 | displayMathEl.replaceChildren(...mjxContainerEl.childNodes);
22 | }
23 | }
24 |
25 | export function getMathTextWithTag(equation: EquationBlock, lineByLine?: boolean): string | undefined {
26 | if (equation.$printName !== null) {
27 | const tagResult = equation.$printName.match(/^\((.*)\)$/);
28 | if (tagResult) {
29 | const tagContent = tagResult[1];
30 | return insertTagInMathText(equation.$mathText, tagContent, lineByLine);
31 | }
32 | }
33 | return equation.$mathText;
34 | }
35 |
36 | export function insertTagInMathText(text: string, tagContent: string, lineByLine?: boolean): string {
37 | if (!lineByLine) return text + `\\tag{${tagContent}}`;
38 |
39 | const alignResult = text.match(/^\s*\\begin\{align\}([\s\S]*)\\end\{align\}\s*$/);
40 | if (!alignResult) return text + `\\tag{${tagContent}}`;
41 |
42 | const envStack: string[] = [];
43 |
44 | // remove comments
45 | let alignContent = alignResult[1]
46 | .split('\n')
47 | .map(line => parseLatexComment(line).nonComment)
48 | .join('\n');
49 | // add tags
50 | let index = 1;
51 | alignContent = alignContent
52 | .split("\\\\")
53 | .map((alignLine) => {
54 | const pattern = /\\(?begin|end)\{(?.*?)\}/g;
55 | let result;
56 | while (result = pattern.exec(alignLine)) {
57 | const { which, env } = result.groups!;
58 | if (which === 'begin') envStack.push(env);
59 | else if (envStack.last() === env) envStack.pop();
60 | }
61 | if (envStack.length || !alignLine.trim() || alignLine.contains("\\nonumber")) return alignLine;
62 | return alignLine + `\\tag{${tagContent}-${index++}}`;
63 | }).join("\\\\");
64 |
65 | if (index <= 2) return text + `\\tag{${tagContent}}`;
66 |
67 | return "\\begin{align}" + alignContent + "\\end{align}";
68 | }
69 |
--------------------------------------------------------------------------------
/src/equations/live-preview.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Display equation numbers in Live Preview.
3 | */
4 |
5 | import { EditorState, StateEffect } from '@codemirror/state';
6 | import { PluginValue, ViewPlugin, EditorView, ViewUpdate } from '@codemirror/view';
7 | import { EquationBlock, MarkdownBlock, MarkdownPage } from 'index/typings/markdown';
8 | import LatexReferencer from 'main';
9 | import { MarkdownView, TFile, editorInfoField, finishRenderMath } from 'obsidian';
10 | import { resolveSettings } from 'utils/plugin';
11 | import { replaceMathTag } from './common';
12 | import { DEFAULT_SETTINGS, MathContextSettings } from 'settings/settings';
13 |
14 |
15 | export function createEquationNumberPlugin(plugin: LatexReferencer) {
16 |
17 | const { app, indexManager: { index } } = plugin;
18 |
19 | const forceUpdateEffect = StateEffect.define();
20 |
21 | plugin.registerEvent(plugin.indexManager.on('index-updated', (file) => {
22 | app.workspace.iterateAllLeaves((leaf) => {
23 | if (
24 | leaf.view instanceof MarkdownView
25 | && leaf.view.file?.path === file.path
26 | && leaf.view.getMode() === 'source'
27 | ) {
28 | leaf.view.editor.cm?.dispatch({ effects: forceUpdateEffect.of(null) });
29 | }
30 | });
31 | }));
32 |
33 | return ViewPlugin.fromClass(class implements PluginValue {
34 | file: TFile | null;
35 | page: MarkdownPage | null;
36 | settings: Required;
37 |
38 | constructor(view: EditorView) {
39 | this.file = view.state.field(editorInfoField).file;
40 | this.page = null;
41 | this.settings = DEFAULT_SETTINGS;
42 |
43 | if (this.file) {
44 | this.settings = resolveSettings(undefined, plugin, this.file);
45 | const page = index.load(this.file.path);
46 | if (MarkdownPage.isMarkdownPage(page)) {
47 | this.page = page;
48 | this.updateEquationNumber(view, this.page);
49 | }
50 | }
51 | }
52 |
53 | updateFile(state: EditorState) {
54 | this.file = state.field(editorInfoField).file;
55 | if (this.file) this.settings = resolveSettings(undefined, plugin, this.file);
56 | }
57 |
58 | async updatePage(file: TFile): Promise {
59 | const page = index.load(file.path);
60 | if (MarkdownPage.isMarkdownPage(page)) this.page = page;
61 | if (!this.page) {
62 | this.page = await plugin.indexManager.reload(file);
63 | }
64 | return this.page;
65 | }
66 |
67 | update(update: ViewUpdate) {
68 | if (!this.file) this.updateFile(update.state);
69 | if (!this.file) return;
70 |
71 | if (update.transactions.some(tr => tr.effects.some(effect => effect.is(forceUpdateEffect)))) {
72 | // index updated
73 | this.settings = resolveSettings(undefined, plugin, this.file);
74 | this.updatePage(this.file).then((updatedPage) => this.updateEquationNumber(update.view, updatedPage))
75 | } else if (update.geometryChanged) {
76 | if (this.page) this.updateEquationNumber(update.view, this.page);
77 | else this.updatePage(this.file).then((updatedPage) => this.updateEquationNumber(update.view, updatedPage));
78 | }
79 | }
80 |
81 | async updateEquationNumber(view: EditorView, page: MarkdownPage) {
82 | const mjxContainerElements = view.contentDOM.querySelectorAll(':scope > .cm-embed-block.math > mjx-container.MathJax[display="true"]');
83 |
84 | for (const mjxContainerEl of mjxContainerElements) {
85 |
86 | // skip if the equation is being edited to avoid the delay of preview
87 | const mightBeClosingDollars = mjxContainerEl.parentElement?.previousElementSibling?.lastElementChild;
88 | const isBeingEdited = mightBeClosingDollars?.matches('span.cm-formatting-math-end');
89 | if (isBeingEdited) continue;
90 |
91 | const pos = view.posAtDOM(mjxContainerEl);
92 | let block: MarkdownBlock | undefined;
93 | try {
94 | const line = view.state.doc.lineAt(pos).number - 1; // sometimes throws an error for reasons that I don't understand
95 | block = page.getBlockByLineNumber(line);
96 | } catch (err) {
97 | block = page.getBlockByOffset(pos);
98 | }
99 | if (!(block instanceof EquationBlock)) continue;
100 |
101 | // only update if necessary
102 | if (mjxContainerEl.getAttribute('data-equation-print-name') !== block.$printName) {
103 | replaceMathTag(mjxContainerEl, block, this.settings);
104 | }
105 | if (block.$printName !== null) mjxContainerEl.setAttribute('data-equation-print-name', block.$printName);
106 | else mjxContainerEl.removeAttribute('data-equation-print-name');
107 | }
108 | // DON'T FOREGET THIS CALL!!
109 | // https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/203
110 | // https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/200
111 | finishRenderMath();
112 | }
113 |
114 | destroy() {
115 | // I don't know if this is really necessary, but just in case...
116 | finishRenderMath();
117 | }
118 | });
119 | }
120 |
--------------------------------------------------------------------------------
/src/equations/reading-view.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Display equation numbers in reading view, embeds, hover page preview, and PDF export.
3 | */
4 |
5 | import { App, MarkdownRenderChild, finishRenderMath, MarkdownPostProcessorContext, TFile, Notice } from "obsidian";
6 |
7 | import LatexReferencer from 'main';
8 | import { resolveSettings } from 'utils/plugin';
9 | import { EquationBlock, MarkdownPage } from "index/typings/markdown";
10 | import { MathIndex } from "index/math-index";
11 | import { isPdfExport, resolveLinktext } from "utils/obsidian";
12 | import { replaceMathTag } from "./common";
13 |
14 |
15 | export const createEquationNumberProcessor = (plugin: LatexReferencer) => async (el: HTMLElement, ctx: MarkdownPostProcessorContext) => {
16 | if (isPdfExport(el)) preprocessForPdfExport(plugin, el, ctx);
17 |
18 | const sourceFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath);
19 | if (!(sourceFile instanceof TFile)) return;
20 |
21 | const mjxContainerElements = el.querySelectorAll('mjx-container.MathJax[display="true"]');
22 | for (const mjxContainerEl of mjxContainerElements) {
23 | ctx.addChild(
24 | new EquationNumberRenderer(mjxContainerEl, plugin, sourceFile, ctx)
25 | );
26 | }
27 | finishRenderMath();
28 | }
29 |
30 |
31 | /**
32 | * As a preprocessing for displaying equation numbers in the exported PDF,
33 | * add an attribute representing a block ID to each numbered equation element
34 | * so that EquationNumberRenderer can find the corresponding block from the index
35 | * without relying on the line number.
36 | */
37 | function preprocessForPdfExport(plugin: LatexReferencer, el: HTMLElement, ctx: MarkdownPostProcessorContext) {
38 |
39 | try {
40 | const topLevelMathDivs = el.querySelectorAll(':scope > div.math.math-block > mjx-container.MathJax[display="true"]');
41 |
42 | const page = plugin.indexManager.index.getMarkdownPage(ctx.sourcePath);
43 | if (!page) {
44 | new Notice(`${plugin.manifest.name}: Failed to fetch the metadata for PDF export; equation numbers will not be displayed in the exported PDF.`);
45 | return;
46 | }
47 |
48 | let equationIndex = 0;
49 | for (const section of page.$sections) {
50 | for (const block of section.$blocks) {
51 | if (!EquationBlock.isEquationBlock(block)) continue;
52 |
53 | const div = topLevelMathDivs[equationIndex++];
54 | if (block.$printName) div.setAttribute('data-equation-id', block.$id);
55 | }
56 | }
57 |
58 | if (topLevelMathDivs.length != equationIndex) {
59 | new Notice(`${plugin.manifest.name}: Something unexpected occured while preprocessing for PDF export. Equation numbers might not be displayed properly in the exported PDF.`);
60 | }
61 | } catch (err) {
62 | new Notice(`${plugin.manifest.name}: Something unexpected occured while preprocessing for PDF export. See the developer console for the details. Equation numbers might not be displayed properly in the exported PDF.`);
63 | console.error(err);
64 | }
65 | }
66 |
67 |
68 | export class EquationNumberRenderer extends MarkdownRenderChild {
69 | app: App
70 | index: MathIndex;
71 |
72 | constructor(containerEl: HTMLElement, public plugin: LatexReferencer, public file: TFile, public context: MarkdownPostProcessorContext) {
73 | // containerEl, currentEL are mjx-container.MathJax elements
74 | super(containerEl);
75 | this.app = plugin.app;
76 | this.index = this.plugin.indexManager.index;
77 |
78 | this.registerEvent(this.plugin.indexManager.on("index-initialized", () => {
79 | setTimeout(() => this.update());
80 | }));
81 |
82 | this.registerEvent(this.plugin.indexManager.on("index-updated", (file) => {
83 | setTimeout(() => {
84 | if (file.path === this.file.path) this.update();
85 | });
86 | }));
87 | }
88 |
89 | getEquationCache(lineOffset: number = 0): EquationBlock | null {
90 | const info = this.context.getSectionInfo(this.containerEl);
91 | const page = this.index.getMarkdownPage(this.file.path);
92 | if (!info || !page) return null;
93 |
94 | // get block ID
95 | const block = page.getBlockByLineNumber(info.lineStart + lineOffset) ?? page.getBlockByLineNumber(info.lineEnd + lineOffset);
96 | if (EquationBlock.isEquationBlock(block)) return block;
97 |
98 | return null;
99 | }
100 |
101 | async onload() {
102 | setTimeout(() => this.update());
103 | }
104 |
105 | onunload() {
106 | // I don't know if this is really necessary, but just in case...
107 | finishRenderMath();
108 | }
109 |
110 | update() {
111 | // for PDF export
112 | const id = this.containerEl.getAttribute('data-equation-id');
113 |
114 | const equation = id ? this.index.getEquationBlock(id) : this.getEquationCacheCaringHoverAndEmbed();
115 | if (!equation) return;
116 | const settings = resolveSettings(undefined, this.plugin, this.file);
117 | replaceMathTag(this.containerEl, equation, settings);
118 | }
119 |
120 | getEquationCacheCaringHoverAndEmbed(): EquationBlock | null {
121 | /**
122 | * https://github.com/RyotaUshio/obsidian-latex-theorem-equation-referencer/issues/179
123 | *
124 | * In the case of embeds or hover popovers, the line numbers contained
125 | * in the result of MarkdownPostProcessorContext.getSectionInfo() is
126 | * relative to the content included in the embed.
127 | * In other words, they does not always represent the offset from the beginning of the file.
128 | * So they require special handling.
129 | */
130 |
131 | const equation = this.getEquationCache();
132 |
133 | let linktext = this.containerEl.closest('[src]')?.getAttribute('src'); // in the case of embeds
134 |
135 | if (!linktext) {
136 | const hoverEl = this.containerEl.closest('.hover-popover:not(.hover-editor)');
137 | if (hoverEl) {
138 | // The current context is hover page preview; read the linktext saved in the plugin instance.
139 | linktext = this.plugin.lastHoverLinktext;
140 | }
141 | }
142 |
143 | if (linktext) { // linktext was found
144 | const { file, subpathResult } = resolveLinktext(this.app, linktext, this.context.sourcePath) ?? {};
145 |
146 | if (!file || !subpathResult) return null;
147 |
148 | const page = this.index.load(file.path);
149 | if (!MarkdownPage.isMarkdownPage(page)) return null;
150 |
151 | if (subpathResult.type === "block") {
152 | const block = page.$blocks.get(subpathResult.block.id);
153 | if (!EquationBlock.isEquationBlock(block)) return null;
154 | return block;
155 | } else {
156 | return this.getEquationCache(subpathResult.start.line);
157 | }
158 | }
159 |
160 | return equation;
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/src/file-io.ts:
--------------------------------------------------------------------------------
1 | import { CachedMetadata, Editor, MarkdownView, Pos, TFile } from "obsidian";
2 |
3 | import LatexReferencer from "./main";
4 | import { isEditingView, locToEditorPosition } from "utils/editor";
5 | import { insertAt, splitIntoLines } from "utils/general";
6 |
7 |
8 | export abstract class FileIO {
9 | constructor(public plugin: LatexReferencer, public file: TFile) { }
10 | abstract setLine(lineNumber: number, text: string): Promise;
11 | abstract setRange(position: Pos, text: string): Promise;
12 | abstract insertLine(lineNumber: number, text: string): Promise;
13 | abstract getLine(lineNumber: number): Promise;
14 | abstract getRange(position: Pos): Promise;
15 | }
16 |
17 |
18 | export class ActiveNoteIO extends FileIO {
19 | /**
20 | * File IO for the currently active markdown view.
21 | * Uses the Editor interface instead of Vault.
22 | * (See https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines#Prefer+the+Editor+API+instead+of+%60Vault.modify%60)
23 | * @param editor
24 | */
25 | constructor(plugin: LatexReferencer, file: TFile, public editor: Editor) {
26 | super(plugin, file);
27 | }
28 |
29 | async setLine(lineNumber: number, text: string): Promise {
30 | this.editor.setLine(lineNumber, text);
31 | }
32 |
33 | async setRange(position: Pos, text: string): Promise {
34 | const from = locToEditorPosition(position.start);
35 | const to = locToEditorPosition(position.end);
36 | this.editor.replaceRange(text, from, to);
37 | }
38 |
39 | async insertLine(lineNumber: number, text: string): Promise {
40 | this.editor.replaceRange(text + "\n", { line: lineNumber, ch: 0 });
41 | }
42 |
43 | async getLine(lineNumber: number): Promise {
44 | return this.editor.getLine(lineNumber);
45 | }
46 |
47 | async getRange(position: Pos): Promise {
48 | const from = locToEditorPosition(position.start);
49 | const to = locToEditorPosition(position.end);
50 | const text = this.editor.getRange(from, to);
51 | return text;
52 | }
53 | }
54 |
55 |
56 | export class NonActiveNoteIO extends FileIO {
57 | _data: string | null = null;
58 |
59 | /**
60 | * File IO for non-active (= currently not opened / currently opened but not focused) notes.
61 | * Uses the Vault interface instead of Editor.
62 | */
63 | constructor(plugin: LatexReferencer, file: TFile) {
64 | super(plugin, file);
65 | }
66 |
67 | async setLine(lineNumber: number, text: string): Promise {
68 | this.plugin.app.vault.process(this.file, (data: string): string => {
69 | const lines = splitIntoLines(data);
70 | lines[lineNumber] = text;
71 | return lines.join('\n');
72 | })
73 | }
74 |
75 | async setRange(position: Pos, text: string): Promise {
76 | this.plugin.app.vault.process(this.file, (data: string): string => {
77 | return data.slice(0, position.start.offset) + text + data.slice(position.end.offset + 1, data.length);
78 | })
79 | }
80 |
81 | async insertLine(lineNumber: number, text: string): Promise {
82 | this.plugin.app.vault.process(this.file, (data: string): string => {
83 | const lines = splitIntoLines(data);
84 | insertAt(lines, text, lineNumber);
85 | return lines.join('\n');
86 | })
87 | }
88 |
89 | async getLine(lineNumber: number): Promise {
90 | const data = await this.plugin.app.vault.cachedRead(this.file);
91 | const lines = splitIntoLines(data);
92 | return lines[lineNumber];
93 | }
94 |
95 | async getRange(position: Pos): Promise {
96 | const content = await this.plugin.app.vault.cachedRead(this.file);
97 | return content.slice(position.start.offset, position.end.offset);
98 | }
99 | }
100 |
101 |
102 | /**
103 | * Automatically judges which of ActiveNoteIO or NonActiveNoteIO
104 | * should be used for the given file.
105 | */
106 | export function getIO(plugin: LatexReferencer, file: TFile, activeMarkdownView?: MarkdownView | null) {
107 | activeMarkdownView = activeMarkdownView ?? plugin.app.workspace.getActiveViewOfType(MarkdownView);
108 | if (activeMarkdownView && activeMarkdownView.file == file && isEditingView(activeMarkdownView)) {
109 | return new ActiveNoteIO(plugin, file, activeMarkdownView.editor);
110 | } else {
111 | return new NonActiveNoteIO(plugin, file);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/index/README.md:
--------------------------------------------------------------------------------
1 | The source code contained in this directory was taken from the Datacore plugin (https://github.com/blacksmithgu/datacore) and adapted.
2 |
3 | MIT License
4 |
5 | Copyright (c) 2023 Michael Brenan
6 |
7 | Permission is hereby granted, free of charge, to any person obtaining a copy
8 | of this software and associated documentation files (the "Software"), to deal
9 | in the Software without restriction, including without limitation the rights
10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the Software is
12 | furnished to do so, subject to the following conditions:
13 |
14 | The above copyright notice and this permission notice shall be included in all
15 | copies or substantial portions of the Software.
16 |
17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | SOFTWARE.
--------------------------------------------------------------------------------
/src/index/expression/link.ts:
--------------------------------------------------------------------------------
1 | import { getFileTitle } from "../utils/normalizers";
2 |
3 | /** The Obsidian 'link', used for uniquely describing a file, header, or block. */
4 | export class Link {
5 | /** The file path this link points to. */
6 | public path: string;
7 | /** The display name associated with the link. */
8 | public display?: string;
9 | /** The block ID or header this link points to within a file, if relevant. */
10 | public subpath?: string;
11 | /** Is this link an embedded link (of form '![[hello]]')? */
12 | public embed: boolean;
13 | /** The type of this link, which determines what 'subpath' refers to, if anything. */
14 | public type: "file" | "header" | "block";
15 |
16 | /** Create a link to a specific file. */
17 | public static file(path: string, embed: boolean = false, display?: string): Link {
18 | return new Link({
19 | path,
20 | embed,
21 | display,
22 | subpath: undefined,
23 | type: "file",
24 | });
25 | }
26 |
27 | /** Infer the type of the link from the full internal link path. */
28 | public static infer(linkpath: string, embed: boolean = false, display?: string): Link {
29 | if (linkpath.includes("#^")) {
30 | let split = linkpath.split("#^");
31 | return Link.block(split[0], split[1], embed, display);
32 | } else if (linkpath.includes("#")) {
33 | let split = linkpath.split("#");
34 | return Link.header(split[0], split[1], embed, display);
35 | } else return Link.file(linkpath, embed, display);
36 | }
37 |
38 | /** Create a link to a specific file and header in that file. */
39 | public static header(path: string, header: string, embed?: boolean, display?: string): Link {
40 | // Headers need to be normalized to alpha-numeric & with extra spacing removed.
41 | return new Link({
42 | path,
43 | embed,
44 | display,
45 | subpath: header, //normalizeHeaderForLink(header),
46 | type: "header",
47 | });
48 | }
49 |
50 | /** Create a link to a specific file and block in that file. */
51 | public static block(path: string, blockId: string, embed?: boolean, display?: string): Link {
52 | return new Link({
53 | path,
54 | embed,
55 | display,
56 | subpath: blockId,
57 | type: "block",
58 | });
59 | }
60 |
61 | /** Load a link from it's raw JSON representation. */
62 | public static fromObject(object: Record): Link {
63 | return new Link(object);
64 | }
65 |
66 | /** Create a link by parsing it's interior part (inside of the '[[]]'). */
67 | public static parseInner(rawlink: string): Link {
68 | let [link, display] = splitOnUnescapedPipe(rawlink);
69 | return Link.infer(link, false, display);
70 | }
71 |
72 | private constructor(fields: Partial) {
73 | Object.assign(this, fields);
74 | }
75 |
76 | /** Update this link with a new path. */
77 | public withPath(path: string): Link {
78 | return new Link(Object.assign({}, this, { path }));
79 | }
80 |
81 | /** Return a new link which points to the same location but with a new display value. */
82 | public withDisplay(display?: string): Link {
83 | return new Link(Object.assign({}, this, { display }));
84 | }
85 |
86 | /** Return a new link which has the given embedded status. */
87 | public withEmbed(embed: boolean): Link {
88 | if (this.embed == embed) return this;
89 |
90 | return new Link(Object.assign({}, this, { embed }));
91 | }
92 |
93 | /** Convert a file link into a link to a specific header. */
94 | public withHeader(header: string): Link {
95 | return Link.header(this.path, header, this.embed, this.display);
96 | }
97 |
98 | /** Convert a file link into a link to a specificb lock. */
99 | public withBlock(block: string): Link {
100 | return Link.block(this.path, block, this.embed, this.display);
101 | }
102 |
103 | /** Checks for link equality (i.e., that the links are pointing to the same exact location). */
104 | public equals(other: Link): boolean {
105 | if (other == undefined || other == null) return false;
106 |
107 | return this.path == other.path && this.type == other.type && this.subpath == other.subpath;
108 | }
109 |
110 | /** Convert this link to it's markdown representation. */
111 | public toString(): string {
112 | return this.markdown();
113 | }
114 |
115 | /** Convert this link to a raw object which is serialization-friendly. */
116 | public toObject(): Record {
117 | return {
118 | path: this.path,
119 | type: this.type,
120 | subpath: this.subpath,
121 | display: this.display,
122 | embed: this.embed,
123 | };
124 | }
125 |
126 | /** Convert any link into a link to its file. */
127 | public toFile() {
128 | return Link.file(this.path, this.embed, this.display);
129 | }
130 |
131 | /** Convert this link into an embedded link. */
132 | public toEmbed(): Link {
133 | return this.withEmbed(true);
134 | }
135 |
136 | /** Convert this link into a non-embedded link. */
137 | public fromEmbed(): Link {
138 | return this.withEmbed(false);
139 | }
140 |
141 | /** Convert this link to markdown so it can be rendered. */
142 | public markdown(): string {
143 | let result = (this.embed ? "!" : "") + "[[" + this.obsidianLink();
144 | result += this.displayOrDefault();
145 | result += "]]";
146 | return result;
147 | }
148 |
149 | /** Obtain the display for this link, or return a simple default display. */
150 | public displayOrDefault() {
151 | if (this.display) {
152 | return this.display;
153 | } else {
154 | let result = getFileTitle(this.path);
155 | if (this.type == "header" || this.type == "block") result += " > " + this.subpath;
156 |
157 | return result;
158 | }
159 | }
160 |
161 | /** Convert the inner part of the link to something that Obsidian can open / understand. */
162 | public obsidianLink(): string {
163 | const escaped = this.path.replace("|", "\\|");
164 | if (this.type == "header") return escaped + "#" + this.subpath?.replace("|", "\\|");
165 | if (this.type == "block") return escaped + "#^" + this.subpath?.replace("|", "\\|");
166 | else return escaped;
167 | }
168 |
169 | /** The stripped name of the file this link points to. */
170 | public fileName(): string {
171 | return getFileTitle(this.path);
172 | }
173 | }
174 |
175 | /** Split on unescaped pipes in an inner link. */
176 | export function splitOnUnescapedPipe(link: string): [string, string | undefined] {
177 | let pipe = -1;
178 | while ((pipe = link.indexOf("|", pipe + 1)) >= 0) {
179 | if (pipe > 0 && link[pipe - 1] == "\\") continue;
180 | return [link.substring(0, pipe).replace(/\\\|/g, "|"), link.substring(pipe + 1)];
181 | }
182 |
183 | return [link.replace(/\\\|/g, "|"), undefined];
184 | }
185 |
--------------------------------------------------------------------------------
/src/index/import/markdown.ts:
--------------------------------------------------------------------------------
1 | import { Link } from "index/expression/link";
2 | import { getFileTitle } from "index/utils/normalizers";
3 | import { CachedMetadata, SectionCache } from "obsidian";
4 | import BTree from "sorted-btree";
5 | import {
6 | JsonMarkdownBlock,
7 | JsonMarkdownPage,
8 | JsonMarkdownSection,
9 | JsonTheoremCalloutBlock,
10 | JsonEquationBlock,
11 | } from "index/typings/json";
12 | import { MinimalTheoremCalloutSettings } from "settings/settings";
13 | import { parseMarkdownComment, parseYamlLike, readTheoremCalloutSettings, trimMathText } from "utils/parse";
14 | import { parseLatexComment } from "utils/parse";
15 |
16 |
17 | /**
18 | * Given the raw source and Obsidian metadata for a given markdown file,
19 | * return full markdown file metadata.
20 | */
21 | export function markdownImport(
22 | path: string,
23 | markdown: string,
24 | metadata: CachedMetadata,
25 | excludeExample: boolean
26 | ): JsonMarkdownPage {
27 | // Total length of the file.
28 | const lines = markdown.split("\n");
29 | const empty = !lines.some((line) => line.trim() !== "");
30 |
31 | //////////////
32 | // Sections //
33 | //////////////
34 |
35 | const metaheadings = metadata.headings ?? [];
36 | metaheadings.sort((a, b) => a.position.start.line - b.position.start.line);
37 |
38 | const sections = new BTree(undefined, (a, b) => a - b);
39 | for (let index = 0; index < metaheadings.length; index++) {
40 | const section = metaheadings[index];
41 | const start = section.position.start.line;
42 | const end =
43 | index == metaheadings.length - 1 ? lines.length - 1 : metaheadings[index + 1].position.start.line - 1;
44 |
45 | sections.set(start, {
46 | $ordinal: index + 1,
47 | $title: section.heading,
48 | $level: section.level,
49 | $position: { start, end },
50 | $blocks: [],
51 | $links: [],
52 | });
53 | }
54 |
55 | // Add an implicit section for the "heading" section of the page if there is not an immediate header but there is
56 | // some content in the file. If there are other sections, then go up to that, otherwise, go for the entire file.
57 | const firstSection: [number, JsonMarkdownSection] | undefined = sections.getPairOrNextHigher(0);
58 | if ((!firstSection && !empty) || (firstSection && !emptylines(lines, 0, firstSection[1].$position.start))) {
59 | const end = firstSection ? firstSection[1].$position.start - 1 : lines.length;
60 | sections.set(0, {
61 | $ordinal: 0,
62 | $title: getFileTitle(path),
63 | $level: 1,
64 | $position: { start: 0, end },
65 | $blocks: [],
66 | $links: [],
67 | });
68 | }
69 |
70 | ////////////
71 | // Blocks //
72 | ////////////
73 |
74 | // All blocks; we will assign tags and other metadata to blocks as we encounter them. At the end, only blocks that
75 | // have actual metadata will be stored to save on memory pressure.
76 | const blocks = new BTree(undefined, (a, b) => a - b);
77 | let blockOrdinal = 1;
78 | for (const block of metadata.sections || []) {
79 | // Skip headings blocks, we handle them specially as sections.
80 | if (block.type === "heading") continue;
81 |
82 | const start = block.position.start.line;
83 | const end = block.position.end.line;
84 |
85 | let theoremCalloutSettings: MinimalTheoremCalloutSettings | null = null;
86 | let v1 = false;
87 | if (block.type === "callout") {
88 | const settings = readTheoremCalloutSettings(lines[start], excludeExample);
89 | theoremCalloutSettings = settings ?? null;
90 | v1 = !!(settings?.legacy);
91 | }
92 |
93 | if (block.type === "math") {
94 | // Read the LaTeX source
95 | const mathText = trimMathText(getBlockText(markdown, block));
96 |
97 | // If manually tagged (`\tag{...}`), extract the tag
98 | const tagMatch = mathText.match(/\\tag\{(.*)\}/);
99 |
100 | // Parse additional metadata from LaTeX comments
101 | const metadata: Record = {};
102 | for (const line of mathText.split('\n')) {
103 | const { comment } = parseLatexComment(line);
104 | if (!comment) continue;
105 | Object.assign(metadata, parseYamlLike(comment));
106 | }
107 |
108 | blocks.set(start, {
109 | $ordinal: blockOrdinal++,
110 | $position: { start, end },
111 | $pos: block.position,
112 | $links: [],
113 | $blockId: block.id,
114 | $manualTag: tagMatch?.[1] ?? null,
115 | $mathText: mathText,
116 | $type: "equation",
117 | $label: metadata.label,
118 | $display: metadata.display,
119 | } as JsonEquationBlock);
120 | } else if (theoremCalloutSettings) {
121 |
122 | // Parse additional metadata from Markdown comments
123 | const contentText = lines.slice(start + 1, end + 1).join('\n');
124 | const commentLines = parseMarkdownComment(contentText);
125 | const metadata: Record = {};
126 | for (let line of commentLines) {
127 | if (line.startsWith('>')) line = line.slice(1).trim();
128 | if (!line) continue;
129 | if (line === 'main') metadata.main = 'true'; // %% main %% is the same as %% main: true %%
130 | else Object.assign(metadata, parseYamlLike(line));
131 | }
132 |
133 | blocks.set(start, {
134 | $ordinal: blockOrdinal++,
135 | $position: { start, end },
136 | $pos: block.position,
137 | $links: [],
138 | $blockId: block.id,
139 | $settings: theoremCalloutSettings,
140 | $type: "theorem",
141 | $label: metadata.label,
142 | $display: metadata.display,
143 | $main: metadata.main === 'true',
144 | $v1: v1,
145 | } as JsonTheoremCalloutBlock);
146 | } else {
147 | blocks.set(start, {
148 | $ordinal: blockOrdinal++,
149 | $position: { start, end },
150 | $pos: block.position,
151 | $links: [],
152 | $blockId: block.id,
153 | $type: block.type,
154 | });
155 | }
156 | }
157 |
158 | // Add blocks to sections.
159 | for (const block of blocks.values() as Iterable) {
160 | const section = sections.getPairOrNextLower(block.$position.start);
161 |
162 | if (section && section[1].$position.end >= block.$position.end) {
163 | section[1].$blocks.push(block);
164 | }
165 | }
166 |
167 | ///////////
168 | // Links //
169 | ///////////
170 |
171 | const links: Link[] = [];
172 | for (let linkdef of metadata.links ?? []) {
173 | const link = Link.infer(linkdef.link);
174 | const line = linkdef.position.start.line;
175 | addLink(links, link);
176 |
177 | const section = sections.getPairOrNextLower(line);
178 | if (section && section[1].$position.end >= line) addLink(section[1].$links, link);
179 |
180 | const block = blocks.getPairOrNextLower(line);
181 | if (block && block[1].$position.end >= line) addLink(block[1].$links, link);
182 |
183 | const listItem = blocks.getPairOrNextHigher(line);
184 | if (listItem && listItem[1].$position.end >= line) addLink(listItem[1].$links, link);
185 | }
186 |
187 | ///////////////////////
188 | // Frontmatter Links //
189 | ///////////////////////
190 |
191 | // Frontmatter links are only assigned to the page.
192 | for (const linkdef of metadata.frontmatterLinks ?? []) {
193 | const link = Link.infer(linkdef.link, false, linkdef.displayText);
194 | addLink(links, link);
195 | }
196 |
197 | return {
198 | $path: path,
199 | $links: links,
200 | $sections: sections.valuesArray(),
201 | $extension: "md",
202 | $position: { start: 0, end: lines.length },
203 | };
204 | }
205 |
206 | /** Check if the given line range is all empty. Start is inclusive, end exclusive. */
207 | function emptylines(lines: string[], start: number, end: number): boolean {
208 | for (let index = start; index < end; index++) {
209 | if (lines[index].trim() !== "") return false;
210 | }
211 |
212 | return true;
213 | }
214 |
215 | /**
216 | * Mutably add the given link to the list only if it is not already present.
217 | * This is O(n) but should be fine for most files; we could eliminate the O(n) by instead
218 | * using intermediate sets but not worth the complexity.
219 | */
220 | function addLink(target: Link[], incoming: Link) {
221 | if (target.find((v) => v.equals(incoming))) return;
222 | target.push(incoming);
223 | }
224 |
225 | function getBlockText(data: string, block: SectionCache) {
226 | return data.slice(block.position.start.offset, block.position.end.offset);
227 | }
--------------------------------------------------------------------------------
/src/index/storage/inverted.ts:
--------------------------------------------------------------------------------
1 | /** Tracks an inverted index of value -> set. */
2 | export class InvertedIndex {
3 | private inverted: Map>;
4 |
5 | public constructor() {
6 | this.inverted = new Map();
7 | }
8 |
9 | /** Set the key to the given values. */
10 | public set(key: string, values: Iterable) {
11 | for (let value of values) {
12 | if (!this.inverted.has(value)) this.inverted.set(value, new Set());
13 | this.inverted.get(value)!.add(key);
14 | }
15 | }
16 |
17 | /** Get all keys that map to the given value. */
18 | public get(value: V): Set {
19 | return this.inverted.get(value) ?? InvertedIndex.EMPTY_SET;
20 | }
21 |
22 | /** Delete a key from the set of associated values. */
23 | public delete(key: string, values: Iterable) {
24 | for (let value of values) {
25 | const set = this.inverted.get(value);
26 | if (set) {
27 | set.delete(key);
28 | }
29 |
30 | if (set && set.size == 0) {
31 | this.inverted.delete(value);
32 | }
33 | }
34 | }
35 |
36 | public clear() {
37 | this.inverted.clear();
38 | }
39 |
40 | private static EMPTY_SET: Set = new Set();
41 | }
42 |
--------------------------------------------------------------------------------
/src/index/typings/indexable.ts:
--------------------------------------------------------------------------------
1 | import { Link } from "../expression/link";
2 | // import { DateTime } from "luxon";
3 |
4 | /** The names of all index fields that are present on ALL indexed types. */
5 | export const INDEX_FIELDS = new Set([
6 | "$types",
7 | // "$typename",
8 | "$id",
9 | "$revision"
10 | ]);
11 |
12 | /** Any indexable field, which must have a few index-relevant properties. */
13 | export interface Indexable {
14 | /** The object types that this indexable is. */
15 | $types: string[];
16 | // /** Textual description of the object, such as `Page` or `Section`. Used in visualizations. */
17 | // $typename: string;
18 | /** The unique index ID for this object. */
19 | $id: string;
20 | /**
21 | * The indexable object that is the parent of this object. Only set after the object is actually indexed.
22 | */
23 | $parent?: Indexable;
24 | /** If present, the revision in the index of this object. */
25 | $revision?: number;
26 | /** The file that this indexable was derived from, if file-backed. */
27 | $file?: string;
28 | }
29 |
30 | /** Metadata for objects which support linking. */
31 | export const LINKABLE_TYPE = "linkable";
32 | export interface Linkable {
33 | /** A link to this linkable object. */
34 | $link: Link;
35 | }
36 |
37 | /** General metadata for any file. */
38 | export const FILE_TYPE = "file";
39 | export interface File extends Linkable {
40 | /** The path this file exists at. */
41 | $path: string;
42 | // /** Obsidian-provided date this page was created. */
43 | // $ctime: DateTime;
44 | // /** Obsidian-provided date this page was modified. */
45 | // $mtime: DateTime;
46 | // /** Obsidian-provided size of this page in bytes. */
47 | // $size: number;
48 | /** The extension of the file. */
49 | $extension: string;
50 | }
51 |
52 | // /** Metadata for taggable objects. */
53 | // export const TAGGABLE_TYPE = "taggable";
54 | // export interface Taggable {
55 | // /** The exact tags on this object. (#a/b/c or #foo/bar). */
56 | // $tags: string[];
57 | // }
58 |
59 | /** Metadata for objects which can link to other things. */
60 | export const LINKBEARING_TYPE = "links";
61 | export interface Linkbearing {
62 | /** The links in this file. */
63 | $links: Link[];
64 | }
65 |
--------------------------------------------------------------------------------
/src/index/typings/json.ts:
--------------------------------------------------------------------------------
1 | //! Note: These are "serialization" types for metadata, which contain
2 | // the absolute minimum information needed to save and load data.
3 |
4 | import { Link } from "index/expression/literal";
5 | import { Pos } from "obsidian";
6 | import { MinimalTheoremCalloutSettings, TheoremCalloutSettings } from "settings/settings";
7 |
8 | /** A span of contiguous lines. */
9 | export interface LineSpan {
10 | /** The inclusive start line. */
11 | start: number;
12 | /** The inclusive end line. */
13 | end: number;
14 | }
15 |
16 | /** Stores just the minimal information needed to create a markdown file; used for saving and loading these files. */
17 | export interface JsonMarkdownPage {
18 | /** The path this file exists at. */
19 | $path: string;
20 | /** The extension; for markdown files, almost always '.md'. */
21 | $extension: string;
22 | /** The full extent of the file (start 0, end the number of lines in the file.) */
23 | $position: LineSpan;
24 | /** All links in the file. */
25 | $links: Link[];
26 | /**
27 | * All child markdown sections of this markdown file. The initial section before any content is special and is
28 | * named with the title of the file.
29 | */
30 | $sections: JsonMarkdownSection[];
31 | }
32 |
33 | export interface JsonMarkdownSection {
34 | /** The index of this section in the file. */
35 | $ordinal: number;
36 | /** The title of the section; the root (implicit) section will have the title of the page. */
37 | $title: string;
38 | /** The indentation level of the section (1 - 6). */
39 | $level: number;
40 | /** The span of lines indicating the position of the section. */
41 | $position: LineSpan;
42 | // /** All tags on the file. */
43 | // $tags: string[];
44 | /** All links in the file. */
45 | /** -> All links in the SECTION? */
46 | $links: Link[];
47 | /** All of the markdown blocks in this section. */
48 | $blocks: JsonMarkdownBlock[];
49 | }
50 |
51 | export interface JsonMarkdownBlock {
52 | /** The index of this block in the file. */
53 | $ordinal: number;
54 | /** The position/extent of the block. */
55 | $position: LineSpan;
56 | $pos: Pos;
57 | /** All links in the file. */
58 | $links: Link[];
59 | /** If present, the distinct block ID for this block. */
60 | $blockId?: string;
61 | /** The type of block - paragraph, list, and so on. */
62 | $type: string;
63 | }
64 |
65 | export interface JsonMathBlock extends JsonMarkdownBlock {
66 | $type: "theorem" | "equation";
67 | $label?: string;
68 | $display?: string;
69 | }
70 |
71 | export interface JsonTheoremCalloutBlock extends JsonMathBlock {
72 | $type: "theorem";
73 | $settings: MinimalTheoremCalloutSettings;
74 | $main: boolean;
75 | $v1: boolean;
76 | }
77 |
78 | export interface JsonEquationBlock extends JsonMathBlock {
79 | $type: "equation";
80 | $manualTag: string | null;
81 | $mathText: string;
82 | }
83 |
--------------------------------------------------------------------------------
/src/index/utils/deferred.ts:
--------------------------------------------------------------------------------
1 | /** A promise that can be resolved directly. */
2 | export type Deferred = Promise & {
3 | resolve: (value: T) => void;
4 | reject: (error: any) => void;
5 | };
6 |
7 | /** Create a new deferred object, which is a resolvable promise. */
8 | export function deferred(): Deferred {
9 | let resolve: (value: T) => void;
10 | let reject: (error: any) => void;
11 |
12 | const promise = new Promise((res, rej) => {
13 | resolve = res;
14 | reject = rej;
15 | });
16 |
17 | const deferred = promise as any as Deferred;
18 | deferred.resolve = resolve!;
19 | deferred.reject = reject!;
20 |
21 | return deferred;
22 | }
23 |
--------------------------------------------------------------------------------
/src/index/utils/normalizers.ts:
--------------------------------------------------------------------------------
1 | /** Get the "title" for a file, by stripping other parts of the path as well as the extension. */
2 | export function getFileTitle(path: string): string {
3 | if (path.includes("/")) path = path.substring(path.lastIndexOf("/") + 1);
4 | if (path.endsWith(".md")) path = path.substring(0, path.length - 3);
5 | return path;
6 | }
7 |
--------------------------------------------------------------------------------
/src/index/web-worker/importer.ts:
--------------------------------------------------------------------------------
1 | /** Controls and creates Dataview file importers, allowing for asynchronous loading and parsing of files. */
2 |
3 | import { Component, MetadataCache, TFile, Vault } from "obsidian";
4 | import LatexReferencer from 'main';
5 | import { Transferable } from "./transferable";
6 | import ImportWorker from "index/web-worker/importer.worker";
7 | import { ImportCommand } from "./message";
8 |
9 | /** Settings for throttling import. */
10 | export interface ImportThrottle {
11 | /** The number of workers to use for imports. */
12 | workers: number;
13 | /** A number between 0.1 and 1 which indicates total cpu utilization target; 0.1 means spend 10% of time */
14 | utilization: number;
15 | }
16 |
17 | /** Default throttle configuration. */
18 | export const DEFAULT_THROTTLE: ImportThrottle = {
19 | workers: 2,
20 | utilization: 0.75,
21 | };
22 |
23 | /** Multi-threaded file parser which debounces rapid file requests automatically. */
24 | export class MathImporter extends Component {
25 | /* Background workers which do the actual file parsing. */
26 | workers: Map;
27 | /** The next worker ID to hand out. */
28 | nextWorkerId: number;
29 | /** Is the importer active? */
30 | shutdown: boolean;
31 |
32 | /** List of files which have been queued for a reload. */
33 | queue: [TFile, (success: any) => void, (failure: any) => void][];
34 | /** Outstanding loads indexed by path. */
35 | outstanding: Map>;
36 | /** Throttle settings. */
37 | throttle: () => ImportThrottle;
38 |
39 | public constructor(public plugin: LatexReferencer, public vault: Vault, public metadataCache: MetadataCache, throttle?: () => ImportThrottle) {
40 | super();
41 | this.workers = new Map();
42 | this.shutdown = false;
43 | this.nextWorkerId = 0;
44 | this.throttle = throttle ?? (() => DEFAULT_THROTTLE);
45 |
46 | this.queue = [];
47 | this.outstanding = new Map();
48 | }
49 |
50 | /**
51 | * Queue the given file for importing. Multiple import requests for the same file in a short time period will be de-bounced
52 | * and all be resolved by a single actual file reload.
53 | */
54 | public import(file: TFile): Promise {
55 | // De-bounce repeated requests for the same file.
56 | let existing = this.outstanding.get(file.path);
57 | if (existing) return existing;
58 |
59 | let promise: Promise = new Promise((resolve, reject) => {
60 | this.queue.push([file, resolve, reject]);
61 | });
62 |
63 | this.outstanding.set(file.path, promise);
64 | this.schedule();
65 | return promise;
66 | }
67 |
68 | /** Reset any active throttles on the importer (such as if the utilization changes). */
69 | public unthrottle() {
70 | for (let worker of this.workers.values()) {
71 | worker.availableAt = Date.now();
72 | }
73 | }
74 |
75 | /** Poll from the queue and execute if there is an available worker. */
76 | private schedule() {
77 | if (this.queue.length == 0 || this.shutdown) return;
78 |
79 | const worker = this.availableWorker();
80 | if (!worker) return;
81 |
82 | const [file, resolve, reject] = this.queue.shift()!;
83 |
84 | worker.active = [file, resolve, reject, Date.now()];
85 | this.vault.cachedRead(file).then((c) =>
86 | worker!.worker.postMessage(
87 | Transferable.transferable({
88 | type: "markdown",
89 | path: file.path,
90 | contents: c,
91 | metadata: this.metadataCache.getFileCache(file),
92 | excludeExampleCallout: this.plugin.extraSettings.excludeExampleCallout,
93 | } as ImportCommand)
94 | )
95 | );
96 | }
97 |
98 | /** Finish the parsing of a file, potentially queueing a new file. */
99 | private finish(worker: PoolWorker, data: any) {
100 | let [file, resolve, reject] = worker.active!;
101 |
102 | // Resolve promises to let users know this file has finished.
103 | if ("$error" in data) reject(data["$error"]);
104 | else resolve(data);
105 |
106 | // Remove file from outstanding.
107 | this.outstanding.delete(file.path);
108 |
109 | // Remove this worker if we are over capacity.
110 | // Otherwise, notify the queue this file is available for new work.
111 | if (this.workers.size > this.throttle().workers) {
112 | this.workers.delete(worker.id);
113 | terminate(worker);
114 | } else {
115 | const now = Date.now();
116 | const start = worker.active![3];
117 | const throttle = Math.max(0.1, this.throttle().utilization) - 1.0;
118 | const delay = (now - start) * throttle;
119 |
120 | worker.active = undefined;
121 |
122 | if (delay <= 1e-10) {
123 | worker.availableAt = now;
124 | this.schedule();
125 | } else {
126 | worker.availableAt = now + delay;
127 |
128 | // Note: I'm pretty sure this will garauntee that this executes AFTER delay milliseconds,
129 | // so this should be fine; if it's not, we'll have to swap to an external timeout loop
130 | // which infinitely reschedules itself to the next available execution time.
131 | setTimeout(this.schedule.bind(this), delay);
132 | }
133 | }
134 | }
135 |
136 | /** Obtain an available worker, returning undefined if one does not exist. */
137 | private availableWorker(): PoolWorker | undefined {
138 | const now = Date.now();
139 | for (let worker of this.workers.values()) {
140 | if (!worker.active && worker.availableAt <= now) {
141 | return worker;
142 | }
143 | }
144 |
145 | // Make a new worker if we can.
146 | if (this.workers.size < this.throttle().workers) {
147 | let worker = this.newWorker();
148 | this.workers.set(worker.id, worker);
149 | return worker;
150 | }
151 |
152 | return undefined;
153 | }
154 |
155 | /** Create a new worker bound to this importer. */
156 | private newWorker(): PoolWorker {
157 | let worker: PoolWorker = {
158 | id: this.nextWorkerId++,
159 | availableAt: Date.now(),
160 | worker: new ImportWorker(),
161 | };
162 |
163 | worker.worker.onmessage = (evt) => this.finish(worker, Transferable.value(evt.data));
164 | return worker;
165 | }
166 |
167 | /** Reject all outstanding promises and close all workers on close. */
168 | public onunload(): void {
169 | for (let worker of this.workers.values()) {
170 | terminate(worker);
171 | }
172 |
173 | for (let [_file, _success, reject] of this.queue) {
174 | reject("Terminated");
175 | }
176 |
177 | this.shutdown = true;
178 | }
179 | }
180 |
181 | /** A worker in the pool of executing workers. */
182 | interface PoolWorker {
183 | /** The id of this worker. */
184 | id: number;
185 | /** The raw underlying worker. */
186 | worker: Worker;
187 | /** UNIX time indicating the next time this worker is available for execution according to target utilization. */
188 | availableAt: number;
189 | /** The active promise this worker is working on, if any. */
190 | active?: [TFile, (success: any) => void, (failure: any) => void, number];
191 | }
192 |
193 | /** Terminate a pool worker. */
194 | function terminate(worker: PoolWorker) {
195 | worker.worker.terminate();
196 |
197 | if (worker.active) worker.active[2]("Terminated");
198 | worker.active = undefined;
199 | }
200 |
--------------------------------------------------------------------------------
/src/index/web-worker/importer.worker.ts:
--------------------------------------------------------------------------------
1 | import { markdownImport } from "index/import/markdown";
2 | import { ImportCommand, MarkdownImportResult } from "index/web-worker/message";
3 | import { Transferable } from "index/web-worker/transferable";
4 |
5 | /** Web worker entry point for importing. */
6 | onmessage = (event) => {
7 | try {
8 | const message = Transferable.value(event.data) as ImportCommand;
9 |
10 | if (message.type === "markdown") {
11 | const markdown = markdownImport(message.path, message.contents, message.metadata, message.excludeExampleCallout);
12 |
13 | postMessage(
14 | Transferable.transferable({
15 | type: "markdown",
16 | result: markdown,
17 | } as MarkdownImportResult)
18 | );
19 | } else {
20 | postMessage({ $error: "Unsupported import method." });
21 | }
22 | } catch (error) {
23 | postMessage({ $error: error.message });
24 | }
25 | };
26 |
--------------------------------------------------------------------------------
/src/index/web-worker/message.ts:
--------------------------------------------------------------------------------
1 | import { JsonMarkdownPage } from "index/typings/json";
2 | import { CachedMetadata, FileStats } from "obsidian";
3 |
4 | /** A command to import a markdown file. */
5 | export interface MarkdownImport {
6 | type: "markdown";
7 |
8 | /** The path we are importing. */
9 | path: string;
10 | /** The file contents to import. */
11 | contents: string;
12 | // /** The stats for the file. */
13 | // stat: FileStats;
14 | /** Metadata for the file. */
15 | metadata: CachedMetadata;
16 | excludeExampleCallout: boolean;
17 | }
18 |
19 |
20 | /** Available import commands to be sent to an import web worker. */
21 | export type ImportCommand = MarkdownImport; //| CanvasImport;
22 |
23 | /** The result of importing a file of some variety. */
24 | export interface MarkdownImportResult {
25 | /** The type of import. */
26 | type: "markdown";
27 | /** The result of importing. */
28 | result: JsonMarkdownPage;
29 | }
30 |
31 | export interface ImportFailure {
32 | /** Failed to import. */
33 | type: "error";
34 |
35 | /** The error that the worker indicated on failure. */
36 | $error: string;
37 | }
38 |
39 | export type ImportResult = MarkdownImportResult | ImportFailure;
40 |
--------------------------------------------------------------------------------
/src/index/web-worker/transferable.ts:
--------------------------------------------------------------------------------
1 | import { Link, Literals } from "index/expression/literal";
2 | // import { DateTime, Duration, SystemZone } from "luxon";
3 |
4 | /** Simplifies passing complex values across the JS web worker barrier. */
5 | export namespace Transferable {
6 | /** Convert a literal value to a serializer-friendly transferable value. */
7 | export function transferable(value: any): any {
8 | // Handle simple universal types first.
9 | if (value instanceof Map) {
10 | let copied = new Map();
11 | for (let [key, val] of value.entries()) copied.set(transferable(key), transferable(val));
12 | return copied;
13 | } else if (value instanceof Set) {
14 | let copied = new Set();
15 | for (let val of value) copied.add(transferable(val));
16 | return copied;
17 | }
18 |
19 | let wrapped = Literals.wrapValue(value);
20 | if (wrapped === undefined) throw Error("Unrecognized transferable value: " + value);
21 |
22 | switch (wrapped.type) {
23 | case "null":
24 | case "number":
25 | case "string":
26 | case "boolean":
27 | return wrapped.value;
28 | // case "date":
29 | // return {
30 | // "$transfer-type": "date",
31 | // value: transferable(wrapped.value.toObject()),
32 | // options: {
33 | // zone: wrapped.value.zone.equals(SystemZone.instance) ? undefined : wrapped.value.zoneName,
34 | // },
35 | // };
36 | // case "duration":
37 | // return {
38 | // "$transfer-type": "duration",
39 | // value: transferable(wrapped.value.toObject()),
40 | // };
41 | case "array":
42 | return wrapped.value.map((v) => transferable(v));
43 | case "link":
44 | return {
45 | "$transfer-type": "link",
46 | value: transferable(wrapped.value.toObject()),
47 | };
48 | case "object":
49 | let result: Record = {};
50 |
51 | // Only copy owned properties, and not derived/readonly properties like getters/computed fields.
52 | for (let key of Object.getOwnPropertyNames(wrapped.value))
53 | result[key] = transferable(wrapped.value[key]);
54 | return result;
55 | }
56 | }
57 |
58 | /** Convert a transferable value back to a literal value we can work with. */
59 | export function value(transferable: any): any {
60 | if (transferable === null) {
61 | return null;
62 | } else if (transferable === undefined) {
63 | return undefined;
64 | } else if (transferable instanceof Map) {
65 | let real = new Map();
66 | for (let [key, val] of transferable.entries()) real.set(value(key), value(val));
67 | return real;
68 | } else if (transferable instanceof Set) {
69 | let real = new Set();
70 | for (let val of transferable) real.add(value(val));
71 | return real;
72 | } else if (Array.isArray(transferable)) {
73 | return transferable.map((v) => value(v));
74 | } else if (typeof transferable === "object") {
75 | if ("$transfer-type" in transferable) {
76 | switch (transferable["$transfer-type"]) {
77 | // case "date":
78 | // let dateOpts = value(transferable.options);
79 | // let dateData = value(transferable.value) as any;
80 |
81 | // return DateTime.fromObject(dateData, { zone: dateOpts.zone });
82 | // case "duration":
83 | // return Duration.fromObject(value(transferable.value));
84 | case "link":
85 | return Link.fromObject(value(transferable.value));
86 | default:
87 | throw Error(`Unrecognized transfer type '${transferable["$transfer-type"]}'`);
88 | }
89 | }
90 |
91 | let result: Record = {};
92 | for (let [key, val] of Object.entries(transferable)) result[key] = value(val);
93 | return result;
94 | }
95 |
96 | return transferable;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/patches/link-completion.ts:
--------------------------------------------------------------------------------
1 | import { EditorSuggest, Notice, TFile, renderMath } from "obsidian";
2 | import { around } from "monkey-around";
3 |
4 | import LatexReferencer from "main";
5 | import { MarkdownPage, TheoremCalloutBlock } from 'index/typings/markdown';
6 | import { isTheoremCallout, resolveSettings } from 'utils/plugin';
7 | import { formatTitle } from 'utils/format';
8 | import { _readTheoremCalloutSettings } from 'utils/parse';
9 | import { capitalize } from 'utils/general';
10 | import { renderTextWithMath } from "utils/render";
11 |
12 | export const patchLinkCompletion = (plugin: LatexReferencer) => {
13 | const suggest = (plugin.app.workspace as any).editorSuggest.suggests[0]; // built-in link completion
14 | if (!Object.hasOwn(suggest, 'suggestManager')) new Notice(`Failed to patch Obsidian\'s built-in link completion. Please reload ${plugin.manifest.name}.`);
15 | const prototype = suggest.constructor.prototype as EditorSuggest;
16 |
17 | plugin.register(around(prototype, {
18 | renderSuggestion(old) {
19 | return function (item: any, el: HTMLElement) {
20 | old.call(this, item, el);
21 |
22 | if (plugin.extraSettings.showTheoremTitleinBuiltin && item.type === 'block' && item.node.type === 'callout' && isTheoremCallout(plugin, item.node.callout.type)) {
23 | let title: string = item.node.children.find((child: any) => child.type === 'callout-title')?.children.map((child: any) => child.value).join('') ?? '';
24 | const content = item.display.slice(title.length);
25 | const page = plugin.indexManager.index.load(item.file.path);
26 | if (MarkdownPage.isMarkdownPage(page)) {
27 | const block = page.getBlockByLineNumber(item.node.position.start.line - 1); // line number starts from 1
28 | if (TheoremCalloutBlock.isTheoremCalloutBlock(block)) {
29 | renderInSuggestionTitleEl(el, (suggestionTitleEl) => {
30 | el.addClass('math-booster', 'suggestion-item-theorem-callout');
31 | suggestionTitleEl.replaceChildren();
32 | const children = renderTextWithMath(block.$printName);
33 | suggestionTitleEl
34 | .createDiv()
35 | .replaceChildren(...children);
36 | if (plugin.extraSettings.showTheoremContentinBuiltin && content) suggestionTitleEl.createDiv({ text: content });
37 | });
38 | return;
39 | }
40 | }
41 | const parsed = _readTheoremCalloutSettings({ type: item.node.callout.type, metadata: item.node.callout.data }, plugin.extraSettings.excludeExampleCallout);
42 | if (parsed) {
43 | const { type, number } = parsed;
44 | if (title === capitalize(type)) title = '';
45 | const formattedTitle = formatTitle(plugin, item.file as TFile, resolveSettings({ type, number, title }, plugin, item.file as TFile), true);
46 | renderInSuggestionTitleEl(el, (suggestionTitleEl) => {
47 | el.addClass('math-booster', 'suggestion-item-theorem-callout');
48 | suggestionTitleEl.replaceChildren();
49 | const children = renderTextWithMath(formattedTitle);
50 | suggestionTitleEl
51 | .createDiv()
52 | .replaceChildren(...children);
53 | if (plugin.extraSettings.showTheoremContentinBuiltin && content) suggestionTitleEl.createDiv({ text: content });
54 | });
55 | return;
56 | }
57 | }
58 | }
59 | }
60 | }));
61 | };
62 |
63 |
64 | function renderInSuggestionTitleEl(el: HTMLElement, cb: (suggestionTitleEl: HTMLElement) => void) {
65 | // setTimeout(() => {
66 | const suggestionTitleEl = el.querySelector('.suggestion-title');
67 | if (suggestionTitleEl) cb(suggestionTitleEl);
68 | // });
69 | }
--------------------------------------------------------------------------------
/src/patches/page-preview.ts:
--------------------------------------------------------------------------------
1 | import { HoverParent } from 'obsidian';
2 | import { around } from 'monkey-around';
3 | import LatexReferencer from '../main';
4 |
5 | // Inspired by Hover Editor (https://github.com/nothingislost/obsidian-hover-editor/blob/c038424acb15c542f0ad5f901d74c75d4316f553/src/main.ts#L396)
6 |
7 | // Save the last linktext that triggered hover page preview in the plugin instance to display theorem/equation numbers in it
8 |
9 | export const patchPagePreview = (plugin: LatexReferencer) => {
10 | const { app } = plugin;
11 |
12 | plugin.register(
13 | // @ts-ignore
14 | around(app.internalPlugins.plugins['page-preview'].instance.constructor.prototype, {
15 | onLinkHover(old: Function) {
16 | return function (parent: HoverParent, targetEl: HTMLElement, linktext: string, ...args: unknown[]) {
17 | old.call(this, parent, targetEl, linktext, ...args);
18 | // Save the linktext in the plugin instance
19 | plugin.lastHoverLinktext = linktext;
20 | }
21 | }
22 | })
23 | );
24 | }
--------------------------------------------------------------------------------
/src/proof/common.ts:
--------------------------------------------------------------------------------
1 | import { Profile } from "settings/profile";
2 |
3 | export function makeProofClasses(which: "begin" | "end", profile: Profile) {
4 | return [
5 | "math-booster-" + which + "-proof", // deprecated
6 | "latex-referencer-" + which + "-proof",
7 | ...profile.meta.tags.map((tag) => "math-booster-" + which + "-proof-" + tag), // deprecated
8 | ...profile.meta.tags.map((tag) => "latex-referencer-" + which + "-proof-" + tag)
9 | ];
10 | }
11 |
12 | export function makeProofElement(which: "begin" | "end", profile: Profile) {
13 | return createSpan({
14 | text: profile.body.proof[which],
15 | cls: makeProofClasses(which, profile)
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/src/proof/live-preview.ts:
--------------------------------------------------------------------------------
1 | import { editorInfoField } from 'obsidian';
2 | import { RangeSetBuilder } from '@codemirror/state';
3 | import { Decoration, DecorationSet, EditorView, PluginValue, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view';
4 | import { SyntaxNodeRef } from '@lezer/common';
5 | import { syntaxTree } from '@codemirror/language';
6 |
7 | import LatexReferencer from 'main';
8 | import { nodeText, rangesHaveOverlap } from 'utils/editor';
9 | import { Profile } from 'settings/profile';
10 | import { renderMarkdown } from 'utils/render';
11 | import { resolveSettings } from 'utils/plugin';
12 | import { makeProofClasses, makeProofElement } from './common';
13 |
14 | export const INLINE_CODE = "inline-code";
15 | export const LINK_BEGIN = "formatting-link_formatting-link-start";
16 | export const LINK = "hmd-internal-link";
17 | export const LINK_END = "formatting-link_formatting-link-end";
18 |
19 |
20 | abstract class ProofWidget extends WidgetType {
21 | containerEl: HTMLElement | null;
22 |
23 | constructor(public plugin: LatexReferencer, public profile: Profile) {
24 | super();
25 | this.containerEl = null;
26 | }
27 |
28 | eq(other: EndProofWidget): boolean {
29 | return this.profile.id === other.profile.id;
30 | }
31 |
32 | toDOM(): HTMLElement {
33 | return this.containerEl ?? (this.containerEl = this.initDOM());
34 | }
35 |
36 | abstract initDOM(): HTMLElement;
37 |
38 | ignoreEvent(event: Event): boolean {
39 | // the DOM element won't respond to clicks without this
40 | return false;
41 | }
42 | }
43 |
44 | class BeginProofWidget extends ProofWidget {
45 | containerEl: HTMLElement | null;
46 |
47 | constructor(
48 | plugin: LatexReferencer, profile: Profile,
49 | public display: string | null,
50 | public linktext: string | null,
51 | public sourcePath: string
52 | ) {
53 | super(plugin, profile);
54 | }
55 |
56 | eq(other: BeginProofWidget): boolean {
57 | return this.profile.id === other.profile.id && this.display === other.display && this.linktext === other.linktext && this.sourcePath == other.sourcePath;
58 | }
59 |
60 | initDOM(): HTMLElement {
61 | let display = this.linktext
62 | ? `${this.profile.body.proof.linkedBeginPrefix} [[${this.linktext}]]${this.profile.body.proof.linkedBeginSuffix}`
63 | : this.display;
64 |
65 | if (display) {
66 | const el = createSpan({ cls: makeProofClasses("begin", this.profile) });
67 | BeginProofWidget.renderDisplay(el, display, this.sourcePath, this.plugin);
68 | return el;
69 | }
70 |
71 | return makeProofElement("begin", this.profile);
72 | }
73 |
74 | static async renderDisplay(el: HTMLElement, display: string, sourcePath: string, plugin: LatexReferencer) {
75 | const children = await renderMarkdown(display, sourcePath, plugin);
76 | if (children) {
77 | el.replaceChildren(...children);
78 | }
79 | }
80 | }
81 |
82 |
83 | class EndProofWidget extends ProofWidget {
84 | containerEl: HTMLElement | null;
85 |
86 | initDOM(): HTMLElement {
87 | return makeProofElement("end", this.profile);
88 | }
89 | }
90 |
91 |
92 | export interface ProofPosition {
93 | display?: string,
94 | linktext?: string,
95 | linknodes?: { linkBegin: SyntaxNodeRef, link: SyntaxNodeRef, linkEnd: SyntaxNodeRef },
96 | }
97 |
98 |
99 | export const createProofDecoration = (plugin: LatexReferencer) => ViewPlugin.fromClass(
100 | class implements PluginValue {
101 | decorations: DecorationSet;
102 |
103 | constructor(view: EditorView) {
104 | this.decorations = this.makeDeco(view);
105 | }
106 |
107 | update(update: ViewUpdate) {
108 | if (update.docChanged || update.viewportChanged || update.selectionSet) {
109 | if (update.view.composing) {
110 | this.decorations = this.decorations.map(update.changes); // User is using IME
111 | } else {
112 | this.decorations = this.makeDeco(update.view);
113 | }
114 | }
115 | }
116 |
117 | makeDeco(view: EditorView): DecorationSet {
118 | const { state } = view;
119 | const { app } = plugin;
120 | const tree = syntaxTree(state);
121 | const ranges = state.selection.ranges;
122 |
123 | const file = state.field(editorInfoField).file;
124 | const sourcePath = file?.path ?? "";
125 | const settings = resolveSettings(undefined, plugin, file ?? app.vault.getRoot());
126 | const profile = plugin.extraSettings.profiles[settings.profile];
127 |
128 | const builder = new RangeSetBuilder();
129 |
130 | for (const { from, to } of view.visibleRanges) {
131 | tree.iterate({
132 | from, to,
133 | enter(node) {
134 | if (node.name !== INLINE_CODE) return;
135 |
136 | let start = -1;
137 | let end = -1;
138 | let display: string | null = null;
139 | let linktext: string | null = null;
140 |
141 | const text = nodeText(node, state);
142 |
143 | if (text.startsWith(settings.beginProof)) {
144 | // handle "\begin{proof}"
145 | const rest = text.slice(settings.beginProof.length);
146 | if (!rest) { // only "\begin{proof}"
147 | start = node.from - 1;
148 | end = node.to + 1; // 1 = "`".length
149 | display = null;
150 | } else {
151 | const match = rest.match(/^\[(.*)\]$/);
152 | if (match) { // custom display text is given, e.g. "\begin{proof}[Solutions.]"
153 | start = node.from - 1;
154 | end = node.to + 1; // 1 = "`".length
155 | display = match[1];
156 | }
157 | }
158 |
159 | if (start === -1 || end === -1) return; // not proof
160 |
161 | if (state.sliceDoc(node.to + 1, node.to + 2) == "@") { // Check if "`\begin{proof}`@[[link]]" or "`\begin{proof}[display]`@[[link]]"
162 | const next = node.node.nextSibling?.nextSibling;
163 | const afterNext = node.node.nextSibling?.nextSibling?.nextSibling;
164 | const afterAfterNext = node.node.nextSibling?.nextSibling?.nextSibling?.nextSibling;
165 | if (next?.name === LINK_BEGIN && afterNext?.name === LINK && afterAfterNext?.name === LINK_END) {
166 | linktext = nodeText(afterNext, state);
167 | end = afterAfterNext.to;
168 | }
169 | }
170 |
171 | if (!rangesHaveOverlap(ranges, start, end)) {
172 | builder.add(
173 | start, end,
174 | Decoration.replace({
175 | widget: new BeginProofWidget(plugin, profile, display, linktext, sourcePath)
176 | })
177 | );
178 | }
179 |
180 | } else if (text === settings.endProof) {
181 | // handle "\end{proof}"
182 | start = node.from - 1;
183 | end = node.to + 1; // 1 = "`".length
184 |
185 | if (!rangesHaveOverlap(ranges, start, end)) {
186 | builder.add(
187 | start, end,
188 | Decoration.replace({
189 | widget: new EndProofWidget(plugin, profile)
190 | })
191 | );
192 | }
193 | }
194 | }
195 | });
196 | }
197 | return builder.finish();
198 | }
199 | }, {
200 | decorations: instance => instance.decorations
201 | });
202 |
203 | // export const proofFoldFactory = (plugin: LatexReferencer) => foldService.of((state: EditorState, lineStart: number, lineEnd: number) => {
204 | // const positions = state.field(plugin.proofPositionField);
205 | // for (const pos of positions) {
206 | // if (pos.begin && pos.end && lineStart <= pos.begin.from && pos.begin.to <= lineEnd) {
207 | // return { from: pos.begin.to, to: pos.end.to };
208 | // }
209 | // }
210 | // return null;
211 | // });
212 |
--------------------------------------------------------------------------------
/src/proof/reading-view.ts:
--------------------------------------------------------------------------------
1 | import LatexReferencer from "main";
2 | import { App, MarkdownPostProcessorContext, MarkdownRenderChild, TFile } from "obsidian";
3 | import { resolveSettings } from "utils/plugin";
4 | import { makeProofClasses, makeProofElement } from "./common";
5 | import { renderMarkdown } from "utils/render";
6 | import { Profile } from "settings/profile";
7 |
8 | export const createProofProcessor = (plugin: LatexReferencer) => (element: HTMLElement, context: MarkdownPostProcessorContext) => {
9 | if (!plugin.extraSettings.enableProof) return;
10 |
11 | const { app } = plugin;
12 |
13 | const file = app.vault.getAbstractFileByPath(context.sourcePath);
14 | if (!(file instanceof TFile)) return;
15 |
16 | const settings = resolveSettings(undefined, plugin, file);
17 | const codes = element.querySelectorAll("code");
18 | for (const code of codes) {
19 | const text = code.textContent;
20 | if (!text) continue;
21 |
22 | if (text.startsWith(settings.beginProof)) {
23 | const rest = text.slice(settings.beginProof.length);
24 | let displayMatch;
25 | if (!rest) {
26 | context.addChild(new ProofRenderer(app, plugin, code, "begin", file));
27 | } else if (displayMatch = rest.match(/^\[(.*)\]$/)) {
28 | const display = displayMatch[1];
29 | context.addChild(new ProofRenderer(app, plugin, code, "begin", file, display));
30 | }
31 | } else if (code.textContent == settings.endProof) {
32 | context.addChild(new ProofRenderer(app, plugin, code, "end", file));
33 | }
34 | }
35 | };
36 |
37 |
38 | function parseAtSignLink(codeEl: HTMLElement) {
39 | const next = codeEl.nextSibling;
40 | const afterNext = next?.nextSibling;
41 | const afterAfterNext = afterNext?.nextSibling;
42 | if (afterNext) {
43 | if (next.nodeType == Node.TEXT_NODE && next.textContent == "@"
44 | && afterNext instanceof HTMLElement && afterNext.matches("a.original-internal-link")
45 | && afterAfterNext instanceof HTMLElement && afterAfterNext.matches("a.mathLink-internal-link")) {
46 | return { atSign: next, links: [afterNext, afterAfterNext] };
47 | }
48 | }
49 | }
50 |
51 | export class ProofRenderer extends MarkdownRenderChild {
52 | atSignParseResult: { atSign: ChildNode, links: HTMLElement[] } | undefined;
53 |
54 | constructor(public app: App, public plugin: LatexReferencer, containerEl: HTMLElement, public which: "begin" | "end", public file: TFile, public display?: string) {
55 | super(containerEl);
56 | this.atSignParseResult = parseAtSignLink(this.containerEl);
57 | }
58 |
59 | onload(): void {
60 | this.update();
61 | this.registerEvent(
62 | this.plugin.indexManager.on("local-settings-updated", (file) => {
63 | if (file == this.file) {
64 | this.update();
65 | }
66 | })
67 | );
68 | this.registerEvent(
69 | this.plugin.indexManager.on("global-settings-updated", () => {
70 | this.update();
71 | })
72 | );
73 | }
74 |
75 | update(): void {
76 | const settings = resolveSettings(undefined, this.plugin, this.file);
77 | const profile = this.plugin.extraSettings.profiles[settings.profile];
78 |
79 | /**
80 | * `\begin{proof}`@[[]] => Proof of Theorem 1.
81 | */
82 | if (this.atSignParseResult) {
83 | const { atSign, links } = this.atSignParseResult;
84 | const newEl = createSpan({ cls: makeProofClasses(this.which, profile) });
85 | newEl.replaceChildren(profile.body.proof.linkedBeginPrefix, ...links, profile.body.proof.linkedBeginSuffix);
86 | this.containerEl.replaceWith(newEl);
87 | this.containerEl = newEl;
88 | atSign.textContent = "";
89 | return;
90 | }
91 |
92 | /**
93 | * `\begin{proof}[Foo.]` => Foo.
94 | */
95 | if (this.display) {
96 | this.renderDisplay(profile);
97 | return;
98 | }
99 |
100 | /**
101 | * `\begin{proof}` => Proof.
102 | */
103 | const newEl = makeProofElement(this.which, profile);
104 | this.containerEl.replaceWith(newEl);
105 | this.containerEl = newEl;
106 | }
107 |
108 | async renderDisplay(profile: Profile) {
109 | if (this.display) {
110 | const children = await renderMarkdown(this.display, this.file.path, this.plugin);
111 | if (children) {
112 | const el = createSpan({ cls: makeProofClasses(this.which, profile) });
113 | el.replaceChildren(...children);
114 | this.containerEl.replaceWith(el);
115 | this.containerEl = el;
116 | }
117 | }
118 | }
119 | }
120 |
121 |
--------------------------------------------------------------------------------
/src/search/editor-suggest.ts:
--------------------------------------------------------------------------------
1 | import { Editor, EditorPosition, EditorSuggest, EditorSuggestContext, EditorSuggestTriggerInfo, Keymap, UserEvent } from "obsidian";
2 |
3 | import LatexReferencer from "main";
4 | import { MathBlock } from "index/typings/markdown";
5 | import { MathSearchCore, SuggestParent, WholeVaultTheoremEquationSearchCore } from "./core";
6 | import { QueryType, SearchRange } from './core';
7 |
8 |
9 | export class LinkAutocomplete extends EditorSuggest implements SuggestParent {
10 | queryType: QueryType;
11 | range: SearchRange;
12 | core: MathSearchCore;
13 | triggers: Map;
14 |
15 | /**
16 | * @param type The type of the block to search for. See: index/typings/markdown.ts
17 | */
18 | constructor(public plugin: LatexReferencer) {
19 | super(plugin.app);
20 | this.setTriggers();
21 | this.core = new WholeVaultTheoremEquationSearchCore(this);
22 | this.core.setScope();
23 | }
24 |
25 | get dvQuery(): string {
26 | return this.plugin.extraSettings.autocompleteDvQuery;
27 | }
28 |
29 | getContext() {
30 | return this.context;
31 | }
32 |
33 | getSelectedItem() {
34 | // Reference: https://github.com/tadashi-aikawa/obsidian-various-complements-plugin/blob/be4a12c3f861c31f2be3c0f81809cfc5ab6bb5fd/src/ui/AutoCompleteSuggest.ts#L595-L619
35 | return this.suggestions.values[this.suggestions.selectedItem];
36 | }
37 |
38 | onTrigger(cursor: EditorPosition, editor: Editor): EditorSuggestTriggerInfo | null {
39 | for (const [trigger, { range, queryType }] of this.triggers) {
40 | const text = editor.getLine(cursor.line);
41 | const index = text.lastIndexOf(trigger);
42 | if (index >= 0) {
43 | const query = text.slice(index + trigger.length);
44 | if (query.startsWith("[[")) return null; // avoid conflict with the built-in link auto-completion
45 | this.queryType = queryType;
46 | this.range = range;
47 | const core = MathSearchCore.getCore(this);
48 | if (!core) return null;
49 | this.core = core;
50 | this.limit = this.plugin.extraSettings.suggestNumber;
51 | return {
52 | start: { line: cursor.line, ch: index },
53 | end: cursor,
54 | query
55 | };
56 | }
57 | }
58 |
59 | return null;
60 | }
61 |
62 | getSuggestions(context: EditorSuggestContext): Promise {
63 | return this.core.getSuggestions(context.query);
64 | }
65 |
66 | renderSuggestion(block: MathBlock, el: HTMLElement): void {
67 | this.core.renderSuggestion(block, el);
68 | }
69 |
70 | selectSuggestion(item: MathBlock, evt: MouseEvent | KeyboardEvent): void {
71 | this.core.selectSuggestion(item, evt);
72 | }
73 |
74 | setTriggers() {
75 | const unsortedTriggers = {} as Record;
76 | if (this.plugin.extraSettings.enableSuggest) {
77 | unsortedTriggers[this.plugin.extraSettings.triggerSuggest] = { range: "vault", queryType: "both" };
78 | }
79 | if (this.plugin.extraSettings.enableTheoremSuggest) {
80 | unsortedTriggers[this.plugin.extraSettings.triggerTheoremSuggest] = { range: "vault", queryType: "theorem" };
81 | }
82 | if (this.plugin.extraSettings.enableEquationSuggest) {
83 | unsortedTriggers[this.plugin.extraSettings.triggerEquationSuggest] = { range: "vault", queryType: "equation" };
84 | }
85 | if (this.plugin.extraSettings.enableSuggestRecentNotes) {
86 | unsortedTriggers[this.plugin.extraSettings.triggerSuggestRecentNotes] = { range: "recent", queryType: "both" };
87 | }
88 | if (this.plugin.extraSettings.enableTheoremSuggestRecentNotes) {
89 | unsortedTriggers[this.plugin.extraSettings.triggerTheoremSuggestRecentNotes] = { range: "recent", queryType: "theorem" };
90 | }
91 | if (this.plugin.extraSettings.enableEquationSuggestRecentNotes) {
92 | unsortedTriggers[this.plugin.extraSettings.triggerEquationSuggestRecentNotes] = { range: "recent", queryType: "equation" };
93 | }
94 | if (this.plugin.extraSettings.enableSuggestActiveNote) {
95 | unsortedTriggers[this.plugin.extraSettings.triggerSuggestActiveNote] = { range: "active", queryType: "both" };
96 | }
97 | if (this.plugin.extraSettings.enableTheoremSuggestActiveNote) {
98 | unsortedTriggers[this.plugin.extraSettings.triggerTheoremSuggestActiveNote] = { range: "active", queryType: "theorem" };
99 | }
100 | if (this.plugin.extraSettings.enableEquationSuggestActiveNote) {
101 | unsortedTriggers[this.plugin.extraSettings.triggerEquationSuggestActiveNote] = { range: "active", queryType: "equation" };
102 | }
103 | if (this.plugin.extraSettings.enableSuggestDataview) {
104 | unsortedTriggers[this.plugin.extraSettings.triggerSuggestDataview] = { range: "dataview", queryType: "both" };
105 | }
106 | if (this.plugin.extraSettings.enableTheoremSuggestDataview) {
107 | unsortedTriggers[this.plugin.extraSettings.triggerTheoremSuggestDataview] = { range: "dataview", queryType: "theorem" };
108 | }
109 | if (this.plugin.extraSettings.enableEquationSuggestDataview) {
110 | unsortedTriggers[this.plugin.extraSettings.triggerEquationSuggestDataview] = { range: "dataview", queryType: "equation" };
111 | }
112 | const sortedTriggers = new Map;
113 |
114 | Object.entries(unsortedTriggers)
115 | .sort((a, b) => b[0].length - a[0].length) // sort by descending order of trigger length
116 | .forEach(([k, v]) => sortedTriggers.set(k, v));
117 |
118 | this.triggers = sortedTriggers;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/search/modal.ts:
--------------------------------------------------------------------------------
1 | import { DataviewQuerySearchCore, QueryType, SearchRange, WholeVaultTheoremEquationSearchCore } from 'search/core';
2 |
3 | import LatexReferencer from "main";
4 | import { App, EditorSuggestContext, MarkdownView, Setting, SuggestModal, TextAreaComponent } from "obsidian";
5 | import { MathSearchCore, SuggestParent } from "./core";
6 | import { MathBlock } from "index/typings/markdown";
7 |
8 | export class MathSearchModal extends SuggestModal implements SuggestParent {
9 | app: App;
10 | core: MathSearchCore;
11 | queryType: QueryType;
12 | range: SearchRange;
13 | dvQueryField: Setting;
14 | topEl: HTMLElement;
15 |
16 | constructor(public plugin: LatexReferencer) {
17 | super(plugin.app);
18 | this.app = plugin.app;
19 | this.core = new WholeVaultTheoremEquationSearchCore(this);
20 | this.core.setScope();
21 | this.setPlaceholder('Type here...');
22 |
23 | this.queryType = this.plugin.extraSettings.searchModalQueryType;
24 | this.range = this.plugin.extraSettings.searchModalRange;
25 |
26 | this.topEl = this.modalEl.createDiv({ cls: 'math-booster-modal-top' });
27 | this.modalEl.insertBefore(this.topEl, this.modalEl.firstChild)
28 | this.inputEl.addClass('math-booster-search-input');
29 |
30 | this.limit = this.plugin.extraSettings.suggestNumber;
31 |
32 | new Setting(this.topEl)
33 | .setName('Query type')
34 | .addDropdown((dropdown) => {
35 | dropdown.addOption('both', 'Theorems and equations')
36 | .addOption('theorem', 'Theorems')
37 | .addOption('equation', 'Equations');
38 |
39 | // recover the last state
40 | dropdown.setValue(this.plugin.extraSettings.searchModalQueryType)
41 |
42 | dropdown.onChange((value: QueryType) => {
43 | this.queryType = value;
44 | this.resetCore();
45 | // @ts-ignore
46 | this.onInput();
47 |
48 | // remember the last state
49 | this.plugin.extraSettings.searchModalQueryType = value;
50 | this.plugin.saveSettings();
51 | })
52 | });
53 |
54 | new Setting(this.topEl)
55 | .setName('Search range')
56 | .addDropdown((dropdown) => {
57 | dropdown.addOption('vault', 'Vault')
58 | .addOption('recent', 'Recent notes')
59 | .addOption('active', 'Active note')
60 | .addOption('dataview', 'Dataview query');
61 |
62 | // recover the last state
63 | dropdown.setValue(this.plugin.extraSettings.searchModalRange)
64 | dropdown.onChange((value: SearchRange) => {
65 | this.range = value;
66 | this.resetCore();
67 | // @ts-ignore
68 | this.onInput();
69 |
70 | // remember the last state
71 | this.plugin.extraSettings.searchModalRange = value;
72 | this.plugin.saveSettings();
73 | })
74 | });
75 |
76 | this.dvQueryField = new Setting(this.topEl)
77 | .setName('Dataview query')
78 | .setDesc('Only LIST queries are supported.')
79 | .then(setting => {
80 | setting.controlEl.style.width = '60%';
81 | })
82 | .addTextArea((text) => {
83 | text.inputEl.addClass('math-booster-dv-query')
84 | text.inputEl.style.width = '100%';
85 |
86 | text.setValue(this.plugin.extraSettings.searchModalDvQuery) // recover the last state
87 | .setPlaceholder('LIST ...')
88 | .onChange((dvQuery) => {
89 | if (this.core instanceof DataviewQuerySearchCore) {
90 | this.core.dvQuery = dvQuery;
91 | // @ts-ignore
92 | this.onInput();
93 |
94 | // remember the last state
95 | this.plugin.extraSettings.searchModalDvQuery = dvQuery;
96 | this.plugin.saveSettings();
97 | }
98 | })
99 | });
100 |
101 | this.modalEl.insertBefore(this.inputEl, this.modalEl.firstChild);
102 |
103 | this.resetCore();
104 | }
105 |
106 | resetCore() {
107 | const core = MathSearchCore.getCore(this);
108 | if (core) this.core = core;
109 | if (this.range === 'dataview') {
110 | if (!core) {
111 | this.dvQueryField.setDisabled(true);
112 | this.dvQueryField.setDesc('Retry after enabling Dataview.')
113 | .then(setting => setting.descEl.style.color = '#ea5555');
114 | }
115 | this.dvQueryField.settingEl.show();
116 | }
117 | else this.dvQueryField.settingEl.hide();
118 | }
119 |
120 | get dvQuery(): string {
121 | return (this.dvQueryField.components[0] as TextAreaComponent).getValue();
122 | }
123 |
124 | getContext(): Omit | null {
125 | const view = this.app.workspace.getActiveViewOfType(MarkdownView);
126 | if (!view?.file) return null;
127 |
128 | const start = view.editor.getCursor('from');
129 | const end = view.editor.getCursor('to');
130 |
131 | return { file: view.file, editor: view.editor, start, end }
132 | }
133 |
134 | getSelectedItem(): MathBlock {
135 | return this.chooser.values![this.chooser.selectedItem];
136 | };
137 |
138 | getSuggestions(query: string) {
139 | return this.core.getSuggestions(query);
140 | }
141 |
142 | renderSuggestion(value: MathBlock, el: HTMLElement) {
143 | this.core.renderSuggestion(value, el);
144 | }
145 |
146 | onChooseSuggestion(item: MathBlock, evt: MouseEvent | KeyboardEvent) {
147 | this.core.selectSuggestion(item, evt);
148 | }
149 | }
--------------------------------------------------------------------------------
/src/settings/modals.ts:
--------------------------------------------------------------------------------
1 | import { TAbstractFile, TFile, App, Modal, Setting, FuzzySuggestModal, TFolder, Component } from 'obsidian';
2 |
3 | import LatexReferencer from 'main';
4 | import { MathSettings, MathContextSettings, DEFAULT_SETTINGS, MinimalTheoremCalloutSettings } from 'settings/settings';
5 | import { MathSettingTab } from "settings/tab";
6 | import { TheoremCalloutSettingsHelper, MathContextSettingsHelper } from "settings/helper";
7 | import { isEqualToOrChildOf } from 'utils/obsidian';
8 | import { resolveSettings } from 'utils/plugin';
9 |
10 |
11 | abstract class MathSettingModal extends Modal {
12 | settings: SettingsType;
13 |
14 | constructor(
15 | app: App,
16 | public plugin: LatexReferencer,
17 | public callback?: (settings: SettingsType) => void,
18 | ) {
19 | super(app);
20 | }
21 |
22 | onClose(): void {
23 | this.contentEl.empty();
24 | }
25 |
26 | addButton(buttonText: string) {
27 | const { contentEl } = this;
28 | new Setting(contentEl)
29 | .addButton((btn) => {
30 | btn.setButtonText(buttonText)
31 | .setCta()
32 | .onClick(() => {
33 | this.close();
34 | if (this.callback) {
35 | this.callback(this.settings);
36 | }
37 | });
38 | btn.buttonEl.classList.add("insert-math-item-button");
39 | });
40 |
41 | const button = contentEl.querySelector(".insert-math-item-button");
42 | const settingTextboxes = contentEl.querySelectorAll("input");
43 | if (button) {
44 | settingTextboxes.forEach((textbox) => {
45 | /**
46 | * 'keypress' is deprecated, but here we need to use it.
47 | * 'keydown' causes a serious inconvenience at least for Japanese users.
48 | * See https://qiita.com/ledsun/items/31e43a97413dd3c8e38e
49 | */
50 | this.plugin.registerDomEvent(textbox, "keypress", (event) => {
51 | if (event.key === "Enter") {
52 | (button as HTMLElement).click();
53 | }
54 | });
55 | });
56 | }
57 | }
58 | }
59 |
60 |
61 | export class TheoremCalloutModal extends MathSettingModal {
62 | defaultSettings: Required;
63 |
64 | constructor(
65 | app: App,
66 | plugin: LatexReferencer,
67 | public file: TFile,
68 | callback: (settings: MathSettings) => void,
69 | public buttonText: string,
70 | public headerText: string,
71 | public currentCalloutSettings?: MinimalTheoremCalloutSettings,
72 | ) {
73 | super(app, plugin, callback);
74 | }
75 |
76 | resolveDefaultSettings(currentFile: TAbstractFile) {
77 | // The if statement is redundant, but probably necessary for the Typescript compiler to work
78 | if (this.currentCalloutSettings === undefined) {
79 | this.defaultSettings = resolveSettings(this.currentCalloutSettings, this.plugin, currentFile)
80 | } else {
81 | this.defaultSettings = resolveSettings(this.currentCalloutSettings, this.plugin, currentFile)
82 | }
83 | }
84 |
85 | onOpen(): void {
86 | this.settings = this.currentCalloutSettings ?? {} as MathSettings;
87 | const { contentEl } = this;
88 | contentEl.empty();
89 |
90 | this.resolveDefaultSettings(this.file);
91 |
92 | if (this.headerText) {
93 | // contentEl.createEl("h4", { text: this.headerText });
94 | this.titleEl.setText(this.headerText);
95 | }
96 |
97 | const helper = new TheoremCalloutSettingsHelper(contentEl, this.settings, this.defaultSettings, this.plugin, this.file);
98 | helper.makeSettingPane();
99 |
100 | new Setting(contentEl)
101 | .setName('Open local settings for the current note')
102 | .addButton((button) => {
103 | button.setButtonText("Open")
104 | .onClick(() => {
105 | const modal = new ContextSettingModal(
106 | this.app,
107 | this.plugin,
108 | this.file,
109 | undefined,
110 | this
111 | );
112 | modal.open();
113 | })
114 | });
115 |
116 | this.addButton(this.buttonText);
117 | }
118 | }
119 |
120 |
121 | export class ContextSettingModal extends MathSettingModal {
122 | component: Component;
123 |
124 | constructor(
125 | app: App,
126 | plugin: LatexReferencer,
127 | public file: TAbstractFile,
128 | callback?: (settings: MathContextSettings) => void,
129 | public parent?: TheoremCalloutModal | undefined
130 | ) {
131 | super(app, plugin, callback);
132 | this.component = new Component();
133 | }
134 |
135 | onOpen(): void {
136 | const { contentEl } = this;
137 | contentEl.empty();
138 | this.component.load();
139 |
140 | // contentEl.createEl('h4', { text: 'Local settings for ' + this.file.path });
141 | this.titleEl.setText('Local settings for ' + this.file.path);
142 |
143 | contentEl.createDiv({
144 | text: "If you want the change to apply to the entire vault, go to the plugin settings.",
145 | cls: ["setting-item-description", "math-booster-setting-item-description"],
146 | });
147 |
148 | if (this.plugin.settings[this.file.path] === undefined) {
149 | this.plugin.settings[this.file.path] = {} as MathContextSettings;
150 | }
151 |
152 | const defaultSettings = this.file.parent ? resolveSettings(undefined, this.plugin, this.file.parent) : DEFAULT_SETTINGS;
153 |
154 | const contextSettingsHelper = new MathContextSettingsHelper(contentEl, this.plugin.settings[this.file.path], defaultSettings, this.plugin, this.file);
155 | this.component.addChild(contextSettingsHelper);
156 |
157 | // if (!(this.file instanceof TFolder && this.file.isRoot())) {
158 | // new ProjectSettingsHelper(contentEl, this).makeSettingPane();
159 | // }
160 |
161 | this.addButton('Save');
162 | }
163 |
164 | onClose(): void {
165 | super.onClose();
166 |
167 | this.plugin.saveSettings();
168 | this.plugin.indexManager.trigger('local-settings-updated', this.file);
169 |
170 | if (this.parent) {
171 | this.parent.open();
172 | }
173 |
174 | this.component.unload();
175 | }
176 | }
177 |
178 |
179 | abstract class FileSuggestModal extends FuzzySuggestModal {
180 |
181 | constructor(app: App, public plugin: LatexReferencer) {
182 | super(app);
183 | }
184 |
185 | getItems(): TAbstractFile[] {
186 | return this.app.vault
187 | .getAllLoadedFiles()
188 | .filter(this.filterCallback.bind(this));
189 | }
190 |
191 | getItemText(file: TAbstractFile): string {
192 | return file.path;
193 | }
194 |
195 | filterCallback(abstractFile: TAbstractFile): boolean {
196 | if (abstractFile instanceof TFile && abstractFile.extension != 'md') {
197 | return false;
198 | }
199 | if (abstractFile instanceof TFolder && abstractFile.isRoot()) {
200 | return false;
201 | }
202 | for (const path of this.plugin.excludedFiles) {
203 | const file = this.app.vault.getAbstractFileByPath(path)
204 | if (file && isEqualToOrChildOf(abstractFile, file)) {
205 | return false
206 | }
207 | }
208 | return true;
209 | }
210 | }
211 |
212 |
213 |
214 | export class LocalContextSettingsSuggestModal extends FileSuggestModal {
215 | constructor(app: App, plugin: LatexReferencer, public settingTab: MathSettingTab) {
216 | super(app, plugin);
217 | }
218 |
219 | onChooseItem(file: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
220 | const modal = new ContextSettingModal(this.app, this.plugin, file);
221 | modal.open();
222 | }
223 | }
224 |
225 |
226 |
227 | export class FileExcludeSuggestModal extends FileSuggestModal {
228 | constructor(app: App, plugin: LatexReferencer, public manageModal: ExcludedFileManageModal) {
229 | super(app, plugin);
230 | }
231 |
232 | onChooseItem(file: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
233 | this.plugin.excludedFiles.push(file.path);
234 | this.manageModal.newDisplay();
235 | }
236 |
237 | filterCallback(abstractFile: TAbstractFile): boolean {
238 | for (const path in this.plugin.settings) {
239 | if (path == abstractFile.path) {
240 | return false;
241 | }
242 | }
243 | return super.filterCallback(abstractFile);
244 | }
245 | }
246 |
247 |
248 | export class ExcludedFileManageModal extends Modal {
249 | constructor(app: App, public plugin: LatexReferencer) {
250 | super(app);
251 | }
252 |
253 | onOpen() {
254 | this.newDisplay();
255 | }
256 |
257 | async newDisplay() {
258 | await this.plugin.saveSettings();
259 | const { contentEl } = this;
260 | contentEl.empty();
261 | contentEl.createEl('h3', { text: 'Excluded files/folders' });
262 |
263 | new Setting(contentEl)
264 | .setName('The files/folders in this list and their descendants will be excluded from suggestion for local settings.')
265 | .addButton((btn) => {
266 | btn.setIcon("plus")
267 | .onClick((event) => {
268 | new FileExcludeSuggestModal(this.app, this.plugin, this).open();
269 | });
270 | });
271 |
272 | if (this.plugin.excludedFiles.length) {
273 | const list = contentEl.createEl('ul');
274 | for (const path of this.plugin.excludedFiles) {
275 | const item = list.createEl('li').createDiv();
276 | new Setting(item).setName(path).addExtraButton((btn) => {
277 | btn.setIcon('x').onClick(async () => {
278 | this.plugin.excludedFiles.remove(path);
279 | this.newDisplay();
280 | await this.plugin.saveSettings();
281 | });
282 | });
283 | }
284 | }
285 |
286 | }
287 |
288 | onClose() {
289 | const { contentEl } = this;
290 | contentEl.empty();
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/src/settings/settings.ts:
--------------------------------------------------------------------------------
1 | import { Modifier } from "obsidian";
2 |
3 | import { DEFAULT_PROFILES, Profile } from "./profile";
4 | import { LeafArgs } from "../typings/type";
5 | import { QueryType, SearchRange } from "search/core";
6 |
7 | // Types
8 |
9 | export const NUMBER_STYLES = [
10 | "arabic",
11 | "alph",
12 | "Alph",
13 | "roman",
14 | "Roman"
15 | ] as const;
16 | export type NumberStyle = typeof NUMBER_STYLES[number];
17 |
18 | export const THEOREM_CALLOUT_STYLES = [
19 | "Custom",
20 | "Plain",
21 | "Framed",
22 | "MathWiki",
23 | "Vivid",
24 | ] as const;
25 | export type TheoremCalloutStyle = typeof THEOREM_CALLOUT_STYLES[number];
26 |
27 | export const THEOREM_REF_FORMATS = [
28 | "[type] [number] ([title])",
29 | "[type] [number]",
30 | "[title] ([type] [number]) if title exists, [type] [number] otherwise",
31 | "[title] if title exists, [type] [number] otherwise",
32 | ] as const;
33 | export type TheoremRefFormat = typeof THEOREM_REF_FORMATS[number];
34 |
35 | export const LEAF_OPTIONS = [
36 | "Current tab",
37 | "Split right",
38 | "Split down",
39 | "New tab",
40 | "New window",
41 | ] as const;
42 | export type LeafOption = typeof LEAF_OPTIONS[number];
43 | export const LEAF_OPTION_TO_ARGS: Record = {
44 | "Current tab": [false],
45 | "Split right": ["split", "vertical"],
46 | "Split down": ["split", "horizontal"],
47 | "New tab": ["tab"],
48 | "New window": ["window"],
49 | }
50 |
51 | export const SEARCH_METHODS = [
52 | "Fuzzy",
53 | "Simple"
54 | ] as const;
55 | export type SearchMethod = typeof SEARCH_METHODS[number];
56 |
57 |
58 | // Context settings are called "local settings" in the documentation and UI.
59 | // Sorry for confusion, this is due to a historical reason. I'll fix it later.
60 | export interface MathContextSettings {
61 | profile: string;
62 | titleSuffix: string;
63 | inferNumberPrefix: boolean;
64 | inferNumberPrefixFromProperty: string;
65 | inferNumberPrefixRegExp: string;
66 | numberPrefix: string;
67 | numberSuffix: string;
68 | numberInit: number;
69 | numberStyle: NumberStyle;
70 | numberDefault: string;
71 | refFormat: TheoremRefFormat;
72 | noteMathLinkFormat: TheoremRefFormat;
73 | ignoreMainTheoremCalloutWithoutTitle: boolean;
74 | numberOnlyReferencedEquations: boolean;
75 | inferEqNumberPrefix: boolean;
76 | inferEqNumberPrefixFromProperty: string;
77 | inferEqNumberPrefixRegExp: string;
78 | eqNumberPrefix: string;
79 | eqNumberSuffix: string;
80 | eqNumberInit: number;
81 | eqNumberStyle: NumberStyle;
82 | eqRefPrefix: string;
83 | eqRefSuffix: string;
84 | labelPrefix: string;
85 | lineByLine: boolean;
86 | theoremCalloutStyle: TheoremCalloutStyle;
87 | theoremCalloutFontInherit: boolean;
88 | beginProof: string;
89 | endProof: string;
90 | insertSpace: boolean;
91 | }
92 |
93 | export const UNION_TYPE_MATH_CONTEXT_SETTING_KEYS: {[k in keyof Partial]: readonly string[]} = {
94 | "numberStyle": NUMBER_STYLES,
95 | "refFormat": THEOREM_REF_FORMATS,
96 | "noteMathLinkFormat": THEOREM_REF_FORMATS,
97 | "eqNumberStyle": NUMBER_STYLES,
98 | "theoremCalloutStyle": THEOREM_CALLOUT_STYLES,
99 | };
100 |
101 | export type FoldOption = '' | '+' | '-';
102 |
103 | export type MinimalTheoremCalloutSettings = {
104 | type: string;
105 | number: string;
106 | title?: string;
107 | fold?: FoldOption;
108 | }
109 |
110 | export type TheoremCalloutSettings = MinimalTheoremCalloutSettings & {
111 | // type: string;
112 | // number: string;
113 | // title?: string;
114 | label?: string;
115 | // setAsNoteMathLink: boolean;
116 | }
117 |
118 | // legacy interface; currently only used temporarily when computing formatted theorem titles
119 | // TODO: refactor
120 | export interface TheoremCalloutPrivateFields {
121 | _index?: number;
122 | }
123 |
124 | export interface ImporterSettings {
125 | importerNumThreads: number;
126 | importerUtilization: number;
127 | }
128 |
129 | export type ExtraSettings = ImporterSettings & {
130 | foldDefault: FoldOption;
131 | noteTitleInTheoremLink: boolean;
132 | noteTitleInEquationLink: boolean;
133 | profiles: Record;
134 | showTheoremTitleinBuiltin: boolean;
135 | showTheoremContentinBuiltin: boolean;
136 | triggerSuggest: string;
137 | triggerTheoremSuggest: string;
138 | triggerEquationSuggest: string;
139 | triggerSuggestActiveNote: string;
140 | triggerTheoremSuggestActiveNote: string;
141 | triggerEquationSuggestActiveNote: string;
142 | triggerSuggestRecentNotes: string;
143 | triggerTheoremSuggestRecentNotes: string;
144 | triggerEquationSuggestRecentNotes: string;
145 | triggerSuggestDataview: string;
146 | triggerTheoremSuggestDataview: string;
147 | triggerEquationSuggestDataview: string;
148 | enableSuggest: boolean;
149 | enableTheoremSuggest: boolean;
150 | enableEquationSuggest: boolean;
151 | enableSuggestActiveNote: boolean;
152 | enableTheoremSuggestActiveNote: boolean;
153 | enableEquationSuggestActiveNote: boolean;
154 | enableSuggestRecentNotes: boolean;
155 | enableTheoremSuggestRecentNotes: boolean;
156 | enableEquationSuggestRecentNotes: boolean;
157 | enableSuggestDataview: boolean;
158 | enableTheoremSuggestDataview: boolean;
159 | enableEquationSuggestDataview: boolean;
160 | renderMathInSuggestion: boolean;
161 | suggestNumber: number;
162 | searchMethod: SearchMethod;
163 | upWeightRecent: number;
164 | searchLabel: boolean;
165 | modifierToJump: Modifier;
166 | modifierToNoteLink: Modifier;
167 | showModifierInstruction: boolean;
168 | suggestLeafOption: LeafOption;
169 | // projectInfix: string;
170 | // projectSep: string;
171 | showTheoremCalloutEditButton: boolean;
172 | setOnlyTheoremAsMain: boolean;
173 | setLabelInModal: boolean;
174 | excludeExampleCallout: boolean;
175 | enableProof: boolean;
176 | autocompleteDvQuery: string;
177 | // searchModal*: not congigurable from the setting tab, just remenbers the last state
178 | searchModalQueryType: QueryType;
179 | searchModalRange: SearchRange;
180 | searchModalDvQuery: string;
181 | }
182 |
183 | export const UNION_TYPE_EXTRA_SETTING_KEYS: {[k in keyof Partial]: readonly string[]} = {
184 | "searchMethod": SEARCH_METHODS,
185 | "suggestLeafOption": LEAF_OPTIONS,
186 | };
187 |
188 | export type MathSettings = Partial & TheoremCalloutSettings & TheoremCalloutPrivateFields;
189 | export type ResolvedMathSettings = Required & TheoremCalloutSettings & TheoremCalloutPrivateFields;
190 |
191 | export const DEFAULT_SETTINGS: Required = {
192 | profile: Object.keys(DEFAULT_PROFILES)[0],
193 | titleSuffix: ".",
194 | inferNumberPrefix: true,
195 | inferNumberPrefixFromProperty: "",
196 | inferNumberPrefixRegExp: "^[0-9]+(\\.[0-9]+)*",
197 | numberPrefix: "",
198 | numberSuffix: "",
199 | numberInit: 1,
200 | numberStyle: "arabic",
201 | numberDefault: "auto",
202 | refFormat: "[type] [number] ([title])",
203 | noteMathLinkFormat: "[title] if title exists, [type] [number] otherwise",
204 | ignoreMainTheoremCalloutWithoutTitle: false,
205 | numberOnlyReferencedEquations: true,
206 | inferEqNumberPrefix: true,
207 | inferEqNumberPrefixFromProperty: "",
208 | inferEqNumberPrefixRegExp: "^[0-9]+(\\.[0-9]+)*",
209 | eqNumberPrefix: "",
210 | eqNumberSuffix: "",
211 | eqNumberInit: 1,
212 | eqNumberStyle: "arabic",
213 | eqRefPrefix: "",
214 | eqRefSuffix: "",
215 | labelPrefix: "",
216 | lineByLine: true,
217 | theoremCalloutStyle: "Framed",
218 | theoremCalloutFontInherit: false,
219 | beginProof: "\\begin{proof}",
220 | endProof: "\\end{proof}",
221 | insertSpace: true,
222 | }
223 |
224 | export const DEFAULT_EXTRA_SETTINGS: Required = {
225 | foldDefault: '',
226 | noteTitleInTheoremLink: true,
227 | noteTitleInEquationLink: true,
228 | profiles: DEFAULT_PROFILES,
229 | showTheoremTitleinBuiltin: true,
230 | showTheoremContentinBuiltin: false,
231 | triggerSuggest: "\\ref",
232 | triggerTheoremSuggest: "\\tref",
233 | triggerEquationSuggest: "\\eqref",
234 | triggerSuggestActiveNote: "\\ref:a",
235 | triggerTheoremSuggestActiveNote: "\\tref:a",
236 | triggerEquationSuggestActiveNote: "\\eqref:a",
237 | triggerSuggestRecentNotes: "\\ref:r",
238 | triggerTheoremSuggestRecentNotes: "\\tref:r",
239 | triggerEquationSuggestRecentNotes: "\\eqref:r",
240 | triggerSuggestDataview: "\\ref:d",
241 | triggerTheoremSuggestDataview: "\\tref:d",
242 | triggerEquationSuggestDataview: "\\eqref:d",
243 | enableSuggest: true,
244 | enableTheoremSuggest: true,
245 | enableEquationSuggest: true,
246 | enableSuggestActiveNote: true,
247 | enableTheoremSuggestActiveNote: true,
248 | enableEquationSuggestActiveNote: true,
249 | enableSuggestRecentNotes: true,
250 | enableTheoremSuggestRecentNotes: true,
251 | enableEquationSuggestRecentNotes: true,
252 | enableSuggestDataview: true,
253 | enableTheoremSuggestDataview: true,
254 | enableEquationSuggestDataview: true,
255 | renderMathInSuggestion: true,
256 | suggestNumber: 20,
257 | searchMethod: "Fuzzy",
258 | upWeightRecent: 0.1,
259 | searchLabel: false,
260 | modifierToJump: "Mod",
261 | modifierToNoteLink: "Shift",
262 | showModifierInstruction: true,
263 | suggestLeafOption: "Current tab",
264 | // projectInfix: " > ",
265 | // projectSep: "/",
266 | importerNumThreads: 2,
267 | importerUtilization: 0.75,
268 | showTheoremCalloutEditButton: false,
269 | setOnlyTheoremAsMain: false,
270 | setLabelInModal: false,
271 | excludeExampleCallout: false,
272 | enableProof: true,
273 | autocompleteDvQuery: '',
274 | searchModalQueryType: 'both',
275 | searchModalRange: 'recent',
276 | searchModalDvQuery: '',
277 | };
278 |
--------------------------------------------------------------------------------
/src/settings/tab.ts:
--------------------------------------------------------------------------------
1 | import { App, Component, PluginSettingTab, Setting } from "obsidian";
2 |
3 | import LatexReferencer, { VAULT_ROOT } from "../main";
4 | import { DEFAULT_EXTRA_SETTINGS, DEFAULT_SETTINGS } from "./settings";
5 | import { ExtraSettingsHelper, MathContextSettingsHelper } from "./helper";
6 | import { ExcludedFileManageModal, LocalContextSettingsSuggestModal } from "settings/modals";
7 | // import { PROJECT_DESCRIPTION } from "project";
8 |
9 |
10 | export class MathSettingTab extends PluginSettingTab {
11 | component: Component;
12 |
13 | constructor(app: App, public plugin: LatexReferencer) {
14 | super(app, plugin);
15 | this.component = new Component();
16 | }
17 |
18 | addRestoreDefaultsButton() {
19 | new Setting(this.containerEl)
20 | .addButton((btn) => {
21 | btn.setButtonText("Restore defaults");
22 | btn.onClick(async () => {
23 | Object.assign(this.plugin.settings[VAULT_ROOT], DEFAULT_SETTINGS);
24 | Object.assign(this.plugin.extraSettings, DEFAULT_EXTRA_SETTINGS);
25 | this.display();
26 | })
27 | });
28 | }
29 |
30 | display() {
31 | const { containerEl } = this;
32 | containerEl.empty();
33 | this.component.load();
34 |
35 | containerEl.createEl("h4", { text: "Global" });
36 |
37 | const root = this.app.vault.getRoot();
38 | const globalHelper = new MathContextSettingsHelper(
39 | this.containerEl,
40 | this.plugin.settings[VAULT_ROOT],
41 | DEFAULT_SETTINGS,
42 | this.plugin,
43 | root
44 | );
45 | this.component.addChild(globalHelper);
46 |
47 | const extraHelper = new ExtraSettingsHelper(
48 | this.containerEl,
49 | this.plugin.extraSettings,
50 | this.plugin.extraSettings,
51 | this.plugin,
52 | false,
53 | false
54 | );
55 | this.component.addChild(extraHelper);
56 |
57 | const heading = extraHelper.addHeading('Equations - general');
58 | const numberingHeading = this.containerEl.querySelector('.equation-heading')!;
59 | this.containerEl.insertBefore(
60 | heading.settingEl,
61 | numberingHeading
62 | );
63 | this.containerEl.insertAfter(
64 | extraHelper.settingRefs.enableProof.settingEl,
65 | this.containerEl.querySelector('.proof-heading')!
66 | );
67 | this.containerEl.insertAfter(
68 | extraHelper.settingRefs.showTheoremCalloutEditButton.settingEl,
69 | globalHelper.settingRefs.profile.settingEl
70 | );
71 | this.containerEl.insertAfter(
72 | extraHelper.settingRefs.excludeExampleCallout.settingEl,
73 | globalHelper.settingRefs.profile.settingEl
74 | );
75 | this.containerEl.insertBefore(
76 | extraHelper.settingRefs.foldDefault.settingEl,
77 | globalHelper.settingRefs.labelPrefix.settingEl
78 | );
79 | this.containerEl.insertBefore(
80 | extraHelper.settingRefs.setOnlyTheoremAsMain.settingEl,
81 | globalHelper.settingRefs.labelPrefix.settingEl
82 | );
83 | this.containerEl.insertBefore(
84 | extraHelper.settingRefs.setLabelInModal.settingEl,
85 | globalHelper.settingRefs.labelPrefix.settingEl
86 | );
87 | this.containerEl.insertAfter(
88 | extraHelper.settingRefs.noteTitleInTheoremLink.settingEl,
89 | globalHelper.settingRefs.refFormat.settingEl
90 | );
91 | this.containerEl.insertAfter(
92 | extraHelper.settingRefs.noteTitleInEquationLink.settingEl,
93 | globalHelper.settingRefs.eqRefSuffix.settingEl
94 | );
95 |
96 | this.containerEl.insertBefore(
97 | globalHelper.settingRefs.insertSpace.settingEl,
98 | extraHelper.settingRefs.searchMethod.settingEl,
99 | );
100 |
101 | // const projectHeading = containerEl.createEl("h3", { text: "Projects (experimental)" });
102 | // const projectDesc = containerEl.createDiv({
103 | // text: PROJECT_DESCRIPTION,
104 | // cls: ["setting-item-description", "math-booster-setting-item-description"]
105 | // });
106 |
107 | // this.containerEl.insertBefore(
108 | // projectHeading,
109 | // extraHelper.settingRefs.projectInfix.settingEl
110 | // );
111 | // this.containerEl.insertAfter(
112 | // projectDesc,
113 | // projectHeading,
114 | // );
115 |
116 | this.addRestoreDefaultsButton();
117 |
118 | containerEl.createEl("h4", { text: "Local" });
119 | new Setting(containerEl).setName("Local settings")
120 | .setDesc("You can set up local (i.e. file-specific or folder-specific) settings, which have more precedence than the global settings. Local settings can be configured in various ways; here in the plugin settings, right-clicking in the file explorer, the \"Open local settings for the current file\" command, and the \"Open local settings for the current file\" button in the theorem callout settings pop-ups.")
121 | .addButton((btn) => {
122 | btn.setButtonText("Search files & folders")
123 | .onClick(() => {
124 | new LocalContextSettingsSuggestModal(this.app, this.plugin, this).open();
125 | });
126 | });
127 |
128 | new Setting(containerEl)
129 | .setName("Excluded files")
130 | .setDesc("You can make your search results more visible by excluding certain files or folders.")
131 | .addButton((btn) => {
132 | btn.setButtonText("Manage")
133 | .onClick(() => {
134 | new ExcludedFileManageModal(this.app, this.plugin).open();
135 | });
136 | });
137 | }
138 |
139 | async hide() {
140 | super.hide();
141 | await this.plugin.saveSettings();
142 | this.plugin.indexManager.trigger('global-settings-updated');
143 | this.plugin.updateLinkAutocomplete();
144 | this.component.unload();
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/theorem-callouts/state-field.ts:
--------------------------------------------------------------------------------
1 | import { StateField, EditorState, Transaction, RangeSet, RangeValue, Range, Text } from '@codemirror/state';
2 | import { ensureSyntaxTree, syntaxTree } from '@codemirror/language';
3 |
4 | import LatexReferencer from 'main';
5 | import { readTheoremCalloutSettings } from 'utils/parse';
6 | import { editorInfoField } from 'obsidian';
7 |
8 | export const CALLOUT = /HyperMD-callout_HyperMD-quote_HyperMD-quote-([1-9][0-9]*)/;
9 |
10 | export class TheoremCalloutInfo extends RangeValue {
11 | constructor(public index: number | null) {
12 | super();
13 | }
14 | }
15 |
16 |
17 | export const createTheoremCalloutsField = (plugin: LatexReferencer) => StateField.define>({
18 | create(state: EditorState) {
19 | // Since because canvas files cannot be indexed currently,
20 | // do not number theorems in canvas to make live preview consistent with reading view
21 | if (!state.field(editorInfoField).file) return RangeSet.empty;
22 |
23 | const ranges = getTheoremCalloutInfos(plugin, state, state.doc, 0, 0);
24 | return RangeSet.of(ranges);
25 | },
26 | update(value: RangeSet, tr: Transaction) {
27 | // Since because canvas files cannot be indexed currently,
28 | // do not number theorems in canvas to make live preview consistent with reading view
29 | if (!tr.state.field(editorInfoField).file) return RangeSet.empty;
30 |
31 | // Because the field can be perfectly determined by the document content,
32 | // we don't need to update it when the document is not changed
33 | if (!tr.docChanged) return value;
34 |
35 | // In order to make the updates efficient, we only update the theorem callout infos that are affected by the changes,
36 | // that is, theorem callouts after the insertion point.
37 |
38 | // Here, we use tr.newDoc instead of tr.state.doc because "Contrary to .state.doc, accessing this won't force the entire new state to be computed right away" (from CM6 docs)
39 |
40 | let minChangedPosition = tr.newDoc.length - 1;
41 | const changeDesc = tr.changes.desc;
42 | changeDesc.iterChangedRanges((fromA, toA, fromB, toB) => {
43 | if (fromB < minChangedPosition) {
44 | minChangedPosition = fromB;
45 | }
46 | });
47 |
48 | value = value.map(changeDesc);
49 |
50 | let init = 0;
51 | value.between(0, minChangedPosition, (from, to, info) => {
52 | if (to < minChangedPosition && info.index !== null) init = info.index + 1;
53 | });
54 |
55 | const updatedRanges = getTheoremCalloutInfos(plugin, tr.state, tr.newDoc, minChangedPosition, init);
56 | return value.update({
57 | add: updatedRanges,
58 | filter: () => false,
59 | filterFrom: minChangedPosition,
60 | });
61 | }
62 | });
63 |
64 |
65 | function getTheoremCalloutInfos(plugin: LatexReferencer, state: EditorState, doc: Text, from: number, init: number): Range[] {
66 | const ranges: Range[] = [];
67 | // syntaxTree returns a potentially imcomplete tree (limited by viewport), so we need to ensure it's complete
68 | const tree = ensureSyntaxTree(state, doc.length) ?? syntaxTree(state);
69 |
70 | let theoremIndex = init; // incremented when a auto-numbered theorem is found
71 |
72 | tree.iterate({
73 | from, to: doc.length,
74 | enter(node) {
75 | if (node.name === 'Document') return; // skip the node for the entire document
76 |
77 | if (node.node.parent?.name !== 'Document') return false; // skip sub-nodes of a line
78 |
79 | const text = doc.sliceString(node.from, node.to);
80 |
81 | const match = node.name.match(CALLOUT);
82 | if (!match) return false;
83 |
84 | const settings = readTheoremCalloutSettings(text, plugin.extraSettings.excludeExampleCallout);
85 | if (!settings) return false;
86 |
87 | const value = new TheoremCalloutInfo(settings.number === 'auto' ? theoremIndex++ : null);
88 | const range = value.range(node.from, node.to);
89 | ranges.push(range);
90 |
91 | return false;
92 | }
93 | });
94 |
95 | return ranges;
96 | }
97 |
--------------------------------------------------------------------------------
/src/theorem-callouts/view-plugin.ts:
--------------------------------------------------------------------------------
1 | import { PluginValue, EditorView, ViewUpdate, ViewPlugin } from '@codemirror/view';
2 |
3 | import LatexReferencer from 'main';
4 | import { MathIndex } from 'index/math-index';
5 |
6 |
7 | export const createTheoremCalloutNumberingViewPlugin = (plugin: LatexReferencer) => ViewPlugin.fromClass(
8 | class implements PluginValue {
9 | index: MathIndex = plugin.indexManager.index;
10 |
11 | constructor(public view: EditorView) {
12 | // Wait until the initial rendering is done so that we can find the callout elements using qeurySelectorAll().
13 | setTimeout(() => this._update(view));
14 | }
15 |
16 | update(update: ViewUpdate) {
17 | this._update(update.view);
18 | }
19 |
20 | _update(view: EditorView) {
21 | const infos = view.state.field(plugin.theoremCalloutsField);
22 |
23 | for (const calloutEl of view.contentDOM.querySelectorAll('.callout.theorem-callout')) {
24 | const pos = view.posAtDOM(calloutEl);
25 | const iter = infos.iter(pos);
26 | if (iter.from !== pos) continue; // insertion or deletion occured before this callout, and the posAtDom is out-dated for some reasons: do not update the theorem number
27 |
28 | const index = iter.value?.index;
29 | if (typeof index === 'number') calloutEl.setAttribute('data-theorem-index', String(index));
30 | else calloutEl.removeAttribute('data-theorem-index');
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/src/typings/type.d.ts:
--------------------------------------------------------------------------------
1 | import { PaneType, SplitDirection } from "obsidian";
2 | import { EditorView } from "@codemirror/view";
3 |
4 | declare module "obsidian" {
5 | interface App {
6 | plugins: {
7 | enabledPlugins: Set;
8 | plugins: {
9 | [id: string]: any;
10 | };
11 | getPlugin: (id: string) => Plugin | null;
12 | };
13 | }
14 | interface Editor {
15 | cm?: EditorView;
16 | }
17 | // Reference: https://github.com/tadashi-aikawa/obsidian-various-complements-plugin/blob/be4a12c3f861c31f2be3c0f81809cfc5ab6bb5fd/src/ui/AutoCompleteSuggest.ts#L595-L619
18 | interface EditorSuggest {
19 | scope: Scope;
20 | suggestions: {
21 | selectedItem: number;
22 | values: T[];
23 | containerEl: HTMLElement;
24 | };
25 | suggestEl: HTMLElement;
26 | }
27 |
28 | // Reference: https://github.com/tadashi-aikawa/obsidian-another-quick-switcher/blob/6aa40a46fe817d25c11847a46ec6c765c742d629/src/ui/UnsafeModalInterface.ts#L5
29 | interface SuggestModal {
30 | chooser: {
31 | values: T[] | null;
32 | selectedItem: number;
33 | setSelectedItem(
34 | item: number,
35 | event?: KeyboardEvent,
36 | ): void;
37 | useSelectedItem(ev: Partial): void;
38 | suggestions: Element[];
39 | };
40 | }
41 | }
42 |
43 | export type LeafArgs = [newLeaf?: PaneType | boolean] | [newLeaf?: 'split', direction?: SplitDirection];
44 |
--------------------------------------------------------------------------------
/src/typings/workers.d.ts:
--------------------------------------------------------------------------------
1 | declare module "index/web-worker/importer.worker" {
2 | const WorkerFactory: new () => Worker;
3 | export default WorkerFactory;
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/editor.ts:
--------------------------------------------------------------------------------
1 | import { EditorState, ChangeSet, RangeValue, RangeSet, SelectionRange } from '@codemirror/state';
2 | import { SyntaxNodeRef } from '@lezer/common';
3 | import { MathInfoSet } from 'render-math-in-callouts';
4 | import { EditorPosition, Loc, MarkdownView, editorLivePreviewField } from "obsidian";
5 |
6 | export function locToEditorPosition(loc: Loc): EditorPosition {
7 | return { ch: loc.col, line: loc.line };
8 | }
9 |
10 | export function isLivePreview(state: EditorState) {
11 | return state.field(editorLivePreviewField);
12 | }
13 |
14 | export function isSourceMode(state: EditorState) {
15 | return !isLivePreview(state);
16 | }
17 |
18 | export function isReadingView(markdownView: MarkdownView) {
19 | return markdownView.getMode() === "preview";
20 | }
21 |
22 | export function isEditingView(markdownView: MarkdownView) {
23 | return markdownView.getMode() === "source";
24 | }
25 |
26 | /** CodeMirror/Lezer utilities */
27 |
28 | export function nodeText(node: SyntaxNodeRef, state: EditorState): string {
29 | return state.sliceDoc(node.from, node.to);
30 | }
31 |
32 | export function printNode(node: SyntaxNodeRef, state: EditorState) {
33 | // Debugging utility
34 | console.log(
35 | `${node.from}-${node.to}: "${nodeText(node, state)}" (${node.name})`
36 | );
37 | }
38 |
39 | export function printMathInfoSet(set: MathInfoSet, state: EditorState) {
40 | // Debugging utility
41 | console.log("MathInfoSet:");
42 | set.between(0, state.doc.length, (from, to, value) => {
43 | console.log(` ${from}-${to}: ${value.mathText} ${value.display ? "(display)" : ""} ${value.insideCallout ? "(in callout)" : ""} ${value.overlap === undefined ? "(overlap not checked yet)" : value.overlap ? "(overlapping)" : "(non-overlapping)"}`);
44 | });
45 | }
46 |
47 | export function nodeTextQuoteSymbolTrimmed(node: SyntaxNodeRef, state: EditorState, quoteLevel: number): string | undefined {
48 | const quoteSymbolPattern = new RegExp(`((>\\s*){${quoteLevel}})(.*)`);
49 | const quoteSymbolMatch = nodeText(node, state).match(quoteSymbolPattern);
50 | if (quoteSymbolMatch) {
51 | return quoteSymbolMatch.slice(-1)[0];
52 | }
53 | }
54 |
55 | export function printChangeSet(changes: ChangeSet) {
56 | changes.iterChanges(
57 | (fromA, toA, fromB, toB, inserted) => {
58 | console.log(`${fromA}-${toA}: "${inserted.toString()}" inserted (${fromB}-${toB} in new state)`);
59 | }
60 | );
61 | }
62 |
63 | export function rangeSetSome(set: RangeSet, predicate: (value: T, index: number, set: RangeSet) => unknown) {
64 | const cursor = set.iter();
65 | let index = 0;
66 | while (cursor.value) {
67 | if (predicate(cursor.value, index, set)) {
68 | return true;
69 | }
70 | cursor.next();
71 | index++;
72 | }
73 | return false;
74 | }
75 |
76 | export function hasOverlap(range1: { from: number, to: number }, range2: { from: number, to: number }): boolean {
77 | return range1.from <= range2.to && range2.from <= range1.to;
78 | }
79 |
80 | export function rangesHaveOverlap(ranges: readonly SelectionRange[], from: number, to: number) {
81 | for (const range of ranges) {
82 | if (range.from <= to && range.to >= from)
83 | return true;
84 | }
85 | return false;
86 | }
87 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | import { App, TFile } from "obsidian";
2 |
3 | import LatexReferencer from "main";
4 | import { getPropertyOrLinkTextInProperty } from "utils/obsidian";
5 | import { DEFAULT_SETTINGS, MathContextSettings, NumberStyle, ResolvedMathSettings } from "settings/settings";
6 | import { THEOREM_LIKE_ENVs, TheoremLikeEnvID } from "env";
7 |
8 |
9 | const ROMAN = ["", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM",
10 | "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC",
11 | "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"];
12 |
13 | export function toRomanUpper(num: number): string {
14 | // https://stackoverflow.com/a/9083076/13613783
15 | const digits = String(num).split("");
16 | let roman = "";
17 | let i = 3;
18 | while (i--) {
19 | // @ts-ignore
20 | roman = (ROMAN[+digits.pop() + (i * 10)] ?? "") + roman;
21 | }
22 | return Array(+digits.join("") + 1).join("M") + roman;
23 | }
24 |
25 | export function toRomanLower(num: number): string {
26 | return toRomanUpper(num).toLowerCase();
27 | }
28 |
29 | export const ALPH = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
30 |
31 | export function toAlphUpper(num: number): string {
32 | return (num - 1).toString(26).split("").map(str => ALPH[parseInt(str, 26)]).join("");
33 | }
34 |
35 | export function toAlphLower(num: number): string {
36 | return toAlphUpper(num).toLowerCase();
37 | }
38 |
39 | export const CONVERTER = {
40 | "arabic": String,
41 | "alph": toAlphLower,
42 | "Alph": toAlphUpper,
43 | "roman": toRomanLower,
44 | "Roman": toRomanUpper,
45 | }
46 |
47 | export function formatTheoremCalloutType(plugin: LatexReferencer, settings: { type: string, profile: string }): string {
48 | const profile = plugin.extraSettings.profiles[settings.profile];
49 | return profile.body.theorem[settings.type as TheoremLikeEnvID];
50 | }
51 |
52 | export function formatTitleWithoutSubtitle(plugin: LatexReferencer, file: TFile, settings: ResolvedMathSettings): string {
53 | let title = formatTheoremCalloutType(plugin, settings);
54 |
55 | if (settings.number) {
56 | if (settings.number == 'auto') {
57 | if (settings._index !== undefined) {
58 | settings.numberInit = settings.numberInit ?? 1;
59 | const num = +settings._index + +settings.numberInit;
60 | const style = settings.numberStyle ?? DEFAULT_SETTINGS.numberStyle as NumberStyle;
61 | title += ` ${getNumberPrefix(plugin.app, file, settings)}${CONVERTER[style](num)}${settings.numberSuffix}`;
62 | }
63 | } else {
64 | title += ` ${settings.number}`;
65 | }
66 | }
67 | return title;
68 | }
69 |
70 | export function formatTitle(plugin: LatexReferencer, file: TFile, settings: ResolvedMathSettings, noTitleSuffix: boolean = false): string {
71 | let title = formatTitleWithoutSubtitle(plugin, file, settings);
72 | return addSubTitle(title, settings, noTitleSuffix);
73 | }
74 |
75 | export function addSubTitle(mainTitle: string, settings: ResolvedMathSettings, noTitleSuffix: boolean = false) {
76 | let title = mainTitle;
77 | if (settings.title) {
78 | title += ` (${settings.title})`;
79 | }
80 | if (!noTitleSuffix && settings.titleSuffix) {
81 | title += settings.titleSuffix;
82 | }
83 | return title;
84 | }
85 |
86 | export function inferNumberPrefix(source: string, regExp: string): string | undefined {
87 | const pattern = new RegExp(regExp);
88 | const match = source.match(pattern);
89 | if (match) {
90 | let prefix = match[0].trim();
91 | if (!prefix.endsWith('.')) prefix += '.';
92 | return prefix;
93 | }
94 | }
95 |
96 | // /**
97 | // * "A note about calculus" => The "A" at the head shouldn't be used as a prefix (indefinite article)
98 | // * "A. note about calculus" => The "A" at the head IS a prefix
99 | // */
100 | // export function areValidLabels(labels: string[]): boolean {
101 | // function isValidLabel(label: string): boolean { // true if every label is an arabic or roman numeral
102 | // if (label.match(/^[0-9]+$/)) {
103 | // // Arabic numerals
104 | // return true;
105 | // }
106 | // if (label.match(/^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$/i)) {
107 | // // Roman numerals
108 | // // Reference: https://stackoverflow.com/a/267405/13613783
109 | // return true;
110 | // }
111 | // if (label.match(/^[a-z]$/i)) {
112 | // return true;
113 | // }
114 | // return false;
115 | // }
116 | // const blankRemoved = labels.filter((label) => label);
117 | // if (blankRemoved.length >= 2) {
118 | // return blankRemoved.every((label) => isValidLabel(label));
119 | // }
120 | // if (blankRemoved.length == 1) {
121 | // return labels.length == 2 && (isValidLabel(labels[0]));
122 | // }
123 | // return false;
124 | // }
125 |
126 | /**
127 | * Get an appropriate prefix for theorem callout numbering.
128 | * @param file
129 | * @param settings
130 | * @returns
131 | */
132 | export function getNumberPrefix(app: App, file: TFile, settings: Required): string {
133 | if (settings.numberPrefix) {
134 | return settings.numberPrefix;
135 | }
136 | const source = settings.inferNumberPrefixFromProperty ? getPropertyOrLinkTextInProperty(app, file, settings.inferNumberPrefixFromProperty) : file.basename;
137 | if (settings.inferNumberPrefix && source) {
138 | return inferNumberPrefix(
139 | source,
140 | settings.inferNumberPrefixRegExp
141 | ) ?? "";
142 | }
143 | return "";
144 | }
145 |
146 | /**
147 | * Get an appropriate prefix for equation numbering.
148 | * @param file
149 | * @param settings
150 | * @returns
151 | */
152 | export function getEqNumberPrefix(app: App, file: TFile, settings: Required): string {
153 | if (settings.eqNumberPrefix) {
154 | return settings.eqNumberPrefix;
155 | }
156 | const source = settings.inferEqNumberPrefixFromProperty ? getPropertyOrLinkTextInProperty(app, file, settings.inferEqNumberPrefixFromProperty) : file.basename;
157 | if (settings.inferEqNumberPrefix && source) {
158 | const prefix = inferNumberPrefix(
159 | source,
160 | settings.inferEqNumberPrefixRegExp
161 | ) ?? "";
162 | return prefix;
163 | }
164 | return "";
165 | }
166 |
167 | export function formatLabel(settings: ResolvedMathSettings): string | undefined {
168 | if (settings.label) {
169 | return settings.labelPrefix + THEOREM_LIKE_ENVs[settings.type as TheoremLikeEnvID].prefix + ":" + settings.label;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/utils/general.ts:
--------------------------------------------------------------------------------
1 | export function splitIntoLines(text: string): string[] {
2 | // https://stackoverflow.com/a/5035005/13613783
3 | return text.split(/\r?\n/);
4 | }
5 |
6 | export function capitalize(text: string): string {
7 | return text.charAt(0).toUpperCase() + text.slice(1);
8 | }
9 |
10 | export function removeFrom(item: Type, array: Array) {
11 | return array.splice(array.indexOf(item), 1);
12 | }
13 |
14 | export function insertAt(array: Array, item: Type, index: number) {
15 | array.splice(index, 0, item);
16 | }
17 |
18 | export function pathToName(path: string): string {
19 | return path.slice(path.lastIndexOf('/') + 1);
20 | }
21 |
22 | export function pathToBaseName(path: string): string {
23 | const name = pathToName(path);
24 | const index = name.lastIndexOf('.');
25 | if (index >= 0) {
26 | return name.slice(0, index);
27 | }
28 | return name;
29 | }
30 |
31 | // https://stackoverflow.com/a/50851710/13613783
32 | export type BooleanKeys = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
33 | export type NumberKeys = { [k in keyof T]: T[k] extends number ? k : never }[keyof T];
34 |
--------------------------------------------------------------------------------
/src/utils/obsidian.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from '@codemirror/view';
2 | import { BlockSubpathResult, CachedMetadata, Component, HeadingSubpathResult, MarkdownPostProcessorContext, MarkdownView, Modifier, Platform, Plugin, Pos, SectionCache, parseLinktext, resolveSubpath } from "obsidian";
3 | import { App, TAbstractFile, TFile, TFolder } from "obsidian";
4 | import { locToEditorPosition } from 'utils/editor';
5 | import { LeafArgs } from 'typings/type';
6 |
7 | ////////////////////
8 | // File utilities //
9 | ////////////////////
10 |
11 | /**
12 | * Similar to Vault.recurseChildren, but this function can be also called for TFile, not just TFolder.
13 | * Also, the callback is only called for TFile.
14 | */
15 | export function iterDescendantFiles(file: TAbstractFile, callback: (descendantFile: TFile) => any) {
16 | if (file instanceof TFile) {
17 | callback(file);
18 | } else if (file instanceof TFolder) {
19 | for (const child of file.children) {
20 | iterDescendantFiles(child, callback);
21 | }
22 | }
23 | }
24 |
25 | export function getAncestors(file: TAbstractFile): TAbstractFile[] {
26 | const ancestors: TAbstractFile[] = [];
27 | let ancestor: TAbstractFile | null = file;
28 | while (ancestor) {
29 | ancestors.push(ancestor);
30 | if (file instanceof TFolder && file.isRoot()) {
31 | break;
32 | }
33 | ancestor = ancestor.parent;
34 | }
35 | ancestors.reverse();
36 | return ancestors;
37 | }
38 |
39 | export function isEqualToOrChildOf(file1: TAbstractFile, file2: TAbstractFile): boolean {
40 | if (file1 == file2) {
41 | return true;
42 | }
43 | if (file2 instanceof TFolder && file2.isRoot()) {
44 | return true;
45 | }
46 | let ancestor = file1.parent;
47 | while (true) {
48 | if (ancestor == file2) {
49 | return true;
50 | }
51 | if (ancestor) {
52 | if (ancestor.isRoot()) {
53 | return false;
54 | }
55 | ancestor = ancestor.parent
56 | }
57 | }
58 | }
59 |
60 | export function getFile(app: App): TAbstractFile {
61 | return app.workspace.getActiveFile() ?? app.vault.getRoot();
62 | }
63 |
64 | //////////////////////
65 | // Cache & metadata //
66 | //////////////////////
67 |
68 | export function getSectionCacheFromPos(cache: CachedMetadata, pos: number, type: string): SectionCache | undefined {
69 | // pos: CodeMirror offset units
70 | if (cache.sections) {
71 | const sectionCache = Object.values(cache.sections).find((sectionCache) =>
72 | sectionCache.type == type
73 | && (sectionCache.position.start.offset == pos || sectionCache.position.end.offset == pos)
74 | );
75 | return sectionCache;
76 | }
77 | }
78 |
79 | export function getSectionCacheOfDOM(el: HTMLElement, type: string, view: EditorView, cache: CachedMetadata) {
80 | const pos = view.posAtDOM(el);
81 | return getSectionCacheFromPos(cache, pos, type);
82 | }
83 |
84 | export function getSectionCacheFromMouseEvent(event: MouseEvent, type: string, view: EditorView, cache: CachedMetadata) {
85 | const pos = view.posAtCoords(event) ?? view.posAtCoords(event, false);
86 | return getSectionCacheFromPos(cache, pos, type);
87 | }
88 |
89 | export function getProperty(app: App, file: TFile, name: string) {
90 | return app.metadataCache.getFileCache(file)?.frontmatter?.[name];
91 | }
92 |
93 | export function getPropertyLink(app: App, file: TFile, name: string) {
94 | const cache = app.metadataCache.getFileCache(file);
95 | if (cache?.frontmatterLinks) {
96 | for (const link of cache.frontmatterLinks) {
97 | if (link.key == name) {
98 | return link;
99 | }
100 | }
101 | }
102 | }
103 |
104 | export function getPropertyOrLinkTextInProperty(app: App, file: TFile, name: string) {
105 | return getPropertyLink(app, file, name)?.link ?? getProperty(app, file, name);
106 | }
107 |
108 | export function generateBlockID(cache: CachedMetadata, length: number = 6): string {
109 | let id = '';
110 |
111 | while (true) {
112 | // Reference: https://stackoverflow.com/a/58326357/13613783
113 | id = [...Array(length)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
114 | if (cache?.blocks && id in cache.blocks) {
115 | continue;
116 | } else {
117 | break;
118 | }
119 | }
120 | return id;
121 | }
122 |
123 | export function resolveLinktext(app: App, linktext: string, sourcePath: string): { file: TFile, subpathResult: HeadingSubpathResult | BlockSubpathResult | null } | null {
124 | const { path, subpath } = parseLinktext(linktext);
125 | const targetFile = app.metadataCache.getFirstLinkpathDest(path, sourcePath);
126 | if (!targetFile) return null;
127 | const targetCache = app.metadataCache.getFileCache(targetFile);
128 | if (!targetCache) return null;
129 | const result = resolveSubpath(targetCache, subpath);
130 | return { file: targetFile, subpathResult: result };
131 | }
132 |
133 |
134 | ///////////////////
135 | // Markdown view //
136 | ///////////////////
137 |
138 | export function getMarkdownPreviewViewEl(view: MarkdownView) {
139 | return Array.from(view.previewMode.containerEl.children).find((child) => child.matches(".markdown-preview-view"));
140 | }
141 |
142 | export function getMarkdownSourceViewEl(view: MarkdownView) {
143 | const firstCandidate = view.editor.cm?.dom.parentElement;
144 | if (firstCandidate) return firstCandidate;
145 | const secondCandidate = view.previewMode.containerEl.previousSibling;
146 | if (secondCandidate instanceof HTMLElement && secondCandidate.matches(".markdown-source-view")) {
147 | return secondCandidate;
148 | }
149 | }
150 |
151 | export async function openFileAndSelectPosition(app: App, file: TFile, position: Pos, ...leafArgs: LeafArgs) {
152 | // @ts-ignore
153 | const leaf = app.workspace.getLeaf(...leafArgs);
154 | await leaf.openFile(file);
155 | if (leaf.view instanceof MarkdownView) {
156 | // Editing view
157 | const editor = leaf.view.editor;
158 | const from = locToEditorPosition(position.start);
159 | const to = locToEditorPosition(position.end);
160 |
161 | editor.setSelection(from, to);
162 | editor.scrollIntoView({ from, to }, true);
163 |
164 | // Reading view: thank you NothingIsLost (https://discord.com/channels/686053708261228577/840286264964022302/952218718711189554)
165 | leaf.view.setEphemeralState({ line: position.start.line });
166 | }
167 | }
168 |
169 | export function findBlockFromReadingViewDom(sizerEl: HTMLElement, cb: (div: HTMLElement, index: number) => boolean): HTMLElement | undefined {
170 | let index = 0;
171 | for (const div of sizerEl.querySelectorAll(':scope > div')) {
172 | if (div.classList.contains('markdown-preview-pusher')) continue;
173 | if (div.classList.contains('mod-header')) continue;
174 | if (div.classList.contains('mod-footer')) continue;
175 |
176 | if (cb(div, index++)) return div;
177 | }
178 | }
179 |
180 | /**
181 | * Given a HTMLElement passed to a MarkdownPostProcessor, check if the current context is PDF export of not.
182 | */
183 | export function isPdfExport(el: HTMLElement): boolean {
184 | // el.classList.contains('markdown-rendered') is true not only for PDf export
185 | // but also CM6 decorations in Live Preview whose widgets are rendered by MarkdownRenderer.
186 | // So we need to check '.print', too.
187 | // el.closest('[src]') === null is necessary to exclude embeds inside a note exported to PDF.
188 | // return el.closest('.print') !== null && el.closest('[src]') === null && el.classList.contains('markdown-rendered');
189 |
190 | // Come to think about it, just the following would suffice:
191 | return (el.parentElement?.classList.contains('print') ?? false) && el.matches('.markdown-preview-view.markdown-rendered');
192 | }
193 |
194 | ////////////
195 | // Others //
196 | ////////////
197 |
198 | // compare the version of given plugin and the required version
199 | export function isPluginOlderThan(plugin: Plugin, version: string): boolean {
200 | return plugin.manifest.version.localeCompare(version, undefined, { numeric: true }) < 0;
201 | }
202 |
203 | export function getModifierNameInPlatform(mod: Modifier): string {
204 | if (mod == "Mod") {
205 | return Platform.isMacOS || Platform.isIosApp ? "⌘" : "ctrl";
206 | }
207 | if (mod == "Shift") {
208 | return "shift";
209 | }
210 | if (mod == "Alt") {
211 | return Platform.isMacOS || Platform.isIosApp ? "⌥" : "alt";
212 | }
213 | if (mod == "Meta") {
214 | return Platform.isMacOS || Platform.isIosApp ? "⌘" : Platform.isWin ? "win" : "meta";
215 | }
216 | return "ctrl";
217 | }
218 |
219 |
220 | export class MutationObservingChild extends Component {
221 | observer: MutationObserver;
222 |
223 | constructor(public targetEl: HTMLElement, public callback: MutationCallback, public options: MutationObserverInit) {
224 | super();
225 | this.observer = new MutationObserver(callback);
226 | }
227 |
228 | onload() {
229 | this.observer.observe(this.targetEl, this.options);
230 | }
231 |
232 | onunload() {
233 | this.observer.disconnect();
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/utils/parse.ts:
--------------------------------------------------------------------------------
1 | import { THEOREM_LIKE_ENV_IDs, THEOREM_LIKE_ENV_PREFIXES, THEOREM_LIKE_ENV_PREFIX_ID_MAP, TheoremLikeEnvID, TheoremLikeEnvPrefix } from "env";
2 | import { FoldOption, MinimalTheoremCalloutSettings } from "settings/settings";
3 |
4 | export const THEOREM_CALLOUT_PATTERN = new RegExp(
5 | `> *\\[\\! *(?${THEOREM_LIKE_ENV_IDs.join('|')}|${THEOREM_LIKE_ENV_PREFIXES.join('|')}|math) *(\\|(?.*?))?\\](?[+-])?(? .*)?`,
6 | 'i'
7 | );
8 |
9 | export function matchTheoremCallout(line: string): RegExpExecArray | null {
10 | return THEOREM_CALLOUT_PATTERN.exec(line)
11 | }
12 |
13 | /** > [!type|HERE IS METADATA]
14 | * a.k.a the "data-callout-metadata" attribute
15 | */
16 | export function parseTheoremCalloutMetadata(metadata: string) {
17 | let number = metadata.trim();
18 | if (!number) number = 'auto';
19 | else if (number === '*') number = '';
20 | return number;
21 | }
22 |
23 | /**
24 | *
25 | * @param line
26 | * @param excludeExample if true, then ""> [!example]" will not be treated as a theorem callout but as the built-in example callout.
27 | * @returns
28 | */
29 | export function readTheoremCalloutSettings(line: string, excludeExample: boolean = false): MinimalTheoremCalloutSettings & { legacy: boolean } | undefined {
30 | const rawSettings = line.match(THEOREM_CALLOUT_PATTERN)?.groups as { type: string, number?: string, title?: string, fold?: string } | undefined;
31 | if (!rawSettings) return;
32 |
33 | let type = rawSettings.type.trim().toLowerCase();
34 |
35 | // TODO: rewrite using _readTheoremCalloutSettings
36 |
37 | if (type === 'math' && rawSettings.number) {
38 | // legacy format
39 | const settings = JSON.parse(rawSettings.number) as MinimalTheoremCalloutSettings & { legacy: boolean };
40 | settings.legacy = true;
41 | return settings;
42 | }
43 |
44 | // new format
45 | if (excludeExample && type === 'example') return undefined;
46 |
47 | if (type.length <= 4) { // use length to avoid iterating over all the prefixes
48 | // convert a prefix to an ID (e.g. "thm" -> "theorem")
49 | type = THEOREM_LIKE_ENV_PREFIX_ID_MAP[type as TheoremLikeEnvPrefix];
50 | }
51 | const number = parseTheoremCalloutMetadata(rawSettings.number ?? '');
52 |
53 | let title: string | undefined = rawSettings.title?.trim();
54 | if (title === '') title = undefined;
55 |
56 | const fold = (rawSettings.fold?.trim() ?? '') as FoldOption;
57 |
58 | return { type, number, title, fold, legacy: false };
59 | }
60 |
61 | export function _readTheoremCalloutSettings(callout: {type: string, metadata: string}, excludeExample: boolean = false): { type: string, number: string, legacy: boolean } | undefined {
62 | let type = callout.type;
63 |
64 | if (callout.type === 'math' && callout.metadata) {
65 | // legacy format
66 | const settings = JSON.parse(callout.metadata) as MinimalTheoremCalloutSettings & { legacy: boolean };
67 | settings.legacy = true;
68 | return settings;
69 | }
70 |
71 | // new format
72 | if (excludeExample && callout.type === 'example') return undefined;
73 |
74 | if (callout.type.length <= 4) { // use length to avoid iterating over all the prefixes
75 | // convert a prefix to an ID (e.g. "thm" -> "theorem")
76 | type = THEOREM_LIKE_ENV_PREFIX_ID_MAP[callout.type as TheoremLikeEnvPrefix];
77 | }
78 | const number = parseTheoremCalloutMetadata(callout.metadata);
79 |
80 | return { type, number, legacy: false };
81 | }
82 |
83 |
84 | export function trimMathText(text: string) {
85 | return text.match(/\$\$([\s\S]*)\$\$/)?.[1].trim() ?? text;
86 | }
87 |
88 | export function parseLatexComment(line: string): { nonComment: string, comment: string } {
89 | const match = line.match(/(? | null {
112 | const result = line.match(/^(?.*?):(?.*)$/)?.groups;
113 | if (!result) return null;
114 | return { [result.key.trim()]: result.value.trim() };
115 | }
116 |
--------------------------------------------------------------------------------
/src/utils/plugin.ts:
--------------------------------------------------------------------------------
1 | import LatexReferencer from "main";
2 | import { CachedMetadata, Editor, MarkdownFileInfo, MarkdownView, Notice, TAbstractFile, TFile } from "obsidian";
3 | import { DEFAULT_SETTINGS, MathContextSettings, MinimalTheoremCalloutSettings, ResolvedMathSettings, TheoremCalloutSettings } from "settings/settings";
4 | import { generateBlockID, getAncestors, getFile } from "./obsidian";
5 | import { EquationBlock, MarkdownBlock, MarkdownPage, TheoremCalloutBlock } from "index/typings/markdown";
6 | import { getIO } from "file-io";
7 | import { splitIntoLines } from "./general";
8 | import { THEOREM_LIKE_ENV_IDs, THEOREM_LIKE_ENV_PREFIXES } from "env";
9 |
10 |
11 | export function resolveSettings(settings: MinimalTheoremCalloutSettings, plugin: LatexReferencer, currentFile: TAbstractFile): ResolvedMathSettings;
12 | export function resolveSettings(settings: undefined, plugin: LatexReferencer, currentFile: TAbstractFile): Required;
13 | export function resolveSettings(settings: MinimalTheoremCalloutSettings | undefined, plugin: LatexReferencer, currentFile: TAbstractFile): Required {
14 | /** Resolves settings. Does not overwride, but returns a new settings object.
15 | * Returned settings can be either
16 | * - ResolvedMathContextSettings or
17 | * - Required & Partial.
18 | * */
19 | const resolvedSettings = Object.assign({}, DEFAULT_SETTINGS);
20 | const ancestors = getAncestors(currentFile);
21 | for (const ancestor of ancestors) {
22 | Object.assign(resolvedSettings, plugin.settings[ancestor.path]);
23 | }
24 | Object.assign(resolvedSettings, settings);
25 | return resolvedSettings;
26 | }
27 |
28 | export function getProfile(plugin: LatexReferencer, file: TFile) {
29 | const settings = resolveSettings(undefined, plugin, file);
30 | const profile = plugin.extraSettings.profiles[settings.profile];
31 | return profile;
32 | }
33 |
34 | export function getProfileByID(plugin: LatexReferencer, profileID: string) {
35 | const profile = plugin.extraSettings.profiles[profileID];
36 | return profile;
37 | }
38 |
39 | export function staticifyEqNumber(plugin: LatexReferencer, file: TFile) {
40 | const page = plugin.indexManager.index.load(file.path);
41 | if (!MarkdownPage.isMarkdownPage(page)) {
42 | new Notice(`Failed to fetch the metadata of file ${file.path}.`);
43 | return;
44 | }
45 | const io = getIO(plugin, file);
46 | for (const block of page.$blocks.values()) {
47 | if (block instanceof EquationBlock && block.$printName !== null) {
48 | io.setRange(
49 | block.$pos,
50 | `$$\n${block.$mathText} \\tag{${block.$printName.slice(1, -1)}}\n$$`
51 | );
52 | }
53 | }
54 | }
55 |
56 | export async function insertBlockIdIfNotExist(plugin: LatexReferencer, targetFile: TFile, cache: CachedMetadata, block: MarkdownBlock, length: number = 6): Promise<{ id: string, lineAdded: number } | undefined> {
57 | // Make sure the section cache is fresh enough!
58 | if (!(cache?.sections)) return;
59 |
60 | if (block.$blockId) return { id: block.$blockId, lineAdded: 0 };
61 |
62 | // The section has no block ID, so let's create a new one
63 | const id = generateBlockID(cache, length);
64 | // and insert it
65 | const io = getIO(plugin, targetFile);
66 | await io.insertLine(block.$position.end + 1, "^" + id);
67 | await io.insertLine(block.$position.end + 1, "")
68 | return { id, lineAdded: 2 };
69 | }
70 |
71 | export function increaseQuoteLevel(content: string): string {
72 | let lines = content.split("\n");
73 | lines = lines.map((line) => "> " + line);
74 | return lines.join("\n");
75 | }
76 |
77 | /**
78 | * Correctly insert a display math even inside callouts or quotes.
79 | */
80 | export function insertDisplayMath(editor: Editor) {
81 | const cursorPos = editor.getCursor();
82 | const line = editor.getLine(cursorPos.line).trimStart();
83 | const nonQuoteMatch = line.match(/[^>\s]/);
84 |
85 | const head = nonQuoteMatch?.index ?? line.length;
86 | const quoteLevel = line.slice(0, head).match(/>\s*/g)?.length ?? 0;
87 | let insert = "$$\n" + "> ".repeat(quoteLevel) + "\n" + "> ".repeat(quoteLevel) + "$$";
88 |
89 | editor.replaceRange(insert, cursorPos);
90 | cursorPos.line += 1;
91 | cursorPos.ch = quoteLevel * 2;
92 | editor.setCursor(cursorPos);
93 | }
94 |
95 | export async function rewriteTheoremCalloutFromV1ToV2(plugin: LatexReferencer, file: TFile) {
96 | const { app, indexManager } = plugin;
97 |
98 | const page = await indexManager.reload(file);
99 | await app.vault.process(file, (data) => convertTheoremCalloutFromV1ToV2(data, page));
100 | }
101 |
102 |
103 | export const convertTheoremCalloutFromV1ToV2 = (data: string, page: MarkdownPage) => {
104 | const lines = data.split('\n');
105 | const newLines = [...lines];
106 | let lineAdded = 0;
107 |
108 | for (const section of page.$sections) {
109 | for (const block of section.$blocks) {
110 | if (!TheoremCalloutBlock.isTheoremCalloutBlock(block)) continue;
111 | if (!block.$v1) continue
112 |
113 | const newHeadLines = [generateTheoremCalloutFirstLine({
114 | type: block.$settings.type,
115 | number: block.$settings.number,
116 | title: block.$settings.title
117 | })];
118 | const legacySettings = block.$settings as any;
119 | if (legacySettings.label) newHeadLines.push(`> %% label: ${legacySettings.label} %%`);
120 | if (legacySettings.setAsNoteMathLink) newHeadLines.push(`> %% main %%`);
121 |
122 | newLines.splice(block.$position.start + lineAdded, 1, ...newHeadLines);
123 |
124 | lineAdded += newHeadLines.length - 1;
125 | }
126 | }
127 |
128 | return newLines.join('\n');
129 | }
130 |
131 | export function generateTheoremCalloutFirstLine(config: TheoremCalloutSettings): string {
132 | const metadata = config.number === 'auto' ? '' : config.number === '' ? '|*' : `|${config.number}`;
133 | let firstLine = `> [!${config.type}${metadata}]${config.fold ?? ''}${config.title ? ' ' + config.title : ''}`
134 | if (config.label) firstLine += `\n> %% label: ${config.label} %%`;
135 | return firstLine;
136 | }
137 |
138 | export function insertTheoremCallout(editor: Editor, config: TheoremCalloutSettings): void {
139 | const selection = editor.getSelection();
140 | const cursorPos = editor.getCursor();
141 |
142 | const firstLine = generateTheoremCalloutFirstLine(config);
143 |
144 | if (selection) {
145 | const nLines = splitIntoLines(selection).length;
146 | editor.replaceSelection(firstLine + '\n' + increaseQuoteLevel(selection));
147 | cursorPos.line += nLines;
148 | } else {
149 | editor.replaceRange(firstLine + '\n> ', cursorPos)
150 | cursorPos.line += 1;
151 | }
152 |
153 | if (config.label) cursorPos.line += 1;
154 | cursorPos.ch = 2;
155 | editor.setCursor(cursorPos);
156 | }
157 |
158 | export function isTheoremCallout(plugin: LatexReferencer, type: string) {
159 | if (plugin.extraSettings.excludeExampleCallout && type === 'example') return false;
160 | return (THEOREM_LIKE_ENV_IDs as unknown as string[]).includes(type) || (THEOREM_LIKE_ENV_PREFIXES as unknown as string[]).includes(type) || type === 'math'
161 | }
162 |
163 | export function insertProof(plugin: LatexReferencer, editor: Editor, context: MarkdownView | MarkdownFileInfo) {
164 | const settings = resolveSettings(undefined, plugin, context.file ?? getFile(plugin.app));
165 | const cursor = editor.getCursor();
166 | editor.replaceRange(`\`${settings.beginProof}\`\n\n\`${settings.endProof}\``, cursor);
167 | editor.setCursor({ line: cursor.line + 1, ch: 0 });
168 | }
169 |
--------------------------------------------------------------------------------
/src/utils/render.ts:
--------------------------------------------------------------------------------
1 | import { Component, MarkdownRenderer, renderMath } from "obsidian";
2 |
3 | export function renderTextWithMath(source: string): (HTMLElement | string)[] {
4 | // Obsidian API's renderMath only can render math itself, but not a text with math in it.
5 | // e.g., it can handle "\\sqrt{x}", but cannot "$\\sqrt{x}$ is a square root"
6 |
7 | const elements: (HTMLElement | string)[] = [];
8 |
9 | const mathPattern = /\$(.*?[^\s])\$/g;
10 | let result;
11 | let textFrom = 0;
12 | let textTo = 0;
13 | while ((result = mathPattern.exec(source)) !== null) {
14 | const mathString = result[1];
15 | textTo = result.index;
16 | if (textTo > textFrom) {
17 | elements.push(source.slice(textFrom, textTo));
18 | }
19 | textFrom = mathPattern.lastIndex;
20 |
21 | const mathJaxEl = renderMath(mathString, false);
22 |
23 | const mathSpan = createSpan({ cls: ["math", "math-inline", "is-loaded"] });
24 | mathSpan.replaceChildren(mathJaxEl);
25 | elements.push(mathSpan);
26 | }
27 |
28 | if (textFrom < source.length) {
29 | elements.push(source.slice(textFrom));
30 | }
31 |
32 | return elements;
33 | }
34 |
35 | /**
36 | * Easy-to-use version of MarkdownRenderer.renderMarkdown.
37 | * @param markdown
38 | * @param sourcePath
39 | * @param component - Typically you can just pass the plugin instance.
40 | * @returns
41 | */
42 | export async function renderMarkdown(markdown: string, sourcePath: string, component: Component): Promise {
43 | const el = createSpan();
44 | await MarkdownRenderer.renderMarkdown(markdown, el, sourcePath, component);
45 | for (const child of el.children) {
46 | if (child.tagName == "P") {
47 | return child.childNodes;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/styles.scss:
--------------------------------------------------------------------------------
1 | @use 'styles/main';
2 | @use 'styles/framed';
3 | @use 'styles/plain';
4 | @use 'styles/mathwiki';
5 | @use 'styles/vivid';
6 |
7 | :has(> .theorem-callout-framed) {
8 | @include framed.framed();
9 | }
10 |
11 | :has(> .theorem-callout-plain) {
12 | @include plain.plain();
13 | }
14 |
15 | :has(> .theorem-callout-mathwiki) {
16 | @include mathwiki.mathwiki();
17 | }
18 |
19 | :has(> .theorem-callout-vivid) {
20 | @include vivid.vivid();
21 | }
22 |
--------------------------------------------------------------------------------
/styles/framed.scss:
--------------------------------------------------------------------------------
1 | @mixin framed {
2 | /*
3 | If you're going to use this file as a CSS snippet, only include the code between
4 | the START and END comments to your snippet file.
5 | */
6 | /* START */
7 | .theorem-callout {
8 | --callout-color: var(--text-normal);
9 | background-color: rgb(0, 0, 0, 0);
10 | border: solid var(--border-width);
11 | border-radius: var(--size-2-3);
12 | font-family: CMU Serif, Times, Noto Serif JP;
13 | }
14 |
15 | .theorem-callout .callout-icon {
16 | display: none;
17 | }
18 |
19 | .theorem-callout-main-title {
20 | font-family: CMU Serif, Times, Noto Sans JP;
21 | font-weight: bolder;
22 | }
23 |
24 | .theorem-callout-subtitle {
25 | font-weight: normal;
26 | }
27 |
28 | :not(.theorem-callout-axiom):not(.theorem-callout-definition):not(.theorem-callout-remark).theorem-callout-en .callout-content {
29 | font-style: italic;
30 | }
31 | /* END */
32 | }
--------------------------------------------------------------------------------
/styles/main.css:
--------------------------------------------------------------------------------
1 | .math-booster-new-feature > p {
2 | color: var(--text-warning);
3 | margin: 0;
4 | }
5 |
6 | .math-booster-version-2-release-note-modal {
7 | --table-border-width: 2px;
8 | }
9 |
10 | .editor-suggest-setting-indented-heading {
11 | margin-left: var(--size-4-1);
12 | }
13 | .editor-suggest-setting-indented-heading + .setting-item,
14 | .editor-suggest-setting-indented-heading + .setting-item + .setting-item {
15 | margin-left: var(--size-4-3);
16 | }
17 |
18 | .math-booster-modal-top {
19 | padding: var(--size-4-3);
20 | }
21 |
22 | .math-booster-modal-top .setting-item {
23 | border-top: none;
24 | border-bottom: 1px solid var(--background-modifier-border);
25 | }
26 |
27 | .math-booster-setting-item-description {
28 | padding-bottom: 0.75em;
29 | }
30 |
31 | .math-booster-search-item-description {
32 | color: var(--text-faint);
33 | }
34 |
35 | .math-booster-backlink-modal {
36 | width: var(--file-line-width);
37 | }
38 |
39 | .math-booster-backlink-preview {
40 | border: var(--border-width) solid var(--background-modifier-border);
41 | border-radius: var(--radius-s);
42 | padding: var(--size-4-6);
43 | }
44 |
45 | .math-booster-begin-proof {
46 | padding-right: 10px;
47 | font-family: CMU Serif, Times, Noto Serif JP;
48 | font-weight: bold;
49 | }
50 |
51 | .math-booster-begin-proof-en {
52 | font-style: italic;
53 | }
54 |
55 |
56 | .math-booster-end-proof {
57 | float: right;
58 | }
59 |
60 | .math-booster-add-profile {
61 | display: flex;
62 | flex-direction: row;
63 | justify-content: space-between;
64 | }
65 |
66 | .math-booster-add-profile > input {
67 | width: 200px;
68 | }
69 |
70 | .math-booster-button-container {
71 | display: flex;
72 | flex-direction: row;
73 | justify-content: flex-end;
74 | gap: var(--size-4-2);
75 | padding: var(--size-4-2);
76 | }
77 |
78 | .theorem-callout {
79 | position: relative;
80 | }
81 |
82 | .theorem-callout-setting-button {
83 | padding-bottom: var(--size-2-2);
84 | padding-right: var(--size-2-3);
85 | position: absolute;
86 | right: var(--size-2-2);
87 | bottom: var(--size-2-2);
88 | opacity: 0;
89 | }
90 |
91 | .theorem-callout:hover .theorem-callout-setting-button {
92 | transition: 0s;
93 | opacity: 1
94 | }
95 |
96 | .theorem-callout-font-family-inherit {
97 | font-family: inherit !important;
98 | }
99 |
100 | .math-booster-title-form,
101 | .math-booster-label-form {
102 | width: 300px;
103 | }
104 |
105 |
106 | /* The code below was taken from the Latex Suite plugin (https://github.com/artisticat1/obsidian-latex-suite/blob/a5914c70c16d5763a182ec51d9716110b40965cf/styles.css) and adapted.
107 |
108 | MIT License
109 |
110 | Copyright (c) 2022 artisticat1
111 |
112 | Permission is hereby granted, free of charge, to any person obtaining a copy
113 | of this software and associated documentation files (the "Software"), to deal
114 | in the Software without restriction, including without limitation the rights
115 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
116 | copies of the Software, and to permit persons to whom the Software is
117 | furnished to do so, subject to the following conditions:
118 |
119 | The above copyright notice and this permission notice shall be included in all
120 | copies or substantial portions of the Software.
121 |
122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
128 | SOFTWARE.
129 | */
130 | .math-booster-dependency-validation {
131 | color: white;
132 | display: inline-block;
133 | border-radius: 1em;
134 | margin-right: var(--size-3);
135 | cursor: default;
136 | pointer-events: none;
137 | }
138 |
139 | .math-booster-dependency-validation svg {
140 | width: 16px !important;
141 | height: 16px !important;
142 | }
143 |
144 | .math-booster-dependency-validation.valid {
145 | background-color: #7dc535;
146 | visibility: visible;
147 | }
148 |
149 | .theme-dark .math-booster-dependency-validation.valid {
150 | background-color: #588b24;
151 | }
152 |
153 | .math-booster-dependency-validation.invalid {
154 | background-color: #ea5555;
155 | visibility: visible;
156 | }
157 |
--------------------------------------------------------------------------------
/styles/mathwiki.scss:
--------------------------------------------------------------------------------
1 | @mixin mathwiki {
2 | /*
3 | If you're going to use this file as a CSS snippet, only include the code between
4 | the START and END comments to your snippet file.
5 | */
6 | /* START */
7 | .theorem-callout {
8 | --callout-color: 248, 248, 255;
9 | font-family: CMU Serif, Times, Noto Serif JP;
10 | }
11 |
12 | .theorem-callout .callout-title-inner {
13 | padding-left: 5px;
14 | }
15 |
16 | .theorem-callout-subtitle {
17 | font-weight: normal;
18 | }
19 |
20 | .theorem-callout-en .callout-content {
21 | font-style: italic;
22 | }
23 |
24 | .theorem-callout-axiom {
25 | /* Font Awesome: lock */
26 | --callout-icon: '';
27 | }
28 |
29 | .theorem-callout-definition {
30 | /* Font Awesome: book */
31 | --callout-icon: '';
32 | }
33 |
34 | .theorem-callout-theorem {
35 | /* Font Awesome: magic */
36 | --callout-icon: '';
37 | }
38 |
39 | .theorem-callout-proposition {
40 | /* Font Awesome: calculator */
41 | --callout-icon: '';
42 | }
43 |
44 | .theorem-callout-example {
45 | /* Font Awesome: anchor */
46 | --callout-icon: '';
47 | }
48 | /* END */
49 | }
--------------------------------------------------------------------------------
/styles/plain.scss:
--------------------------------------------------------------------------------
1 | @mixin plain {
2 | /*
3 | If you're going to use this file as a CSS snippet, only include the code between
4 | the START and END comments to your snippet file.
5 | */
6 | /* START */
7 | .theorem-callout {
8 | --callout-color: var(--text-normal);
9 | background-color: rgb(0, 0, 0, 0);
10 | padding-left: 0;
11 | padding-right: 0;
12 | border: none;
13 | box-shadow: none;
14 | font-family: CMU Serif, Times, Noto Serif JP;
15 | }
16 |
17 | .theorem-callout .callout-icon {
18 | display: none;
19 | }
20 |
21 | .theorem-callout-main-title {
22 | font-family: CMU Serif, Times, Noto Sans JP;
23 | font-weight: bolder;
24 | }
25 |
26 | .theorem-callout-subtitle {
27 | font-weight: normal;
28 | }
29 |
30 | :not(.theorem-callout-axiom):not(.theorem-callout-definition):not(.theorem-callout-remark).theorem-callout-en .callout-content {
31 | font-style: italic;
32 | }
33 | /* END */
34 | }
--------------------------------------------------------------------------------
/styles/vivid.scss:
--------------------------------------------------------------------------------
1 | @mixin vivid {
2 |
3 | /*
4 | If you're going to use this file as a CSS snippet, only include the code between
5 | the START and END comments to your snippet file.
6 | */
7 | /* START */
8 | .theorem-callout {
9 | --callout-color: 238, 15, 149;
10 | border-top: none;
11 | border-bottom: none;
12 | border-left: var(--size-2-2) solid rgb(var(--callout-color));
13 | border-right: none;
14 | border-radius: 0px;
15 | box-shadow: none;
16 | padding: 0px;
17 | font-family: CMU Serif, Times, Noto Serif JP;
18 | }
19 |
20 | .theorem-callout .callout-title {
21 | padding: var(--size-2-3);
22 | padding-left: var(--size-4-3);
23 | }
24 |
25 | .theorem-callout .callout-icon {
26 | display: none;
27 | }
28 |
29 | .theorem-callout .callout-title-inner {
30 | font-family: Inter;
31 | font-weight: normal;
32 | color: rgb(var(--callout-color));
33 | }
34 |
35 | .theorem-callout-subtitle {
36 | font-weight: lighter;
37 | }
38 |
39 | .theorem-callout .callout-content {
40 | background-color: var(--background-primary);
41 | padding: 1px 20px 2px 20px;
42 | }
43 | /* END */
44 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src",
4 | "inlineSourceMap": true,
5 | "inlineSources": true,
6 | "module": "ESNext",
7 | "target": "ES6",
8 | "allowJs": true,
9 | "noImplicitAny": true,
10 | "moduleResolution": "node",
11 | "importHelpers": true,
12 | "isolatedModules": true,
13 | "strictNullChecks": true,
14 | "lib": [
15 | "DOM",
16 | "ES5",
17 | "ES6",
18 | "ES7",
19 | "ES2021.String",
20 | "DOM.Iterable",
21 | "es2022"
22 | ]
23 | },
24 | "include": [
25 | "**/*.ts"
26 | ],
27 | "exclude": [
28 | "src/legacy/*.ts",
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.3.0": "1.3.5",
3 | "0.3.1": "1.3.5",
4 | "0.3.2": "1.3.5"
5 | }
--------------------------------------------------------------------------------