mirror of
https://github.com/kotaindah55/extended-markdown-syntax.git
synced 2026-07-22 05:38:06 +00:00
refactor: avoid using arbitrary RegExp for parsing
This commit is contained in:
parent
ce6a4e763d
commit
c6cc27a1f9
46 changed files with 79 additions and 1444 deletions
|
|
@ -1,5 +0,0 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
|
||||
.deprecated/
|
||||
23
.eslintrc
23
.eslintrc
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
64
src/declaration/index.d.ts
vendored
64
src/declaration/index.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./extendedFormattingPlugin";
|
||||
export * from "./alignerPlugin";
|
||||
export * from "./customHighlightPlugin";
|
||||
|
|
@ -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"})
|
||||
}),
|
||||
]
|
||||
|
|
@ -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"})})]
|
||||
]);
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
|
||||
export const hiddenSpoiler = Decoration.mark({
|
||||
class: "cmx-spoiler cmx-spoiler-hidden"
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
|
||||
export const highlightDecorator = {
|
||||
class: "cmx-highlight cmx-highlight",
|
||||
markerDeco: Decoration.mark({class: "cmx-highlight-marker"})
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export * from "./alignerDecorators";
|
||||
export * from "./highlightDecorator";
|
||||
export * from "./formattingDecorators";
|
||||
export * from "./hiddenSpoiler";
|
||||
|
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./extendedFormattingField";
|
||||
export * from "./customHighlightField";
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./ColorButton";
|
||||
export * from "./HiddenWidget";
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export interface BlockquoteChildSection {
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./BlockquoteChildSection";
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export * from "./AlignerPostProcessor";
|
||||
export * from "./CustomHighlightPostProcessor";
|
||||
export * from "./ExtendedFormattingPostProcessor";
|
||||
|
|
@ -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
|
||||
*/
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./delimRegExps";
|
||||
export * from "./postProcessorDelimRegExps";
|
||||
|
|
@ -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: "||"
|
||||
}]
|
||||
])
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type DelimPos = [number, number, number, number];
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type ExtendedElement = Element & HTMLElement;
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type HighlightProp = [number, number, number, number, string | undefined];
|
||||
|
|
@ -1 +0,0 @@
|
|||
export type Pos = [number, number];
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./checkSelectionOverlap";
|
||||
export * from "./isCodeblock";
|
||||
export * from "./isColumnSeparator";
|
||||
export * from "./isRestrictedPos";
|
||||
export * from "./isTable";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export * from "./getTextAtLine";
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
export * from "./splitCells";
|
||||
export * from "./getDelimCounts";
|
||||
export * from "./getRestrictedRange";
|
||||
export * from "./iterDelimReplacement";
|
||||
export * from "./getBlockquoteSections";
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue