mirror of
https://github.com/kotaindah55/extended-markdown-syntax.git
synced 2026-07-22 05:38:06 +00:00
First push
This commit is contained in:
parent
f3f8b64333
commit
eefa267fbb
59 changed files with 3049 additions and 0 deletions
48
esbuild.config.mjs
Normal file
48
esbuild.config.mjs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||
if you want to view the source, please visit the github repository of this plugin
|
||||
*/
|
||||
`;
|
||||
|
||||
const prod = (process.argv[2] === "production");
|
||||
|
||||
const context = await esbuild.context({
|
||||
banner: {
|
||||
js: banner,
|
||||
},
|
||||
entryPoints: ["main.ts"],
|
||||
bundle: true,
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/collab",
|
||||
"@codemirror/commands",
|
||||
"@codemirror/language",
|
||||
"@codemirror/lint",
|
||||
"@codemirror/search",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
"@lezer/highlight",
|
||||
"@lezer/lr",
|
||||
...builtins],
|
||||
format: "cjs",
|
||||
target: "es2018",
|
||||
logLevel: "info",
|
||||
sourcemap: prod ? false : "inline",
|
||||
treeShaking: true,
|
||||
outfile: "main.js",
|
||||
});
|
||||
|
||||
if (prod) {
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
}
|
||||
37
main.ts
Normal file
37
main.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Plugin } from "obsidian";
|
||||
import { drawSelection } from "@codemirror/view";
|
||||
import { extendedFormattingPlugin, customHighlightPlugin, alignerPlugin } from "./src/editorPlugins";
|
||||
import { customHighlightField, extendedFormattingField } from "./src/editorPlugins/stateFields";
|
||||
import { ExtendedFormattingPostProcessor, CustomHighlightPostProcessor, AlignerPostProcessor } from "./src/postProcessor";
|
||||
|
||||
export default class ExtendedMarkdownSyntax extends Plugin {
|
||||
|
||||
async onload() {
|
||||
await this.loadData();
|
||||
|
||||
// state fields
|
||||
this.registerEditorExtension([
|
||||
extendedFormattingField,
|
||||
customHighlightField
|
||||
]);
|
||||
|
||||
// view plugins
|
||||
this.registerEditorExtension([
|
||||
extendedFormattingPlugin,
|
||||
customHighlightPlugin,
|
||||
alignerPlugin,
|
||||
drawSelection()
|
||||
]);
|
||||
|
||||
// postprocessor
|
||||
this.registerMarkdownPostProcessor(new ExtendedFormattingPostProcessor(this.app.workspace).postProcess);
|
||||
this.registerMarkdownPostProcessor(new CustomHighlightPostProcessor().postProcess);
|
||||
this.registerMarkdownPostProcessor(new AlignerPostProcessor().postProcess);
|
||||
|
||||
console.log("Load Extended Markdown Syntax");
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
console.log("Unload Extended Markdown Syntax");
|
||||
}
|
||||
}
|
||||
9
manifest.json
Normal file
9
manifest.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"id": "extended-markdown-syntax",
|
||||
"name": "Extended Markdown Syntax",
|
||||
"version": "0.1.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Extend your markdown syntax using delimiters instead of HTML tags, such as underlining, superscript, subscript, highlighting, aligning.",
|
||||
"author": "Kotaindah55 (Sheva Ihza)",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
25
package.json
Normal file
25
package.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "extended-markdown-syntax",
|
||||
"version": "0.1.0",
|
||||
"description": "Extend your markdown syntax using delimiters instead of HTML tags, such as underlining, superscript, subscript, highlighting, spoiler, and aligning.",
|
||||
"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"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.3",
|
||||
"obsidian": "latest",
|
||||
"obsidian-typings": "2.2.0",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
}
|
||||
}
|
||||
146
readme.md
Normal file
146
readme.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# Extended Markdown Syntax
|
||||
|
||||
This 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 <u>underlined</u>, <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.
|
||||
|
||||
![[ems1.webm]]
|
||||
|
||||
## Features
|
||||
|
||||
This plugin has four main features:
|
||||
|
||||
1. Inline formatting (underline, superscript, subscript).
|
||||
2. Paragraph aligning.
|
||||
3. Custom highlight color.
|
||||
4. Discord-flavoured spoiler.
|
||||
|
||||
> [!note]
|
||||
> As it should be, the syntax will not be parsed if it is inside inline-code, codeblock, inline-math, mathblock, and internal links. Instead, they will still be styled by those syntaxes if they are inside them.
|
||||
|
||||
### Inline Formatting
|
||||
|
||||
| Format | Delimiter |
|
||||
| ----------- | --------------- |
|
||||
| underline | `++your text++` |
|
||||
| superscript | `^your text^` |
|
||||
| superscript | `~your text~` |
|
||||
|
||||
You can format text by wrapping it with delimiters.
|
||||
|
||||
```markdown
|
||||
++underline++ ^superscript^ ~subscript~
|
||||
```
|
||||
|
||||
This will result:
|
||||
![[Screenshot from 2024-09-24 12-13-28.png]]
|
||||
|
||||
You can also combine various formatting without any rendering issues in live-preview mode.:
|
||||
|
||||
```markdown
|
||||
Lorem ++ip*s*um++ dolor sit amet, consectetur adipiscing elit. Aenean sed enim ut dui vehicula **eleifend ++at ~non~++ magna**. Vestibulum viverra imperdiet magna ut pharetra. Proin eleifend orci felis, eget ultricies velit varius quis. Aliquam quis auctor lectus. Donec ~~cong^ue^~~ sed nibh sollicitudin dignissim.
|
||||
```
|
||||
|
||||
![[ems2.webm]]
|
||||
|
||||
Backslash can also be used to escape delimiters, so the text will not be formatted.
|
||||
|
||||
```markdown
|
||||
++will be formatted++ \++will be escaped++ +\+also will be escaped+\+.
|
||||
```
|
||||
|
||||
![[ems3.png]]
|
||||
|
||||
### Paragraph Aligning
|
||||
|
||||
| Align | Marker |
|
||||
| ------- | ----------- |
|
||||
| left | `!left!` |
|
||||
| right | `!right!` |
|
||||
| center | `!center!` |
|
||||
| justify | `!justify!` |
|
||||
|
||||
You can easily set the alignment of a paragraph by writing the aligning-marker at the beginning of the paragraph.
|
||||
|
||||
```markdown
|
||||
!left!Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed enim ut dui vehicula eleifend at non magna. Vestibulum viverra imperdiet magna ut pharetra. Proin eleifend orci felis, eget ultricies velit varius quis. Aliquam quis auctor lectus. Donec congue sed nibh sollicitudin dignissim.
|
||||
|
||||
!right!Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed enim ut dui vehicula eleifend at non magna. Vestibulum viverra imperdiet magna ut pharetra. Proin eleifend orci felis, eget ultricies velit varius quis. Aliquam quis auctor lectus. Donec congue sed nibh sollicitudin dignissim.
|
||||
|
||||
!center!Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed enim ut dui vehicula eleifend at non magna. Vestibulum viverra imperdiet magna ut pharetra. Proin eleifend orci felis, eget ultricies velit varius quis. Aliquam quis auctor lectus. Donec congue sed nibh sollicitudin dignissim.
|
||||
|
||||
!justify!Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sed enim ut dui vehicula eleifend at non magna. Vestibulum viverra imperdiet magna ut pharetra. Proin eleifend orci felis, eget ultricies velit varius quis. Aliquam quis auctor lectus. Donec congue sed nibh sollicitudin dignissim.
|
||||
```
|
||||
|
||||
This will shown as:
|
||||
![[ems4.png]]
|
||||
|
||||
Marker can also be applied to headings by placing it after hash marks and space:
|
||||
|
||||
```
|
||||
# !{alignment}!your heading
|
||||
```
|
||||
|
||||
![[ems5.webm]]
|
||||
|
||||
Moreover, markers can also be applied to callouts, both titles and content, and table cells.
|
||||
|
||||
```
|
||||
> [!note] !{alignment}!Your callout title
|
||||
> !{alignment}!Your callout content
|
||||
|
||||
To adjust the alignment of callout title, place alignment marker after callout type identifier (e.g. "[!note]") and space.
|
||||
```
|
||||
|
||||
![[ems10.webm]]
|
||||
|
||||
> [!note]
|
||||
> For now, I don't include the alignment feature in blockquotes for several reasons.
|
||||
|
||||
### Customize Your Highlight Color
|
||||
|
||||
You can customize your highlight color simply by adding color tag after opening delimiter with the following format:
|
||||
|
||||
```markdown
|
||||
=={color tag}highlighted text==
|
||||
```
|
||||
|
||||
![[ems6.webm]]
|
||||
|
||||
For the moment, the supported colors are red, orange, yellow, green, cyan, blue, purple, pink (there is also your accent color). If you don't want to use custom colors you can let the highlighted text without color tag (like this: `==default highlighting==`).
|
||||
|
||||
```markdown
|
||||
=={red}Red== =={orange}Orange== =={yellow}Yellow== =={green}Green== =={cyan}Cyan== =={blue}Blue== =={purple}Purple== =={pink}Pink== =={accent}Your accent== ==Default==
|
||||
```
|
||||
|
||||
This will be shown:
|
||||
![[ems7.png]]
|
||||
|
||||
Tired of retyping the color tag, you can change the highlight color by hovering over the color button and selecting any color you want (you can also remove the highlight).
|
||||
|
||||
![[ems8.webm]]
|
||||
|
||||
### Discord-Flavoured Spoiler
|
||||
|
||||
Inspired by discord, you can create **spoilers** by wrapping text with `||`. In live-preview mode, spoilers can be revealed by hovering over them. Whereas in view mode, spoilers are revealed by clicking on them. Just like previous inline-formatting, the backslash can also be applied to escape delimiters.
|
||||
|
||||
```markdown
|
||||
||your spoiler||
|
||||
```
|
||||
|
||||
![[ems9.webm]]
|
||||
|
||||
## Features to be Implemented in The Future
|
||||
|
||||
- [ ] Enable/disable formatting in settings
|
||||
- [ ] Customize formatting styles and highlighting colors
|
||||
- [ ] Applying syntax quickly using shortcuts
|
||||
- [ ] More syntaxes, if necessary
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Cannot escape spoilers that are inside table cells
|
||||
- The syntax in the heading is treated as normal text in the outline
|
||||
- Syntax is not properly parsed in post-processed codeblocks such as Admonition
|
||||
|
||||
Feel free to let me know if you find any bugs...
|
||||
60
src/declarations/index.d.ts
vendored
Normal file
60
src/declarations/index.d.ts
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { type WidgetType } from "@codemirror/view";
|
||||
import { type TableCell } from "obsidian";
|
||||
|
||||
declare module "obsidian" {
|
||||
|
||||
interface Workspace {
|
||||
_: EventListener;
|
||||
}
|
||||
|
||||
interface TableCell {
|
||||
row: number;
|
||||
col: number;
|
||||
text: string;
|
||||
contentEl: HTMLElement;
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
["quick-preview"]: EventRef<MarkdownView>[];
|
||||
}
|
||||
|
||||
interface EventRef<T = unknown> {
|
||||
ctx?: T;
|
||||
}
|
||||
|
||||
interface MarkdownView {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface MarkdownPostProcessorContext {
|
||||
containerEl: HTMLElement;
|
||||
el: HTMLElement;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
declare module "@codemirror/view" {
|
||||
|
||||
interface EditorView {
|
||||
docView: DocView;
|
||||
}
|
||||
|
||||
interface DocView {
|
||||
children: DocViewBlock<any>[];
|
||||
}
|
||||
|
||||
interface DocViewBlock<T extends BlockWidget> {
|
||||
widget?: T;
|
||||
}
|
||||
|
||||
class BlockWidget extends WidgetType {
|
||||
toDOM(view: EditorView): HTMLElement;
|
||||
containerEl: HTMLElement;
|
||||
text: string;
|
||||
}
|
||||
|
||||
class TableWidget extends BlockWidget {
|
||||
cellChildMap: Map<TableCell, unknown>;
|
||||
}
|
||||
|
||||
}
|
||||
71
src/editorPlugins/alignerPlugin.ts
Normal file
71
src/editorPlugins/alignerPlugin.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Decoration, ViewPlugin, type PluginValue, type DecorationSet, type EditorView, type ViewUpdate } from "@codemirror/view";
|
||||
import { type Range } from "@codemirror/state";
|
||||
import { RegExpCursor } from "@codemirror/search";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
import { checkSelectionOverlap } from "../utils/codemirror";
|
||||
import { isCodeblock } from "../utils/codemirror";
|
||||
import { HiddenWidget } from "./widgets";
|
||||
import { alignerDecorators } from "./decorators";
|
||||
|
||||
class Aligner implements PluginValue {
|
||||
|
||||
decorations: DecorationSet;
|
||||
isLivePreview: boolean;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
this.isLivePreview = view.state.field(editorLivePreviewField);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
|
||||
if (update.state.field(editorLivePreviewField) == this.isLivePreview) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
|
||||
this.isLivePreview = update.state.field(editorLivePreviewField);
|
||||
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
|
||||
let newDecorations: Range<Decoration>[] = [];
|
||||
|
||||
alignerDecorators.forEach((deco) => {
|
||||
|
||||
let searchCursor = new RegExpCursor(view.state.doc, deco.spec.query).next();
|
||||
if (searchCursor.done) {return newDecorations};
|
||||
let marker = deco.spec.marker as Decoration;
|
||||
|
||||
while (!searchCursor.done) {
|
||||
let [from, to] = [searchCursor.value.from, searchCursor.value.to];
|
||||
if (!isCodeblock(view, from, to)) {
|
||||
let linePosFrom = view.state.doc.lineAt(from).from;
|
||||
newDecorations.push(deco.range(linePosFrom, linePosFrom));
|
||||
newDecorations.push(marker.range(from, to));
|
||||
if (!checkSelectionOverlap(view.state.selection, from, to) && this.isLivePreview) {
|
||||
this.hideMarker(newDecorations, marker, from, to);
|
||||
}
|
||||
}
|
||||
searchCursor.next();
|
||||
}
|
||||
});
|
||||
|
||||
return Decoration.set(newDecorations, true);
|
||||
}
|
||||
|
||||
hideMarker(decorations: Range<Decoration>[], marker: Decoration, from: number, to: number) {
|
||||
let hiddenMarker = Decoration.replace({
|
||||
widget: new HiddenWidget(marker)
|
||||
});
|
||||
decorations.push(hiddenMarker.range(from, to));
|
||||
}
|
||||
}
|
||||
|
||||
export const alignerPlugin = ViewPlugin.fromClass(Aligner, {
|
||||
decorations: (value) => value.decorations
|
||||
});
|
||||
74
src/editorPlugins/customHighlightPlugin.ts
Normal file
74
src/editorPlugins/customHighlightPlugin.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import { Decoration, ViewPlugin, type PluginValue, type DecorationSet, type EditorView, type ViewUpdate, drawSelection } from "@codemirror/view";
|
||||
import { type Range } from "@codemirror/state";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
import { checkSelectionOverlap } from "../utils/codemirror";
|
||||
import { ColorButton, HiddenWidget } from "./widgets";
|
||||
import { highlightDecorator } from "./decorators";
|
||||
import { customHighlightField } from "./stateFields";
|
||||
|
||||
class CustomHighlight implements PluginValue {
|
||||
|
||||
decorations: DecorationSet;
|
||||
isLivePreview: boolean;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
this.isLivePreview = view.state.field(editorLivePreviewField);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
|
||||
if (update.state.field(editorLivePreviewField) == this.isLivePreview) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
|
||||
this.isLivePreview = update.state.field(editorLivePreviewField);
|
||||
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
|
||||
let newDecorations: Range<Decoration>[] = [];
|
||||
let marker = highlightDecorator.markerDeco as Decoration;
|
||||
|
||||
view.state.field(customHighlightField).hlCollection.forEach(hl => {
|
||||
|
||||
let [outerFrom, innerFrom, innerTo, outerTo, color] = [...hl];
|
||||
let markerLength = color ? color.length + 2 : 0;
|
||||
|
||||
newDecorations.push(
|
||||
Decoration.mark({class: `${highlightDecorator.class}-${color ?? "default"}`}).range(outerFrom, outerTo),
|
||||
Decoration.mark({class: `${highlightDecorator.class}-first-letter`}).range(innerFrom + markerLength, innerFrom + markerLength + 1),
|
||||
Decoration.mark({class: `${highlightDecorator.class}-last-letter`}).range(innerTo - 1, innerTo)
|
||||
);
|
||||
|
||||
if (checkSelectionOverlap(view.state.selection, outerFrom, outerTo) || !this.isLivePreview) {
|
||||
newDecorations.push(Decoration.widget({widget: new ColorButton(
|
||||
color, innerFrom, innerFrom + markerLength, outerFrom, outerTo, innerFrom, innerTo
|
||||
), side: 1}).range(innerFrom));
|
||||
}
|
||||
|
||||
if (color && !(checkSelectionOverlap(view.state.selection, innerFrom, innerFrom + markerLength) || !this.isLivePreview)) {
|
||||
this.hideMarker(newDecorations, marker, innerFrom, innerFrom + markerLength);
|
||||
}
|
||||
});
|
||||
|
||||
return Decoration.set(newDecorations, true);
|
||||
}
|
||||
|
||||
hideMarker(decorations: Range<Decoration>[], marker: Decoration, from: number, to: number) {
|
||||
let hiddenMarker = Decoration.replace({
|
||||
widget: new HiddenWidget(marker)
|
||||
});
|
||||
decorations.push(
|
||||
hiddenMarker.range(from, to)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const customHighlightPlugin = ViewPlugin.fromClass(CustomHighlight, {
|
||||
decorations: (value) => value.decorations
|
||||
});
|
||||
28
src/editorPlugins/decorators/alignerDecorators.ts
Normal file
28
src/editorPlugins/decorators/alignerDecorators.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Decoration } from "@codemirror/view"
|
||||
|
||||
export const alignerDecorators = [
|
||||
Decoration.line({
|
||||
class: "cmx-align-left",
|
||||
query: /(?<=^|^#{1,6} +)!left!/gm,
|
||||
style: "text-align: left;",
|
||||
marker: Decoration.mark({class: "cmx-align-left-marker"})
|
||||
}),
|
||||
Decoration.line({
|
||||
class: "cmx-align-right",
|
||||
query: /(?<=^|^#{1,6} +)!right!/gm,
|
||||
style: "text-align: right;",
|
||||
marker: Decoration.mark({class: "cmx-align-right-marker"})
|
||||
}),
|
||||
Decoration.line({
|
||||
class: "cmx-align-center",
|
||||
query: /(?<=^|^#{1,6} +)!center!/gm,
|
||||
style: "text-align: center;",
|
||||
marker: Decoration.mark({class: "cmx-align-center-marker"})
|
||||
}),
|
||||
Decoration.line({
|
||||
class: "cmx-align-justify",
|
||||
query: /(?<=^|^#{1,6} +)!justify!/gm,
|
||||
style: "text-align: justify;",
|
||||
marker: Decoration.mark({class: "cmx-align-justify-marker"})
|
||||
}),
|
||||
]
|
||||
9
src/editorPlugins/decorators/formattingDecorators.ts
Normal file
9
src/editorPlugins/decorators/formattingDecorators.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
import { DelimType } from "../../enums";
|
||||
|
||||
export const formattingDecorators = new Map([
|
||||
[DelimType.U, Decoration.mark({class: "cmx-underline", delimDeco: Decoration.mark({class: "cmx-underline cmx-formatting-underline"})})],
|
||||
[DelimType.Sup, Decoration.mark({class: "cmx-superscript", delimDeco: Decoration.mark({class: "cmx-superscript cmx-formatting-superscript"})})],
|
||||
[DelimType.Sub, Decoration.mark({class: "cmx-subscript", delimDeco: Decoration.mark({class: "cmx-subscript cmx-formatting-subscript"})})],
|
||||
[DelimType.Spoiler, Decoration.mark({class: "cmx-spoiler", delimDeco: Decoration.mark({class: "cmx-spoiler cmx-formatting-spoiler"})})]
|
||||
]);
|
||||
5
src/editorPlugins/decorators/hiddenSpoiler.ts
Normal file
5
src/editorPlugins/decorators/hiddenSpoiler.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
|
||||
export const hiddenSpoiler = Decoration.mark({
|
||||
class: "cmx-spoiler cmx-spoiler-hidden"
|
||||
});
|
||||
6
src/editorPlugins/decorators/highlightDecorator.ts
Normal file
6
src/editorPlugins/decorators/highlightDecorator.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
|
||||
export const highlightDecorator = {
|
||||
class: "cmx-highlight cmx-highlight",
|
||||
markerDeco: Decoration.mark({class: "cmx-highlight-marker"})
|
||||
}
|
||||
4
src/editorPlugins/decorators/index.ts
Normal file
4
src/editorPlugins/decorators/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./alignerDecorators";
|
||||
export * from "./highlightDecorator";
|
||||
export * from "./formattingDecorators";
|
||||
export * from "./hiddenSpoiler";
|
||||
85
src/editorPlugins/extendedFormattingPlugin.ts
Normal file
85
src/editorPlugins/extendedFormattingPlugin.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { Decoration, type EditorView, type PluginValue, type DecorationSet, type ViewUpdate, ViewPlugin } from "@codemirror/view";
|
||||
import { type Range } from "@codemirror/state";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
import { extendedFormattingField } from "./stateFields";
|
||||
import { checkSelectionOverlap } from "../utils/codemirror";
|
||||
import { HiddenWidget } from "./widgets";
|
||||
import { type DelimPos } from "../types";
|
||||
import { formattingDecorators, hiddenSpoiler } from "./decorators";
|
||||
|
||||
class ExtendedFormatting implements PluginValue {
|
||||
|
||||
decorations: DecorationSet;
|
||||
isLivePreview: boolean;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = this.buildDecorations(view);
|
||||
this.isLivePreview = view.state.field(editorLivePreviewField);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
|
||||
if (update.state.field(editorLivePreviewField) == this.isLivePreview) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
|
||||
this.isLivePreview = update.state.field(editorLivePreviewField);
|
||||
|
||||
if (update.docChanged || update.selectionSet) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
buildDecorations(view: EditorView) {
|
||||
|
||||
let newDecorations: Range<Decoration>[] = [];
|
||||
|
||||
view.state.field(extendedFormattingField).delimField.forEach((posCollection, type) => {
|
||||
|
||||
if (posCollection.length === 0) {return};
|
||||
|
||||
let decorator = formattingDecorators.get(type) as Decoration;
|
||||
let delimDecorator = decorator?.spec.delimDeco as Decoration;
|
||||
|
||||
|
||||
posCollection.forEach(pos => {
|
||||
|
||||
let hasClosingDelim = pos[2] != pos[3];
|
||||
|
||||
if (type != "spoiler") {
|
||||
newDecorations.push(decorator.range(pos[1], pos[2]));
|
||||
}
|
||||
|
||||
newDecorations.push(delimDecorator.range(pos[0], pos[1]));
|
||||
if (hasClosingDelim) {
|
||||
newDecorations.push(delimDecorator.range(pos[2], pos[3]));
|
||||
}
|
||||
|
||||
if ((!view.hasFocus || !checkSelectionOverlap(view.state.selection, pos[0], pos[3])) && this.isLivePreview) {
|
||||
type == "spoiler" && newDecorations.push(hiddenSpoiler.range(pos[1], pos[2]));
|
||||
this.hideDelim(newDecorations, delimDecorator, pos, hasClosingDelim);
|
||||
} else if (type == "spoiler") {
|
||||
newDecorations.push(decorator.range(pos[1], pos[2]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Decoration.set(newDecorations, true);
|
||||
}
|
||||
|
||||
hideDelim(decorations: Range<Decoration>[], decorator: Decoration, pos: DelimPos, hasClosingDelim: boolean) {
|
||||
|
||||
let hiddenDelim = Decoration.replace({
|
||||
widget: new HiddenWidget(decorator)
|
||||
});
|
||||
|
||||
decorations.push(hiddenDelim.range(pos[0], pos[1]));
|
||||
if (hasClosingDelim) {
|
||||
decorations.push(hiddenDelim.range(pos[2], pos[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const extendedFormattingPlugin = ViewPlugin.fromClass(ExtendedFormatting, {
|
||||
decorations: value => value.decorations
|
||||
});
|
||||
3
src/editorPlugins/index.ts
Normal file
3
src/editorPlugins/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./extendedFormattingPlugin";
|
||||
export * from "./alignerPlugin";
|
||||
export * from "./customHighlightPlugin";
|
||||
110
src/editorPlugins/stateFields/customHighlightField.ts
Normal file
110
src/editorPlugins/stateFields/customHighlightField.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { type Text, StateField, type StateEffect } from "@codemirror/state";
|
||||
import { ParseContext, syntaxTree } from "@codemirror/language";
|
||||
import { type SyntaxNode, type Tree } from "@lezer/common";
|
||||
import { type HighlightProp } from "../../types";
|
||||
|
||||
function getHighlight(doc: Text, tree: Tree, previous?: {from: number, hlCollection: HighlightProp[]}) {
|
||||
|
||||
let text = doc.toString();
|
||||
let hlChecker = /highlight/;
|
||||
let hlDelim = /(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))==+(?=(?:\{([\w\d\-]+)\})|[^=\s])/g;
|
||||
let hlCollection: HighlightProp[] = [];
|
||||
|
||||
if (previous) {
|
||||
|
||||
hlCollection = previous.hlCollection;
|
||||
let {from} = previous,
|
||||
lastIndex = from - 2;
|
||||
|
||||
hlCollection.length && hlCollection.findLast((hlProp, index, collection) => {
|
||||
|
||||
if (from >= hlProp[1]) {
|
||||
|
||||
if (from < hlProp[3] || from == hlProp[3]) {
|
||||
lastIndex = hlProp[0];
|
||||
collection.splice(index);
|
||||
} else {
|
||||
collection.splice(index + 1);
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (index == 0) {
|
||||
collection.splice(0);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
hlDelim.lastIndex = lastIndex;
|
||||
}
|
||||
|
||||
for (
|
||||
let hlMatch = hlDelim.exec(text),
|
||||
node: SyntaxNode;
|
||||
hlMatch && (node = tree.resolveInner(hlMatch.index, 1));
|
||||
hlMatch = hlDelim.exec(text)
|
||||
) {
|
||||
|
||||
let openingFrom = node.from;
|
||||
|
||||
if (openingFrom > tree.length) { break }
|
||||
|
||||
if (!hlChecker.test(node.name)) { continue }
|
||||
|
||||
let to: number;
|
||||
|
||||
while (true) {
|
||||
to = node.to;
|
||||
node = tree.resolveInner(node.to, 1);
|
||||
if (!hlChecker.test(node.name)) { break }
|
||||
}
|
||||
|
||||
let lastNode = tree.resolveInner(to, -1);
|
||||
let innerHlEnd = /formatting-highlight/.test(lastNode.name) ? lastNode.from : to;
|
||||
|
||||
// opening delim start : openingFrom
|
||||
// end : hlDelim.lastIndex
|
||||
// closing delim start : innerHlEnd
|
||||
// end : to
|
||||
// highlight color : hlMatch[1]
|
||||
hlCollection.push([openingFrom, hlDelim.lastIndex, innerHlEnd, to, hlMatch[1]]);
|
||||
hlDelim.lastIndex = to;
|
||||
}
|
||||
|
||||
return hlCollection;
|
||||
}
|
||||
|
||||
export const customHighlightField: StateField<{hlCollection: ReturnType<typeof getHighlight>, treeLength: number}> = StateField.define({
|
||||
|
||||
create(state) {
|
||||
|
||||
let tree = syntaxTree(state);
|
||||
|
||||
return {
|
||||
hlCollection: getHighlight(state.doc, tree),
|
||||
treeLength: tree.length
|
||||
};
|
||||
},
|
||||
|
||||
update(value, transaction) {
|
||||
|
||||
let tree = syntaxTree(transaction.state);
|
||||
|
||||
transaction.effects.forEach((value: StateEffect<{context: ParseContext, tree: Tree}>) => {
|
||||
tree = (value.value?.tree ?? tree);
|
||||
});
|
||||
|
||||
if (transaction.docChanged || tree.length != value.treeLength) {
|
||||
|
||||
let from = Math.min(value.treeLength, tree.length);
|
||||
value.treeLength = tree.length;
|
||||
|
||||
transaction.changes.iterChangedRanges((fromA) => {
|
||||
from = Math.min(from, fromA);
|
||||
}, false);
|
||||
|
||||
getHighlight(transaction.state.doc, tree, {from, hlCollection: value.hlCollection})
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
})
|
||||
166
src/editorPlugins/stateFields/extendedFormattingField.ts
Normal file
166
src/editorPlugins/stateFields/extendedFormattingField.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import { StateField, type Text, type StateEffect } from '@codemirror/state';
|
||||
import { syntaxTree, type ParseContext } from '@codemirror/language';
|
||||
import { type Tree } from "@lezer/common"
|
||||
import { type DelimPos } from "../../types";
|
||||
import { isColumnSeparator, isRestrictedPos, isTable } from '../../utils/codemirror';
|
||||
import { delimRegExps } from '../../regExps';
|
||||
import { DelimType } from '../../enums';
|
||||
|
||||
/**
|
||||
* Used to get the position of each delimiter in the editor.
|
||||
*
|
||||
*/
|
||||
function getDelimiterFromEditor(doc: Text, tree: Tree, type: DelimType, openingDelim: RegExp, closingDelim: RegExp, previous?: {from: number, delimLength: number, posCollection: DelimPos[]}) {
|
||||
|
||||
let text = doc.toString(),
|
||||
doubleNewLineChar = /\n\n+/g,
|
||||
verticalBarChar = /(?<!^)\|/g,
|
||||
delimPosCollection: DelimPos[] = [];
|
||||
|
||||
if (previous) {
|
||||
|
||||
delimPosCollection = previous.posCollection;
|
||||
let {from, delimLength} = previous,
|
||||
lastIndex = /* type == "spoiler" ? doc.lineAt(from).from : */ from - delimLength;
|
||||
|
||||
delimPosCollection.length && delimPosCollection.findLast((delimPos, index, collection) => {
|
||||
|
||||
if (from >= delimPos[1]) {
|
||||
|
||||
if (from < delimPos[3] || (from == delimPos[3] && delimPos[2] == delimPos[3])) {
|
||||
lastIndex = delimPos[0];
|
||||
collection.splice(index);
|
||||
} else if (from - delimPos[3] < delimLength) {
|
||||
lastIndex = delimPos[3];
|
||||
collection.splice(index + 1);
|
||||
} else {
|
||||
// type == "spoiler" && (lastIndex = Math.max(closingTo, lastIndex));
|
||||
collection.splice(index + 1);
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (index == 0) {
|
||||
collection.splice(0);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
openingDelim.lastIndex = doubleNewLineChar.lastIndex = lastIndex;
|
||||
}
|
||||
|
||||
mainLoop: for (
|
||||
let openingPos = openingDelim.exec(text)?.indices![0],
|
||||
endLineOffset = doubleNewLineChar.exec(text)?.index ?? text.length;
|
||||
openingPos;
|
||||
openingPos = openingDelim.exec(text)?.indices![0]
|
||||
) {
|
||||
|
||||
if (openingPos[1] > tree.length) { break }
|
||||
|
||||
if (isRestrictedPos(tree, openingPos[0])) { continue }
|
||||
|
||||
let delimPos: DelimPos = [0, 0, 0, 0];
|
||||
|
||||
closingDelim.lastIndex = openingPos[1];
|
||||
let endCellOffset = 0,
|
||||
currentLine = doc.lineAt(openingPos[0]),
|
||||
isTableRow = isTable(tree, openingPos[0]);
|
||||
|
||||
if (isTableRow) {
|
||||
|
||||
if (type == "spoiler") {
|
||||
openingDelim.lastIndex = currentLine.to;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (verticalBarChar.lastIndex = openingPos[0] - currentLine.from;;) {
|
||||
let verticalBarPos = verticalBarChar.exec(currentLine.text)?.index;
|
||||
|
||||
if (!verticalBarPos) {
|
||||
endCellOffset = currentLine.to;
|
||||
break;
|
||||
|
||||
} else if (isColumnSeparator(tree, verticalBarPos + currentLine.from)) {
|
||||
endCellOffset = verticalBarPos + currentLine.from;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (openingDelim.lastIndex > endLineOffset) {
|
||||
|
||||
doubleNewLineChar.lastIndex = openingDelim.lastIndex;
|
||||
endLineOffset = doubleNewLineChar.exec(text)?.index || text.length;
|
||||
}
|
||||
|
||||
[delimPos[0], delimPos[1]] = openingPos;
|
||||
|
||||
while (true) {
|
||||
|
||||
let closingPos = closingDelim.exec(text)?.indices![0];
|
||||
|
||||
if (!closingPos || closingDelim.lastIndex > endLineOffset || (isTableRow && closingDelim.lastIndex > endCellOffset)) {
|
||||
delimPos[2] = delimPos[3] = openingDelim.lastIndex = isTableRow ? endCellOffset : endLineOffset;
|
||||
closingDelim.lastIndex > endLineOffset && (endLineOffset = doubleNewLineChar.exec(text)?.index || text.length);
|
||||
// if (type == "spoiler") { continue mainLoop }
|
||||
|
||||
} else {
|
||||
if (isRestrictedPos(tree, closingPos[0])) { continue }
|
||||
openingDelim.lastIndex = closingPos[1];
|
||||
[delimPos[2], delimPos[3]] = closingPos;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
delimPosCollection.push(delimPos);
|
||||
}
|
||||
|
||||
closingDelim.lastIndex &&= 0;
|
||||
return delimPosCollection;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns
|
||||
*/
|
||||
export const extendedFormattingField: StateField<{delimField: Map<DelimType, ReturnType<typeof getDelimiterFromEditor>>, treeLength: number}> = StateField.define({
|
||||
|
||||
create(state) {
|
||||
|
||||
let delimField = new Map<DelimType, ReturnType<typeof getDelimiterFromEditor>>();
|
||||
let tree = syntaxTree(state);
|
||||
|
||||
delimRegExps.forEach((value, type) => {
|
||||
delimField.set(type, getDelimiterFromEditor(state.doc, tree, type, value.openingDelim, value.closingDelim));
|
||||
});
|
||||
|
||||
return {delimField, treeLength: tree.length};
|
||||
|
||||
},
|
||||
|
||||
update(value, transaction) {
|
||||
|
||||
let tree = syntaxTree(transaction.state);
|
||||
|
||||
transaction.effects.forEach((value: StateEffect<{context: ParseContext, tree: Tree}>) => {
|
||||
tree = (value.value?.tree ?? tree);
|
||||
});
|
||||
|
||||
if (transaction.docChanged || tree.length != value.treeLength) {
|
||||
|
||||
let from = Math.min(value.treeLength, tree.length);
|
||||
value.treeLength = tree.length;
|
||||
|
||||
transaction.changes.iterChangedRanges((fromA) => {
|
||||
from = Math.min(from, fromA);
|
||||
}, false);
|
||||
|
||||
value.delimField.forEach((posCollection, type) => {
|
||||
let {openingDelim, closingDelim, length} = delimRegExps.get(type)!;
|
||||
getDelimiterFromEditor(transaction.state.doc, tree, type, openingDelim, closingDelim, {from, delimLength: length, posCollection});
|
||||
});
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
});
|
||||
2
src/editorPlugins/stateFields/index.ts
Normal file
2
src/editorPlugins/stateFields/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./extendedFormattingField";
|
||||
export * from "./customHighlightField";
|
||||
110
src/editorPlugins/widgets/ColorButton.ts
Normal file
110
src/editorPlugins/widgets/ColorButton.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import { WidgetType, EditorView } from "@codemirror/view"
|
||||
import { Menu } from "obsidian";
|
||||
|
||||
/**
|
||||
* These code snippets are taken from
|
||||
* https://github.com/Superschnizel/obisdian-fast-text-color/blob/master/src/widgets/ColorWidget.ts
|
||||
* with some modifications.
|
||||
*/
|
||||
export class ColorButton extends WidgetType {
|
||||
|
||||
color: string;
|
||||
markerFrom: number;
|
||||
markerTo: number;
|
||||
menu: Menu | null;
|
||||
outerFrom: number;
|
||||
outerTo: number;
|
||||
innerFrom: number;
|
||||
innerTo: number;
|
||||
|
||||
private readonly colors = ["red", "orange", "yellow", "green", "cyan", "blue", "purple", "pink", "accent", "default"];
|
||||
|
||||
constructor(color: string = "default", markerFrom: number, markerTo: number, outerFrom: number, outerTo: number, innerFrom: number, innerTo: number) {
|
||||
super();
|
||||
this.color = color;
|
||||
this.markerFrom = markerFrom;
|
||||
this.markerTo = markerTo;
|
||||
this.outerFrom = outerFrom;
|
||||
this.outerTo = outerTo;
|
||||
this.innerFrom = innerFrom;
|
||||
this.innerTo = innerTo;
|
||||
}
|
||||
|
||||
eq(other: ColorButton) {
|
||||
return other.color == this.color;
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
let btn = document.createElement("span");
|
||||
|
||||
btn.setAttribute("aria-hidden", "true");
|
||||
btn.className = "cmx-color-btn";
|
||||
|
||||
btn.onclick = evt => {
|
||||
view.dispatch({
|
||||
selection: {
|
||||
anchor: this.markerFrom,
|
||||
head: this.markerTo
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
btn.onmouseover = evt => {
|
||||
if (this.menu != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.menu = new Menu();
|
||||
|
||||
// @ts-ignore
|
||||
(this.menu.dom as HTMLElement).addClass("es-highlight-colors-modal");
|
||||
|
||||
this.colors.forEach((color) => {
|
||||
this.menu!.addItem((item) => {
|
||||
item
|
||||
.setTitle(color)
|
||||
.onClick(evt => {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: this.markerFrom,
|
||||
to: this.markerTo,
|
||||
insert: color != "default" ? `{${color}}` : ""
|
||||
}
|
||||
})
|
||||
})
|
||||
.setIcon("palette");
|
||||
// @ts-ignore
|
||||
(item.dom as HTMLElement).addClass(`es-item-${color}`);
|
||||
})
|
||||
});
|
||||
|
||||
this.menu.addItem(item => {
|
||||
item
|
||||
.setTitle("Remove")
|
||||
.setIcon("ban")
|
||||
.onClick((evt) => {
|
||||
view.dispatch({
|
||||
changes: [{
|
||||
from: this.outerFrom,
|
||||
to: this.markerTo,
|
||||
insert: ''
|
||||
}, {
|
||||
from: this.innerTo,
|
||||
to: this.outerTo,
|
||||
insert: ''
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
this.menu.showAtPosition({ x: rect.left, y: rect.bottom })
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
19
src/editorPlugins/widgets/HiddenWidget.ts
Normal file
19
src/editorPlugins/widgets/HiddenWidget.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { WidgetType, Decoration, EditorView } from "@codemirror/view"
|
||||
|
||||
export class HiddenWidget extends WidgetType {
|
||||
|
||||
mark: Decoration;
|
||||
|
||||
constructor(formattingMark: Decoration) {
|
||||
super();
|
||||
this.mark = formattingMark;
|
||||
}
|
||||
|
||||
eq(other: HiddenWidget) {
|
||||
return other.mark == this.mark;
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
return document.createElement("span");
|
||||
}
|
||||
}
|
||||
2
src/editorPlugins/widgets/index.ts
Normal file
2
src/editorPlugins/widgets/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./ColorButton";
|
||||
export * from "./HiddenWidget";
|
||||
6
src/enums/DelimType.ts
Normal file
6
src/enums/DelimType.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum DelimType {
|
||||
U = "u",
|
||||
Sup = "sup",
|
||||
Sub = "sub",
|
||||
Spoiler = "spoiler"
|
||||
}
|
||||
1
src/enums/index.ts
Normal file
1
src/enums/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./DelimType";
|
||||
4
src/interfaces/BlockquoteChildSection.ts
Normal file
4
src/interfaces/BlockquoteChildSection.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface BlockquoteChildSection {
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
1
src/interfaces/index.ts
Normal file
1
src/interfaces/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./BlockquoteChildSection";
|
||||
49
src/postProcessor/AlignerPostProcessor.ts
Normal file
49
src/postProcessor/AlignerPostProcessor.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { type MarkdownPostProcessor } from "obsidian";
|
||||
import { type ExtendedElement } from "../types";
|
||||
|
||||
export class AlignerPostProcessor {
|
||||
|
||||
private readonly targetedElements = 'p, h1, h2, h3, h4, h5, h6, td, th, .callout-title-inner';
|
||||
|
||||
constructor() {}
|
||||
|
||||
private format = (el: ExtendedElement) => {
|
||||
|
||||
let alignMark = /^!((?:left)|(?:right)|(?:center)|(?:justify))!/;
|
||||
let alignMarkExecArr: RegExpExecArray | null;
|
||||
|
||||
if (el.parentElement?.tagName == "BLOCKQUOTE") { return }
|
||||
|
||||
if (el.parentElement?.hasClass("callout-content")) {
|
||||
alignMark = /^ *!((?:left)|(?:right)|(?:center)|(?:justify))!/;
|
||||
alignMarkExecArr = alignMark.exec(el.innerHTML);
|
||||
|
||||
} else if (el.toString() == "[object HTMLHeadingElement]" && el.childNodes[1].nodeType == 3) {
|
||||
alignMarkExecArr = alignMark.exec(el.childNodes[1].textContent!);
|
||||
|
||||
} else {
|
||||
alignMarkExecArr = alignMark.exec(el.innerHTML);
|
||||
}
|
||||
|
||||
if (alignMarkExecArr) {
|
||||
|
||||
el.innerHTML = el.innerHTML.replace(alignMarkExecArr[0], "");
|
||||
|
||||
if (el.hasClass("callout-title-inner")) {
|
||||
el.parentElement!.style.justifyContent = alignMarkExecArr[1] != "justify" ? alignMarkExecArr[1] : "space-between";
|
||||
} else {
|
||||
el.style.textAlign = alignMarkExecArr[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
postProcess: MarkdownPostProcessor = (container) => {
|
||||
if (container.classList.contains("table-cell-wrapper")) {
|
||||
this.format(container);
|
||||
} else {
|
||||
container.querySelectorAll<HTMLElement>(this.targetedElements).forEach((el) => {
|
||||
this.format(el);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/postProcessor/CustomHighlightPostProcessor.ts
Normal file
25
src/postProcessor/CustomHighlightPostProcessor.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { type MarkdownPostProcessor } from "obsidian";
|
||||
import { type ExtendedElement } from "../types";
|
||||
|
||||
export class CustomHighlightPostProcessor {
|
||||
|
||||
constructor() {}
|
||||
|
||||
private format = (el: ExtendedElement) => {
|
||||
|
||||
let markElements = el.querySelectorAll<ExtendedElement>("mark");
|
||||
let colorMark = /^\{([\w\d\-_]+)\}/;
|
||||
|
||||
markElements.forEach((mark) => {
|
||||
let colorMarkExecArr = colorMark.exec(mark.innerText);
|
||||
if (colorMarkExecArr) {
|
||||
mark.innerHTML = mark.innerHTML.replace(colorMarkExecArr[0], "");
|
||||
mark.classList.add(`cmx-highlight-${colorMarkExecArr[1]}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
postProcess: MarkdownPostProcessor = (container, t) => {
|
||||
this.format(container);
|
||||
}
|
||||
}
|
||||
98
src/postProcessor/ExtendedFormattingPostProcessor.ts
Normal file
98
src/postProcessor/ExtendedFormattingPostProcessor.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { type TableWidget, type DocViewBlock, type BlockWidget } from "@codemirror/view";
|
||||
import { type MarkdownView, type Workspace, type MarkdownPostProcessor } from "obsidian";
|
||||
import { postProcessorDelimRegExps } from "../regExps";
|
||||
import { iterDelimReplacement, splitCells, getBlockquoteSections } from "../utils/postProcess";
|
||||
import { getTextAtLine } from "../utils";
|
||||
|
||||
export class ExtendedFormattingPostProcessor {
|
||||
|
||||
view: MarkdownView | null;
|
||||
workspace: Workspace;
|
||||
|
||||
private readonly targetedElements = 'p, li, h1, h2, h3, h4, h5, h6, .callout-title-inner';
|
||||
private readonly excludedSelector = 'code, a.internal-link';
|
||||
|
||||
constructor(workspace: Workspace) {
|
||||
this.view = workspace.activeEditor as typeof this.view;
|
||||
this.workspace = workspace;
|
||||
}
|
||||
|
||||
private format(contentEl: HTMLElement, rawText: string, isTableCell?: boolean) {
|
||||
|
||||
postProcessorDelimRegExps.forEach((delimQuery, tagName) => {
|
||||
iterDelimReplacement(contentEl, rawText, delimQuery, tagName, this.excludedSelector, isTableCell);
|
||||
});
|
||||
}
|
||||
|
||||
postProcess: MarkdownPostProcessor = (container, ctx) => {
|
||||
|
||||
let sectionInfo = ctx.getSectionInfo(container);
|
||||
|
||||
if (sectionInfo) {
|
||||
|
||||
let rawTextData = getTextAtLine(sectionInfo.text, sectionInfo.lineStart, sectionInfo.lineEnd);
|
||||
let firstChild = container.firstElementChild;
|
||||
|
||||
if (firstChild?.tagName == "TABLE") {
|
||||
|
||||
splitCells(rawTextData).forEach((rawText, i) => {
|
||||
this.format(container.querySelectorAll<HTMLElement>("td, th")[i], rawText, true);
|
||||
});
|
||||
|
||||
} else if (firstChild?.tagName == "BLOCKQUOTE") {
|
||||
|
||||
getBlockquoteSections(rawTextData, false).forEach((section, i) => {
|
||||
this.format(container.querySelectorAll<HTMLElement>(this.targetedElements)[i], section.text);
|
||||
});
|
||||
|
||||
} else if (firstChild?.classList.contains("callout")) {
|
||||
|
||||
getBlockquoteSections(rawTextData, true).forEach((section, i) => {
|
||||
this.format(container.querySelectorAll<HTMLElement>(this.targetedElements)[i], section.text);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
container.querySelectorAll<HTMLElement>(this.targetedElements).forEach(contentEl => {
|
||||
this.format(contentEl, rawTextData);
|
||||
});
|
||||
}
|
||||
|
||||
container.querySelectorAll<HTMLElement>("spoiler").forEach(el => {
|
||||
el.addEventListener("click", event => {
|
||||
let spoiler = event.currentTarget as HTMLElement;
|
||||
spoiler.hasClass("cmx-revealed") ? spoiler.removeClass("cmx-revealed") : spoiler.addClass("cmx-revealed");
|
||||
})
|
||||
})
|
||||
|
||||
} else {
|
||||
|
||||
let cmEditor = this.view?.editor?.cm ??
|
||||
this.workspace._["quick-preview"]
|
||||
.find((evtRef) => evtRef.ctx?.hasOwnProperty("editMode") && evtRef.ctx?.path == ctx.sourcePath)
|
||||
?.ctx?.editMode.cm!;
|
||||
|
||||
if (container.classList.contains("table-cell-wrapper")) {
|
||||
|
||||
let tableBlock: DocViewBlock<TableWidget> | undefined = cmEditor.docView.children.find(el => el.widget?.containerEl == ctx.containerEl);
|
||||
let tableCells = tableBlock?.widget?.cellChildMap.keys()!;
|
||||
|
||||
for (let cell of tableCells) {
|
||||
if (cell.contentEl == ctx.el) {
|
||||
this.format(container, cell.text, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (ctx.containerEl.classList.contains("cm-callout")) {
|
||||
|
||||
let calloutBlock: DocViewBlock<BlockWidget> | undefined = cmEditor.docView.children.find(el => el.widget?.containerEl == ctx.containerEl);
|
||||
let calloutText = calloutBlock?.widget?.text!;
|
||||
|
||||
getBlockquoteSections(calloutText, true).forEach((section, i) => {
|
||||
this.format(container.querySelectorAll<HTMLElement>(this.targetedElements)[i], section.text);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/postProcessor/index.ts
Normal file
3
src/postProcessor/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./AlignerPostProcessor";
|
||||
export * from "./CustomHighlightPostProcessor";
|
||||
export * from "./ExtendedFormattingPostProcessor";
|
||||
32
src/regExps/delimRegExps.ts
Normal file
32
src/regExps/delimRegExps.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { DelimType } from "../enums";
|
||||
|
||||
export const delimRegExps = new Map([
|
||||
[DelimType.U, {
|
||||
openingDelim: /(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))\+\+(?![\+\s]|$)/gd,
|
||||
closingDelim: /(?:(?<![\s\\]|^)|(?<=(?<!\\)(?:\\{2})+))\+\+/gd,
|
||||
length: 2
|
||||
}],
|
||||
[DelimType.Sup, {
|
||||
openingDelim: /(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))\^(?![\^\s]|$)/gd,
|
||||
closingDelim: /(?:(?<![\s\\]|^)|(?<=(?<!\\)(?:\\{2})+))\^/gd,
|
||||
length: 1
|
||||
}],
|
||||
[DelimType.Sub, {
|
||||
openingDelim: /(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))~(?![~\s]|$)/gd,
|
||||
closingDelim: /(?:(?<![\s\\]|^)|(?<=(?<!\\)(?:\\{2})+))~/gd,
|
||||
length: 1
|
||||
}],
|
||||
[DelimType.Spoiler, {
|
||||
openingDelim: /(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))\|\|(?![|\s]|$)/gd,
|
||||
closingDelim: /(?:(?<![\s\\]|^)|(?<=(?<!\\)(?:\\{2})+))\|\|/gd,
|
||||
length: 2
|
||||
}]
|
||||
]);
|
||||
|
||||
/*
|
||||
single: /((?<!\\)(?:\\{2})*\\)?(\^()?)(?!$|\s)(?=(\^)?)/gd
|
||||
double: /((?<!\\)(?:\\{2})*\\)?(\^(\\)?\^)(?!$|\s)(?=(\^)?)/gd
|
||||
|
||||
single: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\^()?)/gd
|
||||
double: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\^(\\)?\^)/gd
|
||||
*/
|
||||
2
src/regExps/index.ts
Normal file
2
src/regExps/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./delimRegExps";
|
||||
export * from "./postProcessorDelimRegExps";
|
||||
40
src/regExps/postProcessorDelimRegExps.ts
Normal file
40
src/regExps/postProcessorDelimRegExps.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { DelimType } from "../enums";
|
||||
|
||||
export const postProcessorDelimRegExps = new Map([
|
||||
[DelimType.U, {
|
||||
openingDelim: /\+\+(?![\s]|$|(?:<br>\n?))/gd,
|
||||
closingDelim: /(?<![\s]|(?:<br>\n?))\+\+/gd,
|
||||
raw: {
|
||||
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\+(\\)?\+)(?!$|\s)(?=(\+)?)/gd,
|
||||
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\+(\\)?\+)/gd
|
||||
},
|
||||
length: 2
|
||||
}],
|
||||
[DelimType.Sup, {
|
||||
openingDelim: /\^(?![\s]|$|(?:<br>\n?))/gd,
|
||||
closingDelim: /(?<![\s]|(?:<br>\n?))\^/gd,
|
||||
raw: {
|
||||
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\^()?)(?!$|\s)(?=(\^)?)/gd,
|
||||
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\^()?)/gd
|
||||
},
|
||||
length: 1
|
||||
}],
|
||||
[DelimType.Sub, {
|
||||
openingDelim: /~(?![\s]|$|(?:<br>\n?))/gd,
|
||||
closingDelim: /(?<![\s]|(?:<br>\n?))~/gd,
|
||||
raw: {
|
||||
openingDelim: /((?<!\\)(?:\\{2})*\\)?(~()?)(?!$|\s)(?=(~)?)/gd,
|
||||
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(~()?)/gd
|
||||
},
|
||||
length: 1
|
||||
}],
|
||||
[DelimType.Spoiler, {
|
||||
openingDelim: /\|\|(?![\s]|$|(?:<br>\n?))/gd,
|
||||
closingDelim: /(?<![\s]|(?:<br>\n?))\|\|/gd,
|
||||
raw: {
|
||||
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\|(\\)?\|)(?!$|\s)(?=(\|)?)/gd,
|
||||
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\|(\\)?\|)/gd
|
||||
},
|
||||
length: 2
|
||||
}]
|
||||
])
|
||||
1
src/types/DelimPos.ts
Normal file
1
src/types/DelimPos.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type DelimPos = [number, number, number, number];
|
||||
1
src/types/ExtendedElement.ts
Normal file
1
src/types/ExtendedElement.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type ExtendedElement = Element & HTMLElement;
|
||||
1
src/types/HighlightProp.ts
Normal file
1
src/types/HighlightProp.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type HighlightProp = [number, number, number, number, string | undefined];
|
||||
1
src/types/Pos.ts
Normal file
1
src/types/Pos.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export type Pos = [number, number];
|
||||
4
src/types/index.ts
Normal file
4
src/types/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./ExtendedElement";
|
||||
export * from "./Pos";
|
||||
export * from "./HighlightProp";
|
||||
export * from "./DelimPos";
|
||||
17
src/utils/codemirror/checkSelectionOverlap.ts
Normal file
17
src/utils/codemirror/checkSelectionOverlap.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { EditorSelection } from "@codemirror/state";
|
||||
|
||||
/**
|
||||
* These code snippets are taken from
|
||||
* https://github.com/Mara-Li/obsidian-regex-mark/blob/master/src/cmPlugin.ts
|
||||
* with some modifications.
|
||||
*/
|
||||
export function checkSelectionOverlap(selection: EditorSelection, from: number, to: number): boolean {
|
||||
if (!selection) {return false}
|
||||
for (const range of selection.ranges) {
|
||||
if (range.to >= from && range.from <= to) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
7
src/utils/codemirror/index.ts
Normal file
7
src/utils/codemirror/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from "./checkSelectionOverlap";
|
||||
export * from "./isCodeblock";
|
||||
export * from "./isHighlight";
|
||||
export * from "./isRestrictedRange";
|
||||
export * from "./isColumnSeparator";
|
||||
export * from "./isRestrictedPos";
|
||||
export * from "./isTable";
|
||||
18
src/utils/codemirror/isCodeblock.ts
Normal file
18
src/utils/codemirror/isCodeblock.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
export function isCodeblock(view: EditorView, from: number, to: number): boolean {
|
||||
let isCodeblock: boolean = false;
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: (node) => {
|
||||
// console.log(node.name);
|
||||
if (/^inline-code/.test(node.name) || node.name == 'HyperMD-codeblock_HyperMD-codeblock-bg') {
|
||||
isCodeblock = true;
|
||||
return false; // short circuit child iteration
|
||||
}
|
||||
}
|
||||
});
|
||||
return isCodeblock;
|
||||
}
|
||||
5
src/utils/codemirror/isColumnSeparator.ts
Normal file
5
src/utils/codemirror/isColumnSeparator.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { type Tree } from "@lezer/common";
|
||||
|
||||
export function isColumnSeparator(tree: Tree, pos: number): boolean {
|
||||
return /hmd-table-sep/.test(tree.resolveInner(pos, 1).name);
|
||||
}
|
||||
17
src/utils/codemirror/isHighlight.ts
Normal file
17
src/utils/codemirror/isHighlight.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
export function isHighlight(view: EditorView, from: number, to: number): boolean {
|
||||
let isHighlightFromPos: boolean = false;
|
||||
syntaxTree(view.state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: (node) => {
|
||||
if (/highlight/.test(node.name)) {
|
||||
isHighlightFromPos = true;
|
||||
return false; // short circuit child iteration
|
||||
}
|
||||
}
|
||||
});
|
||||
return isHighlightFromPos;
|
||||
}
|
||||
6
src/utils/codemirror/isRestrictedPos.ts
Normal file
6
src/utils/codemirror/isRestrictedPos.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { type Tree } from "@lezer/common";
|
||||
|
||||
export function isRestrictedPos(tree: Tree, pos: number): boolean {
|
||||
return /(?:inline-code)|(?:tag)|(?:HyperMD-codeblock)|(?:footref)|(?:hmd-internal-link)|(?:hmd-footnote)|(?:math)|(?:hmd-codeblock)|(?:formatting-strikethrough)/
|
||||
.test(tree.resolveInner(pos, 1).name);
|
||||
}
|
||||
27
src/utils/codemirror/isRestrictedRange.ts
Normal file
27
src/utils/codemirror/isRestrictedRange.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { syntaxTree } from "@codemirror/language";
|
||||
import { type EditorState } from "@codemirror/state";
|
||||
|
||||
/**
|
||||
* Ensure whether this range will be parsed or not
|
||||
*
|
||||
* @param type - the type of delimiter
|
||||
* @returns {boolean} if it returns true then the range between {@link from} and {@link to} will not be parsed
|
||||
*/
|
||||
export function isRestrictedRange(state: EditorState, from: number, to: number, type: string): boolean {
|
||||
let isRestrictedRange: boolean = false;
|
||||
syntaxTree(state).iterate({
|
||||
from,
|
||||
to,
|
||||
enter: (node) => {
|
||||
if (/(?:^inline-code)|(?:^tag)|(?:HyperMD-codeblock)|(?:blockid)|(?:footref_hmd-barelink)|(?:hmd-internal-link)|(?:hmd-barelink_hmd-footnote)|(?:^math)|(?:formatting-code-block)/.test(node.name)) {
|
||||
isRestrictedRange = true;
|
||||
return false; // short circuit child iteration
|
||||
}
|
||||
if (type == "sub" && /formatting-strikethrough/.test(node.name)) {
|
||||
isRestrictedRange = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return isRestrictedRange;
|
||||
}
|
||||
16
src/utils/codemirror/isTable.ts
Normal file
16
src/utils/codemirror/isTable.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { type Tree } from "@lezer/common";
|
||||
|
||||
export function isTable(tree: Tree, pos: number): boolean {
|
||||
let isTable: boolean = false;
|
||||
tree.iterate({
|
||||
from: pos,
|
||||
to: pos,
|
||||
enter: (node) => {
|
||||
if (/table/.test(node.name)) {
|
||||
isTable = true;
|
||||
return false; // short circuit child iteration
|
||||
}
|
||||
}
|
||||
});
|
||||
return isTable;
|
||||
}
|
||||
6
src/utils/getTextAtLine.ts
Normal file
6
src/utils/getTextAtLine.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* Get text on a specific line, based on newline character (\n).
|
||||
*/
|
||||
export function getTextAtLine(text: string, lineStart: number, lineEnd: number) {
|
||||
return text.split("\n").slice(lineStart, lineEnd + 1).join("\n");
|
||||
}
|
||||
2
src/utils/index.ts
Normal file
2
src/utils/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./getTextAtLine";
|
||||
export * from "./replaceStringAtPos";
|
||||
27
src/utils/postProcess/excludeElements.ts
Normal file
27
src/utils/postProcess/excludeElements.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export function excludeElements<T, P extends any[]>(contentEl: HTMLElement, selector: string, callback: (...args: P) => T, ...args: Parameters<typeof callback>): T {
|
||||
|
||||
let excludedContents: string[] = [];
|
||||
let excludedEl = contentEl.querySelectorAll<HTMLElement>(selector);
|
||||
|
||||
if (excludedEl.length) {
|
||||
|
||||
excludedEl.forEach((el) => {
|
||||
excludedContents.push(el.innerHTML);
|
||||
el.innerHTML = "";
|
||||
});
|
||||
|
||||
let returnValue = callback(...args);
|
||||
|
||||
excludedEl.forEach((el, index) => {
|
||||
el.innerHTML = excludedContents[index];
|
||||
});
|
||||
|
||||
return returnValue;
|
||||
|
||||
} else {
|
||||
return callback(...args);
|
||||
}
|
||||
}
|
||||
106
src/utils/postProcess/getBlockquoteSections.ts
Normal file
106
src/utils/postProcess/getBlockquoteSections.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { BlockquoteChildSection } from "../../interfaces";
|
||||
|
||||
/**
|
||||
* Splits the blockquote/callout into {@link BlockquoteChildSection sections}.
|
||||
* This function also removes codeblocks, mathblocks, and empty row instead
|
||||
* of keeping them in the sections, as they will not be parsed. Rows that are
|
||||
* not interspersed by empty row will be considered as one section.
|
||||
*
|
||||
* @example
|
||||
* ````
|
||||
* > first section
|
||||
* > merged to the first
|
||||
* >
|
||||
* > $$
|
||||
* > math block
|
||||
* > $$
|
||||
* > ```
|
||||
* > codeblock
|
||||
* > ```
|
||||
* >
|
||||
* > second section
|
||||
* ````
|
||||
*
|
||||
* The blockquote above will be divided into 2 sections
|
||||
*
|
||||
* @param text - Must be a markdown text representing a blockquote/callout
|
||||
* @param isCallout - If true then the first line representing callout title will be a standalone section
|
||||
*/
|
||||
export function getBlockquoteSections(text: string, isCallout: boolean) {
|
||||
|
||||
let blockquoteDelim = /^ *>/;
|
||||
let sections: BlockquoteChildSection[] = [];
|
||||
|
||||
text.split("\n").forEach((text) => { // splits blockquotes into lines based on "\n" character
|
||||
|
||||
/** blockquote level */
|
||||
let level = 0;
|
||||
|
||||
while (blockquoteDelim.test(text) && ++level) { // the blockquote level is dependent on the number of delimiters
|
||||
text = text.replace(blockquoteDelim, ""); // strips out the blockquote/callout delimiters (">")
|
||||
}
|
||||
|
||||
text = text.trimStart();
|
||||
sections.push({
|
||||
text: text,
|
||||
level: level
|
||||
});
|
||||
});
|
||||
|
||||
// splits the blockquote/callout into sections
|
||||
loop1: for (
|
||||
let i = 0,
|
||||
/** if true then the following row will be merged with the previous row */
|
||||
mergePrevious = false;
|
||||
i < sections.length;
|
||||
) {
|
||||
|
||||
let currentLine = sections[i];
|
||||
|
||||
if (isCallout && i === 0) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (/^```/.test(currentLine.text)) { // removes codeblock
|
||||
|
||||
let k = 1;
|
||||
while (sections[i + k]) {
|
||||
let {level, text} = sections[i + k++];
|
||||
if (
|
||||
(text === "" && level < currentLine.level) ||
|
||||
(/^``` *$/.test(text) && level == currentLine.level)
|
||||
) { break }
|
||||
}
|
||||
|
||||
sections.splice(i, k), mergePrevious &&= false;
|
||||
continue loop1;
|
||||
|
||||
} else if (/^\$\$/.test(currentLine.text)) { // removes mathblock
|
||||
|
||||
let k = 1;
|
||||
while (sections[i + k]) {
|
||||
let {level, text} = sections[i + k++];
|
||||
if (
|
||||
(text === "" && level < currentLine.level && k--) ||
|
||||
(/^\$\$ *$/.test(text) && level == currentLine.level)
|
||||
) { break }
|
||||
}
|
||||
|
||||
if (k !== 1) {
|
||||
sections.splice(i, k), mergePrevious &&= false;
|
||||
continue loop1;
|
||||
}
|
||||
|
||||
} else if (currentLine.text === "") { // removes empty row
|
||||
sections.splice(i, 1), mergePrevious &&= false;
|
||||
continue loop1;
|
||||
}
|
||||
|
||||
// rows that are not interspersed by empty row will be considered as one section.
|
||||
(mergePrevious && sections[i - 1].level >= currentLine.level) && (sections[i - 1].text += `\n${sections.splice(i, 1)[0].text}`, i--);
|
||||
i++, mergePrevious ||= true;
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
122
src/utils/postProcess/getDelimiterFromPreview.ts
Normal file
122
src/utils/postProcess/getDelimiterFromPreview.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { type DelimPos } from "../../types";
|
||||
import { type Pos } from "../../types";
|
||||
import { getEscapedDelims } from "./getEscapedDelims";
|
||||
import { type postProcessorDelimRegExps } from "../../regExps";
|
||||
|
||||
/**
|
||||
* This function is used to get the range containing delimiter
|
||||
* position of text in preview mode. It uses a similar
|
||||
* algorithm to [`getDelimiterFromEditor`](../../editorPlugins/stateFields/extendedFormattingField.ts).
|
||||
*
|
||||
* @param text - preprocessed text, not raw markdown
|
||||
* @param rawText - raw markdown text to retrieve the index of escaped delimiters if any
|
||||
*/
|
||||
export function getDelimiterFromPreview(text: string, rawText: string, delimQuery: ReturnType<typeof postProcessorDelimRegExps.get>) {
|
||||
|
||||
let newLineChar = /(<br>\n?)\1+/g;
|
||||
let htmlTagRegExp = /<[\w\d\-]+(?: *[\w\d\-]+(?:=(?:"[^"]*")|(?:'[^']*'))?)*>/g;
|
||||
let currentLineEndOffset = newLineChar.exec(text)?.index ?? text.length;
|
||||
|
||||
let currentTagExecArr = htmlTagRegExp.exec(text);
|
||||
let currentTagRange: Pos = [
|
||||
currentTagExecArr?.index ?? text.length,
|
||||
htmlTagRegExp.lastIndex || text.length
|
||||
];
|
||||
|
||||
let {openingDelim, closingDelim, raw} = delimQuery!;
|
||||
let escapedIndexes = getEscapedDelims(rawText, raw.openingDelim, raw.closingDelim);
|
||||
|
||||
let delimPosCollection: DelimPos[] = [];
|
||||
|
||||
openingDelim.lastIndex = 0;
|
||||
closingDelim.lastIndex = 0;
|
||||
|
||||
for (
|
||||
let openingMatch = openingDelim.exec(text), lastIndex = openingDelim.lastIndex, i = 0;
|
||||
openingMatch;
|
||||
openingMatch = openingDelim.exec(text), lastIndex = openingDelim.lastIndex
|
||||
) {
|
||||
|
||||
let delimPos: DelimPos = [0, 0, 0, 0]
|
||||
|
||||
if (lastIndex > currentTagRange[1]) {
|
||||
htmlTagRegExp.lastIndex = lastIndex;
|
||||
currentTagRange = [
|
||||
htmlTagRegExp.exec(text)?.index || text.length,
|
||||
htmlTagRegExp.lastIndex || text.length
|
||||
];
|
||||
}
|
||||
|
||||
if (lastIndex > currentTagRange[0] && lastIndex < currentTagRange[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* if the order of the matched delimiter ({@link i}) is the same as the index of
|
||||
* the escaped delimiter ({@link escapedIndexes}) then the previously matched
|
||||
* delimiter is considered escaped.
|
||||
*/
|
||||
if (escapedIndexes[0] === i) {
|
||||
openingDelim.lastIndex = openingMatch.index + 1;
|
||||
escapedIndexes.shift();
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lastIndex > currentLineEndOffset) {
|
||||
newLineChar.lastIndex = lastIndex;
|
||||
currentLineEndOffset = newLineChar.exec(text)?.index || text.length;
|
||||
}
|
||||
|
||||
closingDelim.lastIndex = lastIndex;
|
||||
[delimPos[0], delimPos[1]] = openingMatch.indices![0];
|
||||
i++;
|
||||
|
||||
for (let closingMatch = closingDelim.exec(text);; closingMatch = closingDelim.exec(text)) {
|
||||
|
||||
lastIndex = closingDelim.lastIndex;
|
||||
|
||||
if (!closingMatch || lastIndex > currentLineEndOffset) {
|
||||
|
||||
openingDelim.lastIndex = delimPos[2] = delimPos[3] = currentLineEndOffset;
|
||||
currentLineEndOffset = newLineChar.exec(text)?.index || text.length;
|
||||
|
||||
} else if (lastIndex > currentTagRange[0] && lastIndex < currentTagRange[1]) {
|
||||
|
||||
continue;
|
||||
|
||||
/**
|
||||
* if the order of the matched delimiter ({@link i}) is the same as the index of
|
||||
* the escaped delimiter ({@link escapedIndexes}) then the previously matched
|
||||
* delimiter is considered escaped.
|
||||
*/
|
||||
} else if (escapedIndexes[0] === i) {
|
||||
|
||||
closingDelim.lastIndex = closingMatch.index + 1;
|
||||
escapedIndexes.shift();
|
||||
i++;
|
||||
continue;
|
||||
|
||||
} else {
|
||||
|
||||
if (lastIndex > currentTagRange[1]) {
|
||||
htmlTagRegExp.lastIndex = lastIndex;
|
||||
currentTagRange = [
|
||||
htmlTagRegExp.exec(text)?.index || text.length,
|
||||
htmlTagRegExp.lastIndex || text.length
|
||||
];
|
||||
}
|
||||
|
||||
[delimPos[2], delimPos[3]] = closingMatch.indices![0];
|
||||
openingDelim.lastIndex = lastIndex;
|
||||
i++;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
delimPosCollection.push(delimPos);
|
||||
}
|
||||
|
||||
return delimPosCollection;
|
||||
}
|
||||
56
src/utils/postProcess/getEscapedDelims.ts
Normal file
56
src/utils/postProcess/getEscapedDelims.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { getRestrictedRanges } from "../postProcess";
|
||||
|
||||
export function getEscapedDelims(text: string, openingDelim: RegExp, closingDelim: RegExp) {
|
||||
|
||||
let restrictedRanges = getRestrictedRanges(text);
|
||||
let indexes: number[] = [];
|
||||
|
||||
openingDelim.lastIndex = 0;
|
||||
closingDelim.lastIndex = 0;
|
||||
|
||||
for (let openingMatch = openingDelim.exec(text), i = 0; openingMatch; openingMatch = openingDelim.exec(text)) {
|
||||
|
||||
let [all, escaper, target, slippedBackslash, next] = openingMatch.indices!;
|
||||
|
||||
if (restrictedRanges.some(range => range[0] <= all[0] && range[1] >= all[1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
closingDelim.lastIndex = all[1];
|
||||
|
||||
if (escaper || slippedBackslash || next) {
|
||||
indexes.push(i);
|
||||
openingDelim.lastIndex = target[0] + 1;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
for(
|
||||
let closingMatch = closingDelim.exec(text);
|
||||
closingMatch || ((openingDelim.lastIndex = text.length), false);
|
||||
closingMatch = closingDelim.exec(text)
|
||||
) {
|
||||
|
||||
let [all, escaper, nonEscaper, target, slippedBackslash] = [...closingMatch!.indices!];
|
||||
|
||||
if (restrictedRanges.some(range => range[0] <= all[0] && range[1] >= all[1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (escaper || slippedBackslash) {
|
||||
indexes.push(i);
|
||||
closingDelim.lastIndex = target[0] + 1;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
i++;
|
||||
openingDelim.lastIndex = all[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
13
src/utils/postProcess/getRestrictedRange.ts
Normal file
13
src/utils/postProcess/getRestrictedRange.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Pos } from "../../types";
|
||||
|
||||
export function getRestrictedRanges(text: string) {
|
||||
|
||||
let query = /(\\)|`(?:[^`])+`|\[\[\[?(?!\[)(?:.|\s)+?(?<!\])\]\]|\$\$(?:.|\s)*?\$\$|\$(?!\s|\$).+?(?<!\s)\$/gd;
|
||||
let restrictedRanges: Pos[] = [];
|
||||
|
||||
for (let match = query.exec(text); match; match = query.exec(text)) {
|
||||
match[1] ? query.lastIndex += 1 : restrictedRanges.push(match.indices![0]);
|
||||
}
|
||||
|
||||
return restrictedRanges;
|
||||
}
|
||||
7
src/utils/postProcess/index.ts
Normal file
7
src/utils/postProcess/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export * from "./splitCells";
|
||||
export * from "./excludeElements";
|
||||
export * from "./getDelimiterFromPreview";
|
||||
export * from "./getEscapedDelims";
|
||||
export * from "./getRestrictedRange";
|
||||
export * from "./iterDelimReplacement";
|
||||
export * from "./getBlockquoteSections";
|
||||
41
src/utils/postProcess/iterDelimReplacement.ts
Normal file
41
src/utils/postProcess/iterDelimReplacement.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { type DelimType } from "../../enums";
|
||||
import { type postProcessorDelimRegExps } from "../../regExps";
|
||||
import { replaceStringByPos } from "../../utils";
|
||||
import { getDelimiterFromPreview } from "./getDelimiterFromPreview";
|
||||
|
||||
export function iterDelimReplacement(contentEl: HTMLElement, rawText: string, delimQuery: ReturnType<typeof postProcessorDelimRegExps.get>, tagName: DelimType, excludedSelector: string, isTableCell?: boolean) {
|
||||
|
||||
isTableCell && tagName == "spoiler" && (rawText = "");
|
||||
|
||||
let excludedContents: string[] = [];
|
||||
let excludedEl = contentEl.querySelectorAll<HTMLElement>(excludedSelector);
|
||||
|
||||
excludedEl.length && excludedEl.forEach((el) => {
|
||||
excludedContents.push(el.innerHTML);
|
||||
el.innerHTML = "";
|
||||
});
|
||||
|
||||
let sanitizedText = contentEl.innerHTML;
|
||||
let ranges = getDelimiterFromPreview(sanitizedText, rawText, delimQuery);
|
||||
|
||||
if (ranges.length) {
|
||||
|
||||
let delimLength = delimQuery?.length!;
|
||||
let lengthA = tagName.length + 2 - delimLength;
|
||||
let lengthB = tagName.length + 3;
|
||||
let i: number = 0;
|
||||
|
||||
ranges.forEach((value) => {
|
||||
sanitizedText = replaceStringByPos(sanitizedText, `<${tagName}>`, value[0] + i, value[1] + i);
|
||||
i += lengthA;
|
||||
sanitizedText = replaceStringByPos(sanitizedText, `</${tagName}>`, value[2] + i, value[3] + i);
|
||||
i = value[2] == value[3] ? i + lengthB : i + lengthB - delimLength;
|
||||
})
|
||||
};
|
||||
|
||||
contentEl.innerHTML = sanitizedText;
|
||||
|
||||
excludedContents.forEach((content, index) => {
|
||||
contentEl.querySelectorAll<HTMLElement>(excludedSelector)[index].innerHTML = content;
|
||||
});
|
||||
}
|
||||
20
src/utils/postProcess/splitCells.ts
Normal file
20
src/utils/postProcess/splitCells.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export function splitCells(text: string) {
|
||||
|
||||
let rows = text.split("\n");
|
||||
let cells: string[] = [];
|
||||
|
||||
rows.forEach((row, index) => {
|
||||
|
||||
if (index === 1) { return }
|
||||
|
||||
row = row.trim();
|
||||
row.startsWith("|") && (row = row.substring(1));
|
||||
row.endsWith("|") && (row = row.substring(0, row.length - 1));
|
||||
|
||||
row.split(/(?:(?<!\\)|(?<=(?<!\\)(?:\\{2})+))\|/).forEach(cell => {
|
||||
cells.push(cell);
|
||||
});
|
||||
});
|
||||
|
||||
return cells;
|
||||
}
|
||||
8
src/utils/replaceStringAtPos.ts
Normal file
8
src/utils/replaceStringAtPos.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Replace text by position
|
||||
*/
|
||||
export function replaceStringByPos(text: string, replaceValue: string, from: number, to: number) {
|
||||
let replaceArea = new RegExp(`.{${to - from}}`, "ys");
|
||||
replaceArea.lastIndex = from;
|
||||
return text.replace(replaceArea, replaceValue);
|
||||
}
|
||||
153
styles.css
Normal file
153
styles.css
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
:root {
|
||||
--color-accent-rgb: 51, 139, 255;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
&.cmx-align-left {
|
||||
text-align: left;
|
||||
}
|
||||
&.cmx-align-right {
|
||||
text-align: right;
|
||||
}
|
||||
&.cmx-align-center {
|
||||
text-align: center;
|
||||
}
|
||||
&.cmx-align-justify {
|
||||
text-align: justify;
|
||||
}
|
||||
}
|
||||
|
||||
.cmx-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.cmx-superscript {
|
||||
vertical-align: top;
|
||||
font-size: var(--font-smallest);
|
||||
}
|
||||
.cmx-subscript {
|
||||
vertical-align: bottom;
|
||||
font-size: var(--font-smallest);
|
||||
}
|
||||
|
||||
.cm-selectionBackground {
|
||||
transition-duration: 0.1s;
|
||||
transition-property: left, width;
|
||||
}
|
||||
.cm-s-obsidian .cm-cursor {
|
||||
transition-property: left, top;
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
|
||||
.cm-s-obsidian span.cm-highlight:has(.cmx-highlight) {
|
||||
background-color: transparent;
|
||||
.cmx-highlight {
|
||||
&:has(.cmx-highlight-first-letter) {
|
||||
padding-inline-start: 2px;
|
||||
border-top-left-radius: 2px;
|
||||
border-bottom-left-radius: 2px;
|
||||
}
|
||||
&:has(.cmx-highlight-last-letter) {
|
||||
padding-inline-end: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom-right-radius: 2px;
|
||||
}
|
||||
}
|
||||
&.cm-formatting-highlight .cmx-highlight {
|
||||
padding-inline: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
mark {
|
||||
padding-inline: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.cmx-highlight:has(.cmx-color-btn) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
mark.cmx-highlight-red, .cmx-highlight-red, .cmx-highlight-red .cmx-color-btn, .es-item-red .menu-item-title {
|
||||
background-color: rgba(var(--color-red-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-orange, .cmx-highlight-orange, .cmx-highlight-orange .cmx-color-btn, .es-item-orange .menu-item-title {
|
||||
background-color: rgba(var(--color-orange-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-yellow, .cmx-highlight-yellow, .cmx-highlight-yellow .cmx-color-btn, .es-item-yellow .menu-item-title {
|
||||
background-color: rgba(var(--color-yellow-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-green, .cmx-highlight-green, .cmx-highlight-green .cmx-color-btn, .es-item-green .menu-item-title {
|
||||
background-color: rgba(var(--color-green-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-cyan, .cmx-highlight-cyan, .cmx-highlight-cyan .cmx-color-btn, .es-item-cyan .menu-item-title {
|
||||
background-color: rgba(var(--color-cyan-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-blue, .cmx-highlight-blue, .cmx-highlight-blue .cmx-color-btn, .es-item-blue .menu-item-title {
|
||||
background-color: rgba(var(--color-blue-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-purple, .cmx-highlight-purple, .cmx-highlight-purple .cmx-color-btn, .es-item-purple .menu-item-title {
|
||||
background-color: rgba(var(--color-purple-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-pink, .cmx-highlight-pink, .cmx-highlight-pink .cmx-color-btn, .es-item-pink .menu-item-title {
|
||||
background-color: rgba(var(--color-pink-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-accent, .cmx-highlight-accent, .cmx-highlight-accent .cmx-color-btn, .es-item-accent .menu-item-title {
|
||||
background-color: rgba(var(--color-accent-rgb), 0.5);
|
||||
}
|
||||
mark.cmx-highlight-default, .cmx-highlight-default, .cmx-highlight-default .cmx-color-btn, .es-item-default .menu-item-title {
|
||||
background-color: var(--text-highlight-bg);
|
||||
}
|
||||
|
||||
.cmx-color-btn {
|
||||
margin-inline: 2px;
|
||||
width: 12px;
|
||||
display: inline-block;
|
||||
height: 12px;
|
||||
border: solid 1px;
|
||||
border-color: #ffffff44;
|
||||
vertical-align: middle;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.menu.es-highlight-colors-modal {
|
||||
& > .menu-item {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
& > .menu-item-title {
|
||||
/* flex: 0 1 auto; */
|
||||
padding-inline: 2px;
|
||||
padding-block: 1px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cmx-spoiler:not(.cmx-spoiler-formatting) {
|
||||
border-radius: 2px;
|
||||
transition-duration: 0.1s;
|
||||
transition-property: color;
|
||||
.theme-dark & {
|
||||
background-color: var(--color-base-25);
|
||||
}
|
||||
.theme-light & {
|
||||
background-color: var(--color-base-40);
|
||||
}
|
||||
&.cmx-spoiler-hidden:not(:is(:hover)) {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
spoiler {
|
||||
border-radius: 2px;
|
||||
transition-duration: 0.1s;
|
||||
transition-property: color;
|
||||
.theme-dark & {
|
||||
background-color: var(--color-base-25);
|
||||
}
|
||||
.theme-light & {
|
||||
background-color: var(--color-base-40);
|
||||
}
|
||||
&:not(:is(.cmx-revealed)) {
|
||||
color: transparent;
|
||||
}
|
||||
}
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"target": "ES2023",
|
||||
"jsx": "react",
|
||||
"allowImportingTsExtensions": false,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"sourceMap": true,
|
||||
"types": ["obsidian-typings"]
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/node_modules/*"
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue