Merge commit '4a4941e4be86b9fc6b879af7598e7c3d69dd0c65'
3
.gitignore
vendored
|
|
@ -30,4 +30,5 @@ build
|
|||
|
||||
# ignored
|
||||
.ignored
|
||||
*.ignored
|
||||
*.ignored
|
||||
*.svg
|
||||
182
README.md
|
|
@ -1,132 +1,147 @@
|
|||
# Extended Markdown Syntax - Obsidian Plugin
|
||||
|
||||
Provides some alternatives for inline formatting using non-standard syntaxes instead of using html tags, such as underline, superscript, and much more.
|
||||

|
||||
|
||||
You can easily create text that is <ins>underlined</ins>, <sup>superscripted</sup>, or <sub>subscripted</sub> without any pain of writing html tags. And don't forget, this plugin supports both modes: editor mode and preview mode.
|
||||
Provides some alternatives for inline and block formatting using non-standard syntaxes instead of using html tags, such as underline, superscript, and much more.
|
||||
|
||||

|
||||
## 🚀 Main Features
|
||||
|
||||
## Features
|
||||
|
||||
- Extends your Markdown with some inline syntax which aren't available in the native Obsidian syntax, including:
|
||||
- Extended inline syntax, including:
|
||||
- insertion (underline),
|
||||
- Discord-flavored spoiler,
|
||||
- Pandoc-style superscript and subscript,
|
||||
- custom color tag for highlight, and
|
||||
- custom span with ability to insert your own class(es).
|
||||
- _Modified_ Pandoc-style fenced div syntax for customizing block-level format.
|
||||
- Uses non-prerendered Markdown preview when formatting such syntax in live-preview mode, gives a more intuitive impression in such a way.
|
||||
- Be aware of inline and block context, so syntax won't be parsed while it encounters such a codeblock, codespan, or context boundary.
|
||||
- _Modified_ Pandoc-style fenced div for block-level syntax.
|
||||
- Edit formatted text directly without omitting its style, in contrast to using HTML tags.
|
||||
- Context-aware, syntax won't be parsed while it encounters such a codeblock, codespan, or context boundary.
|
||||
- Quick format with commands and context menu.
|
||||
- Has an ability to customize the color of the highlight with your own desire through settings.
|
||||
- Provides color menu for highlight, improves more efficient way to change the color.
|
||||
- Provides tag menu for custom span and fenced div.
|
||||
- Toggling specific syntax on and off through the settings.
|
||||
- Configurable tag display behaviour for the tag of the highlight, custom span, and fenced div.
|
||||
- Supports parsing exported PDF (can be switched off in the settings).
|
||||
- Toggle specific syntax on and off.
|
||||
- Customize your own color tags for custom highlight.
|
||||
- Predefine your own tags for custom span and fenced div.
|
||||
- Supports rendering exported PDF.
|
||||
|
||||
## Usage
|
||||
## ✍️ Usage
|
||||
|
||||
### 1. Inline Formatting
|
||||
|
||||
There is four formatting types that currently developed in this plugin: **insertion** (underline), **Discord-flavoured spoiler**, **Pandoc-flavoured superscript and subscript**, custom tag for ==highlight==, and custom span that can be inserted with your own class(es).
|
||||
There are six inline formattings that currently developed in this plugin:
|
||||
|
||||
| Type | Syntax | Result |
|
||||
| ----------- | ---------------------- | ------------------------------------------------------------- |
|
||||
| insertion | `++your text++` | <ins>your text</ins> |
|
||||
| spoiler | `\|\|your text\|\|` | <span style="background: #2e2e2e">your text</span> |
|
||||
| spoiler | `\|\|your text\|\|` | <span style="background:#2e2e2e">your text</span> |
|
||||
| superscript | `^your-text^` | <sup>your-text</sup> |
|
||||
| subscript | `~your-text~` | <sub>your-text</sub> |
|
||||
| highlight | `=={color}your text==` | <mark>your text</mark> |
|
||||
| custom span | `!!{myCls}your text!!` | your text (should be rendered with the `myCls` class defined) |
|
||||
|
||||
By default, **insertion** give the text underline style, **spoiler** hide the text and can be revealed by clicking it (or hovering over it in editor mode), while **superscript** and **subscript** make the text being raised or lowered as `<sup>` and `<sub>` do.
|
||||
By default:
|
||||
- **insertion** give the text underline style,
|
||||
- **spoiler** hide the text and can be revealed by clicking it (or hovering over it in editor mode),
|
||||
- **superscript** and **subscript** make the text being raised or lowered as `<sup>` and `<sub>` do.
|
||||
|
||||
The main advantage of using those syntaxes over html tags is that those syntaxes are rendered properly in the editor alongside other built-in syntaxes. So you can combine them without blocking other style being rendered in editor, in stark contrast to the html tags, for instance `this *is ++italic-underlined text++*`.
|
||||
|
||||
To give those formats, you can wrap the text with specific delimiters, covering it on both sides (see the table above). Additionally, for the highlight and custom span, you may insert a tag right after the opening delimiter, specifying the color for the highlight, and the classes for the custom span.
|
||||
|
||||
For ease access, in the editor, the color button will appear after the opening delimiter when the cursor or selection touches the highlight. Clicking on it shows colors menu and let us choose the desired color (you can disable the color botton in settings, and thus will be hidden in the editor).
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> You can use typing-helper plugins such as [Easy Typing](https://github.com/Yaozhuwa/easy-typing-obsidian) to make formatting faster and efficient.
|
||||
Additionally, for the **highlight** and **custom span**, you may insert a tag right after the opening delimiter, specifying the color for the highlight, and the classes for the custom span.
|
||||
|
||||
### 2. Block Formatting
|
||||
|
||||
Currenty, this plugin only support one type of block syntax, that's Pandoc-style fenced div (with some modifications).
|
||||
Currenty, this plugin only support **Pandoc-style fenced div** with some modifications. You only need an opening delimiter (three consecutive colons at least) to start the syntax. Blank line or the end of the document will act as a syntax closing. No need of closing delimiter.
|
||||
|
||||
```markdown
|
||||
::: my-class-1 my-class-2
|
||||
This is fenced div content.
|
||||
|
||||
::: another-class
|
||||
Another fenced div content.
|
||||
```
|
||||
|
||||
But it has been modified in this plugin to become a way more restricted, avoiding conflict with the built-in syntax. So, you will only need an opening delimiter to start this format in the concerned text (closing delimiter isn't needed). Optionally, you can insert a tag containing your own classes, which then can be styled through the CSS snippet.
|
||||
### 3. Customizable Highlight
|
||||
|
||||
For example:
|
||||
Under "Custom highlight" section in the settings, you can:
|
||||
|
||||
```css
|
||||
/* Your CSS snippet */
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
- **show the color button**, providing color menu when you click on it,
|
||||
- **adjust opacity** of the highlight,
|
||||
- **customize color palettes** based on its tag:
|
||||
- first field for its name displayed in the color menu,
|
||||
- second field for its tag inserted into the highlight syntax,
|
||||
- show or hide each palette from the color menu,
|
||||
- rearrange colors by dragging 6-dots icon on the left side.
|
||||
|
||||
.medium-size {
|
||||
font-size: 24px;
|
||||
}
|
||||
<details>
|
||||
<img src="docs/assets/custom-highlight.gif" alt="custom-highlight.gif"/>
|
||||
</details>
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
```
|
||||
### 4. Predefined Tags
|
||||
|
||||
You can apply those rules above like this.
|
||||
In the settings, you can predefine specific tags for **custom span** and **fenced div**, then can be displayed in the tag menu. Same as the color palattes, you can set their name and arrange them.
|
||||
|
||||
```markdown
|
||||
::: align-center medium-size monospace
|
||||
Center-aligned text
|
||||
with medium font size and monospace style.
|
||||
```
|
||||
### 5. Commands and Context Menu
|
||||
|
||||
This plugin provides commands to toggle each formatting type, also commands to show the color and tag menu. You can set the keymap for each of them through "Hotkeys" in the settings.
|
||||
|
||||
It also brings functionality of all those commands (except for fenced div) to the context menu, by right-clicking on the editor and choosing "More format".
|
||||
|
||||
<details>
|
||||
<img src="docs/assets/commands.gif" alt="commands.gif"/>
|
||||
</details>
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> Custom align no longer available in this plugin, because it is already covered by the fenced div.
|
||||
> In mobile devices, those commands can be added into the toolbar.
|
||||
|
||||
### 3. Commands and Context Menu
|
||||
### 6. Tidier Formatting
|
||||
|
||||
This plugin provides commands to toggle each formatting type on and off, also commands to show the color and tag menu for each of highlight, custom span, and fenced div. You can set the keymap for each of them through "Hotkeys" in the settings.
|
||||
With "Tidier formatting" enabled in the settings, you can format a single word simply by placing the cursor on it, without the need to select the entire word. Otherwise, it acts like a normal wrapper.
|
||||
|
||||
This plugin also adds functionality of all those commands (except for fenced div) to the context menu, by right-clicking on the editor and choosing "More format" (below "Insert" in the formatting section).
|
||||
|
||||
Mimicking how Obsidian formats the text, this plugin brings an ability to do "tidier" formatting. That means that the plugin will try to format the whole word that touches the cursor. So, you don't need to select all over the word. But, if you still want the formatting to behave like normal auto-wrap, you toggle off "Tidier formatting" in the settings.
|
||||
|
||||
### 4. Tweaking the Settings
|
||||
|
||||
Because some syntax may not be required, or just simply you don't want to use it, this plugin provides ability to turn specific syntax off under the "Syntax switch" section. Also, this plugin give some option to turn those off only in one mode (either editor or preview mode), gives you more freely and flexible choice depend on your needs.
|
||||
|
||||
Moreover, the tag display behaviour of the highlight, custom span, and fenced div can be configured through the "Tag display behaviour" section. If you don't want the tag to be hidden, you can set it to "Always visible" in the live-preview mode, or toggle it on in the preview mode.
|
||||
|
||||
This plugin provides a way for tweaking the highlight, without exhaustingly bring the CSS snippet into it. In the "Custom highlight" section, you can adjust:
|
||||
- visibilty of the color button,
|
||||
- the opacity of the highlight color for each light mode and dark mode, range from 0 to 1 (zero means that the highlight will be fully transparent),
|
||||
- the list of the color palattes, by adding the new one, changing their color and their tag string, or deleting them,
|
||||
- also, it is equipped with ability to change the name, arrange, and decide the colors to be displayed in the color menu.
|
||||
|
||||
Along with the latest version, you can add some predefined tags to the custom span and fenced div (under the "Custom span" and "Fenced div" sections), so those will be displayed in the tag menu. These features behave like the color palattes in the "Custom highlight", except you have to manually set the style rules within the CSS snippet manually if you want to give each tag some styles.
|
||||
|
||||
For the last section, the "Others" section, you can choose:
|
||||
- whether the delimiter can be escaped by backslash or not (editor only), and
|
||||
- whether the syntax will be parsed in the exported PDF or not.
|
||||
<details>
|
||||
<img src="docs/assets/tidier-formatting.gif" alt="tidier-formatting.gif"/>
|
||||
</details>
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> You don't need to reload the app after changing the settings.
|
||||
|
||||
## Syntax Rules
|
||||
### 7. Other Tweaks
|
||||
|
||||
- Enable/disable each formatting type individually under "Syntax switch" section.
|
||||
- Prevent fenced div from being styled in source mode.
|
||||
- Specifies when tags should be displayed under "Tag display behavior" section.
|
||||
- etc...
|
||||
|
||||
## 🎨 Styling Custom Spans and Fenced Divs
|
||||
|
||||
With tag attached to them, you can insert your own CSS style rules based on their tag. For instance, you can apply this CSS rules to the markdown below:
|
||||
|
||||
```css
|
||||
.text-large { font-size: 48px }
|
||||
.text-green { color: green }
|
||||
|
||||
.align-center { text-align: center }
|
||||
.bordered { border: 4px solid red }
|
||||
```
|
||||
|
||||
```markdown
|
||||
!!{text-large text-green}Large text with green color.!!
|
||||
|
||||
:::align-center bordered
|
||||
Should be aligned to the center
|
||||
and bordered.
|
||||
```
|
||||
|
||||
<details>
|
||||
<summary>Expected result:</summary>
|
||||
<img src="docs/assets/tag-styling.png" alt="tag-styling.png"/>
|
||||
</details>
|
||||
|
||||
> [!Note]
|
||||
>
|
||||
> For which characters are allowed in the tag, see "Syntax Rules" section below.
|
||||
|
||||
## 📜 Syntax Rules
|
||||
|
||||
We move the explanation to [this page](./docs/rule.md).
|
||||
|
||||
## Installation
|
||||
## 📦 Installation
|
||||
|
||||
- In-app
|
||||
- Open settings.
|
||||
|
|
@ -139,9 +154,14 @@ We move the explanation to [this page](./docs/rule.md).
|
|||
- Create a folder named `extended-markdown-parser` under `YOUR_VAULT_NAME/.obsidian/plugins`.
|
||||
- Place `manifest.json`, `main.js`, and `style.css` from the latest release into the folder.
|
||||
- Enable it through the "Community plugin" setting tab.
|
||||
- Using [BRAT](https://github.com/TfTHacker/obsidian42-brat).
|
||||
- Using [BRAT](https://github.com/TfTHacker/obsidian42-brat)
|
||||
- Install and enable "BRAT" plugin in "Community plugins" list.
|
||||
- Choose "BRAT" setting tab.
|
||||
- Click "Add beta plugin" under "Beta plugin list" section.
|
||||
- Enter this repo link into "Repository" input field.
|
||||
- Check "Enable after installing the plugin" and click "Add Plugin".
|
||||
|
||||
## FAQ's
|
||||
## ❓ FAQ's
|
||||
|
||||
### Is it working on the table and callout?
|
||||
Of course it's working on both, except for the fenced div.
|
||||
|
|
@ -152,7 +172,7 @@ I tested it on a 250kB file and it's still working fine. This plugin also uses p
|
|||
### Will it cause conflict with other plugins that concerns extending syntax?
|
||||
It depends on what character the others use in their syntax.
|
||||
|
||||
## Roadmap
|
||||
## 📋 Roadmap
|
||||
|
||||
- [x] Enable/disable formatting in settings
|
||||
- [x] Applicable on mobile
|
||||
|
|
@ -163,18 +183,18 @@ It depends on what character the others use in their syntax.
|
|||
- [x] ~~Class suggester~~ Predefined tags for custom span and fenced div
|
||||
- [ ] ~~More syntax, if necessary~~ *I'll be over here for a while*
|
||||
|
||||
## Compatibility Note
|
||||
## ⚙️ Compatibility Note
|
||||
|
||||
This plugin have been tested in the latest version of Obsidian (about 1.7.x - 1.8.x), and haven't been tested yet in the version 1.6.x and below.
|
||||
|
||||
## Known Issues
|
||||
## 🐞 Known Issues
|
||||
|
||||
- Delimiter escaping doesn't work in the preview mode.
|
||||
- Cannot escape spoilers that are inside table cells (in source mode). (**cannot be fixed**)
|
||||
|
||||
Feel free to let me know if you find any bugs...
|
||||
|
||||
## Credit
|
||||
## 🙏 Acknowledgment
|
||||
|
||||
Thanks to:
|
||||
- [Pandoc](https://pandoc.org/MANUAL.html) for the idea of some syntax,
|
||||
|
|
|
|||
BIN
docs/assets/banner.png
Normal file
|
After Width: | Height: | Size: 4.6 MiB |
BIN
docs/assets/commands.gif
Normal file
|
After Width: | Height: | Size: 8.8 MiB |
BIN
docs/assets/custom-highlight.gif
Normal file
|
After Width: | Height: | Size: 8.5 MiB |
BIN
docs/assets/tag-styling.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/assets/tidier-formatting.gif
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 2.1 MiB |
2
main.ts
|
|
@ -9,7 +9,7 @@ import { editorCommands } from "src/editor-mode/formatting/commands";
|
|||
import { extendEditorCtxMenu } from "src/editor-mode/ui-components";
|
||||
import { TagManager } from "src/tag-manager";
|
||||
import { ReadingModeSyntaxExtender } from "src/preview-mode/post-processor/core";
|
||||
import { editorSyntaxExtender, refreshCall } from "src/editor-mode/cm-extension";
|
||||
import { editorSyntaxExtender, refreshCall } from "src/editor-mode/cm-extensions";
|
||||
|
||||
export default class ExtendedMarkdownSyntax extends Plugin {
|
||||
settings: PluginSettings;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,18 @@
|
|||
import ExtendedMarkdownSyntax from "main";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
import { Annotation, EditorState, Extension, Facet, Prec, StateEffect, StateField, Transaction } from "@codemirror/state";
|
||||
import { Annotation, EditorState, Extension, Facet, Prec, StateField, Transaction } from "@codemirror/state";
|
||||
import { EditorView, PluginValue, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { ParseContext, syntaxTree } from "@codemirror/language";
|
||||
import { Tree } from "@lezer/common";
|
||||
import { syntaxTree, syntaxTreeAvailable } from "@codemirror/language";
|
||||
import { IndexCache } from "src/types";
|
||||
import { EditorParser } from "src/editor-mode/preprocessor/parser";
|
||||
import { SelectionObserver } from "src/editor-mode/preprocessor/observer";
|
||||
import { DecorationBuilder } from "src/editor-mode/decorator/builder";
|
||||
import { Formatter } from "src/editor-mode/formatting/formatter";
|
||||
import { hookLangState } from "src/editor-mode/utils/hook";
|
||||
import { Tree } from "@lezer/common";
|
||||
|
||||
type TagMenuOptionCaches = Record<"colorMenuItem" | "spanTagMenuItem" | "divTagMenuItem", IndexCache>;
|
||||
|
||||
interface BuiltinParserState {
|
||||
context: ParseContext,
|
||||
tree: Tree
|
||||
}
|
||||
|
||||
interface RefreshDesc {
|
||||
deep?: boolean;
|
||||
}
|
||||
|
|
@ -39,41 +35,33 @@ export interface ActivityRecord {
|
|||
isModeChanged?: boolean;
|
||||
}
|
||||
|
||||
function _isBuiltinParserStateEffect(effect: StateEffect<unknown>): effect is StateEffect<BuiltinParserState> {
|
||||
let effectVal = effect;
|
||||
return (
|
||||
"context" in effectVal && effectVal.context instanceof ParseContext &&
|
||||
"tree" in effectVal && effectVal.tree instanceof Tree
|
||||
);
|
||||
}
|
||||
|
||||
function _isSelectionMoved(transaction: Transaction): boolean {
|
||||
return !(
|
||||
transaction.selection &&
|
||||
transaction.startState.selection.eq(transaction.selection)
|
||||
!transaction.selection ||
|
||||
transaction.startState.selection.eq(transaction.selection, false)
|
||||
);
|
||||
}
|
||||
|
||||
function _ensureMostUpdatedTree(transaction: Transaction): Tree {
|
||||
let currentTree = syntaxTree(transaction.state),
|
||||
mostUpdatedTree = transaction.effects.reduce(
|
||||
(tree: Tree, effect: StateEffect<unknown>) => {
|
||||
if (_isBuiltinParserStateEffect(effect)) {
|
||||
tree = effect.value.tree;
|
||||
}
|
||||
return tree;
|
||||
},
|
||||
currentTree
|
||||
);
|
||||
return mostUpdatedTree;
|
||||
}
|
||||
|
||||
function _isEditorModeChanged(transaction: Transaction): boolean {
|
||||
let isLivePreviewCurrently = transaction.state.field(editorLivePreviewField),
|
||||
isLivePreviewPreviously = transaction.startState.field(editorLivePreviewField);
|
||||
return isLivePreviewCurrently != isLivePreviewPreviously;
|
||||
}
|
||||
|
||||
function _ensureMostUpdatedTree(transaction: Transaction): { tree: Tree, targetLen: number } {
|
||||
let {
|
||||
tree,
|
||||
treeLen: targetLen
|
||||
} = transaction.state.field(langStateField).context;
|
||||
|
||||
transaction.effects.forEach((effect) => {
|
||||
if (effect.is(langStateFxType))
|
||||
({ tree, treeLen: targetLen } = effect.value.context)
|
||||
});
|
||||
|
||||
return { tree, targetLen };
|
||||
}
|
||||
|
||||
class _EditorPlugin implements PluginValue {
|
||||
public readonly builder: DecorationBuilder;
|
||||
public readonly mainPlugin: ExtendedMarkdownSyntax;
|
||||
|
|
@ -141,6 +129,8 @@ export const tagMenuOptionCaches = Facet.define<TagMenuOptionCaches, TagMenuOpti
|
|||
}
|
||||
});
|
||||
|
||||
export const { langStateField, langStateFxType } = hookLangState();
|
||||
|
||||
export const editorSyntaxExtender = (plugin: ExtendedMarkdownSyntax): Extension => {
|
||||
let preprocessField = StateField.define({
|
||||
create(state: EditorState) {
|
||||
|
|
@ -156,10 +146,10 @@ export const editorSyntaxExtender = (plugin: ExtendedMarkdownSyntax): Extension
|
|||
},
|
||||
|
||||
update(prevField, transaction: Transaction) {
|
||||
let newTree = _ensureMostUpdatedTree(transaction),
|
||||
oldTree = syntaxTree(transaction.startState),
|
||||
refreshDesc = transaction.annotation(refreshCall),
|
||||
{ parser, observer, builder, activities } = prevField;
|
||||
let { tree: newTree, targetLen: stopAt } = _ensureMostUpdatedTree(transaction),
|
||||
{ parser, observer, builder, activities } = prevField,
|
||||
completedTree = syntaxTreeAvailable(transaction.state, stopAt),
|
||||
refreshDesc = transaction.annotation(refreshCall);
|
||||
|
||||
let isParsing = false,
|
||||
isObserving = false,
|
||||
|
|
@ -167,11 +157,18 @@ export const editorSyntaxExtender = (plugin: ExtendedMarkdownSyntax): Extension
|
|||
isDeepRefreshed = !!refreshDesc?.deep,
|
||||
isModeChanged = _isEditorModeChanged(transaction);
|
||||
|
||||
if (transaction.docChanged) parser.storeChanges(transaction.changes);
|
||||
|
||||
if (isDeepRefreshed) {
|
||||
parser.initParse(transaction.newDoc, newTree);
|
||||
isParsing = true;
|
||||
} else if (transaction.docChanged || oldTree.length != newTree.length) {
|
||||
parser.applyChange(transaction.newDoc, newTree, oldTree, transaction.changes);
|
||||
}
|
||||
|
||||
else if (
|
||||
(parser.hasStoredChanges() || parser.lastStop != stopAt) &&
|
||||
completedTree
|
||||
) {
|
||||
parser.applyChange(transaction.newDoc, newTree, stopAt);
|
||||
isParsing = true;
|
||||
}
|
||||
|
||||
|
|
@ -7,11 +7,11 @@ import { BlockRules, InlineRules } from "src/format-configs/rules";
|
|||
import { trimTag } from "src/format-configs/format-utils";
|
||||
import { EditorParser } from "src/editor-mode/preprocessor/parser";
|
||||
import { SelectionObserver } from "src/editor-mode/preprocessor/observer";
|
||||
import { ActivityRecord } from "src/editor-mode/cm-extension";
|
||||
import { ActivityRecord } from "src/editor-mode/cm-extensions";
|
||||
import { LineBreak, HiddenWidget, ColorButton } from "src/editor-mode/decorator/widgets";
|
||||
import { REVEALED_SPOILER_DECO } from "src/editor-mode/decorator/decorations";
|
||||
import { getTagRange, iterTokenGroup, provideTokenPartsRanges } from "src/editor-mode/utils/token-utils"
|
||||
import { TextCursor } from "../utils/doc-utils";
|
||||
import { TextCursor } from "src/editor-mode/utils/doc-utils";
|
||||
|
||||
interface RangeSetUpdate<T extends RangeValue> {
|
||||
add?: readonly Range<T>[];
|
||||
|
|
@ -200,10 +200,6 @@ export class DecorationBuilder {
|
|||
private readonly _lineBreakReplacer: _LineBreakReplacer;
|
||||
private readonly _selectionObserver: SelectionObserver;
|
||||
private readonly _settings: PluginSettings;
|
||||
private readonly _indexCaches = {
|
||||
inlineToken: { number: 0 }, // 0-based
|
||||
blockToken: { number: 0 } // 0-based
|
||||
}
|
||||
|
||||
public readonly holder: _DecorationHolder;
|
||||
|
||||
|
|
@ -322,7 +318,6 @@ export class DecorationBuilder {
|
|||
iterTokenGroup({
|
||||
tokens: this._parser.inlineTokens,
|
||||
ranges: visibleRanges,
|
||||
indexCache: this._indexCaches.inlineToken,
|
||||
callback: token => {
|
||||
if (token.status != TokenStatus.ACTIVE) return;
|
||||
if (token.type == Format.HIGHLIGHT) hlTokens.push(token);
|
||||
|
|
@ -354,7 +349,6 @@ export class DecorationBuilder {
|
|||
iterTokenGroup({
|
||||
tokens: this._parser.blockTokens,
|
||||
ranges: visibleRanges,
|
||||
indexCache: this._indexCaches.blockToken,
|
||||
callback: token => {
|
||||
if (token.status != TokenStatus.ACTIVE) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Command } from "obsidian";
|
|||
import { EditorView } from "@codemirror/view";
|
||||
import { Format } from "src/enums";
|
||||
import { CtxMenuCommand } from "src/types";
|
||||
import { instancesStore } from "src/editor-mode/cm-extension";
|
||||
import { instancesStore } from "src/editor-mode/cm-extensions";
|
||||
import { TagMenu } from "src/editor-mode/ui-components";
|
||||
import { Formatter } from "src/editor-mode/formatting/formatter";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,25 @@
|
|||
import { ChangeSet, Line, Text } from "@codemirror/state";
|
||||
import { Tree, TreeCursor } from "@lezer/common";
|
||||
import { PluginSettings, StateConfig, TokenGroup, ChangedRange, PlainRange, InlineFormat, Token } from "src/types";
|
||||
import { PluginSettings, TokenGroup, ChangedRange, PlainRange, InlineFormat, Token } from "src/types";
|
||||
import { Format, LineCtx, MarkdownViewMode, TokenLevel, TokenStatus } from "src/enums";
|
||||
import { Formats, InlineRules } from "src/format-configs/rules";
|
||||
import { handleFencedDivTag, handleInlineTag, Tokenizer } from "src/editor-mode/preprocessor/tokenizer";
|
||||
import { findNode, getContextFromNode, disableEscape, findShifterAt, getShifterStart, hasInterferer, reenableEscape } from "src/editor-mode/preprocessor/syntax-node";
|
||||
import { EditorDelimLookup, SKIPPED_NODE_RE } from "src/editor-mode/preprocessor/parser-configs";
|
||||
import { isBlankLine, getBlockEndAt, TextCursor } from "src/editor-mode/utils/doc-utils";
|
||||
import { provideTokenPartsRanges } from "src/editor-mode/utils/token-utils"
|
||||
import { findTokenIndexAt, provideTokenPartsRanges } from "src/editor-mode/utils/token-utils";
|
||||
|
||||
/**
|
||||
* Used for (re)configuring the state, especially in
|
||||
* the case of document or tree change
|
||||
*/
|
||||
type ParserStateConfig = {
|
||||
doc: Text,
|
||||
tree: Tree,
|
||||
startAt: number,
|
||||
stopAt?: number | Line
|
||||
settings: PluginSettings,
|
||||
}
|
||||
|
||||
function _isTerminalChar(char: string, settings: PluginSettings): boolean {
|
||||
for (let definedDelim in EditorDelimLookup)
|
||||
|
|
@ -40,6 +52,19 @@ function _composeChanges(changes: ChangeSet): ChangedRange | null {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Interferer node is node that, if it was found within the changed range
|
||||
* the end offset of stream will be moved forward to the end offset of
|
||||
* the new tree. That's including the delimiters of codeblock, mathblock,
|
||||
* or comment block.
|
||||
*/
|
||||
function _checkInterferer(changedRange: ChangedRange, newTree: Tree, oldTree: Tree): boolean {
|
||||
return (
|
||||
hasInterferer(newTree, changedRange.from, changedRange.changedTo) ||
|
||||
hasInterferer(oldTree, changedRange.from, changedRange.initTo)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A place storing token based on its type, to be resolved through the
|
||||
* `EditorParser` and `EditorParserState` when satisfies certain
|
||||
|
|
@ -178,27 +203,29 @@ export class EditorParserState {
|
|||
public curCtx: LineCtx = LineCtx.NONE;
|
||||
public prevCtx: LineCtx = LineCtx.NONE;
|
||||
|
||||
constructor(config: StateConfig, inlineTokens: TokenGroup, blockTokens: TokenGroup) {
|
||||
constructor(config: ParserStateConfig, inlineTokens: TokenGroup, blockTokens: TokenGroup) {
|
||||
this.doc = config.doc;
|
||||
this.tree = config.tree;
|
||||
this.textCursor = TextCursor.atOffset(this.doc, config.offset);
|
||||
this.textCursor = TextCursor.atOffset(this.doc, config.startAt);
|
||||
this.line = this.textCursor.curLine;
|
||||
this.endOfStream = this.doc.lineAt(this.tree.length).number;
|
||||
this.offset = config.offset - this.line.from;
|
||||
this.offset = config.startAt - this.line.from;
|
||||
this.inlineTokens = inlineTokens;
|
||||
this.blockTokens = blockTokens;
|
||||
this.cursor = this.tree.cursor();
|
||||
this.settings = config.settings;
|
||||
this.nextCursor();
|
||||
|
||||
this.endOfStream = config.stopAt instanceof Line
|
||||
? config.stopAt.number
|
||||
: this.doc.lineAt(config.stopAt ?? this.tree.length).number;
|
||||
|
||||
// if previous line is a blank line or the
|
||||
// current line is the first line, then the current one
|
||||
// should be a block start
|
||||
let prevLine = this.prevLine;
|
||||
if (prevLine)
|
||||
this.isBlockStart = isBlankLine(prevLine);
|
||||
else
|
||||
this.isBlockStart = true;
|
||||
this.isBlockStart = prevLine
|
||||
? isBlankLine(prevLine)
|
||||
: true;
|
||||
}
|
||||
|
||||
/** global offset */
|
||||
|
|
@ -435,12 +462,15 @@ export class EditorParserState {
|
|||
export class EditorParser {
|
||||
private _state: EditorParserState;
|
||||
private _queue: _TokenQueue = new _TokenQueue();
|
||||
private _changeSet: ChangeSet | null = null;
|
||||
|
||||
public inlineTokens: TokenGroup = [];
|
||||
public blockTokens: TokenGroup = [];
|
||||
|
||||
public reparsedRanges: Record<TokenLevel, { from: number, initTo: number, changedTo: number }>;
|
||||
public lastStreamPoint: PlainRange = { from: 0, to: 0 };
|
||||
public oldTree: Tree;
|
||||
public lastStop: number;
|
||||
|
||||
readonly settings: PluginSettings;
|
||||
|
||||
|
|
@ -456,13 +486,23 @@ export class EditorParser {
|
|||
? this.inlineTokens
|
||||
: this.blockTokens;
|
||||
}
|
||||
|
||||
public hasStoredChanges(): boolean {
|
||||
return !!this._changeSet;
|
||||
}
|
||||
|
||||
public storeChanges(changeSet: ChangeSet) {
|
||||
this._changeSet = this._changeSet
|
||||
? this._changeSet.compose(changeSet)
|
||||
: changeSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize parsing, so the parser would parse from the start of the
|
||||
* document. Should be use when actual initialization, or if there is a
|
||||
* setting change that need the parser to parse from the start.
|
||||
*/
|
||||
public initParse(doc: Text, tree: Tree): void {
|
||||
public initParse(doc: Text, tree: Tree, stopAt?: number): void {
|
||||
// Toggle escape.
|
||||
if (this.settings.editorEscape)
|
||||
reenableEscape();
|
||||
|
|
@ -475,43 +515,66 @@ export class EditorParser {
|
|||
this.blockTokens = [];
|
||||
|
||||
// Start parsing.
|
||||
this._defineState({ doc, tree, offset: 0, settings: this.settings });
|
||||
this._defineState({ doc, tree, startAt: 0, stopAt, settings: this.settings });
|
||||
this._streamParse();
|
||||
this.reparsedRanges = {
|
||||
[TokenLevel.INLINE]: { from: 0, initTo: 0, changedTo: this.inlineTokens.length },
|
||||
[TokenLevel.BLOCK]: { from: 0, initTo: 0, changedTo: this.blockTokens.length }
|
||||
}
|
||||
|
||||
this.oldTree = tree;
|
||||
this.lastStop = stopAt ?? tree.length;
|
||||
|
||||
this._changeSet = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the change comes from the document or the length difference
|
||||
* between previous parsed tree and the current one.
|
||||
*/
|
||||
public applyChange(doc: Text, tree: Tree, oldTree: Tree, changes: ChangeSet): void {
|
||||
public applyChange(doc: Text, tree: Tree, stopAt = tree.length): void {
|
||||
|
||||
// Start stream offset can be the shortest length of both old and new
|
||||
// tree, or the start offset of the changed range.
|
||||
let changedRange = _composeChanges(changes),
|
||||
startStreamOffset = Math.min(oldTree.length, tree.length) + 1;
|
||||
let changedRange = this._changeSet ? _composeChanges(this._changeSet) : null,
|
||||
startStreamOffset = this.lastStop == stopAt
|
||||
? stopAt
|
||||
: Math.min(stopAt, this.lastStop) + 1;
|
||||
|
||||
this._changeSet = null;
|
||||
|
||||
if (changedRange)
|
||||
startStreamOffset = Math.min(startStreamOffset, changedRange.from);
|
||||
|
||||
let config: StateConfig = { doc, tree, offset: startStreamOffset, settings: this.settings };
|
||||
this._defineState(config, oldTree);
|
||||
|
||||
// Nearest next blank line is the end stream, unless there is no blank
|
||||
// line exist within the new tree range, or the changed range encounters
|
||||
// such interferer node.
|
||||
let endStreamLine = changedRange && !this._checkInterferer(oldTree, changedRange)
|
||||
let endStreamLine = changedRange && _checkInterferer(changedRange, tree, this.oldTree)
|
||||
? getBlockEndAt(doc, changedRange.changedTo)
|
||||
: getBlockEndAt(doc, tree.length);
|
||||
this._state.endOfStream = endStreamLine.number;
|
||||
: getBlockEndAt(doc, stopAt);
|
||||
|
||||
let config: ParserStateConfig = {
|
||||
doc, tree,
|
||||
startAt: startStreamOffset,
|
||||
stopAt: endStreamLine,
|
||||
settings: this.settings
|
||||
};
|
||||
|
||||
this._defineState(config);
|
||||
|
||||
// Tokens that are located after the end of stream will be considered as
|
||||
// left tokens, and will be shifted by the length of the changed range.
|
||||
let leftTokens: Record<"inline" | "block", TokenGroup> = {
|
||||
inline: this._getLeftTokens(this._filterTokens(this.inlineTokens, this.reparsedRanges[TokenLevel.INLINE]), changedRange, endStreamLine),
|
||||
block: this._getLeftTokens(this._filterTokens(this.blockTokens, this.reparsedRanges[TokenLevel.BLOCK]), changedRange, endStreamLine)
|
||||
inline: this._getLeftTokens(
|
||||
this._filterTokens(this.inlineTokens, this.reparsedRanges[TokenLevel.INLINE]),
|
||||
changedRange,
|
||||
endStreamLine
|
||||
),
|
||||
block: this._getLeftTokens(
|
||||
this._filterTokens(this.blockTokens, this.reparsedRanges[TokenLevel.BLOCK]),
|
||||
changedRange,
|
||||
endStreamLine
|
||||
)
|
||||
};
|
||||
|
||||
// If there are any terminal characters -are that being used as
|
||||
|
|
@ -531,13 +594,15 @@ export class EditorParser {
|
|||
this.reparsedRanges[TokenLevel.BLOCK].changedTo = this.blockTokens.length;
|
||||
this.inlineTokens = this.inlineTokens.concat(leftTokens.inline);
|
||||
this.blockTokens = this.blockTokens.concat(leftTokens.block);
|
||||
this.oldTree = tree;
|
||||
this.lastStop = stopAt ?? tree.length;
|
||||
}
|
||||
|
||||
/** Define new state as parsing begins. */
|
||||
private _defineState(config: StateConfig, oldTree?: Tree): void {
|
||||
private _defineState(config: ParserStateConfig): void {
|
||||
this._state = new EditorParserState(config, this.inlineTokens, this.blockTokens);
|
||||
this._queue.attachState(this._state);
|
||||
if (oldTree) this._shiftOffsetByNode(oldTree);
|
||||
if (this.oldTree) this._shiftOffsetByNode();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -548,10 +613,10 @@ export class EditorParser {
|
|||
*
|
||||
* Must be done along with defining new state.
|
||||
*/
|
||||
private _shiftOffsetByNode(oldTree: Tree) {
|
||||
private _shiftOffsetByNode() {
|
||||
let oldOffset = this._state.globalOffset,
|
||||
newNode = findShifterAt(this._state.tree, oldOffset),
|
||||
oldNode = findShifterAt(oldTree, oldOffset),
|
||||
oldNode = findShifterAt(this.oldTree, oldOffset),
|
||||
newOffset: number | null = null;
|
||||
|
||||
// Try to find it in the new tree.
|
||||
|
|
@ -658,24 +723,11 @@ export class EditorParser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interferer node is node that, if it was found within the changed range
|
||||
* the end offset of stream will be moved forward to the end offset of
|
||||
* the new tree. That's including the delimiters of codeblock, mathblock,
|
||||
* or comment block.
|
||||
*/
|
||||
private _checkInterferer(oldTree: Tree, changedRange: ChangedRange): boolean {
|
||||
return (
|
||||
hasInterferer(this._state.tree, changedRange.from, changedRange.changedTo) ||
|
||||
hasInterferer(oldTree, changedRange.from, changedRange.initTo)
|
||||
);
|
||||
}
|
||||
|
||||
private _filterTokens(tokens: TokenGroup, reparsedRange: typeof this.reparsedRanges[TokenLevel]): TokenGroup {
|
||||
let index = 0,
|
||||
reparsedFrom: number | undefined,
|
||||
let reparsedFrom: number | undefined,
|
||||
reparsedTo: number | undefined,
|
||||
offset = this._state.globalOffset;
|
||||
offset = this._state.globalOffset,
|
||||
index = findTokenIndexAt(tokens, offset) ?? tokens.length;
|
||||
|
||||
for (let curToken = tokens[index]; index < tokens.length; curToken = tokens[++index]) {
|
||||
// Keep find token touched by the current offset
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Format, TokenLevel, TokenStatus } from "src/enums";
|
|||
import { getActiveCanvasNodeCoords } from "src/editor-mode/utils/canvas-utils";
|
||||
import { Formatter } from "src/editor-mode/formatting/formatter";
|
||||
import { supportTag } from "src/format-configs/format-utils";
|
||||
import { tagMenuOptionCaches, instancesStore } from "src/editor-mode/cm-extension";
|
||||
import { tagMenuOptionCaches, instancesStore } from "src/editor-mode/cm-extensions";
|
||||
import { ctxMenuCommands } from "src/editor-mode/formatting/commands";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Line, Text, TextLeaf, TextNode, ILine } from "@codemirror/state";
|
||||
import { Line, Text, TextLeaf, TextNode } from "@codemirror/state";
|
||||
import { PlainRange } from "src/types";
|
||||
|
||||
// TODO: utilize TextCursor to all functions
|
||||
|
|
@ -13,9 +13,24 @@ function _isTextNode(doc: Text): doc is TextNode {
|
|||
return doc.children !== null;
|
||||
}
|
||||
|
||||
export class ILine extends Line {
|
||||
readonly from: number;
|
||||
readonly to: number;
|
||||
readonly number: number;
|
||||
readonly text: string;
|
||||
|
||||
constructor(from: number, to: number, number: number, text: string) {
|
||||
super();
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
this.number = number;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
export class TextCursor {
|
||||
public address: LineAddress;
|
||||
public curLine: ILine;
|
||||
public curLine: Line;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
|
|
@ -28,25 +43,21 @@ export class TextCursor {
|
|||
let { parent, index } = this.address[deep],
|
||||
branches = _isTextLeaf(parent) ? parent.text : parent.children;
|
||||
if (index + 1 < branches.length) {
|
||||
this.address[deep].index++;
|
||||
while (++deep < this.address.length) {
|
||||
let { parent, index } = this.address[deep - 1];
|
||||
if (_isTextNode(parent)) {
|
||||
this.address[deep].parent = parent.children[index] as TextNode | TextLeaf;
|
||||
this.address[deep].index = 0;
|
||||
}
|
||||
this.address[deep].index = ++index;
|
||||
if (this.address.splice(deep + 1).length) while (_isTextNode(parent)) {
|
||||
parent = parent.children[index] as TextNode | TextLeaf;
|
||||
index = 0;
|
||||
this.address.push({ parent, index });
|
||||
}
|
||||
let { parent, index } = this.address[this.address.length - 1];
|
||||
if (!_isTextLeaf(parent)) throw TypeError("TextLeaf not found!");
|
||||
let length = parent.text[index].length,
|
||||
from = this.curLine.to + 1;
|
||||
this.curLine = {
|
||||
number: this.curLine.number + 1,
|
||||
text: parent.text[index],
|
||||
this.curLine = new ILine(
|
||||
from,
|
||||
to: from + length,
|
||||
length
|
||||
};
|
||||
from + length,
|
||||
this.curLine.number + 1,
|
||||
parent.text[index]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -57,25 +68,29 @@ export class TextCursor {
|
|||
for (let deep = this.address.length - 1; deep >= 0; deep--) {
|
||||
if (this.address[deep].index > 0) {
|
||||
this.address[deep].index--;
|
||||
while (++deep < this.address.length) {
|
||||
let { parent, index } = this.address[deep - 1];
|
||||
if (_isTextNode(parent)) {
|
||||
let prevBranch = parent.children[index] as TextNode | TextLeaf;
|
||||
this.address[deep].parent = prevBranch;
|
||||
this.address[deep].index = (_isTextLeaf(prevBranch) ? prevBranch.text : prevBranch.children).length - 1;
|
||||
let { parent, index } = this.address[deep];
|
||||
if (this.address.splice(deep + 1).length && _isTextNode(parent)) while (true) {
|
||||
let isTextLeaf: boolean;
|
||||
parent = (parent as TextNode).children[index] as TextNode | TextLeaf;
|
||||
if (_isTextLeaf(parent)) {
|
||||
isTextLeaf = true;
|
||||
index = parent.text.length - 1;
|
||||
} else {
|
||||
isTextLeaf = false;
|
||||
index = parent.children.length - 1;
|
||||
}
|
||||
this.address.push({ parent, index });
|
||||
if (isTextLeaf) break;
|
||||
}
|
||||
let { parent, index } = this.address[this.address.length - 1];
|
||||
if (!_isTextLeaf(parent)) throw TypeError("TextLeaf not found!");
|
||||
let length = parent.text[index].length,
|
||||
to = this.curLine.from - 1;
|
||||
this.curLine = {
|
||||
number: this.curLine.number - 1,
|
||||
text: parent.text[index],
|
||||
from: to - length,
|
||||
this.curLine = new ILine(
|
||||
to - length,
|
||||
to,
|
||||
length
|
||||
};
|
||||
this.curLine.number - 1,
|
||||
parent.text[index]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -95,36 +110,37 @@ export class TextCursor {
|
|||
}
|
||||
|
||||
public gotoLast(): TextCursor {
|
||||
this.address.forEach((branchAddress, i) => {
|
||||
let { parent } = branchAddress,
|
||||
branches = _isTextLeaf(parent) ? parent.text : parent.children;
|
||||
branchAddress.index = branches.length - 1;
|
||||
if (i + 1 < this.address.length) {
|
||||
this.address[i + 1].parent = branches[branchAddress.index] as TextLeaf | TextNode;
|
||||
} else {
|
||||
let to = this.doc.length,
|
||||
lineNum = this.doc.lines,
|
||||
lineStr = branches[branchAddress.index] as string;
|
||||
this.curLine = {
|
||||
from: to - lineStr.length,
|
||||
to,
|
||||
number: lineNum,
|
||||
length: lineStr.length,
|
||||
text: lineStr
|
||||
};
|
||||
}
|
||||
});
|
||||
this.address.splice(1);
|
||||
let { parent, index } = this.address[0],
|
||||
root = parent;
|
||||
while (_isTextNode(parent)) {
|
||||
index = parent.children.length - 1;
|
||||
if (root !== parent) this.address.push({ parent, index });
|
||||
else this.address[0].index = index;
|
||||
parent = parent.children[index] as TextNode | TextLeaf;
|
||||
}
|
||||
index = parent.text.length - 1;
|
||||
if (root !== parent) this.address[0].index = index;
|
||||
else this.address.push({ parent, index });
|
||||
let lineStr = parent.text[index];
|
||||
this.curLine = {
|
||||
from: root.length - lineStr.length,
|
||||
to: root.length,
|
||||
text: lineStr,
|
||||
number: root.lines,
|
||||
length: lineStr.length
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPrevLine(): ILine | null {
|
||||
public getPrevLine(): Line | null {
|
||||
if (!this.prev() || this.curLine.number <= 1) return null;
|
||||
let prevLine = this.curLine;
|
||||
this.next();
|
||||
return prevLine;
|
||||
}
|
||||
|
||||
public getNextLine(): ILine | null {
|
||||
public getNextLine(): Line | null {
|
||||
if (!this.next() || this.curLine.number >= this.doc.lines) return null;
|
||||
let nextLine = this.curLine;
|
||||
this.prev();
|
||||
|
|
@ -229,7 +245,7 @@ export function sliceStrFromLine(line: Line, from: number, to: number): string {
|
|||
return line.text.slice(from, to);
|
||||
}
|
||||
|
||||
export function getLineAddressAt(doc: Text, offset: number): { address: LineAddress, line: ILine } {
|
||||
export function getLineAddressAt(doc: Text, offset: number): { address: LineAddress, line: Line } {
|
||||
let parent = doc,
|
||||
address: LineAddress = [],
|
||||
lineNum = 0,
|
||||
|
|
@ -265,12 +281,11 @@ export function getLineAddressAt(doc: Text, offset: number): { address: LineAddr
|
|||
passedLen += curLineStr.length + 1;
|
||||
}
|
||||
|
||||
let line: ILine = {
|
||||
number: lineNum,
|
||||
text: lineStr,
|
||||
from: passedLen,
|
||||
to: passedLen + lineStr.length,
|
||||
length: lineStr.length
|
||||
}
|
||||
let line = new ILine(
|
||||
passedLen,
|
||||
passedLen + lineStr.length,
|
||||
lineNum,
|
||||
lineStr
|
||||
);
|
||||
return { address, line };
|
||||
}
|
||||
15
src/editor-mode/utils/hook.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Language, LanguageState } from "@codemirror/language";
|
||||
import { StateEffectType, StateField } from "@codemirror/state";
|
||||
|
||||
export function hookLangState(): {
|
||||
langStateField: StateField<LanguageState>,
|
||||
langStateFxType: StateEffectType<LanguageState>
|
||||
} {
|
||||
let langStateField = Object.getOwnPropertyDescriptor(Language, "state")?.value as unknown,
|
||||
langStateFxType = Object.getOwnPropertyDescriptor(Language, "setState")?.value as unknown;
|
||||
if (!(langStateField instanceof StateField && langStateFxType instanceof StateEffectType)) {
|
||||
throw TypeError();
|
||||
}
|
||||
|
||||
return { langStateField, langStateFxType };
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
import { IndexCache, PlainRange, Token, TokenGroup } from "src/types";
|
||||
import { PlainRange, Token, TokenGroup } from "src/types";
|
||||
|
||||
type TokenPartsRanges = Record<"openRange" | "closeRange" | "tagRange" | "contentRange", PlainRange>;
|
||||
interface IterTokenGroupSpec {
|
||||
tokens: TokenGroup,
|
||||
ranges: PlainRange[] | readonly PlainRange[],
|
||||
indexCache: IndexCache
|
||||
ranges: PlainRange[] | readonly PlainRange[]
|
||||
callback: (token: Token) => unknown,
|
||||
}
|
||||
|
||||
|
|
@ -27,41 +26,52 @@ export function isToken(range: PlainRange): range is Token {
|
|||
return type !== undefined && openLen !== undefined && tagLen !== undefined && closeLen !== undefined;
|
||||
}
|
||||
|
||||
export function moveTokenIndexCache(tokens: TokenGroup, offset: number, indexCache: IndexCache): void {
|
||||
if (tokens.length == 0) {
|
||||
indexCache.number = 0;
|
||||
return;
|
||||
export function iterTokenGroup(spec: IterTokenGroupSpec): void {
|
||||
let { tokens, ranges, callback } = spec,
|
||||
tokenI = findTokenIndexAt(tokens, ranges[0]?.from ?? 0),
|
||||
rangeI = 0;
|
||||
|
||||
if (tokenI === null) return;
|
||||
|
||||
while (
|
||||
tokenI < tokens.length &&
|
||||
rangeI < ranges.length
|
||||
) {
|
||||
if (ranges[rangeI].to <= tokens[tokenI].from) { rangeI++; continue }
|
||||
if (
|
||||
tokens[tokenI].from < ranges[rangeI].to &&
|
||||
tokens[tokenI].to > ranges[rangeI].from
|
||||
) {
|
||||
callback(tokens[tokenI]);
|
||||
}
|
||||
tokenI++;
|
||||
}
|
||||
|
||||
if (indexCache.number >= tokens.length)
|
||||
indexCache.number = tokens.length - 1;
|
||||
|
||||
let curIndex = indexCache.number,
|
||||
curToken = tokens[curIndex];
|
||||
if (offset < curToken.from && curIndex != 0) {
|
||||
do {
|
||||
curToken = tokens[--curIndex];
|
||||
} while (offset < curToken.from && curIndex != 0)
|
||||
} else if (offset > curToken.to && curIndex != tokens.length - 1) {
|
||||
do {
|
||||
curToken = tokens[++curIndex];
|
||||
} while (offset > curToken.to && curIndex != tokens.length - 1)
|
||||
}
|
||||
|
||||
indexCache.number = curIndex;
|
||||
}
|
||||
|
||||
export function iterTokenGroup(spec: IterTokenGroupSpec): void {
|
||||
let { tokens, ranges, callback, indexCache } = spec;
|
||||
moveTokenIndexCache(tokens, ranges[0]?.from ?? 0, indexCache);
|
||||
for (
|
||||
let i = indexCache.number, j = 0;
|
||||
i < tokens.length && j < ranges.length;
|
||||
) {
|
||||
if (ranges[j].to <= tokens[i].from) { j++; continue }
|
||||
if (tokens[i].from < ranges[j].to && tokens[i].to > ranges[j].from) {
|
||||
callback(tokens[i]);
|
||||
}
|
||||
i++;
|
||||
export function findTokenIndexAt(tokens: TokenGroup, offset: number): number | null {
|
||||
if (!tokens.length) return null;
|
||||
|
||||
let base = 32768,
|
||||
factor = 2,
|
||||
index = 0;
|
||||
|
||||
while (base !== 32) {
|
||||
if (tokens.length > base)
|
||||
while (tokens[index].from <= offset) {
|
||||
let end = Math.min(index + base, tokens.length);
|
||||
if (tokens[end - 1].to < offset) index = end;
|
||||
else break;
|
||||
}
|
||||
|
||||
base /= factor;
|
||||
factor *= 2;
|
||||
}
|
||||
|
||||
for (
|
||||
let end = Math.min(index + base, tokens.length);
|
||||
index < end && tokens[index].to < offset;
|
||||
index++
|
||||
);
|
||||
|
||||
return index < tokens.length ? index : null;
|
||||
}
|
||||
|
|
@ -203,11 +203,10 @@ export class ExtendedSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
this._makeDragable(groupEl, evt => {
|
||||
let oldIndex = evt.oldDraggableIndex,
|
||||
newIndex = evt.newDraggableIndex;
|
||||
if (this.isHidden || oldIndex === undefined || newIndex === undefined || oldIndex == newIndex) return;
|
||||
let { oldIndex, newIndex } = evt;
|
||||
if (oldIndex === undefined || newIndex === undefined || oldIndex == newIndex) return;
|
||||
tagManager.move(spec.type, oldIndex, newIndex);
|
||||
spec.onMove?.(this, oldIndex, newIndex);
|
||||
spec.onMoved?.(this, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -349,7 +348,7 @@ export class ExtendedSettingTab extends PluginSettingTab {
|
|||
});
|
||||
}
|
||||
|
||||
private _makeDragable(groupEl: HTMLElement, onEnd: (evt: SortableEvent) => unknown): void {
|
||||
private _makeDragable(groupEl: HTMLElement, onMoved: (evt: SortableEvent) => unknown): void {
|
||||
Sortable.create(groupEl, {
|
||||
handle: ".ems-button-drag-handle",
|
||||
animation: 100,
|
||||
|
|
@ -358,7 +357,7 @@ export class ExtendedSettingTab extends PluginSettingTab {
|
|||
fallbackOnBody: true,
|
||||
fallbackClass: "ems-setting-item-dragged",
|
||||
fallbackTolerance: 4,
|
||||
onEnd: onEnd,
|
||||
onEnd: onMoved,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -77,14 +77,13 @@ export class StyleSheetHandler {
|
|||
this._subHandlers.forEach(handler => handler.moveSingleRule(index, to));
|
||||
}
|
||||
|
||||
public moveRule(fromIndex: number, toIndex: number): void {
|
||||
if (fromIndex == toIndex) { return }
|
||||
let greaterIndex = Math.max(fromIndex, toIndex),
|
||||
smallerIndex = Math.min(fromIndex, toIndex),
|
||||
ruleStr = this._stylesheet.cssRules.item(greaterIndex)!.cssText;
|
||||
this.removeSingle(greaterIndex);
|
||||
this.insert(ruleStr, smallerIndex);
|
||||
this._subHandlers.forEach(handler => handler.moveRule(fromIndex, toIndex));
|
||||
public moveRule(oldIndex: number, newIndex: number): void {
|
||||
if (oldIndex == newIndex) return;
|
||||
if (newIndex > oldIndex) newIndex--;
|
||||
let ruleStr = this._stylesheet.cssRules.item(oldIndex)!.cssText;
|
||||
this.removeSingle(oldIndex);
|
||||
this.insert(ruleStr, newIndex);
|
||||
this._subHandlers.forEach(handler => handler.moveRule(oldIndex, newIndex));
|
||||
}
|
||||
|
||||
public removeSingle(index: number): void {
|
||||
|
|
|
|||
|
|
@ -70,13 +70,10 @@ export class TagManager {
|
|||
}
|
||||
|
||||
public move(format: TagSupportFormat, oldIndex: number, newIndex: number): void {
|
||||
if (newIndex == oldIndex) return;
|
||||
let configs = this.configsMap[format],
|
||||
movedConfig = configs[oldIndex];
|
||||
configs.splice(
|
||||
Math.min(oldIndex, newIndex),
|
||||
0,
|
||||
...configs.splice(Math.max(oldIndex, newIndex), 1)
|
||||
);
|
||||
movedConfig = configs.splice(oldIndex, 1)[0];
|
||||
configs.splice(newIndex, 0, movedConfig);
|
||||
this._fireHandlers(format, "move", movedConfig, oldIndex, newIndex);
|
||||
}
|
||||
|
||||
|
|
|
|||
48
src/types.d.ts
vendored
|
|
@ -1,6 +1,7 @@
|
|||
import type { SyntaxNode, Tree } from "@lezer/common";
|
||||
import type { Format, MarkdownViewMode, TokenLevel, Delimiter, TokenStatus, Field, DisplayBehaviour } from "src/enums";
|
||||
import { Line, RangeSet, RangeValue, Text, Range, SelectionRange, EditorState } from "@codemirror/state"
|
||||
import { Line, RangeSet, RangeValue, Text, Range, SelectionRange, EditorState } from "@codemirror/state";
|
||||
import {} from "@codemirror/language";
|
||||
import { Decoration, DecorationSet } from "@codemirror/view";
|
||||
import { ColorComponent, Command, DropdownComponent, Editor, ExtraButtonComponent, IconName, MarkdownFileInfo, MarkdownView, Setting, SliderComponent, TextAreaComponent, TextComponent, ToggleComponent } from "obsidian";
|
||||
import ExtendedMarkdownSyntax from "main";
|
||||
|
|
@ -8,19 +9,52 @@ import { TagMenu } from "src/editor-mode/ui-components";
|
|||
import { ExtendedSettingTab } from "src/settings/ui/setting-tab";
|
||||
|
||||
declare module "@codemirror/state" {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ILine extends Line {}
|
||||
|
||||
/**
|
||||
* MIT licensed, copyright (c) by Marijn Haverbeke and others at
|
||||
* CodeMirror.
|
||||
*
|
||||
* @see https://github.com/codemirror/state/blob/main/src/text.ts
|
||||
*/
|
||||
interface TextNode extends Text {
|
||||
readonly children: readonly Text[];
|
||||
}
|
||||
|
||||
/**
|
||||
* MIT licensed, copyright (c) by Marijn Haverbeke and others at
|
||||
* CodeMirror.
|
||||
*
|
||||
* @see https://github.com/codemirror/state/blob/main/src/text.ts
|
||||
*/
|
||||
interface TextLeaf extends Text {
|
||||
readonly children: null;
|
||||
text: string[];
|
||||
}
|
||||
}
|
||||
|
||||
declare module "@codemirror/language" {
|
||||
/**
|
||||
* MIT licensed, copyright (c) by Marijn Haverbeke and others at
|
||||
* CodeMirror.
|
||||
*
|
||||
* @see https://github.com/codemirror/language/blob/main/src/language.ts
|
||||
*/
|
||||
class LanguageState {
|
||||
readonly tree: Tree;
|
||||
readonly context: ParseContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* MIT licensed, copyright (c) by Marijn Haverbeke and others at
|
||||
* CodeMirror.
|
||||
*
|
||||
* @see https://github.com/codemirror/language/blob/main/src/language.ts
|
||||
*/
|
||||
interface ParseContext {
|
||||
tree: Tree;
|
||||
treeLen: number;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "obsidian" {
|
||||
interface MarkdownPostProcessorContext {
|
||||
el: HTMLElement;
|
||||
|
|
@ -32,6 +66,10 @@ declare global {
|
|||
interface Window {
|
||||
globalThis: typeof globalThis;
|
||||
}
|
||||
|
||||
type Writable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
}
|
||||
}
|
||||
|
||||
/** Token interface */
|
||||
|
|
@ -347,7 +385,7 @@ export type TagSettingsSpec = {
|
|||
tagFieldPlaceholder: string;
|
||||
tagFilter: RegExp;
|
||||
onAdd?: (settingTab: ExtendedSettingTab, tagSettingItem: Setting, newConfig: TagConfig) => unknown;
|
||||
onMove?: (settingTab: ExtendedSettingTab, oldIndex: number, newIndex: number) => unknown;
|
||||
onMoved?: (settingTab: ExtendedSettingTab, oldIndex: number, newIndex: number) => unknown;
|
||||
onResetted?: (settingTab: ExtendedSettingTab) => unknown;
|
||||
onDelete?: (settingTab: ExtendedSettingTab, deletedIndex: number) => unknown;
|
||||
onTagChange?: (settingTab: ExtendedSettingTab, changedConfig: TagConfig, index: number) => unknown;
|
||||
|
|
|
|||