refactor: avoid using arbitrary RegExp for parsing

This commit is contained in:
kotaindah55 2025-02-03 20:30:02 +02:00
parent ce6a4e763d
commit c6cc27a1f9
46 changed files with 79 additions and 1444 deletions

View file

@ -1,5 +0,0 @@
node_modules/
main.js
.deprecated/

View file

@ -1,23 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

View file

@ -1,64 +0,0 @@
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 {
[name: string]: EventRef[];
}
interface EventRef<T = unknown> {
ctx?: T;
}
interface MarkdownView {
path: string;
}
interface MarkdownPostProcessorContext {
containerEl: HTMLElement;
el: HTMLElement;
}
}
declare module "@codemirror/view" {
interface BlockWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement;
containerEl: HTMLElement;
text: string;
}
interface DocViewBlock<T = BlockWidget> {
widget?: T;
}
interface DocView {
children: DocViewBlock[];
}
interface EditorView {
docView: DocView;
}
interface TableWidget extends BlockWidget {
cellChildMap: Map<TableCell, unknown>;
}
interface TableBlock extends DocViewBlock<TableWidget> {
widget: TableWidget;
}
}

View file

@ -1,71 +0,0 @@
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
});

View file

@ -1,68 +0,0 @@
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));
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);

View file

@ -1,85 +0,0 @@
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
});

View file

@ -1,3 +0,0 @@
export * from "./extendedFormattingPlugin";
export * from "./alignerPlugin";
export * from "./customHighlightPlugin";

View file

@ -1,28 +0,0 @@
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"})
}),
]

View file

@ -1,9 +0,0 @@
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"})})]
]);

View file

@ -1,5 +0,0 @@
import { Decoration } from "@codemirror/view";
export const hiddenSpoiler = Decoration.mark({
class: "cmx-spoiler cmx-spoiler-hidden"
});

View file

@ -1,6 +0,0 @@
import { Decoration } from "@codemirror/view";
export const highlightDecorator = {
class: "cmx-highlight cmx-highlight",
markerDeco: Decoration.mark({class: "cmx-highlight-marker"})
}

View file

@ -1,4 +0,0 @@
export * from "./alignerDecorators";
export * from "./highlightDecorator";
export * from "./formattingDecorators";
export * from "./hiddenSpoiler";

View file

@ -1,110 +0,0 @@
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;
}
})

View file

@ -1,166 +0,0 @@
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;
}
});

View file

@ -1,2 +0,0 @@
export * from "./extendedFormattingField";
export * from "./customHighlightField";

View file

@ -1,110 +0,0 @@
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;
}
}

View file

@ -1,19 +0,0 @@
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");
}
}

View file

@ -1,2 +0,0 @@
export * from "./ColorButton";
export * from "./HiddenWidget";

View file

@ -1,4 +0,0 @@
export interface BlockquoteChildSection {
text: string;
level: number;
}

View file

@ -1 +0,0 @@
export * from "./BlockquoteChildSection";

View file

