mirror of
https://github.com/ytliu74/obsidian-pseudocode.git
synced 2026-07-22 07:40:25 +00:00
Compare commits
76 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ace9075db | ||
|
|
6adbd70bef | ||
|
|
935403182e | ||
|
|
91df3ded05 | ||
|
|
4bb26746dd | ||
|
|
a9aed6f856 | ||
|
|
1c3846f0a2 | ||
|
|
100c1c5bad | ||
|
|
6614713042 | ||
|
|
1acc571cae | ||
|
|
d677e7a5d2 | ||
|
|
47f5b562c7 | ||
|
|
13be11b65a | ||
|
|
bdec504edc | ||
|
|
882556f4e3 | ||
|
|
94432ec1aa | ||
|
|
81bc30282f | ||
|
|
3f28ab42b2 | ||
|
|
dd112a5b5f | ||
|
|
9828188724 | ||
|
|
11b9a7e72f | ||
|
|
5ae081a50e | ||
|
|
e6b4375c25 | ||
|
|
6001c0f6d8 | ||
|
|
731961466d | ||
|
|
d73fc6a663 | ||
|
|
1eca967479 | ||
|
|
5e1d57922d | ||
|
|
f6e04024f6 | ||
|
|
1d0be42123 | ||
|
|
2f0ac81165 | ||
|
|
d1bdd16fb5 | ||
|
|
a0ae3321c9 | ||
|
|
743cc7652e | ||
|
|
b8ef48f07e | ||
|
|
e797755282 | ||
|
|
1b6dda0929 | ||
|
|
4be888e236 | ||
|
|
5aa1faec57 | ||
|
|
d5c13357bf | ||
|
|
56485b47a8 | ||
|
|
2aa4bae01b | ||
|
|
0e09e52568 | ||
|
|
776414c042 | ||
|
|
31a2475fdf | ||
|
|
7c72b8caa9 | ||
|
|
0e4a8e273a | ||
|
|
ae392a2fdd | ||
|
|
ffb1a15cb9 | ||
|
|
f163b33861 | ||
|
|
5b83128ca8 | ||
|
|
3e656c81b8 | ||
|
|
f7a14564f7 | ||
|
|
c21e3c7704 | ||
|
|
65feca5f69 | ||
|
|
7363aa6a03 | ||
|
|
fdcc7d5405 | ||
|
|
53fde479fd | ||
|
|
e2662f15b0 | ||
|
|
4cab4a02c4 | ||
|
|
f4a09bbaff | ||
|
|
2f44b862d9 | ||
|
|
fce4261a21 | ||
|
|
f4d31a05c5 | ||
|
|
6b02497375 | ||
|
|
874fdbb489 | ||
|
|
dfeb6ad4d1 | ||
|
|
8d014eeb96 | ||
|
|
e7a4897b82 | ||
|
|
21d86e2ffc | ||
|
|
53083f74e9 | ||
|
|
91acb139bb | ||
|
|
9447807091 | ||
|
|
995fa7d6de | ||
|
|
fdb8218805 | ||
|
|
ad6ffb5e33 |
18 changed files with 1071 additions and 346 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -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
94
CLAUDE.md
Normal 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
101
README.md
|
|
@ -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
394
main.ts
|
|
@ -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{}",
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
17
package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -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
195
src/auto_complete.ts
Normal 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
90
src/export_button.ts
Normal 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
24
src/inline_macro.ts
Normal 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
69
src/latex_translator.ts
Normal 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
45
src/setting.ts
Normal 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
193
src/setting_tab.ts
Normal 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
77
src/theme.ts
Normal 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();
|
||||
};
|
||||
64
styles.css
64
styles.css
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue