Compare commits

..

76 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
18 changed files with 1071 additions and 346 deletions

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"
}
}

View file

@ -21,7 +21,9 @@ jobs:
node-version: "18"
- name: Install Dependencies
run: npm install
run: |
npm config set registry https://registry.npmjs.org/
npm install
- name: Build
run: npm run build

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

101
README.md
View file

@ -1,12 +1,16 @@
# Obsidian-Pseudocode
- [Features](#features)
- [Future Features](#future-features)
- [Usage](#usage)
- [Installation](#installation)
- [Install from the Community Plugins in Obsidian.](#install-from-the-community-plugins-in-obsidian)
- [Use BRAT](#use-brat)
- [Manual install](#manual-install)
- [Credits](#credits)
- [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.
@ -15,14 +19,19 @@ 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.
- [x] Auto-completion inside `pseudo` code block. (Release 1.1.0)
## Usage
### 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.**
@ -34,41 +43,69 @@ Here is an example:
\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
### Install from the Community Plugins in Obsidian.
<!-- ### Install from the Community Plugins in Obsidian. -->
: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)
<!-- ### 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`.
@ -81,12 +118,10 @@ This will be rendered as:
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.
4. Enjoy. -->
<!-- ## Known Issues -->
## Credits
This plugin is based on [pseudocode.js](https://github.com/SaswatPadhi/pseudocode.js), a JavaScript library that typesets pseudocode beautifully to HTML. Many thanks to the pseudocode.js team for their great work!

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 105 KiB

394
main.ts
View file

@ -1,66 +1,90 @@
import { Editor, MarkdownView, Plugin, Notice } from "obsidian";
import { PseudocodeSettingTab } from "src/setting_tab";
import { PseudocodeSuggestor } from "src/auto_complete";
import {
App,
Editor,
EditorPosition,
EditorSuggest,
EditorSuggestContext,
EditorSuggestTriggerInfo,
MarkdownView,
Plugin,
PluginSettingTab,
Setting,
TFile,
} from "obsidian";
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";
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,
};
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```";
const BLOCK_NAME = "pseudo";
export default class PseudocodePlugin extends Plugin {
settings: PseudocodeSettings;
preamble: string = "";
async pseudocodeHandler(
source: string,
el: HTMLElement,
ctx: any
): Promise<any> {
const preEl = el.createEl("pre", { cls: "code", text: source });
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();
});
// Extract inline macros
const [inlineMacros, nonMacroLines] = extractInlineMacros(source);
const allPreamble = this.preamble + inlineMacros;
// 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}$`;
});
const preEl = blockDiv.createEl("pre", {
cls: "code",
text: withPreamble,
});
try {
pseudocode.renderElement(preEl, this.settings);
pseudocode.renderElement(preEl, this.settings.jsSettings);
createExportButton(this, blockDiv, inlineMacros, nonMacroLines);
} catch (error) {
console.log(error);
const errorSpan = el.createEl("span", { text: error.message });
const errorSpan = blockDiv.createEl("span", {
text: "\u274C " + error.message,
});
errorSpan.classList.add("error-message");
el.empty();
el.appendChild(errorSpan);
blockDiv.empty();
blockDiv.appendChild(errorSpan);
}
// Set the pseudocode theme
if (this.settings.followSystemTheme) {
setPseudocodeTheme(blockDiv);
}
}
async onload() {
await this.loadSettings();
setObserver();
if (this.settings.preambleEnabled) {
console.log("Preamble is enabled.");
await this.loadPreamble();
}
this.registerMarkdownCodeBlockProcessor(
BLOCK_NAME,
this.pseudocodeHandler.bind(this)
@ -82,7 +106,47 @@ export default class PseudocodePlugin extends Plugin {
});
}
onunload() { }
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(
@ -96,251 +160,3 @@ export default class PseudocodePlugin extends Plugin {
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();
})
);
}
}
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();
editor.replaceRange(suggestion, start, end);
const newCursor = end;
newCursor.ch = start.ch + suggestion.length;
editor.setCursor(newCursor);
this.close();
}
}
private pseudocodeKeywords: string[] = [
"\\begin{algorithmic}",
"\\begin{algorithm}",
"\\end{algorithmic}",
"\\end{algorithm}",
"\\caption{}",
"\\PROCEDURE{}{}",
"\\ENDPROCEDURE",
"\\FUNCTION{}{}",
"\\ENDFUNCTION",
"\\REQUIRE",
"\\ENSURE",
"\\INPUT",
"\\OUTPUT",
"\\STATE",
"\\RETURN",
"\\PRINT",
"\\FOR{}",
"\\ENDFOR",
"\\IF{}",
"\\ELIF{}",
"\\ENDIF",
"\\WHILE{}",
"\\ENDWHILE",
"\\REPEAT",
"\\UNTIL{}",
"\\COMMENT{}",
"\\{",
"\\}",
"\\$",
"\\&",
"\\#",
"\\%",
"\\_",
"\\gets",
"\\CALL{}{}",
"\\AND",
"\\OR",
"\\XOR",
"\\NOT",
"\\TO",
"\\DOWNTO",
"\\TRUE",
"\\FALSE",
"\\tiny",
"\\scriptsize",
"\\footnotesize",
"\\small",
"\\normalsize",
"\\large",
"\\Large",
"\\LARGE",
"\\huge",
"\\HUGE",
"\\rmfamily",
"\\sffamily",
"\\ttfamily",
"\\upshape",
"\\itshape",
"\\slshape",
"\\scshape",
"\\bfseries",
"\\mdseries",
"\\lfseries",
"\\textnormal{}",
"\\textrm{}",
"\\textsf{}",
"\\texttt{}",
"\\textup{}",
"\\textit{}",
"\\textsl{}",
"\\textsc{}",
"\\uppercase{}",
"\\lowercase{}",
"\\textbf{}",
"\\textmd{}",
"\\textlf{}",
];
}

View file

@ -1,10 +1,10 @@
{
"id": "pseudocode-in-obs",
"name": "Pseudocode",
"version": "1.1.7",
"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",
"authorUrl": "https://github.com/ytliu74",
"isDesktopOnly": false
}

17
package-lock.json generated
View file

@ -1,18 +1,19 @@
{
"name": "pseudocode-in-obs",
"version": "1.1.6",
"version": "1.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pseudocode-in-obs",
"version": "1.1.6",
"version": "1.6.4",
"license": "MIT",
"dependencies": {
"katex": "0.11.1",
"pseudocode": "Yaotian-Liu/pseudocode.js#master"
"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",
@ -546,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",
@ -1880,8 +1887,8 @@
}
},
"node_modules/pseudocode": {
"version": "2.1.1",
"resolved": "git+ssh://git@github.com/Yaotian-Liu/pseudocode.js.git#040c7ca05af0dc3e568d9b2831bf17f4fd00da8f",
"version": "2.3.0",
"resolved": "git+ssh://git@github.com/ytliu74/pseudocode.js.git#db17188d06528b12b3279f4f1025c1a978519d1e",
"license": "MIT"
},
"node_modules/punycode": {

View file

@ -1,17 +1,18 @@
{
"name": "pseudocode-in-obs",
"version": "1.1.7",
"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,7 +23,7 @@
"typescript": "4.7.4"
},
"dependencies": {
"pseudocode": "Yaotian-Liu/pseudocode.js#master",
"katex": "0.11.1"
"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();
};

View file

@ -1399,12 +1399,22 @@ If your plugin does not need CSS, delete this file.
text-transform: none;
}
.ps-root .ps-comment {
font-family: KaTeX_Main, "Times New Roman", Times, serif;
font-weight: 400;
font-variant: normal;
font-style: normal;
text-transform: none;
.ps-root {
font-family: KaTeX_Main, "Times New Roman", Times, serif;
font-weight: 400;
font-variant: normal;
font-style: normal;
text-transform: none;
}
.ps-comment {
font-family: KaTeX_Main, "Times New Roman", Times, serif;
font-weight: 400;
font-variant: normal;
font-style: italic;
text-transform: none;
opacity: 70%;
float: right;
}
.ps-root .ps-linenum {
@ -1425,11 +1435,47 @@ If your plugin does not need CSS, delete this file.
text-indent: 0;
}
/* scope lines support */
.ps-root .ps-algorithmic.with-scopelines div.ps-block {
border-left-style: solid;
border-left-width: 0.1em;
padding-left: 0.6em;
}
.ps-root .ps-algorithmic.with-scopelines > div.ps-block {
border-left: none;
}
.error-message {
font-family: var(--font-monospace);
font-size: 0.875rem;
text-align: center;
border: 2px dashed red;
padding: 7px;
border: none;
box-shadow: 0 0 0.4rem 0.1rem #f00;
padding: 0.2rem;
margin: 1rem 4rem;
display: inline-block;
}
}
.pseudocode-block {
position: relative;
max-width: 100%;
box-sizing: border-box;
margin: auto;
}
.hover-button {
position: absolute;
right: 0;
bottom: 0;
display: none;
}
.pseudocode-block:hover .hover-button {
display: block;
}
.ps-root {
background-color: white;
opacity: 1;
color: black;
}

View file

@ -7,5 +7,35 @@
"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.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"
}