@ -1,44 +0,0 @@
import { type MarkdownPostProcessor } from "obsidian";
export class AlignerPostProcessor {
private readonly targetedElements = 'p, h1, h2, h3, h4, h5, h6, td, th, .callout-title-inner';
constructor() {}
private format = (el: HTMLElement) => {
let alignMark = /^!((?:left)|(?:right)|(?:center)|(?:justify))!/d;
let alignMarkExecArr: RegExpExecArray | null;
let firstChildNode = el.firstChild;
if (!firstChildNode || el.parentElement?.tagName == "BLOCKQUOTE") { return }
if (el instanceof HTMLHeadingElement && (firstChildNode = el.childNodes[1]) instanceof Text) {
alignMarkExecArr = alignMark.exec(firstChildNode.textContent ?? "");
} else {
if (!(firstChildNode instanceof Text)) { return }
el.parentElement?.hasClass("callout-content") && (alignMark = /^ *!((?:left)|(?:right)|(?:center)|(?:justify))!/d);
alignMarkExecArr = alignMark.exec(firstChildNode.textContent ?? "");
}
if (alignMarkExecArr) {
let [from, to] = alignMarkExecArr.indices![0];
firstChildNode.replaceData(from, to - from, "");
el.addClass(`cmx-align-${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);
});
}
}
}

View file

@ -1,32 +0,0 @@
import { type MarkdownPostProcessor } from "obsidian";
export class CustomHighlightPostProcessor {
constructor() {}
private format = (el: HTMLElement) => {
let markElements = el.querySelectorAll<HTMLElement>("mark");
let colorMark = /^\{([\w\d\-_]+)\}/d;
markElements.forEach((mark) => {
let firstChildNode = mark.firstChild;
if (!(firstChildNode instanceof Text && firstChildNode.textContent)) { return }
let colorMarkExecArr = colorMark.exec(firstChildNode.textContent);
if (colorMarkExecArr) {
let [from, to] = colorMarkExecArr.indices![0];
firstChildNode.replaceData(from, to - from, "");
mark.classList.add(`cmx-highlight-${colorMarkExecArr[1]}`);
}
});
}
postProcess: MarkdownPostProcessor = (container, t) => {
this.format(container);
}
}

View file

@ -1,95 +0,0 @@
import { type TableBlock } from "@codemirror/view";
import { type Workspace, type MarkdownPostProcessor, MarkdownView, EventRef } from "obsidian";
import { postProcessorDelimRegExps } from "../regExps";
import { iterDelimReplacement, splitCells, getBlockquoteSections } from "../utils/postProcess";
import { getTextAtLine } from "../utils";
export class ExtendedFormattingPostProcessor {
workspace: Workspace;
private readonly targetedElements = 'p, li, h1, h2, h3, h4, h5, h6, .callout-title-inner';
constructor(workspace: Workspace) {
this.workspace = workspace;
}
private format(contentEl: HTMLElement, rawText: string, isTableCell: boolean = false) {
postProcessorDelimRegExps.forEach((delimQuery, tagName) => {
iterDelimReplacement(contentEl, rawText, delimQuery, tagName, 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 instanceof HTMLTableElement) {
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?.hasClass("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.workspace.activeEditor?.editor?.cm ??
this.workspace._["quick-preview"]
.find((evtRef): evtRef is EventRef<MarkdownView> => evtRef.ctx instanceof MarkdownView && evtRef.ctx?.path == ctx.sourcePath)
?.ctx?.editor.cm!;
if (container.classList.contains("table-cell-wrapper")) {
let tableBlock = cmEditor.docView.children.find((block): block is TableBlock => block.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= 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);
});
}
}
}
}

View file

@ -1,3 +0,0 @@
export * from "./AlignerPostProcessor";
export * from "./CustomHighlightPostProcessor";
export * from "./ExtendedFormattingPostProcessor";

View file

@ -1,32 +0,0 @@
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
*/

View file

@ -1,2 +0,0 @@
export * from "./delimRegExps";
export * from "./postProcessorDelimRegExps";

View file

@ -1,44 +0,0 @@
import { DelimType } from "../enums";
export const postProcessorDelimRegExps = new Map([
[DelimType.U, {
openingDelim: /\+(?=\+(?!\s))/gd,
closingDelim: /\+(?=\+)/gd,
raw: {
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\+(\\)?\+)(?!$|\s)(?=(\+)?)/gd,
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\+(\\)?\+)/gd
},
length: 2,
origin: "++"
}],
[DelimType.Sup, {
openingDelim: /\^(?!\s)/gd,
closingDelim: /\^/gd,
raw: {
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\^()?)(?!$|\s)(?=(\^)?)/gd,
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\^()?)/gd
},
length: 1,
origin: "^"
}],
[DelimType.Sub, {
openingDelim: /~(?!\s)/gd,
closingDelim: /~/gd,
raw: {
openingDelim: /((?<!\\)(?:\\{2})*\\)?(~()?)(?!$|\s)(?=(~)?)/gd,
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(~()?)/gd
},
length: 1,
origin: "~"
}],
[DelimType.Spoiler, {
openingDelim: /\|(?=\|(?!\s))/gd,
closingDelim: /\|(?=\|)/gd,
raw: {
openingDelim: /((?<!\\)(?:\\{2})*\\)?(\|(\\)?\|)(?!$|\s)(?=(\|)?)/gd,
closingDelim: /(?:((?<!\\)(?:\\{2})*(?<!\s)\\)|(?<![\\\s])|((?<!\\)(?:\\{2})+))(\|(\\)?\|)/gd
},
length: 2,
origin: "||"
}]
])

View file

@ -1 +0,0 @@
export type DelimPos = [number, number, number, number];

View file

@ -1 +0,0 @@
export type ExtendedElement = Element & HTMLElement;

View file

@ -1 +0,0 @@
export type HighlightProp = [number, number, number, number, string | undefined];

View file

@ -1 +0,0 @@
export type Pos = [number, number];

View file

@ -1,4 +1,79 @@
export * from "./ExtendedElement";
export * from "./Pos";
export * from "./HighlightProp";
export * from "./DelimPos";
import type { SyntaxNode, Tree } from "@lezer/common";
import type { Format, TokenRole, TokenStatus } from "src/enums";
import { Text } from "@codemirror/state"
/** Token interface */
export type Token = {
/** Format type */
type: Format, // 3-bit
/** Token status */
status: TokenStatus, // 2-bit
/** Start offset */
from: number, // 16-bit
/** End offset */
to: number, // 16-bit
/** Token role */
role: TokenRole, // 3-bit
/**
* Pointer to a specific token, which is an index refering
* to that pointed token in its token group. If the token was
* inside a token sequence, it will refer to its opening delimiter.
* Otherwise, it will point to itself.
*/
pointer: number, // 16-bit
size: number // 8-bit
}
/** Accumulation of `ChangeSet` generated by the `composeChange` function */
export type ChangedRange = {
/**
* Start offset of a changed range, measured from
* the text both before and after the change
*/
from: number,
/** End offset of a changed range, measured from the doc before the change */
initTo: number,
/** End offset of a changed range, measured from the doc after the change */
changedTo: number;
length: number;
}
/** Describe the rule that has to be satisfied by the delimiter */
export type DelimSpec = {
/** Should be single character */
char: string,
/** Delimiter length */
length: number,
/**
* If true, then delimiter length must be the same as
* in the predetermined rule. Otherwise, the defined length
* act as minimum length.
*/
exactLen: boolean,
/** Should be `TokenRole.OPEN` or `TokenRole.CLOSE` */
role: TokenRole
}
/**
* Used for (re)configuring the state, especially in
* the case of document or tree change
*/
export type StateConfig = {
doc: Text,
tree: Tree,
offset: number,
maxLine?: number
}
export type NodeSpec = {
node: SyntaxNode,
type: string
}
export type AlignFormat = Extract<Format, Format.ALIGN_LEFT | Format.ALIGN_RIGHT | Format.ALIGN_CENTER | Format.ALIGN_JUSTIFY>;
export type MainFormat = Extract<Format, Format.INSERTION | Format.SPOILER | Format.SUPERSCRIPT | Format.SUBSCRIPT | Format.HIGHLIGHT>;
export type TokenGroup = Token[];
export type MainFormat2 = Exclude<MainFormat, Format.HIGHLIGHT>;

View file

@ -1,17 +0,0 @@
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;
}

View file

@ -1,5 +0,0 @@
export * from "./checkSelectionOverlap";
export * from "./isCodeblock";
export * from "./isColumnSeparator";
export * from "./isRestrictedPos";
export * from "./isTable";

View file

@ -1,18 +0,0 @@
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;
}

View file

@ -1,5 +0,0 @@
import { type Tree } from "@lezer/common";
export function isColumnSeparator(tree: Tree, pos: number): boolean {
return /hmd-table-sep/.test(tree.resolveInner(pos, 1).name);
}

View file

@ -1,6 +0,0 @@
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);
}

View file

@ -1,16 +0,0 @@
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;
}

View file

@ -1,6 +0,0 @@
/**
* 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");
}

View file

@ -1 +0,0 @@
export * from "./getTextAtLine";

View file

@ -1,106 +0,0 @@
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;
}

View file

@ -1,57 +0,0 @@
import { getRestrictedRanges } from ".";
export function getDelimCounts(text: string, openingDelim: RegExp, closingDelim: RegExp, originDelim: string, ignoreEscaper: boolean = false) {
let restrictedRanges = getRestrictedRanges(text);
let counts: number[] = [];
let delimChar = originDelim.charAt(0);
ignoreEscaper && (text = text.replaceAll(`\\${delimChar}`, delimChar));
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) {
openingDelim.lastIndex = target[0] + 1;
i++;
continue;
}
counts.push(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) {
closingDelim.lastIndex = target[0] + 1;
i++;
continue;
}
counts.push(i++);
openingDelim.lastIndex = all[1];
break;
}
}
return counts;
}

View file

@ -1,13 +0,0 @@
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;
}

View file

@ -1,5 +0,0 @@
export * from "./splitCells";
export * from "./getDelimCounts";
export * from "./getRestrictedRange";
export * from "./iterDelimReplacement";
export * from "./getBlockquoteSections";

View file

@ -1,124 +0,0 @@
import { getDelimCounts } from ".";
import { type DelimType } from "../../enums";
import { type postProcessorDelimRegExps } from "../../regExps";
export function iterDelimReplacement(contentEl: HTMLElement, rawText: string, delimQuery: ReturnType<typeof postProcessorDelimRegExps.get>, tagName: DelimType, isTableCell?: boolean) {
let ignoreEscaper = !!(isTableCell && tagName == "spoiler");
let {openingDelim, closingDelim, raw, length: delimLength, origin: originDelim} = delimQuery!;
let delimIndexes = getDelimCounts(rawText, raw.openingDelim, raw.closingDelim, originDelim, ignoreEscaper);
let lastDelimCount = delimIndexes[delimIndexes.length - 1];
let formattedRange: Range[] = [];
if (delimIndexes.length == 0) { return }
openingDelim.lastIndex = closingDelim.lastIndex = 0;
let treeWalker = document.createTreeWalker(contentEl, 5, (node: Text | Element) => {
if (node instanceof Text) {
if (node.textContent?.includes(originDelim)) {
return NodeFilter.FILTER_ACCEPT;
} else {
return NodeFilter.FILTER_REJECT;
}
} else {
if (node instanceof HTMLBRElement && node.nextSibling instanceof HTMLBRElement) {
return NodeFilter.FILTER_ACCEPT;
} else if (node.tagName == "CODE" || node.hasClass("internal-link") || node.hasClass("math")) {
return NodeFilter.FILTER_REJECT;
} else {
return NodeFilter.FILTER_SKIP;
}
}
});
treeWalker.nextNode();
for (
let i = 0,
node = treeWalker.currentNode,
matched: RegExpExecArray | null,
closingTurn = false,
endOfTree = false,
range: Range;
i <= lastDelimCount + 1 || endOfTree;
) {
if (node instanceof Text && !closingTurn) {
matched = openingDelim.exec(node.textContent ?? "");
if (matched) {
if (matched.index + delimLength == node.length && node.nextSibling instanceof HTMLBRElement) { continue }
if (delimIndexes[0] === i++) {
closingTurn = true;
node.deleteData((closingDelim.lastIndex = matched.index), delimLength);
(range = document.createRange()).setStart(node, matched.index);
delimIndexes.shift();
}
} else {
openingDelim.lastIndex = 0;
if (treeWalker.nextNode()) {
node = treeWalker.currentNode;
} else { break }
}
continue;
}
if (closingTurn) {
if (node instanceof Text) {
matched = closingDelim.exec(node.textContent ?? "");
if (matched) {
if (matched.index == 0 && node.previousSibling instanceof HTMLBRElement) { continue }
if (delimIndexes[0] == i++) {
/\s/.test(node.textContent!.charAt(matched.index - 1)) && matched.index++;
node.deleteData((openingDelim.lastIndex = matched.index), delimLength);
range!.setEnd(node, matched.index);
delimIndexes.shift();
} else {
continue;
}
} else {
closingDelim.lastIndex = 0;
if (treeWalker.nextNode()) {
node = treeWalker.currentNode;
continue;
}
range!.setEndAfter(node);
endOfTree = true;
}
} else {
range!.setEndBefore(node as HTMLBRElement);
}
formattedRange.push(range!.cloneRange());
closingTurn = false;
}
if (endOfTree) { break }
}
formattedRange.forEach(range => {
let formattedEl = document.createElement(tagName);
formattedEl.append(range!.extractContents());
range!.insertNode(formattedEl);
});
}

View file

@ -1,20 +0,0 @@
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;
}