Compare commits

..

36 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
15 changed files with 330 additions and 41 deletions

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

View file

@ -5,6 +5,9 @@
- [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)
@ -17,8 +20,9 @@ This is a plugin for [Obsidian](https://obsidian.md/) that allows you to render
- 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)
- [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
@ -71,13 +75,24 @@ This will be rendered as (to render line number, you need to toggle it in settin
### Preamble style customization
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`. Currently supported macros can be found at [this link](https://katex.org/docs/supported.html#macros) and below(might not be fully supported):
#### 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`
Please reload the plugin after you change the preamble file.
### Export to a compilable LaTeX file

23
main.ts
View file

@ -14,6 +14,7 @@ import {
} 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";
@ -29,6 +30,15 @@ export default class PseudocodePlugin extends Plugin {
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);
@ -48,7 +58,7 @@ export default class PseudocodePlugin extends Plugin {
try {
pseudocode.renderElement(preEl, this.settings.jsSettings);
createExportButton(this, blockDiv, source);
createExportButton(this, blockDiv, inlineMacros, nonMacroLines);
} catch (error) {
console.log(error);
const errorSpan = blockDiv.createEl("span", {
@ -58,11 +68,18 @@ export default class PseudocodePlugin extends Plugin {
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();
@ -89,7 +106,9 @@ export default class PseudocodePlugin extends Plugin {
});
}
onunload() {}
onunload() {
detachObserver();
}
async loadPreamble() {
try {

View file

@ -1,7 +1,7 @@
{
"id": "pseudocode-in-obs",
"name": "Pseudocode",
"version": "1.5.0",
"version": "1.6.5",
"minAppVersion": "0.15.0",
"description": "This is an obsidian plugin that helps to render a LaTeX-style pseudocode inside a code block.",
"author": "Yaotian Liu",

8
package-lock.json generated
View file

@ -1,16 +1,16 @@
{
"name": "pseudocode-in-obs",
"version": "1.5.0",
"version": "1.6.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pseudocode-in-obs",
"version": "1.5.0",
"version": "1.6.4",
"license": "MIT",
"dependencies": {
"katex": "0.11.1",
"pseudocode": "ytliu74/pseudocode.js#master"
"pseudocode": "github:ytliu74/pseudocode.js#master"
},
"devDependencies": {
"@types/katex": "0.11.1",
@ -1888,7 +1888,7 @@
},
"node_modules/pseudocode": {
"version": "2.3.0",
"resolved": "git+ssh://git@github.com/ytliu74/pseudocode.js.git#4ac293a977b191d66b3cba640804814651c95aaf",
"resolved": "git+ssh://git@github.com/ytliu74/pseudocode.js.git#db17188d06528b12b3279f4f1025c1a978519d1e",
"license": "MIT"
},
"node_modules/punycode": {

View file

@ -1,6 +1,6 @@
{
"name": "pseudocode-in-obs",
"version": "1.5.0",
"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": {
@ -12,6 +12,7 @@
"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",
@ -19,11 +20,10 @@
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4",
"@types/katex": "0.11.1"
"typescript": "4.7.4"
},
"dependencies": {
"pseudocode": "ytliu74/pseudocode.js#master",
"katex": "0.11.1"
"katex": "0.11.1",
"pseudocode": "github:ytliu74/pseudocode.js#master"
}
}

View file

@ -81,7 +81,16 @@ export class PseudocodeSuggestor extends EditorSuggest<string> {
const start = this.context.start;
const end = editor.getCursor();
editor.replaceRange(suggestion, start, end);
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;
@ -91,6 +100,19 @@ export class PseudocodeSuggestor extends EditorSuggest<string> {
}
}
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}",
@ -109,13 +131,17 @@ export class PseudocodeSuggestor extends EditorSuggest<string> {
"\\Return",
"\\Print",
"\\For{}",
"\\ForAll{}",
"\\EndFor",
"\\If{}",
"\\Else",
"\\Elif{}",
"\\EndIf",
"\\While{}",
"\\EndWhile",
"\\Repeat",
"\\Continue",
"\\Break",
"\\Until{}",
"\\Comment{}",
"\\{",

View file

@ -7,6 +7,7 @@ import PseudocodePlugin from "main";
export function createExportButton(
parentPlugin: PseudocodePlugin,
parentDiv: HTMLDivElement,
inlineMacros: string,
blockContent: string
) {
const button = parentDiv.createEl("button");
@ -17,7 +18,7 @@ export function createExportButton(
if (blockContent !== null) {
const exportContent =
"\\documentclass{article}\n" +
macros(parentPlugin) +
macros(parentPlugin, inlineMacros) +
"\n" +
"\\begin{document}\n" +
processBlock(blockContent, parentPlugin) +
@ -44,10 +45,13 @@ export function createExportButton(
});
}
const macros = (parentPlugin: PseudocodePlugin): string => {
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}
@ -64,6 +68,9 @@ const macros = (parentPlugin: PseudocodePlugin): string => {
\\renewcommand{\\Output}{\\item[\\textbf{Output:}]}
\\newcommand{\\Print}{\\State \\textbf{print~}}
\\renewcommand{\\Return}{\\State \\textbf{return~}}
\\usepackage{amsmath}
${inlineMacrosLine}
`;
};

View file

@ -4,23 +4,21 @@ import {
} from "./latex_translator";
export function extractInlineMacros(source: string): [string, string] {
const lines = source.split("\n");
const sourceLines = source.split("\n");
const macroStartIndex = sourceLines.findIndex(line => line.includes("\\begin{algorithm}"));
let i: number;
for (i = 0; i < lines.length; i++) {
if (lines[i].includes("\\begin{algorithm}")) break;
}
const macroLines = sourceLines.slice(0, macroStartIndex).join("\n");
const nonMacroLines = sourceLines.slice(macroStartIndex).join("\n");
const macroLines = lines.slice(0, i).join("\n");
const nonMacroLines = lines.slice(i).join("\n");
let inlineMacros = "";
let inlineMacros = "";
try {
const translated = translateUnsupportedMacrosPerf(macroLines);
inlineMacros = checkTranslatedMacros(translated);
} catch (error) {
console.error(error);
}
try {
const translated = translateUnsupportedMacrosPerf(macroLines);
inlineMacros = checkTranslatedMacros(translated);
} catch (error) {
return [inlineMacros, nonMacroLines];
console.error(error);
}
return [inlineMacros, nonMacroLines];
}

View file

@ -15,6 +15,7 @@ export interface PseudocodeSettings {
preambleEnabled: boolean;
preamblePath: string;
preambleLoadedNotice: boolean;
followSystemTheme: boolean;
jsSettings: PseudocodeJsSettings;
}
@ -23,6 +24,7 @@ export const DEFAULT_SETTINGS: PseudocodeSettings = {
preambleEnabled: false,
preamblePath: "preamble.sty",
preambleLoadedNotice: false,
followSystemTheme: false,
jsSettings: {
indentSize: "1.2em",
commentDelimiter: "//",

View file

@ -1,4 +1,5 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import { setObserver, detachObserver } from "./theme";
import PseudocodePlugin from "main";
@ -36,6 +37,24 @@ export class PseudocodeSettingTab extends PluginSettingTab {
})
);
// 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")

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 {
@ -1463,3 +1473,9 @@ If your plugin does not need CSS, delete this file.
.pseudocode-block:hover .hover-button {
display: block;
}
.ps-root {
background-color: white;
opacity: 1;
color: black;
}

View file

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