Compare commits

...

122 commits

Author SHA1 Message Date
Yaotian-Liu
5ace9075db
new release 1.6.5 2026-05-05 18:00:30 -07:00
Yaotian-Liu
6adbd70bef
new release 1.6.4 2026-01-15 15:49:00 -07:00
Yaotian-Liu
935403182e
new release 1.6.3 2026-01-01 16:25:20 -05:00
Yaotian-Liu
91df3ded05
chore: Add CLAUDE.md and update the pseudocode.js 2026-01-01 16:24:15 -05:00
Yaotian-Liu
4bb26746dd
new release 1.6.2 2025-11-11 15:55:36 -07:00
Yaotian Liu
a9aed6f856
Merge pull request #29 from Nambers/clickEdit
Enable Click edit
2025-11-11 15:51:09 -07:00
Eritque arcus
1c3846f0a2
chore: use soft null check 2025-11-09 23:58:25 -06:00
Eritque arcus
100c1c5bad
fix: fix click button will also enter edit 2025-11-09 23:56:00 -06:00
Eritque arcus
6614713042
chore: cleanup 2025-11-09 22:01:39 -06:00
Eritque arcus
1acc571cae
feat: implement click-edit 2025-11-09 21:59:29 -06:00
Yaotian Liu
d677e7a5d2
Update README.md 2025-02-03 18:08:36 -07:00
Yaotian-Liu
47f5b562c7
new release 1.6.1 2024-08-31 16:46:19 -07:00
Yaotian Liu
13be11b65a
Merge pull request #22 from anton-dergunov/patch-1
Right alignment for the comments
2024-08-31 16:42:43 -07:00
Anton Dergunov
bdec504edc
Right alignment for the comments
Make the comments look more like in LaTeX package documentation: https://ctan.math.washington.edu/tex-archive/macros/latex/contrib/algpseudocodex/algpseudocodex.pdf
2024-08-31 23:34:49 +01:00
Yaotian-Liu
882556f4e3
build: Update release workflow 2024-06-05 11:19:23 -07:00
Yaotian-Liu
94432ec1aa
new release 1.6.0 2024-06-05 11:14:24 -07:00
Yaotian-Liu
81bc30282f
faet: Add a toggle for theme following 2024-06-05 01:13:40 -07:00
Yaotian-Liu
3f28ab42b2
feat: Update border theme 2024-06-04 18:32:10 -07:00
Yaotian-Liu
dd112a5b5f
feat: Update theme detector 2024-06-04 18:22:37 -07:00
Yaotian-Liu
9828188724
new release 1.5.8 2024-06-01 12:12:46 +08:00
Yaotian-Liu
11b9a7e72f
fix: Add Repeat Until to pair complete 2024-06-01 12:11:50 +08:00
Yaotian-Liu
5ae081a50e
new release 1.5.7 2024-02-21 17:17:37 -07:00
Yaotian-Liu
e6b4375c25
Fix: Add ForAll to auto_complete 2024-02-21 17:16:40 -07:00
Yaotian-Liu
6001c0f6d8
new release 1.5.6 2024-02-07 21:12:45 -07:00
Yaotian-Liu
731961466d
feat: Insert end tag while auto-complete 2024-02-07 21:11:30 -07:00
Yaotian-Liu
d73fc6a663
new release 1.5.5 2024-01-17 16:42:01 -07:00
Yaotian-Liu
1eca967479
Fix: 'continue' and 'break' missing in auto-complete 2024-01-17 16:40:45 -07:00
Yaotian-Liu
5e1d57922d
new release 1.5.4 2023-10-23 10:06:00 -07:00
Yaotian-Liu
f6e04024f6
fix: Else is missing in auto-complete 2023-10-22 23:08:02 -07:00
Yaotian-Liu
1d0be42123 new release 1.5.3 2023-10-14 12:35:56 -07:00
Yaotian-Liu
2f0ac81165 style: Correct the style when using dark theme 2023-10-14 12:34:43 -07:00
Yaotian-Liu
d1bdd16fb5
new release 1.5.2 2023-06-08 10:37:21 +08:00
Yaotian-Liu
a0ae3321c9
fix: Cannot export inline macro correctly 2023-06-08 10:36:50 +08:00
Yaotian-Liu
743cc7652e
new release 1.5.1 2023-06-08 00:40:34 +08:00
Yaotian-Liu
b8ef48f07e
perf: Optimize inline extraction 2023-06-08 00:40:01 +08:00
Yaotian-Liu
e797755282
doc: Update doc for in-block macros 2023-06-08 00:24:43 +08:00
Yaotian-Liu
1b6dda0929
new release 1.5.0 2023-06-08 00:13:26 +08:00
Yaotian-Liu
4be888e236
feat: Add inline macro support 2023-06-08 00:12:36 +08:00
Yaotian-Liu
5aa1faec57
new release 1.4.4 2023-06-07 15:58:03 +08:00
Yaotian Liu
d5c13357bf
doc: Add manual for render line number 2023-06-05 19:49:50 +08:00
Yaotian-Liu
56485b47a8
new release 1.4.3 2023-06-04 16:49:19 +08:00
Yaotian-Liu
2aa4bae01b
feat: Custom settings available for export 2023-06-04 16:48:16 +08:00
Yaotian-Liu
0e09e52568
new release 1.4.2 2023-06-02 09:36:14 +08:00
Yaotian-Liu
776414c042
fix: preamble undefined if not init 2023-06-02 09:34:38 +08:00
Yaotian-Liu
31a2475fdf
chore: Update eslint setting 2023-06-02 09:34:17 +08:00
Yaotian-Liu
7c72b8caa9
new release 1.4.1 2023-05-31 12:01:50 +08:00
Yaotian-Liu
0e4a8e273a
feat: Add a toggle for preamble loading 2023-05-31 12:00:32 +08:00
Yaotian-Liu
ae392a2fdd
refactor: Use template literals 2023-05-29 23:23:43 +08:00
Yaotian-Liu
ffb1a15cb9
new release 1.4.0 2023-05-28 15:54:17 +08:00
Yaotian-Liu
f163b33861
feat: Add scope line support 2023-05-28 15:53:29 +08:00
Yaotian-Liu
5b83128ca8
new release 1.3.1 2023-05-23 10:57:01 +08:00
Yaotian-Liu
3e656c81b8
fix: Macro definition wrong 2023-05-23 10:56:20 +08:00
Yaotian-Liu
f7a14564f7
doc: Remove redundant installation guide 2023-05-22 16:22:54 +08:00
Yaotian-Liu
c21e3c7704
doc: Update README to latest release 2023-05-22 16:21:22 +08:00
Yaotian-Liu
65feca5f69
new release 1.3.0 2023-05-22 16:05:30 +08:00
Yaotian-Liu
7363aa6a03
feat: Add an export button 2023-05-22 16:03:45 +08:00
Yaotian-Liu
fdcc7d5405
doc: Update new macros desc 2023-05-21 19:36:01 +08:00
Yaotian-Liu
53fde479fd
new release 1.2.3 2023-05-21 18:51:25 +08:00
Yaotian-Liu
e2662f15b0
feat: Add a macro translator. 2023-05-21 18:50:15 +08:00
Yaotian-Liu
4cab4a02c4
refactor: Use multi-file to organize 2023-05-21 16:57:23 +08:00
Yaotian-Liu
f4a09bbaff
new release 1.2.2 2023-05-20 17:23:35 +08:00
Yaotian-Liu
2f44b862d9
feat: Add a toggle for preamble loaded notice 2023-05-20 17:21:56 +08:00
Yaotian-Liu
fce4261a21
new release 1.2.1 2023-05-19 21:43:49 +08:00
Yaotian-Liu
f4d31a05c5
doc: Remove misleading info 2023-05-19 21:43:08 +08:00
Yaotian-Liu
6b02497375
doc: Update reload tip for preamble 2023-05-19 21:39:53 +08:00
Yaotian-Liu
874fdbb489
new release 1.2.0 2023-05-19 21:31:46 +08:00
Yaotian-Liu
dfeb6ad4d1
feat: Add preamble.sty support 2023-05-19 21:19:03 +08:00
Yaotian-Liu
8d014eeb96
new release 1.1.11 2023-04-29 13:24:42 +08:00
Yaotian-Liu
e7a4897b82
fix: Setting bug 2023-04-29 13:24:02 +08:00
Yaotian-Liu
21d86e2ffc
new release 1.1.10 2023-04-29 13:17:27 +08:00
Yaotian-Liu
53083f74e9
feat: Add block width config 2023-04-29 13:15:40 +08:00
Yaotian-Liu
91acb139bb
new release 1.1.9 2023-04-24 15:51:02 +08:00
Yaotian-Liu
9447807091
chore: Fix the style of auto-completion 2023-04-24 15:48:13 +08:00
Yaotian-Liu
995fa7d6de
doc: Update README 2023-04-24 15:32:30 +08:00
Yaotian-Liu
fdb8218805
new release 1.1.8 2023-04-16 13:11:30 +08:00
Yaotian-Liu
ad6ffb5e33
style: Make error msg look better 2023-04-16 13:09:35 +08:00
Yaotian-Liu
2de841b52f
new release 1.1.7 2023-04-16 12:17:42 +08:00
Yaotian-Liu
69541d65a3
feat: Use CSS to assign error-msg style 2023-04-16 12:15:32 +08:00
Yaotian-Liu
515069e1d3
doc: Update TOC 2023-04-16 11:45:15 +08:00
Yaotian-Liu
d9a455bccf
doc: Update TOC 2023-04-16 11:44:19 +08:00
Yaotian-Liu
cf5bee2e57
doc: Update installation 2023-04-16 11:39:55 +08:00
Yaotian-Liu
bdefb1e407
doc: Add a TOC 2023-03-30 22:59:20 +08:00
Yaotian-Liu
10236275ba
doc: update installation methods 2023-03-30 22:56:45 +08:00
Yaotian-Liu
b61636da5f
Update README.md 2023-03-30 13:54:18 +08:00
Yaotian-Liu
6169bb9db5
bug: fix a wrong expression 2023-03-25 23:06:45 +08:00
Yaotian-Liu
92870222c8
new release 1.1.6 2023-03-24 21:03:28 +08:00
Yaotian-Liu
8622d54950
feat: update auto-completion logic 2023-03-24 21:00:50 +08:00
Yaotian-Liu
3084c1ac08
doc: update README 2023-03-24 20:05:41 +08:00
Yaotian-Liu
96bc613199
new release 1.1.5 2023-03-24 19:53:36 +08:00
Yaotian-Liu
42442a0fc4
feat: display error msg in the block 2023-03-24 19:52:02 +08:00
Yaotian-Liu
37cac5a6d8
doc: update README on new command 2023-03-23 16:53:48 +08:00
Yaotian-Liu
09db3a077d
new release 1.1.4 2023-03-23 00:16:04 +08:00
Yaotian-Liu
6f1ddb618c
feat: Add new command to auto-gen pseudocode block. 2023-03-23 00:11:44 +08:00
Yaotian-Liu
244a53a0c0
chore: update actions' version 2023-03-20 12:25:38 +08:00
Yaotian-Liu
e762834040
doc: update known issues 2023-03-19 16:11:14 +08:00
Yaotian-Liu
b018e7ee0c
new release 1.1.3 2023-03-19 16:08:25 +08:00
Yaotian-Liu
38fcad2b35
doc: add known issues 2023-03-19 14:04:58 +08:00
Yaotian-Liu
9b1e0322c6
feat: add FOR to auto-complete 2023-03-19 13:56:21 +08:00
Yaotian-Liu
0f18b392eb
new release 1.1.2 2023-03-19 13:45:45 +08:00
Yaotian-Liu
d375bb5dba
Update release.yml 2023-03-19 13:12:08 +08:00
Yaotian-Liu
1f73041d5e
Update release.yml 2023-03-19 13:08:35 +08:00
Yaotian-Liu
6513d11b3c
new release 1.1.2 2023-03-19 12:03:10 +08:00
Yaotian-Liu
f64e527248
Merge branch 'master' of github.com:Yaotian-Liu/obsidian-pseudocode 2023-03-19 12:00:40 +08:00
Yaotian-Liu
a6bd7b1f90
new version 1.1.2 2023-03-19 12:00:20 +08:00
Yaotian-Liu
9d90dd7152
doc: Change the keyword 2023-03-19 11:53:35 +08:00
Yaotian-Liu
27bbfe3b14
chore: add auto-release workflow 2023-03-19 11:51:32 +08:00
Yaotian-Liu
035fabb046
Delete main.yml 2023-03-19 00:21:08 +08:00
Yaotian-Liu
0df6da68d9
Update main.yml 2023-03-19 00:18:42 +08:00
Yaotian-Liu
e25b1662cc
Create main.yml 2023-03-19 00:15:05 +08:00
Yaotian-Liu
32fb8c228d
faet: auto-complete is case-insensitive now 2023-03-17 11:10:30 +08:00
Yaotian-Liu
720b9e8786
chore: replace code block kw 2023-03-17 11:03:13 +08:00
Yaotian-Liu
9d50c97eca
release 1.1.1 2023-03-16 13:16:34 +08:00
Yaotian-Liu
5de5736a1f
chore: import katex 2023-03-16 13:13:19 +08:00
Yaotian-Liu
e926b25f2a
Merge branches 'master' and 'master' of github.com:Yaotian-Liu/obsidian-pseudocode 2023-03-16 01:11:20 +08:00
Yaotian-Liu
dcef609fe2
chore: inline online fonts with base64 2023-03-16 01:10:59 +08:00
Yaotian-Liu
50d1e08e02
Update README.md 2023-03-14 22:26:12 +08:00
Yaotian-Liu
c248bacec8
chore: release related update 2023-03-14 22:22:07 +08:00
Yaotian-Liu
5722b13589
Merge branch 'master' of github.com:Yaotian-Liu/obsidian-pseudocode 2023-03-14 22:14:15 +08:00
Yaotian-Liu
c140928b1b
feat: add auto-completion 2023-03-14 22:13:41 +08:00
Yaotian-Liu
901f1a0a91
Update README.md 2023-03-13 22:41:25 +08:00
Yaotian-Liu
057c402d1b
Update manifest.json 2023-03-13 20:17:31 +08:00
Yaotian-Liu
30c3ff3384
Create LICENSE 2023-03-13 20:12:41 +08:00
23 changed files with 3877 additions and 215 deletions

View file

@ -8,3 +8,7 @@ insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
[*.{yml,yaml}]
indent_size = 2
tab_width = 2

View file

@ -18,6 +18,7 @@
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-inferrable-types": "off"
}
}

46
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: Create Release
on:
push:
branches:
- master
jobs:
create_release:
if: contains(github.event.head_commit.message, 'new release')
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Install Dependencies
run: |
npm config set registry https://registry.npmjs.org/
npm install
- name: Build
run: npm run build
- name: Get Release Version
id: release-version
run: |
echo "VER=$(echo ${{ github.event.head_commit.message }} | cut -d' ' -f3)" >> $GITHUB_OUTPUT
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.release-version.outputs.VER }}
body: "New release ${{ steps.release-version.outputs.VER }}"
draft: false
prerelease: false
files: |
main.js
manifest.json
styles.css

4
.gitignore vendored
View file

@ -1,5 +1,5 @@
# vscode
.vscode
.vscode
# Intellij
*.iml
@ -20,3 +20,5 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
tmp/

94
CLAUDE.md Normal file
View file

@ -0,0 +1,94 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Obsidian-Pseudocode is an Obsidian plugin that renders LaTeX-style pseudocode inside code blocks. It uses pseudocode.js to convert LaTeX algorithmic constructs to HTML, with support for math formulas via KaTeX.
## Build and Development Commands
```bash
# Development mode with file watching
npm run dev
# Production build (includes TypeScript type checking)
npm run build
# Version bump
npm run version
```
## Architecture
### Plugin Entry Point
- `main.ts`: Core plugin class (`PseudocodePlugin`)
- Registers markdown code block processor for `pseudo` language
- Manages settings and preamble loading
- Handles theme observer lifecycle
### Core Processing Flow
1. User creates a code block with `pseudo` language specifier
2. `pseudocodeHandler()` in main.ts processes the block:
- Extracts inline macros (before `\begin{algorithm}`)
- Combines with global preamble if enabled
- Injects preamble into all math expressions (`$...$`)
- Renders using pseudocode.js library
- Adds export button and applies theme
### Key Modules
**src/inline_macro.ts**
- Separates inline macro definitions from algorithm content
- Macros before `\begin{algorithm}` are treated as preamble
**src/latex_translator.ts**
- Converts unsupported LaTeX macros to KaTeX-compatible commands
- Handles `\DeclarePairedDelimiter`, `\DeclareMathOperator*`, `\DeclareMathOperator`
- Validates macros using KaTeX parser and converts `\newcommand` to `\renewcommand` when redefining
**src/export_button.ts**
- Creates "Export to clipboard" button for each pseudocode block
- Generates compilable LaTeX document with required packages (algorithm, algpseudocodex, amsmath)
- Includes both global and inline macros
**src/theme.ts**
- MutationObserver watches document.body for theme changes
- Applies Obsidian theme colors (--background-primary, --text-normal) to pseudocode blocks
- Updates .ps-root, .ps-algorithm, and border colors dynamically
**src/setting_tab.ts**
- UI for plugin settings (block size, preamble path, line numbers, theme following, etc.)
**src/auto_complete.ts**
- EditorSuggestor for autocomplete within `pseudo` code blocks
### Build System
- Uses esbuild (config in `esbuild.config.mjs`)
- Bundles to CommonJS format targeting ES2018
- Main entry: `main.ts``main.js`
- External: Obsidian API, CodeMirror 6, Electron
## Important Implementation Details
### Preamble System
- Global preamble: loaded from file (default `preamble.sty`)
- Inline preamble: defined per-block before `\begin{algorithm}`
- Preamble is injected into every math expression in the pseudocode
- Must reload plugin after changing global preamble file
### Theme Integration
- When `followSystemTheme` is enabled, plugin observes Obsidian theme changes
- CSS custom properties are read from document.body and applied to rendered pseudocode
- Observer must be detached on plugin unload to prevent memory leaks
### Error Handling
- Pseudocode rendering errors display in-block with ✖ symbol
- Preamble loading failures show Notice to user
- Invalid LaTeX in preamble is caught and logged to console
## Dependencies
- `pseudocode`: Uses forked version from ytliu74/pseudocode.js#master
- `katex`: Fixed at 0.11.1 for macro rendering
- `obsidian`: Latest API
- TypeScript 4.7.4 with strict null checks enabled

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Yaotian-Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md
View file

@ -1,4 +1,16 @@
# Obsidian-Pseudocode
- [Obsidian-Pseudocode](#obsidian-pseudocode)
- [Features](#features)
- [Future Features](#future-features)
- [Usage](#usage)
- [Basic](#basic)
- [Preamble style customization](#preamble-style-customization)
- [Use a `.sty` file](#use-a-sty-file)
- [Use in-block preamble](#use-in-block-preamble)
- [Supported macros](#supported-macros)
- [Export to a compilable LaTeX file](#export-to-a-compilable-latex-file)
- [Installation](#installation)
- [Credits](#credits)
This is a plugin for [Obsidian](https://obsidian.md/) that allows you to render LaTeX-style pseudocode inside a code block. The plugin is based on [pseudocode.js](https://github.com/SaswatPadhi/pseudocode.js), a JavaScript library that typesets pseudocode beautifully to HTML.
@ -7,56 +19,108 @@ This is a plugin for [Obsidian](https://obsidian.md/) that allows you to render
- Intuitive grammar: The plugin takes a LaTeX-style input that supports the algorithmic constructs from LaTeX's algorithm packages. With or without LaTeX experience, a user should find the grammar fairly intuitive.
- Print quality: The HTML output produced by the plugin is (almost) identical with the pretty algorithms printed on publications that are typeset by LaTeX.
- Math formula support: Inserting math formulas in the pseudocode is as easy as LaTeX. Just enclose math expression in `$...$` or `\(...\)`.
- Auto-completion inside `pseudo` code block. (Release 1.1.0)
- [Preamble style (macros) customization.](#preamble-style-customization) (Release 1.2.0 & 1.5.0)
- [Export a LaTeX file that can be compiled, including any required additional macros.](#export-to-a-compilable-latex-file) (Release 1.3.0)
- Pseudocode block follows Obsidian theme and supports both light and dark ones. (Release 1.6.0)
### Future Features
- [ ] Syntax highlighting.
- [ ] Auto-completion inside `pcode` code block.
## Usage
To use the plugin, simply create a code block in your Obsidian note and add your pseudocode inside it. Then, add the language specifier `pcode` (short for "pseudocode") to the code block. The plugin will automatically render the pseudocode as LaTeX.
### Basic
To use the plugin, simply create a code block in your Obsidian note and add your pseudocode inside it. Then, add the language specifier `pseudo` (short for "pseudocode") to the code block. The plugin will automatically render the pseudocode as LaTeX.
**Rocommend: use the command `Pseudocode: Insert a new pseudocode block` to start.**
Here is an example:
```
```pcode
```pseudo
\begin{algorithm}
\caption{Quicksort}
\begin{algorithmic}
\PROCEDURE{Quicksort}{$A, p, r$}
\IF{$p < r$}
\STATE $q = $ \CALL{Partition}{$A, p, r$}
\STATE \CALL{Quicksort}{$A, p, q - 1$}
\STATE \CALL{Quicksort}{$A, q + 1, r$}
\ENDIF
\ENDPROCEDURE
\PROCEDURE{Partition}{$A, p, r$}
\STATE $x = A[r]$
\STATE $i = p - 1$
\FOR{$j = p$ \TO $r - 1$}
\IF{$A[j] < x$}
\STATE $i = i + 1$
\STATE exchange
\Procedure{Quicksort}{$A, p, r$}
\If{$p < r$}
\State $q \gets $ \Call{Partition}{$A, p, r$}
\State \Call{Quicksort}{$A, p, q - 1$}
\State \Call{Quicksort}{$A, q + 1, r$}
\EndIf
\EndProcedure
\Procedure{Partition}{$A, p, r$}
\State $x \gets A[r]$
\State $i \gets p - 1$
\For{$j \gets p$ \To $r - 1$}
\If{$A[j] < x$}
\State $i \gets i + 1$
\State exchange
$A[i]$ with $A[j]$
\ENDIF
\STATE exchange $A[i]$ with $A[r]$
\ENDFOR
\ENDPROCEDURE
\EndIf
\State exchange $A[i]$ with $A[r]$
\EndFor
\EndProcedure
\end{algorithmic}
\end{algorithm}
```
```
This will be rendered as:
This will be rendered as (to render line number, you need to toggle it in setting tab):
<div align="center">
<img src="assets/example.png" alt="example" width="70%">
</div>
### Preamble style customization
#### Use a `.sty` file
You can use a `.sty` file (actually the suffix does not matter) to customize with some macros. The plugin will locate the file according to the setting. The default path is `preamble.sty`.
Please reload the plugin after you change the preamble file.
#### Use in-block preamble
You can simply write your own macros in the pseudocode block before `\begin{algorithm}`. These macros will only be applicable within this specific block.
#### Supported macros
Currently supported macros can be found at [this link](https://katex.org/docs/supported.html#macros) and below(might not be fully supported):
1. `\DeclarePairedDelimiter`
2. `\DeclareMathOperator*`
3. `\DeclareMathOperator`
### Export to a compilable LaTeX file
You can easily export a compilable LaTeX file by clicking the `Export to clipboard` button at the bottom right corner for each pseudocode block. The plugin will automatically generate a compilable LaTeX file, including any required additional macros, to your clipboard.
## Installation
To install the plugin, download the latest release from the [releases page](https://github.com/yourusername/obsidian-pseudocode/releases) and extract it to your Obsidian plugins folder. Then, enable the plugin in your Obsidian settings.
<!-- ### Install from the Community Plugins in Obsidian. -->
Or just download it within Obsidian's community plugins.
:tada: The Pseudocode plugin is now available in the Community Plugins section of Obsidian. To install it, simply search for **Pseudocode** and click on the installation button.
<!-- ### Use [BRAT](https://github.com/TfTHacker/obsidian42-brat#Quick-Guide-for-using-BRAT)
1. Install **Obsidian-42 BRAT** from the Community Plugins in Obsidian.
2. Open the command palette and run the command `BRAT: Add a beta plugin for testing`. Input this repo's URL `https://github.com/Yaotian-Liu/obsidian-pseudocode`.
3. Click on **Add Plugin** -- wait a few seconds and BRAT will tell you what is going on.
4. After BRAT confirms the installation, in Settings go to the **Community plugins** tab.
5. Refresh the list, find `Pseudocode` and enable it.
### Manual install
1. Create a folder named `pseudocode-in-obs` in your Obsidian vault plugin folder (which is {Your Vault}/.obsidian/plugins).
2. Download `main.js`, `manifest.json` and `styles.css` from the [releases page](https://github.com/yaotian-liu/obsidian-pseudocode/releases/latest), to the folder you just created in step 1.
3. Open your Obsidian, and enable the plugin in "Community Plugins" setting page.
4. Enjoy. -->
<!-- ## Known Issues -->
## Credits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 105 KiB

36
extract_katexcss.py Normal file
View file

@ -0,0 +1,36 @@
import re
import urllib.request
import base64
import os
# define a function to encode a file as base64
def encode_file_as_base64(filename):
print("processing: ", filename)
url_base = "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.11.1/fonts/"
url = url_base + filename
if not os.path.isfile("tmp/" + filename):
urllib.request.urlretrieve(url, "tmp/" + filename)
with open("tmp/" + filename, 'rb') as f:
return base64.b64encode(f.read()).decode("utf-8")
# read the CSS file into a string
with open('katex.min.css', 'r') as f:
css_string = f.read()
# define the regular expression pattern
pattern = r'url\((.*?)\) format\((.*?)\)'
# define a function to replace the matched pattern with the base64-encoded data URI
def replace_match(match):
url = match.group(1)
filename = url[6:]
format = match.group(2)
base64_code = encode_file_as_base64(filename)
return f'url(data:application/{filename};charset=utf-8;base64,{base64_code}) format({format})'
new_css_string = re.sub(pattern, replace_match, css_string)
with open('new_styles.css', 'w') as f:
f.write(new_css_string)

1237
katex.min.css vendored Normal file

File diff suppressed because it is too large Load diff

256
main.ts
View file

@ -1,156 +1,162 @@
import { App, Notice, Plugin, PluginSettingTab, Setting, MarkdownView, Editor, Modal } from 'obsidian';
import { Editor, MarkdownView, Plugin, Notice } from "obsidian";
import { PseudocodeSettingTab } from "src/setting_tab";
import { PseudocodeSuggestor } from "src/auto_complete";
import {
PseudocodeSettings,
DEFAULT_SETTINGS,
PseudocodeBlockInit,
BLOCK_NAME,
} from "src/setting";
import {
translateUnsupportedMacrosPerf,
checkTranslatedMacros,
} from "src/latex_translator";
import { createExportButton } from "src/export_button";
import { extractInlineMacros } from "src/inline_macro";
import { setObserver, detachObserver, setPseudocodeTheme } from "src/theme";
import * as pseudocode from "pseudocode";
// Remember to rename these classes and interfaces!
interface PseudocodeSettings {
indentSize: string,
commentDelimiter: string,
lineNumber: boolean,
lineNumberPunc: string,
noEnd: boolean,
captionCount: undefined
}
const DEFAULT_SETTINGS: PseudocodeSettings = {
indentSize: '1.2em',
commentDelimiter: '//',
lineNumber: false,
lineNumberPunc: ':',
noEnd: false,
captionCount: undefined
}
export default class PseudocodePlugin extends Plugin {
settings: PseudocodeSettings;
preamble: string = "";
async pseudocodeHandler(source: string, el: HTMLElement, ctx: any): Promise<any> {
// const rawRows: string[] = source.split("\n");
async pseudocodeHandler(
source: string,
el: HTMLElement,
ctx: any
): Promise<any> {
const blockDiv = el.createDiv({ cls: "pseudocode-block" });
const blockWidth = this.settings.blockSize;
blockDiv.style.width = `${blockWidth}em`;
blockDiv.addEventListener("click", (event) => {
const target = (event.target as HTMLElement | null);
if(target && target.tagName !== 'BUTTON')
(target
.closest('.pseudocode-block')
?.parentElement?.parentElement
?.querySelector('.edit-block-button') as HTMLElement | null)
?.click();
});
const katex = el.createEl("script");
katex.src = "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.11.1/katex.min.js";
katex.integrity = "sha256-F/Xda58SPdcUCr+xhSGz9MA2zQBPb0ASEYKohl8UCHc=";
katex.crossOrigin = "anonymous";
// Extract inline macros
const [inlineMacros, nonMacroLines] = extractInlineMacros(source);
const allPreamble = this.preamble + inlineMacros;
const preEl = el.createEl("pre", { cls: "code", text: source });
// find all $ enclosements in source, and add the preamble.
// TODO: Might be able to optimize.
const mathRegex = /\$(.*?)\$/g;
const withPreamble = nonMacroLines.replace(mathRegex, (_, group1) => {
return `$${allPreamble}${group1}$`;
});
console.log(el);
pseudocode.renderElement(preEl, this.settings);
const preEl = blockDiv.createEl("pre", {
cls: "code",
text: withPreamble,
});
try {
pseudocode.renderElement(preEl, this.settings.jsSettings);
createExportButton(this, blockDiv, inlineMacros, nonMacroLines);
} catch (error) {
console.log(error);
const errorSpan = blockDiv.createEl("span", {
text: "\u274C " + error.message,
});
errorSpan.classList.add("error-message");
blockDiv.empty();
blockDiv.appendChild(errorSpan);
}
// Set the pseudocode theme
if (this.settings.followSystemTheme) {
setPseudocodeTheme(blockDiv);
}
}
async onload() {
await this.loadSettings();
this.registerMarkdownCodeBlockProcessor("pcode", this.pseudocodeHandler.bind(this));
setObserver();
if (this.settings.preambleEnabled) {
console.log("Preamble is enabled.");
await this.loadPreamble();
}
this.registerMarkdownCodeBlockProcessor(
BLOCK_NAME,
this.pseudocodeHandler.bind(this)
);
// Register suggest
this.registerEditorSuggest(new PseudocodeSuggestor(this));
// This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new PseudocodeSettingTab(this.app, this));
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
// Auto-gen pseudocode block command.
this.addCommand({
id: "pseudocode-in-obs",
name: "Insert a new pseudocode block",
editorCallback: (editor: Editor, view: MarkdownView) => {
editor.replaceSelection(PseudocodeBlockInit);
},
});
}
onunload() {
detachObserver();
}
async loadPreamble() {
try {
this.preamble = await this.app.vault.adapter.read(
this.settings.preamblePath
);
} catch (error) {
console.log(error);
// Extract the search path from the error message.
const searchPath = error.message.match(/'(.*?)'/g)[0];
new Notice(
"Pseudocode Plugin: Preamble file not found at " +
searchPath +
"."
);
this.preamble = "";
return;
}
try {
this.preamble = translateUnsupportedMacrosPerf(this.preamble);
this.preamble = checkTranslatedMacros(this.preamble);
console.log("Loaded preamble:\n" + this.preamble);
console.log(
"Preamble file loaded. You can check the detail in console."
);
if (this.settings.preambleLoadedNotice) {
new Notice("Pseudocode Plugin: Preamble file loaded.");
}
} catch (error) {
console.log(error);
new Notice(
"Pseudocode Plugin: Preamble file contains invalid LaTeX. " +
"Please refer to console for details."
);
this.preamble = "";
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
}
async saveSettings() {
await this.saveData(this.settings);
}
}
class PseudocodeSettingTab extends PluginSettingTab {
plugin: PseudocodePlugin;
constructor(app: App, plugin: PseudocodePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h1', { text: 'Pseudocode Plugin Settings' });
// Instantiate Indent Size setting
new Setting(containerEl)
.setName("Indent Size")
.setDesc("The indent size of inside a control block, e.g. if, for, etc. The unit must be in 'em'.")
.addText(text => text
.setValue(this.plugin.settings.indentSize)
.onChange(async (value) => {
this.plugin.settings.indentSize = value;
await this.plugin.saveSettings();
})
);
// Instantiate Comment Delimiter setting
new Setting(containerEl)
.setName("Comment Delimiter")
.setDesc("The string used to indicate a comment in the pseudocode.")
.addText(text => text
.setValue(this.plugin.settings.commentDelimiter)
.onChange(async (value) => {
this.plugin.settings.commentDelimiter = value;
await this.plugin.saveSettings();
})
);
// Instantiate Show Line Numbers setting
new Setting(containerEl)
.setName("Show Line Numbers")
.setDesc("Whether line numbering is enabled.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.lineNumber)
.onChange(async (value) => {
this.plugin.settings.lineNumber = value;
await this.plugin.saveSettings();
})
);
// Instantiate Line Number Punctuation setting
new Setting(containerEl)
.setName("Line Number Punctuation")
.setDesc("The punctuation used to separate the line number from the pseudocode.")
.addText(text => text
.setValue(this.plugin.settings.lineNumberPunc)
.onChange(async (value) => {
this.plugin.settings.lineNumberPunc = value;
await this.plugin.saveSettings();
})
);
// Instantiate No End setting
new Setting(containerEl)
.setName("No End")
.setDesc("If enabled, pseudocode blocks will not have an 'end' statement.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.noEnd)
.onChange(async (value) => {
this.plugin.settings.noEnd = value;
await this.plugin.saveSettings();
})
);
// Instantiate Caption Count setting
// new Setting(containerEl)
// .setName("Caption Count")
// .setDesc("The number to start captioning pseudocode blocks from.")
// .addText(text => text
// .setValue(this.plugin.settings.captionCount)
// .onChange(async (value) => {
// this.plugin.settings.captionCount = value;
// await this.plugin.saveSettings();
// })
// );
}
}
``

View file

@ -1,11 +1,10 @@
{
"id": "obsidian-pseudocode",
"id": "pseudocode-in-obs",
"name": "Pseudocode",
"version": "1.0.0",
"version": "1.6.5",
"minAppVersion": "0.15.0",
"description": "This is an obsidian plugin that helps to render a LaTeX-style pseudocode inside a code block.",
"author": "Yaotian Liu",
"authorUrl": "https://github.com/Yaotian-Liu",
"fundingUrl": "",
"authorUrl": "https://github.com/ytliu74",
"isDesktopOnly": false
}

40
package-lock.json generated
View file

@ -1,17 +1,19 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"name": "pseudocode-in-obs",
"version": "1.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"name": "pseudocode-in-obs",
"version": "1.6.4",
"license": "MIT",
"dependencies": {
"pseudocode": "latest"
"katex": "0.11.1",
"pseudocode": "github:ytliu74/pseudocode.js#master"
},
"devDependencies": {
"@types/katex": "0.11.1",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
@ -545,6 +547,12 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
"node_modules/@types/katex": {
"version": "0.11.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-DUlIj2nk0YnJdlWgsFuVKcX27MLW0KbKmGVoUHmFr+74FYYNUDAaj9ZqTADvsbE8rfxuVmSFc7KczYn5Y09ozg==",
"dev": true
},
"node_modules/@types/node": {
"version": "16.18.14",
"resolved": "https://mirrors.cloud.tencent.com/npm/@types/node/-/node-16.18.14.tgz",
@ -915,6 +923,11 @@
"dev": true,
"peer": true
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://mirrors.cloud.tencent.com/npm/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz",
@ -1608,6 +1621,17 @@
"dev": true,
"peer": true
},
"node_modules/katex": {
"version": "0.11.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/katex/-/katex-0.11.1.tgz",
"integrity": "sha512-5oANDICCTX0NqYIyAiFCCwjQ7ERu3DQG2JFHLbYOf+fXaMoH8eg/zOq5WSYJsKMi/QebW+Eh3gSM+oss1H/bww==",
"dependencies": {
"commander": "^2.19.0"
},
"bin": {
"katex": "cli.js"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/levn/-/levn-0.4.1.tgz",
@ -1863,9 +1887,9 @@
}
},
"node_modules/pseudocode": {
"version": "2.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/pseudocode/-/pseudocode-2.2.0.tgz",
"integrity": "sha512-ZA2Tbrxntmwg2bywYbu5Ay4iWrb0F7104GoOqXaJ39lSZ2alE+xlLnLiJwkG+ztmLwKWI/brc3nAM7qk2kV90A=="
"version": "2.3.0",
"resolved": "git+ssh://git@github.com/ytliu74/pseudocode.js.git#db17188d06528b12b3279f4f1025c1a978519d1e",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.0",

View file

@ -1,17 +1,18 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "pseudocode-in-obs",
"version": "1.6.5",
"description": "This is an obsidian plugin that helps to render a LaTeX-style pseudocode inside a code block.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs"
},
"keywords": [],
"author": "",
"license": "MIT",
"devDependencies": {
"@types/katex": "0.11.1",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
@ -22,6 +23,7 @@
"typescript": "4.7.4"
},
"dependencies": {
"pseudocode": "latest"
"katex": "0.11.1",
"pseudocode": "github:ytliu74/pseudocode.js#master"
}
}
}

195
src/auto_complete.ts Normal file
View file

@ -0,0 +1,195 @@
import {
Editor,
EditorPosition,
EditorSuggest,
EditorSuggestContext,
EditorSuggestTriggerInfo,
TFile,
} from "obsidian";
import { BLOCK_NAME } from "src/setting";
import PseudocodePlugin from "main";
export class PseudocodeSuggestor extends EditorSuggest<string> {
plugin: PseudocodePlugin;
constructor(plugin: PseudocodePlugin) {
super(plugin.app);
this.plugin = plugin;
}
onTrigger(
cursor: EditorPosition,
editor: Editor,
file: TFile
): EditorSuggestTriggerInfo | null {
// perf: Use the "\" to tell whether to return.
const currentLineToCursor = editor
.getLine(cursor.line)
.slice(0, cursor.ch);
const currentLineLastWordStart = currentLineToCursor.lastIndexOf("\\");
// if there is no word, return null
if (currentLineLastWordStart === -1) return null;
// If is within a LaTeX $$ wrap, return null
const currentLineLastMoneyMark = currentLineToCursor.lastIndexOf("$");
if (currentLineLastMoneyMark != -1) return null;
const currentFileToCursor = editor.getRange({ line: 0, ch: 0 }, cursor);
const indexOfLastCodeBlockStart =
currentFileToCursor.lastIndexOf("```");
// check if this is a pseudocode block
const isPseudocode =
currentFileToCursor.slice(
indexOfLastCodeBlockStart + 3,
indexOfLastCodeBlockStart + 3 + BLOCK_NAME.length
) == BLOCK_NAME;
if (!isPseudocode) return null;
return {
start: { line: cursor.line, ch: currentLineLastWordStart },
end: cursor,
query: currentLineToCursor.slice(currentLineLastWordStart),
};
}
getSuggestions(
context: EditorSuggestContext
): string[] | Promise<string[]> {
const query = context.query;
const suggestions = this.pseudocodeKeywords.filter((value) =>
value.toLowerCase().startsWith(query.toLowerCase())
);
return suggestions;
}
renderSuggestion(value: string, el: HTMLElement): void {
el.addClass("suggestion");
const suggestContent = el.createDiv({ cls: "suggestion-content" });
suggestContent.setText(value);
}
selectSuggestion(value: string, evt: MouseEvent | KeyboardEvent): void {
if (this.context) {
const editor = this.context.editor;
const suggestion = value;
const start = this.context.start;
const end = editor.getCursor();
const pairSuggestion = this.pairSuggestions[suggestion];
let insertText = suggestion;
if (pairSuggestion) {
const line = editor.getLine(start.line);
const indentMatch = line.match(/^(\s*)/)?.[0] ?? "";
const indent = indentMatch.replace(/\t/g, " "); // replace each tab with four spaces
insertText += "\n" + indent + pairSuggestion;
}
editor.replaceRange(insertText, start, end);
const newCursor = end;
newCursor.ch = start.ch + suggestion.length;
editor.setCursor(newCursor);
this.close();
}
}
private pairSuggestions: Record<string, string> = {
"\\begin{algorithmic}": "\\end{algorithmic}",
"\\begin{algorithm}": "\\end{algorithm}",
"\\Procedure{}{}": "\\EndProcedure",
"\\Function{}{}": "\\EndFunction",
"\\For{}": "\\EndFor",
"\\ForAll{}": "\\EndFor",
"\\If{}": "\\EndIf",
"\\While{}": "\\EndWhile",
"\\Repeat": "\\Until{}",
// Add more pairs as needed
};
private pseudocodeKeywords: string[] = [
"\\begin{algorithmic}",
"\\begin{algorithm}",
"\\end{algorithmic}",
"\\end{algorithm}",
"\\caption{}",
"\\Procedure{}{}",
"\\EndProcedure",
"\\Function{}{}",
"\\EndFunction",
"\\Require",
"\\Ensure",
"\\Input",
"\\Output",
"\\State",
"\\Return",
"\\Print",
"\\For{}",
"\\ForAll{}",
"\\EndFor",
"\\If{}",
"\\Else",
"\\Elif{}",
"\\EndIf",
"\\While{}",
"\\EndWhile",
"\\Repeat",
"\\Continue",
"\\Break",
"\\Until{}",
"\\Comment{}",
"\\{",
"\\}",
"\\$",
"\\&",
"\\#",
"\\%",
"\\_",
"\\gets",
"\\Call{}{}",
"\\And",
"\\Or",
"\\Xor",
"\\Not",
"\\To",
"\\DownTo",
"\\True",
"\\False",
"\\tiny",
"\\scriptsize",
"\\footnotesize",
"\\small",
"\\normalsize",
"\\Large",
"\\Huge",
"\\rmfamily",
"\\sffamily",
"\\ttfamily",
"\\upshape",
"\\itshape",
"\\slshape",
"\\scshape",
"\\bfseries",
"\\mdseries",
"\\lfseries",
"\\textnormal{}",
"\\textrm{}",
"\\textsf{}",
"\\texttt{}",
"\\textup{}",
"\\textit{}",
"\\textsl{}",
"\\textsc{}",
"\\uppercase{}",
"\\lowercase{}",
"\\textbf{}",
"\\textmd{}",
"\\textlf{}",
];
}

90
src/export_button.ts Normal file
View file

@ -0,0 +1,90 @@
const BUTTON_INFO = "Export to clipboard";
const BUTTON_EXPORTED = "Exported!";
const BUTTON_FAILED = "Failed to export";
import PseudocodePlugin from "main";
export function createExportButton(
parentPlugin: PseudocodePlugin,
parentDiv: HTMLDivElement,
inlineMacros: string,
blockContent: string
) {
const button = parentDiv.createEl("button");
button.classList.add("hover-button");
button.textContent = BUTTON_INFO;
button.addEventListener("click", () => {
console.log(BUTTON_INFO);
if (blockContent !== null) {
const exportContent =
"\\documentclass{article}\n" +
macros(parentPlugin, inlineMacros) +
"\n" +
"\\begin{document}\n" +
processBlock(blockContent, parentPlugin) +
"\n\\end{document}";
navigator.clipboard
.writeText(exportContent)
.then(() => {
console.log("Copied to clipboard");
button.textContent = BUTTON_EXPORTED;
setTimeout(() => {
button.textContent = BUTTON_INFO;
}, 3000);
})
.catch((error) => {
console.error("Failed to copy to clipboard: ", error);
button.textContent = BUTTON_FAILED;
});
}
});
button.addEventListener("mouseover", () => {
button.textContent = BUTTON_INFO;
});
}
const macros = (parentPlugin: PseudocodePlugin, inlineMacros: string): string => {
const noEnd = parentPlugin.settings.jsSettings.noEnd;
const scopeLines = parentPlugin.settings.jsSettings.scopeLines;
// Split inline macros into lines and remove heading or trailing spaces
const inlineMacrosLine = inlineMacros.split("\n").map(line => line.trim());
return `
\\usepackage{algorithm}
\\usepackage[noEnd=${noEnd},indLines=${scopeLines}]{algpseudocodex}
\\newcommand{\\And}{\\textbf{and~}}
\\newcommand{\\Or}{\\textbf{or~}}
\\newcommand{\\Xor}{\\textbf{xor~}}
\\newcommand{\\Not}{\\textbf{not~}}
\\newcommand{\\To}{\\textbf{to~}}
\\newcommand{\\DownTo}{\\textbf{downto~}}
\\newcommand{\\True}{\\textbf{true~}}
\\newcommand{\\False}{\\textbf{false~}}
\\newcommand{\\Input}{\\item[\\textbf{Input:}]}
\\renewcommand{\\Output}{\\item[\\textbf{Output:}]}
\\newcommand{\\Print}{\\State \\textbf{print~}}
\\renewcommand{\\Return}{\\State \\textbf{return~}}
\\usepackage{amsmath}
${inlineMacrosLine}
`;
};
const processBlock = (
block: string,
parentPlugin: PseudocodePlugin
): string => {
if (parentPlugin.settings.jsSettings.lineNumber)
// Replace "\begin{algorithmic}" with "\begin{algorithmic}[1]"
block = block.replace(
"\\begin{algorithmic}",
"\\begin{algorithmic}[1]"
);
else;
return block;
};

24
src/inline_macro.ts Normal file
View file

@ -0,0 +1,24 @@
import {
translateUnsupportedMacrosPerf,
checkTranslatedMacros,
} from "./latex_translator";
export function extractInlineMacros(source: string): [string, string] {
const sourceLines = source.split("\n");
const macroStartIndex = sourceLines.findIndex(line => line.includes("\\begin{algorithm}"));
const macroLines = sourceLines.slice(0, macroStartIndex).join("\n");
const nonMacroLines = sourceLines.slice(macroStartIndex).join("\n");
let inlineMacros = "";
try {
const translated = translateUnsupportedMacrosPerf(macroLines);
inlineMacros = checkTranslatedMacros(translated);
} catch (error) {
console.error(error);
}
return [inlineMacros, nonMacroLines];
}

69
src/latex_translator.ts Normal file
View file

@ -0,0 +1,69 @@
import * as katex from "katex";
export function translateUnsupportedMacros(input: string): string {
// handle \DeclarePairedDelimiter
let output = input.replace(
/\\DeclarePairedDelimiter\{(.*?)\}\{(.*?)\}\{(.*?)\}/g,
"\\newcommand{$1}[1]{\\left$2 #1 \\right$3}"
);
// handle \DeclareMathOperator
output = output.replace(
/\\DeclareMathOperator\*\{(.*?)\}\{(.*?)\}/g,
"\\newcommand{$1}{\\mathop{\\mathrm{$2}}}"
);
output = output.replace(
/\\DeclareMathOperator\{(.*?)\}\{(.*?)\}/g,
"\\newcommand{$1}{\\mathop{\\mathrm{$2}}}"
);
return output;
}
// This is a performance improvement for translateUnsupportedMacros
export function translateUnsupportedMacrosPerf(input: string): string {
const stripped = input
.replace(/(?<!\\)%.*/gm, "")
.split("\n")
.filter((line) => line.trim() !== "")
.join("\n");
return stripped.replace(
/(\\DeclarePairedDelimiter\{(.*?)\}\{(.*?)\}\{(.*?)\})|(\\DeclareMathOperator\*\{(.*?)\}\{(.*?)\})|(\\DeclareMathOperator\{(.*?)\}\{(.*?)\})/g,
(match, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) => {
if (p1) {
// It's a \DeclarePairedDelimiter
return `\\newcommand{${p2}}[1]{\\left${p3} #1 \\right${p4}}`;
} else if (p5) {
// It's a \DeclareMathOperator*
return `\\newcommand{${p6}}{\\mathop{\\mathrm{${p7}}}}`;
} else if (p8) {
// It's a \DeclareMathOperator
return `\\newcommand{${p9}}{\\mathop{\\mathrm{${p10}}}}`;
} else {
// This should never happen if the regex is correct
console.error(`Unexpected match: ${match}`);
return match;
}
}
);
}
export function checkTranslatedMacros(input: string): string {
const lines = input.split("\n");
for (let i = 0; i < lines.length; i++) {
try {
katex.renderToString(lines[i]);
} catch (error) {
if (
error instanceof katex.ParseError &&
/redefine/.test(error.message)
) {
lines[i] = lines[i].replace("\\newcommand", "\\renewcommand");
console.log(`Redefining ${lines[i]}`);
} else {
throw error;
}
}
}
return lines.join("\n");
}

45
src/setting.ts Normal file
View file

@ -0,0 +1,45 @@
// This is the setting for pseudocode.js
export interface PseudocodeJsSettings {
indentSize: string;
commentDelimiter: string;
lineNumber: boolean;
lineNumberPunc: string;
noEnd: boolean;
captionCount: undefined;
scopeLines: boolean;
}
// Setting for this plugin
export interface PseudocodeSettings {
blockSize: number;
preambleEnabled: boolean;
preamblePath: string;
preambleLoadedNotice: boolean;
followSystemTheme: boolean;
jsSettings: PseudocodeJsSettings;
}
export const DEFAULT_SETTINGS: PseudocodeSettings = {
blockSize: 99,
preambleEnabled: false,
preamblePath: "preamble.sty",
preambleLoadedNotice: false,
followSystemTheme: false,
jsSettings: {
indentSize: "1.2em",
commentDelimiter: "//",
lineNumber: false,
lineNumberPunc: ":",
noEnd: false,
captionCount: undefined,
scopeLines: false,
},
};
export const PseudocodeBlockInit =
"```pseudo\n" +
"\t\\begin{algorithm}\n\t\\caption{Algo Caption}\n\t\\begin{algorithmic}" +
"\n\n\t\\end{algorithmic}\n\t\\end{algorithm}" +
"\n```";
export const BLOCK_NAME = "pseudo";

193
src/setting_tab.ts Normal file
View file

@ -0,0 +1,193 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import { setObserver, detachObserver } from "./theme";
import PseudocodePlugin from "main";
export class PseudocodeSettingTab extends PluginSettingTab {
plugin: PseudocodePlugin;
constructor(app: App, plugin: PseudocodePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h1", { text: "Pseudocode Plugin Settings" });
containerEl.createEl("h2", { text: "Render Behevior" });
// Instantiate Block Size setting
new Setting(containerEl)
.setName("Block Size")
.setDesc(
"The width of the pseudocode block. The unit is 'em'." +
" The default value is 99, which will work as the max width of the editor." +
" '30' looks good for me."
)
.addText((text) =>
text
.setValue(this.plugin.settings.blockSize.toString())
.onChange(async (value) => {
this.plugin.settings.blockSize = Number(value);
await this.plugin.saveSettings();
})
);
// Instantiate Follow System Theme setting
new Setting(containerEl)
.setName("Follow System Theme")
.setDesc("Whether to follow the system theme.")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.followSystemTheme)
.onChange(async (value) => {
this.plugin.settings.followSystemTheme = value;
if (value) {
setObserver();
} else {
detachObserver();
}
await this.plugin.saveSettings();
})
);
// Instantiate Indent Size setting
new Setting(containerEl)
.setName("Indent Size")
.setDesc(
"The indent size of inside a control block, e.g. if, for, etc. The unit must be in 'em'."
)
.addText((text) =>
text
.setValue(this.plugin.settings.jsSettings.indentSize)
.onChange(async (value) => {
this.plugin.settings.jsSettings.indentSize = value;
await this.plugin.saveSettings();
})
);
// Instantiate Comment Delimiter setting
new Setting(containerEl)
.setName("Comment Delimiter")
.setDesc("The string used to indicate a comment in the pseudocode.")
.addText((text) =>
text
.setValue(this.plugin.settings.jsSettings.commentDelimiter)
.onChange(async (value) => {
this.plugin.settings.jsSettings.commentDelimiter =
value;
await this.plugin.saveSettings();
})
);
// Instantiate Show Line Numbers setting
new Setting(containerEl)
.setName("Show Line Numbers")
.setDesc("Whether line numbering is enabled.")
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.jsSettings.lineNumber)
.onChange(async (value) => {
this.plugin.settings.jsSettings.lineNumber = value;
await this.plugin.saveSettings();
})
);
// Instantiate Line Number Punctuation setting
new Setting(containerEl)
.setName("Line Number Punctuation")
.setDesc(
"The punctuation used to separate the line number from the pseudocode."
)
.addText((text) =>
text
.setValue(this.plugin.settings.jsSettings.lineNumberPunc)
.onChange(async (value) => {
this.plugin.settings.jsSettings.lineNumberPunc = value;
await this.plugin.saveSettings();
})
);
// Instantiate No End setting
new Setting(containerEl)
.setName("No End")
.setDesc(
"If enabled, pseudocode blocks will not have an 'end' statement."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.jsSettings.noEnd)
.onChange(async (value) => {
this.plugin.settings.jsSettings.noEnd = value;
await this.plugin.saveSettings();
})
);
// Instantiate scope lines toggle setting
new Setting(containerEl)
.setName("Scope Lines")
.setDesc(
"If enabled, pseudocode blocks will have lines indicating the scope of the block."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.jsSettings.scopeLines)
.onChange(async (value) => {
this.plugin.settings.jsSettings.scopeLines = value;
await this.plugin.saveSettings();
})
);
containerEl.createEl("h2", { text: "Preamble Settings" });
// Instantiate Preamble Enabled setting
new Setting(containerEl)
.setName("Preamble Enabled")
.setDesc(
"Whether to load the preamble file. Please reload the plugin for this setting to take effect."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.preambleEnabled)
.onChange(async (value) => {
this.plugin.settings.preambleEnabled = value;
await this.plugin.saveSettings();
})
);
// Instantiate Preamble Path setting
new Setting(containerEl)
.setName("Preamble Path")
.setDesc(
"The path to the preamble file. The path is relative to the vault root."
)
.addText((text) =>
text
// .setValue(this.plugin.settings.preamblePath)
.setValue(this.plugin.settings.preamblePath)
.onChange(async (value) => {
this.plugin.settings.preamblePath = value;
await this.plugin.saveSettings();
})
);
// Instantiate Preamble Load Sign setting
new Setting(containerEl)
.setName("Preamble Loaded Notice")
.setDesc(
"Whether to show a notice everytime the preamble is loaded."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.preambleLoadedNotice)
.onChange(async (value) => {
this.plugin.settings.preambleLoadedNotice = value;
await this.plugin.saveSettings();
})
);
}
}

77
src/theme.ts Normal file
View file

@ -0,0 +1,77 @@
// Reference: https://forum.obsidian.md/t/obsidian-publish-api-how-to-track-changing-theme/73637/4
export const themeObserver = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
const target = mutation.target as HTMLElement;
if (
// dark -> dark & light -> light
mutation.oldValue?.contains("theme-dark") &&
!mutation.oldValue?.contains("theme-light") && // key line, avoid calling twice
target.classList.value.contains("theme-light")
) {
console.log("light theme detected");
setPseudocodeTheme();
} else if (
// light -> empty -> dark
mutation.oldValue?.contains("theme-light") && // key line, avoid calling twice
!mutation.oldValue?.contains("theme-dark") &&
target.classList.value.contains("theme-dark")
) {
console.log("dark theme detected");
setPseudocodeTheme();
}
});
});
export function setPseudocodeTheme(psBlock?: Element) {
const bodyElement = document.body;
const backgroundValue = getComputedStyle(bodyElement)
.getPropertyValue("--background-primary")
.trim();
const fontValue = getComputedStyle(bodyElement)
.getPropertyValue("--text-normal")
.trim();
console.log(backgroundValue, fontValue);
const psRootElements = psBlock
? psBlock.querySelectorAll(".ps-root")
: document.querySelectorAll(".ps-root");
// console.log(psRootElements);
// Loop through each element and modify the CSS properties
psRootElements.forEach((element) => {
const htmlElement = element as HTMLElement;
htmlElement.style.backgroundColor = backgroundValue;
htmlElement.style.opacity = "1";
htmlElement.style.color = fontValue;
// Change border colors for .ps-algorithm and .ps-algorithm.with-caption > .ps-line:first-child
const algorithmElements = htmlElement.querySelectorAll(".ps-algorithm");
algorithmElements.forEach((algElement) => {
const algHtmlElement = algElement as HTMLElement;
algHtmlElement.style.borderTopColor = fontValue;
algHtmlElement.style.borderBottomColor = fontValue;
});
const lineElements = htmlElement.querySelectorAll(
".ps-algorithm.with-caption > .ps-line:first-child"
);
lineElements.forEach((lineElement) => {
const lineHtmlElement = lineElement as HTMLElement;
lineHtmlElement.style.borderBottomColor = fontValue;
});
});
}
export const setObserver = () => {
themeObserver.observe(document.body, {
attributes: true,
attributeOldValue: true,
attributeFilter: ["class"],
});
};
export const detachObserver = () => {
themeObserver.disconnect();
};

1483
styles.css

File diff suppressed because one or more lines are too long

View file

@ -1,3 +1,41 @@
{
"1.0.0": "0.15.0"
}
"1.0.0": "0.15.0",
"1.1.0": "0.15.0",
"1.1.1": "0.15.0",
"1.1.2": "0.15.0",
"1.1.3": "0.15.0",
"1.1.4": "0.15.0",
"1.1.5": "0.15.0",
"1.1.6": "0.15.0",
"1.1.7": "0.15.0",
"1.1.8": "0.15.0",
"1.1.9": "0.15.0",
"1.1.10": "0.15.0",
"1.1.11": "0.15.0",
"1.2.0": "0.15.0",
"1.2.1": "0.15.0",
"1.2.2": "0.15.0",
"1.2.3": "0.15.0",
"1.3.0": "0.15.0",
"1.3.1": "0.15.0",
"1.4.0": "0.15.0",
"1.4.1": "0.15.0",
"1.4.2": "0.15.0",
"1.4.3": "0.15.0",
"1.4.4": "0.15.0",
"1.5.0": "0.15.0",
"1.5.1": "0.15.0",
"1.5.2": "0.15.0",
"1.5.3": "0.15.0",
"1.5.4": "0.15.0",
"1.5.5": "0.15.0",
"1.5.6": "0.15.0",
"1.5.7": "0.15.0",
"1.5.8": "0.15.0",
"1.6.0": "0.15.0",
"1.6.1": "0.15.0",
"1.6.2": "0.15.0",
"1.6.3": "0.15.0",
"1.6.4": "0.15.0",
"1.6.5": "0.15.0"
}