mirror of
https://github.com/kotaindah55/extended-markdown-syntax.git
synced 2026-07-22 05:38:06 +00:00
build: remake of decoration builder to let the decorations have more behavior
This commit is contained in:
parent
564e337c30
commit
bf6ec3d94a
32 changed files with 729 additions and 456 deletions
|
|
@ -1,113 +1,225 @@
|
|||
import { EditorState, Range } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { ChangeDesc, ChangeSet, EditorState, Range, RangeSet, Text, Transaction } from "@codemirror/state";
|
||||
import { Decoration, EditorView, ViewUpdate } from "@codemirror/view";
|
||||
import { Parser } from "src/editor-mode/parser";
|
||||
import { Format, TokenRole, TokenStatus } from "src/enums";
|
||||
import { createHlDeco } from "src/editor-mode/decorator/utils";
|
||||
import { AlignDeco, AlignMarkDeco, ColorTagDeco, ContentDeco, DelimDeco, RevealedSpoiler } from "src/editor-mode/decorator/decorations";
|
||||
import { AlignFormat, CharPos, MainFormat } from "src/types";
|
||||
import { Format, MarkdownViewMode, TokenStatus } from "src/enums";
|
||||
import { REVEALED_SPOILER_DECO } from "src/editor-mode/decorator/decorations";
|
||||
import { BlockFormat, PlainRange, InlineFormat, Token, TokenGroup, TokenDecoration, PluginSettings } from "src/types";
|
||||
import { ColorButton } from "src/editor-mode/decorator/widgets";
|
||||
import { BlockRules, InlineRules } from "src/shared-configs";
|
||||
import { DecorationHolder, DelimOmitter, LineBreakReplacer, TokensCatcher } from "src/editor-mode/decorator/builder";
|
||||
import { createInlineDecoRange, createLineDecoRange, getLineAt, getTagRange, isEditorModeChanged, iterLine, iterTokenGroup, sliceStrFromLine } from "src/editor-mode/decorator/utils";
|
||||
import { trimTag } from "src/utils";
|
||||
import { SelectionObserver } from "src/editor-mode/observer";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
import { refresherAnnot } from "src/editor-mode/annotations";
|
||||
|
||||
/** Decoration builder (excludes omitted delimiter), should be attached to `ExtendedSyntax` */
|
||||
export class DecorationBuilder {
|
||||
/** Attached `Parser` for obtaining tokens quickly */
|
||||
parser: Parser;
|
||||
constructor() {
|
||||
readonly parser: Parser;
|
||||
readonly omitter: DelimOmitter;
|
||||
readonly catcher: TokensCatcher;
|
||||
readonly lineBreakReplacer: LineBreakReplacer;
|
||||
readonly selectionObserver: SelectionObserver;
|
||||
readonly holder: DecorationHolder;
|
||||
readonly settings: PluginSettings;
|
||||
readonly indexCaches = {
|
||||
linePos: { number: 1 }, // 1-based
|
||||
inlineToken: { number: 0 }, // 0-based
|
||||
blockToken: { number: 0 } // 0-based
|
||||
}
|
||||
constructor(parser: Parser, selectionObserver: SelectionObserver) {
|
||||
this.parser = parser;
|
||||
this.selectionObserver = selectionObserver;
|
||||
this.settings = parser.settings;
|
||||
this.omitter = new DelimOmitter(this.settings, selectionObserver);
|
||||
this.catcher = new TokensCatcher();
|
||||
this.lineBreakReplacer = new LineBreakReplacer(parser);
|
||||
this.holder = new DecorationHolder();
|
||||
}
|
||||
/**
|
||||
* Produces _necessary_ decoration ranges from tokens provided by
|
||||
* `Builder.parser`. Therefore, it doesn't reproduce new ranges
|
||||
* from reused tokens. Doesn't include supplementary and omitted
|
||||
* delimiter.
|
||||
* Main decorations hold basic formatting style of the tokens.
|
||||
*
|
||||
* Intended to build non-height-altering decorations. So, it doesn't
|
||||
* include line breaks and fenced div opening omitter. (It runs only in
|
||||
* the view update)
|
||||
*/
|
||||
build(state: EditorState) {
|
||||
let doc = state.doc,
|
||||
tokens = this.parser.tokens,
|
||||
mainDecoRanges: Range<Decoration>[] = [],
|
||||
outerDecoRanges: Range<Decoration>[] = [],
|
||||
delimiterRanges: Range<Decoration>[] = [],
|
||||
{ startToken, endToken } = this.parser.lastParsed;
|
||||
for (
|
||||
let i = startToken, token = tokens[i];
|
||||
i < endToken;
|
||||
token = tokens[++i]
|
||||
) {
|
||||
if (token.status != TokenStatus.ACTIVE) { continue }
|
||||
if (token.role == TokenRole.OPEN) {
|
||||
if (token.type == Format.HIGHLIGHT) {
|
||||
let color = "",
|
||||
open = token,
|
||||
close = tokens[i + token.size - 1],
|
||||
mayBeColorTag = tokens[i + 2],
|
||||
hasColorTag = mayBeColorTag?.type == Format.COLOR_TAG;
|
||||
// gets color tag
|
||||
if (hasColorTag) {
|
||||
color = doc.sliceString(mayBeColorTag.from + 1, mayBeColorTag.to - 1);
|
||||
i++;
|
||||
}
|
||||
let hlDeco = createHlDeco(color, {
|
||||
open: { from: open.from, to: open.to } as CharPos,
|
||||
close: { from: close.from, to: close.to } as CharPos
|
||||
});
|
||||
// highlight should be pushed in outer decorations
|
||||
outerDecoRanges.push(hlDeco.range(open.from, close.to));
|
||||
if (hasColorTag) {
|
||||
delimiterRanges.push(ColorTagDeco.range(mayBeColorTag.from, mayBeColorTag.to));
|
||||
}
|
||||
} else {
|
||||
let content = tokens[++i];
|
||||
delimiterRanges.push(DelimDeco[token.type as MainFormat].range(token.from, token.to));
|
||||
if (token.type == Format.SPOILER) { // spoiler should be pushed in outer decorations
|
||||
outerDecoRanges.push(ContentDeco[token.type as MainFormat].range(content.from, content.to));
|
||||
} else {
|
||||
mainDecoRanges.push(ContentDeco[token.type as MainFormat].range(content.from, content.to));
|
||||
}
|
||||
}
|
||||
} else if (token.role == TokenRole.CLOSE && token.type != Format.HIGHLIGHT) {
|
||||
delimiterRanges.push(DelimDeco[token.type as MainFormat].range(token.from, token.to));
|
||||
} else if (token.role == TokenRole.BLOCK_TAG) {
|
||||
delimiterRanges.push(AlignMarkDeco[token.type as AlignFormat].range(token.from, token.to));
|
||||
mainDecoRanges.push(AlignDeco[token.type as AlignFormat].range(token.from, token.from));
|
||||
buildMain(view: EditorView, state: EditorState) {
|
||||
this.buildInline(state, view.visibleRanges);
|
||||
this.buildBlock(state, view.visibleRanges);
|
||||
}
|
||||
/**
|
||||
* Supplementary decorations consist omitted delimiter of inline tokens,
|
||||
* color buttons for the highlight, and revealed spoiler when touched the
|
||||
* cursor or selection.
|
||||
*
|
||||
* Intended to build non-height-altering decorations. So, it doesn't
|
||||
* include line breaks and fenced div opening omitter. (It runs only in
|
||||
* the view update)
|
||||
*/
|
||||
buildSupplementary(isLivePreview: boolean) {
|
||||
if (isLivePreview) {
|
||||
this.omitInlineDelim(this.catcher.activeTokens);
|
||||
this.createColorBtnWidgets(this.catcher.hlTokens);
|
||||
this.revealSpoiler(this.catcher.spoilerTokens);
|
||||
} else {
|
||||
this.holder.colorBtnSet = this.holder.revealedSpoilerSet = RangeSet.empty;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Runs once on editor intialization, should be inside the view update
|
||||
* (i.e. the ViewPlugin update).
|
||||
*/
|
||||
onViewInit(view: EditorView) {
|
||||
let state = view.state,
|
||||
isLivePreview = state.field(editorLivePreviewField);
|
||||
this.buildMain(view, state);
|
||||
this.buildSupplementary(isLivePreview);
|
||||
}
|
||||
onViewUpdate(update: ViewUpdate) {
|
||||
let state = update.state,
|
||||
view = update.view,
|
||||
isLivePreview = state.field(editorLivePreviewField);
|
||||
if (this.parser.isReparsing || this.parser.isInitializing || update.viewportMoved) {
|
||||
this.buildMain(view, state);
|
||||
}
|
||||
if (this.selectionObserver.isObserving || update.viewportMoved) {
|
||||
this.buildSupplementary(isLivePreview);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Runs once on editor intialization, should be inside the state update
|
||||
* (i.e. the StateField update).
|
||||
*/
|
||||
onStateInit(state: EditorState) {
|
||||
let isLivePreview = state.field(editorLivePreviewField);
|
||||
if (isLivePreview) {
|
||||
this.omitFencedDivOpening();
|
||||
}
|
||||
this.replaceLineBreaks(state.doc);
|
||||
}
|
||||
onStateUpdate(transaction: Transaction) {
|
||||
let state = transaction.state,
|
||||
isLivePreview = state.field(editorLivePreviewField),
|
||||
isRefreshed = transaction.annotation(refresherAnnot),
|
||||
isModeChanged = isEditorModeChanged(state, transaction.startState);
|
||||
if (this.parser.isReparsing || this.parser.isInitializing) {
|
||||
this.replaceLineBreaks(transaction.newDoc, transaction.changes);
|
||||
}
|
||||
if (isModeChanged || isRefreshed) {
|
||||
this.selectionObserver.restartObserver(transaction.newSelection, transaction.docChanged);
|
||||
this.holder.blockOmittedSet = RangeSet.empty;
|
||||
}
|
||||
if (!isLivePreview) {
|
||||
this.removeOmitter();
|
||||
} else if (this.selectionObserver.isObserving || isModeChanged) {
|
||||
this.omitFencedDivOpening(transaction.changes);
|
||||
}
|
||||
}
|
||||
buildInline(state: EditorState, visibleRanges: readonly PlainRange[]) {
|
||||
let inlineDecoRanges: Range<TokenDecoration>[] = [],
|
||||
activeTokens: TokenGroup = [],
|
||||
hlTokens: TokenGroup = [],
|
||||
spoilerTokens: TokenGroup = [];
|
||||
iterTokenGroup({
|
||||
tokens: this.parser.inlineTokens,
|
||||
ranges: visibleRanges,
|
||||
indexCache: this.indexCaches.inlineToken,
|
||||
callback: (token) => {
|
||||
if (token.status != TokenStatus.ACTIVE) { return }
|
||||
if (token.type == Format.HIGHLIGHT) { hlTokens.push(token) }
|
||||
if (token.type == Format.SPOILER) { spoilerTokens.push(token) }
|
||||
inlineDecoRanges.push(this.transformInlineToken(token, state.doc));
|
||||
activeTokens.push(token);
|
||||
},
|
||||
});
|
||||
this.catcher.catch(activeTokens, hlTokens, spoilerTokens);
|
||||
return this.holder.inlineSet = Decoration.set(inlineDecoRanges);
|
||||
}
|
||||
buildBlock(state: EditorState, visibleRanges: readonly PlainRange[]) {
|
||||
let lineDecoRanges: Range<TokenDecoration>[] = [];
|
||||
iterTokenGroup({
|
||||
tokens: this.parser.blockTokens,
|
||||
ranges: visibleRanges,
|
||||
indexCache: this.indexCaches.blockToken,
|
||||
callback: (token) => {
|
||||
if (token.status != TokenStatus.ACTIVE) { return }
|
||||
lineDecoRanges.push(...this.transformBlockToken(token, state.doc));
|
||||
},
|
||||
});
|
||||
return this.holder.blockSet = Decoration.set(lineDecoRanges);
|
||||
}
|
||||
transformInlineToken(token: Token, doc: Text) {
|
||||
let cls = "cm-" + InlineRules[token.type as InlineFormat].class;
|
||||
if (token.tagLen) {
|
||||
let line = getLineAt(doc, token.from, this.indexCaches.linePos),
|
||||
tagRange = getTagRange(token),
|
||||
tagStr = sliceStrFromLine(line, tagRange.from + 1, tagRange.to - 1);
|
||||
if (token.type == Format.CUSTOM_SPAN) {
|
||||
tagStr = trimTag(tagStr);
|
||||
cls += " " + tagStr;
|
||||
} else {
|
||||
cls += " " + cls + "-" + tagStr;
|
||||
}
|
||||
}
|
||||
return { mainDecoRanges, outerDecoRanges, delimiterRanges };
|
||||
return createInlineDecoRange(token, cls);
|
||||
}
|
||||
/**
|
||||
* Filters out some ranges from old set that needs to be replaced by the new one,
|
||||
* resulting new `DecorationSet`. Doesn't intended to filtering supplementary
|
||||
* and omitted delimiter set.
|
||||
*/
|
||||
updateSet(update: ViewUpdate, decoSet: DecorationSet, ranges: Range<Decoration>[]) {
|
||||
let { startToken: start, endToken: end } = this.parser.lastParsed,
|
||||
docLen = update.state.doc.length,
|
||||
filterFrom = (this.parser.tokens[start - 1]?.to ?? 0),
|
||||
filterTo = (this.parser.tokens[end]?.from ?? docLen);
|
||||
return decoSet.map(update.changes).update({
|
||||
add: ranges, filterFrom, filterTo,
|
||||
filter(from, to, val) {
|
||||
return (to == filterFrom || from == filterTo) && ((val.spec.type as Format) <= Format.ALIGN_JUSTIFY && from != 0 || to != from);
|
||||
}
|
||||
transformBlockToken(token: Token, doc: Text) {
|
||||
let baseCls = "cm-" + BlockRules[token.type as BlockFormat].class,
|
||||
openLine = getLineAt(doc, token.from, this.indexCaches.linePos),
|
||||
tagRange = getTagRange(token),
|
||||
tagStr = trimTag(sliceStrFromLine(openLine, tagRange.from, tagRange.to)),
|
||||
openDelimCls = baseCls + " cm-fenced-div-start",
|
||||
contentCls = baseCls + " " + tagStr,
|
||||
ranges: Range<TokenDecoration>[] = [createLineDecoRange(token, openDelimCls, openLine)];
|
||||
iterLine({
|
||||
doc, fromLn: openLine.number + 1,
|
||||
callback(line) {
|
||||
if (line.to > token.to) { return false }
|
||||
if (line.to == token.to && !line.text.trimEnd()) { return false }
|
||||
let decoRange = createLineDecoRange(token, contentCls, line);
|
||||
ranges.push(decoRange);
|
||||
},
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
/** Produces supplementary decorations (color button and reveled text for spoiler). */
|
||||
// TODO: Make it incrementally
|
||||
getSupplemental(state: EditorState, outerDecoSet: DecorationSet) {
|
||||
let decoRanges: Range<Decoration>[] = [],
|
||||
selectRanges = state.selection.ranges,
|
||||
i = 0;
|
||||
outerDecoSet.between(0, state.doc.length, (from, to, val) => {
|
||||
while (selectRanges[i] && selectRanges[i].to < from) { i++ }
|
||||
if (!selectRanges[i]) { return false }
|
||||
if (selectRanges[i].from > to) { return }
|
||||
let type = val.spec.type as Format.HIGHLIGHT | Format.SPOILER;
|
||||
if (type == Format.HIGHLIGHT && this.parser.settings.colorButton) {
|
||||
let color = val.spec.color as string,
|
||||
open = (val.spec.open as CharPos),
|
||||
close = (val.spec.close as CharPos);
|
||||
decoRanges.push(ColorButton.of(color, open, close));
|
||||
} else if (type == Format.SPOILER) {
|
||||
decoRanges.push(RevealedSpoiler.range(from, to));
|
||||
createColorBtnWidgets(hlTokens: TokenGroup) {
|
||||
if (!this.settings.colorButton) {
|
||||
return this.holder.colorBtnSet = RangeSet.empty;
|
||||
}
|
||||
let btnWidgets: Range<Decoration>[] = [];
|
||||
for (let i = 0; i < hlTokens.length; i++) {
|
||||
let token = hlTokens[i];
|
||||
if (this.selectionObserver.touchSelection(token.from, token.to)) {
|
||||
btnWidgets.push(ColorButton.of(token));
|
||||
}
|
||||
});
|
||||
return Decoration.set(decoRanges);
|
||||
}
|
||||
return this.holder.colorBtnSet = Decoration.set(btnWidgets);
|
||||
}
|
||||
revealSpoiler(spoilerTokens: TokenGroup) {
|
||||
let revealedRanges: Range<Decoration>[] = [];
|
||||
for (let i = 0; i < spoilerTokens.length; i++) {
|
||||
let token = spoilerTokens[i];
|
||||
if (this.selectionObserver.touchSelection(token.from, token.to)) {
|
||||
revealedRanges.push(REVEALED_SPOILER_DECO.range(token.from, token.to));
|
||||
}
|
||||
}
|
||||
return this.holder.revealedSpoilerSet = Decoration.set(revealedRanges);
|
||||
}
|
||||
omitInlineDelim(activeTokens: TokenGroup) {
|
||||
return this.holder.inlineOmittedSet = this.omitter.omitInline(activeTokens);
|
||||
}
|
||||
/** Executed only in the state update. */
|
||||
replaceLineBreaks(doc: Text, changes?: ChangeSet) {
|
||||
return this.holder.lineBreaksSet = this.lineBreakReplacer.replace(doc, changes);
|
||||
}
|
||||
/** Executed only in the state update. */
|
||||
omitFencedDivOpening(changes?: ChangeDesc) {
|
||||
let isOmitted = !(this.settings.alwaysShowFencedDivTag & MarkdownViewMode.EDITOR_MODE);
|
||||
if (!isOmitted) {
|
||||
return this.holder.blockOmittedSet = RangeSet.empty;
|
||||
}
|
||||
return this.holder.blockOmittedSet = this.omitter.omitBlock(this.holder.blockOmittedSet, changes);
|
||||
}
|
||||
removeOmitter() {
|
||||
this.holder.inlineOmittedSet = this.holder.blockOmittedSet = RangeSet.empty;
|
||||
}
|
||||
}
|
||||
13
src/editor-mode/decorator/builder/DecorationHolder.ts
Normal file
13
src/editor-mode/decorator/builder/DecorationHolder.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { RangeSet } from "@codemirror/state";
|
||||
import { DecorationSet } from "@codemirror/view";
|
||||
|
||||
export class DecorationHolder {
|
||||
inlineSet: DecorationSet = RangeSet.empty;
|
||||
blockSet: DecorationSet = RangeSet.empty;
|
||||
inlineOmittedSet: DecorationSet = RangeSet.empty;
|
||||
blockOmittedSet: DecorationSet = RangeSet.empty;
|
||||
colorBtnSet: DecorationSet = RangeSet.empty;
|
||||
revealedSpoilerSet: DecorationSet = RangeSet.empty;
|
||||
lineBreaksSet: DecorationSet = RangeSet.empty;
|
||||
constructor() {}
|
||||
}
|
||||
73
src/editor-mode/decorator/builder/DelimOmitter.ts
Normal file
73
src/editor-mode/decorator/builder/DelimOmitter.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { ChangeDesc, Range } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet } from "@codemirror/view";
|
||||
import { DisplayBehaviour, Format, TokenLevel, TokenStatus } from "src/enums";
|
||||
import { PluginSettings, TokenGroup } from "src/types";
|
||||
import { HiddenWidget } from "src/editor-mode/decorator/widgets";
|
||||
import { SelectionObserver } from "src/editor-mode/observer";
|
||||
|
||||
export class DelimOmitter {
|
||||
selectionObserver: SelectionObserver;
|
||||
settings: PluginSettings;
|
||||
constructor(settings: PluginSettings, selectionObserver: SelectionObserver) {
|
||||
this.settings = settings;
|
||||
this.selectionObserver = selectionObserver;
|
||||
}
|
||||
omitBlock(omittedSet?: DecorationSet, changes?: ChangeDesc) {
|
||||
let omittedRanges: Range<Decoration>[] = [],
|
||||
filterRegion = this.selectionObserver.filterRegions[TokenLevel.BLOCK];
|
||||
this.selectionObserver.iterateChangedRegion(TokenLevel.BLOCK, (token, index, tokens, inSelection) => {
|
||||
if (inSelection || token.status != TokenStatus.ACTIVE || !token.validTag) { return }
|
||||
let openFrom = token.from,
|
||||
openTo = openFrom + token.openLen + token.tagLen;
|
||||
if (token.to > openTo) { openTo++ }
|
||||
omittedRanges.push(HiddenWidget.of(openFrom, openTo, token, true));
|
||||
});
|
||||
if (!omittedSet?.size) {
|
||||
omittedSet = Decoration.set(omittedRanges);
|
||||
} else {
|
||||
if (changes) {
|
||||
omittedSet = omittedSet.map(changes);
|
||||
}
|
||||
for (let i = 0; i < filterRegion.length; i++) {
|
||||
let filterRange = filterRegion[i];
|
||||
omittedSet = omittedSet.update({
|
||||
filterFrom: filterRange.from,
|
||||
filterTo: filterRange.to,
|
||||
filter: () => false,
|
||||
});
|
||||
}
|
||||
omittedSet = omittedSet.update({ add: omittedRanges });
|
||||
}
|
||||
return omittedSet;
|
||||
}
|
||||
omitInline(activeTokens: TokenGroup) {
|
||||
let alwaysShowHlTag = this.settings.hlTagDisplayBehaviour & DisplayBehaviour.ALWAYS,
|
||||
alwaysShowSpanTag = this.settings.spanTagDisplayBehaviour & DisplayBehaviour.ALWAYS,
|
||||
showHlTagIfTouched = this.settings.hlTagDisplayBehaviour & DisplayBehaviour.TAG_TOUCHED,
|
||||
showSpanTagIfTouched = this.settings.spanTagDisplayBehaviour & DisplayBehaviour.TAG_TOUCHED;
|
||||
let omittedRanges: Range<Decoration>[] = [];
|
||||
for (let i = 0; i < activeTokens.length; i++) {
|
||||
let token = activeTokens[i],
|
||||
openFrom = token.from,
|
||||
openTo = openFrom + token.openLen,
|
||||
tagTo = openTo + token.tagLen;
|
||||
if (this.selectionObserver.touchSelection(token.from, token.to)) {
|
||||
if (
|
||||
token.validTag && !this.selectionObserver.touchSelection(openTo, tagTo) &&
|
||||
(token.type == Format.HIGHLIGHT && showHlTagIfTouched || token.type == Format.CUSTOM_SPAN && showSpanTagIfTouched)
|
||||
) {
|
||||
omittedRanges.push(HiddenWidget.of(openTo, tagTo, token));
|
||||
}
|
||||
} else {
|
||||
if (token.type == Format.HIGHLIGHT && !alwaysShowHlTag || token.type == Format.CUSTOM_SPAN && !alwaysShowSpanTag) {
|
||||
openTo = tagTo;
|
||||
}
|
||||
omittedRanges.push(HiddenWidget.of(openFrom, openTo, token));
|
||||
if (token.closeLen) {
|
||||
omittedRanges.push(HiddenWidget.of(token.to - token.closeLen, token.to, token));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Decoration.set(omittedRanges);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { Decoration, DecorationSet, EditorView, PluginValue, ViewUpdate } from "@codemirror/view";
|
||||
import { Parser } from "src/editor-mode/parser";
|
||||
import { DecorationBuilder, RangeOmitter } from "src/editor-mode/decorator/builder";
|
||||
import { parserField } from "src/editor-mode/state-fields";
|
||||
import { EditorState, RangeSet } from "@codemirror/state";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
|
||||
export class ExtendedSyntax implements PluginValue {
|
||||
/** Insertion, superscript, and subscript. */
|
||||
mainDecoSet: DecorationSet;
|
||||
/**
|
||||
* Spoiler and highlight, positioned at upper DOM hierarchy and
|
||||
* not split by other decorations. Useful for making them appear
|
||||
* continous in the editor, especially when overlap with others.
|
||||
*/
|
||||
outerDecoSet: DecorationSet;
|
||||
/** Any type of tag and delimiter */
|
||||
delimiterSet: DecorationSet;
|
||||
/**
|
||||
* Any type of tag and delimiter omitted due to touching cursor or selection.
|
||||
* Used only when in live preview mode.
|
||||
*/
|
||||
omittedSet: DecorationSet;
|
||||
/**
|
||||
* Color button for highlight and revealed text for spoiler,
|
||||
* only when selection or cursor are touching them.
|
||||
*/
|
||||
supplementalSet: DecorationSet;
|
||||
combinedSet: DecorationSet;
|
||||
parser: Parser;
|
||||
builder: DecorationBuilder;
|
||||
omitter: RangeOmitter;
|
||||
noOmit = true;
|
||||
constructor(view: EditorView) {
|
||||
this.builder = new DecorationBuilder();
|
||||
this.omitter = new RangeOmitter();
|
||||
this.parser = view.state.field(parserField);
|
||||
this.builder.parser = this.parser;
|
||||
this.init(view.state);
|
||||
}
|
||||
init(state: EditorState) {
|
||||
let { mainDecoRanges, outerDecoRanges, delimiterRanges } = this.builder.build(state);
|
||||
this.mainDecoSet = Decoration.set(mainDecoRanges);
|
||||
this.outerDecoSet = Decoration.set(outerDecoRanges);
|
||||
this.delimiterSet = Decoration.set(delimiterRanges);
|
||||
this.supplementalSet = this.builder.getSupplemental(state, this.outerDecoSet);
|
||||
if (state.field(editorLivePreviewField)) {
|
||||
this.noOmit = false;
|
||||
this.omittedSet = this.omitter.omit(state, this.delimiterSet);
|
||||
} else {
|
||||
this.omittedSet = RangeSet.empty;
|
||||
}
|
||||
this.combine();
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged || this.parser.isReparsing) {
|
||||
let { mainDecoRanges, outerDecoRanges, delimiterRanges } = this.builder.build(update.state);
|
||||
this.mainDecoSet = this.builder.updateSet(update, this.mainDecoSet, mainDecoRanges);
|
||||
this.outerDecoSet = this.builder.updateSet(update, this.outerDecoSet, outerDecoRanges);
|
||||
this.delimiterSet = this.builder.updateSet(update, this.delimiterSet, delimiterRanges);
|
||||
}
|
||||
if (update.docChanged || update.selectionSet || this.parser.isReparsing) {
|
||||
this.supplementalSet = this.builder.getSupplemental(update.state, this.outerDecoSet);
|
||||
}
|
||||
if (update.state.field(editorLivePreviewField)) {
|
||||
if (update.docChanged || update.selectionSet || this.parser.isReparsing || this.noOmit) {
|
||||
this.omittedSet = this.omitter.omit(update.state, this.delimiterSet);
|
||||
}
|
||||
this.noOmit = false;
|
||||
} else {
|
||||
this.omittedSet = RangeSet.empty;
|
||||
this.noOmit = true;
|
||||
}
|
||||
this.combine();
|
||||
this.parser.isReparsing = false;
|
||||
}
|
||||
combine() {
|
||||
this.combinedSet = RangeSet.join([
|
||||
this.mainDecoSet,
|
||||
this.delimiterSet,
|
||||
this.omittedSet,
|
||||
this.supplementalSet
|
||||
]);
|
||||
}
|
||||
}
|
||||
66
src/editor-mode/decorator/builder/LineBreakReplacer.ts
Normal file
66
src/editor-mode/decorator/builder/LineBreakReplacer.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { RangeSet, Range, Text, ChangeSet } from "@codemirror/state";
|
||||
import { Decoration } from "@codemirror/view";
|
||||
import { Parser } from "src/editor-mode/parser";
|
||||
import { IndexCache, RangeSetUpdate, TokenGroup } from "src/types";
|
||||
import { getLineAt, iterLine } from "src/editor-mode/decorator/utils";
|
||||
import { LineBreak } from "src/editor-mode/decorator/widgets";
|
||||
import { TokenLevel, TokenStatus } from "src/enums";
|
||||
|
||||
export class LineBreakReplacer {
|
||||
private linePosCache: IndexCache = { number: 1 };
|
||||
parser: Parser;
|
||||
lineBreakSet: RangeSet<Decoration> = RangeSet.empty;
|
||||
constructor(parser: Parser) {
|
||||
this.parser = parser;
|
||||
}
|
||||
replace(doc: Text, changes?: ChangeSet) {
|
||||
let reparsedRange = this.parser.reparsedRanges[TokenLevel.BLOCK],
|
||||
blockTokens = this.parser.blockTokens,
|
||||
updateSpec: RangeSetUpdate<Decoration> = {
|
||||
add: this.produceWidgetRanges(doc)
|
||||
};
|
||||
if (!blockTokens.length) {
|
||||
return this.lineBreakSet = RangeSet.empty;
|
||||
}
|
||||
if (reparsedRange.from != reparsedRange.initTo || reparsedRange.from != reparsedRange.changedTo) {
|
||||
updateSpec.filterFrom = Math.min(blockTokens[reparsedRange.from]?.from ?? this.parser.lastStreamPoint.from, this.parser.lastStreamPoint.from);
|
||||
updateSpec.filterTo = this.parser.lastStreamPoint.to;
|
||||
updateSpec.filter = () => false;
|
||||
}
|
||||
if (changes) {
|
||||
this.lineBreakSet = this.lineBreakSet.map(changes);
|
||||
}
|
||||
return this.lineBreakSet = this.lineBreakSet.update(updateSpec);
|
||||
}
|
||||
private getReparsedBlockTokens(): TokenGroup {
|
||||
let range = this.parser.reparsedRanges[TokenLevel.BLOCK];
|
||||
return this.parser.blockTokens.slice(range.from, range.changedTo);
|
||||
}
|
||||
private produceWidgetRanges(doc: Text) {
|
||||
let tokens = this.getReparsedBlockTokens(),
|
||||
ranges: Range<Decoration>[] = [];
|
||||
if (!tokens.length) { return ranges }
|
||||
let startLine = getLineAt(doc, tokens[0].from, this.linePosCache),
|
||||
tokenIndex = 0;
|
||||
iterLine({
|
||||
doc,
|
||||
fromLn: startLine.number,
|
||||
callback: (line) => {
|
||||
let curToken = tokens[tokenIndex];
|
||||
if (!curToken) { return false }
|
||||
if (curToken.status != TokenStatus.ACTIVE) { tokenIndex++; return }
|
||||
if (
|
||||
line.from == curToken.from ||
|
||||
line.from - 1 == curToken.from + curToken.openLen + curToken.tagLen
|
||||
) { return }
|
||||
if (
|
||||
line.from >= curToken.to ||
|
||||
line.to < curToken.from ||
|
||||
line.to == curToken.to && curToken.closedByBlankLine
|
||||
) { tokenIndex++; return }
|
||||
ranges.push(LineBreak.of(line.from));
|
||||
},
|
||||
});
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
import { EditorState, Range } from "@codemirror/state";
|
||||
import { Decoration, DecorationSet } from "@codemirror/view";
|
||||
import { HiddenWidget } from "src/editor-mode/decorator/widgets";
|
||||
import { Format } from "src/enums";
|
||||
import { MainFormat } from "src/types";
|
||||
|
||||
/**
|
||||
* Omits delimiters that touch the cursor or selection,
|
||||
* so those don't appear in the editor.
|
||||
*/
|
||||
export class RangeOmitter {
|
||||
constructor() {};
|
||||
/** Should be run in preview mode only */
|
||||
omit(state: EditorState, delimiterSet: DecorationSet) {
|
||||
let omittedRanges: Range<Decoration>[] = [],
|
||||
selectRanges = state.selection.ranges, i = 0,
|
||||
openOffset: { [T in MainFormat]: null | number } = {
|
||||
[Format.INSERTION]: null,
|
||||
[Format.SPOILER]: null,
|
||||
[Format.SUPERSCRIPT]: null,
|
||||
[Format.SUBSCRIPT]: null,
|
||||
[Format.HIGHLIGHT]: null
|
||||
};
|
||||
delimiterSet.between(0, state.doc.length, (from, to, val) => {
|
||||
let type = val.spec.type as Format;
|
||||
if (type == Format.COLOR_TAG || type <= Format.ALIGN_JUSTIFY) {
|
||||
while (i + 1 < selectRanges.length && selectRanges[i].to < from) { i++ }
|
||||
if (selectRanges[i].from > to || selectRanges[i].to < from) {
|
||||
omittedRanges.push(HiddenWidget.of(from, to, val));
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (openOffset[type as MainFormat] === null) {
|
||||
openOffset[type as MainFormat] = from;
|
||||
} else {
|
||||
let length = to - from,
|
||||
openFrom = openOffset[type as MainFormat]!;
|
||||
while (i + 1 < selectRanges.length && selectRanges[i].to < openFrom) { i++ }
|
||||
if (selectRanges[i].from > to || selectRanges[i].to < openFrom) {
|
||||
omittedRanges.push(
|
||||
HiddenWidget.of(openFrom, openFrom + length, val),
|
||||
HiddenWidget.of(from, to, val)
|
||||
);
|
||||
}
|
||||
openOffset[type as MainFormat] = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
return Decoration.set(omittedRanges, true);
|
||||
}
|
||||
}
|
||||
18
src/editor-mode/decorator/builder/TokensCatcher.ts
Normal file
18
src/editor-mode/decorator/builder/TokensCatcher.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { TokenGroup } from "src/types";
|
||||
|
||||
export class TokensCatcher {
|
||||
activeTokens: TokenGroup = [];
|
||||
hlTokens: TokenGroup = [];
|
||||
spoilerTokens: TokenGroup = [];
|
||||
constructor () {}
|
||||
catch(activeTokens: TokenGroup, hlTokens: TokenGroup, spoilerTokens: TokenGroup) {
|
||||
this.activeTokens = activeTokens;
|
||||
this.hlTokens = hlTokens;
|
||||
this.spoilerTokens = spoilerTokens;
|
||||
}
|
||||
empty() {
|
||||
this.activeTokens = [];
|
||||
this.hlTokens = [];
|
||||
this.spoilerTokens = [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
export * from "./DecorationBuilder";
|
||||
export * from "./RangeOmitter";
|
||||
export * from "./ExtendedSyntax";
|
||||
export * from "./DelimOmitter";
|
||||
export * from "./TokensCatcher";
|
||||
export * from "./LineBreakReplacer";
|
||||
export * from "./DecorationHolder";
|
||||
|
|
@ -1,111 +1,5 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
import { Format } from "src/enums";
|
||||
|
||||
export const AlignDeco = {
|
||||
[Format.ALIGN_LEFT]: Decoration.line({
|
||||
class: "cm-align-left",
|
||||
type: Format.ALIGN_LEFT
|
||||
}),
|
||||
[Format.ALIGN_RIGHT]: Decoration.line({
|
||||
class: "cm-align-right",
|
||||
type: Format.ALIGN_RIGHT
|
||||
}),
|
||||
[Format.ALIGN_CENTER]: Decoration.line({
|
||||
class: "cm-align-center",
|
||||
type: Format.ALIGN_CENTER
|
||||
}),
|
||||
[Format.ALIGN_JUSTIFY]: Decoration.line({
|
||||
class: "cm-align-justify",
|
||||
type: Format.ALIGN_JUSTIFY
|
||||
})
|
||||
};
|
||||
|
||||
export const AlignMarkDeco = {
|
||||
[Format.ALIGN_LEFT]: Decoration.mark({
|
||||
class: "cm-align-mark-left",
|
||||
type: Format.ALIGN_LEFT,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.ALIGN_RIGHT]: Decoration.mark({
|
||||
class: "cm-align-mark-right",
|
||||
type: Format.ALIGN_RIGHT,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.ALIGN_CENTER]: Decoration.mark({
|
||||
class: "cm-align-mark-center",
|
||||
type: Format.ALIGN_CENTER,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.ALIGN_JUSTIFY]: Decoration.mark({
|
||||
class: "cm-align-mark-justify",
|
||||
type: Format.ALIGN_JUSTIFY,
|
||||
omitted: true
|
||||
}),
|
||||
};
|
||||
|
||||
export const DelimDeco = {
|
||||
[Format.INSERTION]: Decoration.mark({
|
||||
class: "cm-delim cm-ins",
|
||||
type: Format.INSERTION,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.SPOILER]: Decoration.mark({
|
||||
class: "cm-delim cm-spoiler",
|
||||
type: Format.SPOILER,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.SUPERSCRIPT]: Decoration.mark({
|
||||
class: "cm-delim cm-sup",
|
||||
type: Format.SUPERSCRIPT,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.SUBSCRIPT]: Decoration.mark({
|
||||
class: "cm-delim cm-sub",
|
||||
type: Format.SUBSCRIPT,
|
||||
omitted: true
|
||||
}),
|
||||
[Format.HIGHLIGHT]: Decoration.mark({
|
||||
class: "cm-highlight",
|
||||
type: Format.HIGHLIGHT,
|
||||
omitted: true
|
||||
})
|
||||
};
|
||||
|
||||
export const ContentDeco = {
|
||||
[Format.INSERTION]: Decoration.mark({
|
||||
class: "cm-ins",
|
||||
type: Format.INSERTION,
|
||||
inclusive: true
|
||||
}),
|
||||
[Format.SPOILER]: Decoration.mark({
|
||||
class: "cm-spoiler",
|
||||
type: Format.SPOILER,
|
||||
inclusive: true
|
||||
}),
|
||||
[Format.SUPERSCRIPT]: Decoration.mark({
|
||||
class: "cm-sup",
|
||||
type: Format.SUPERSCRIPT,
|
||||
inclusive: true
|
||||
}),
|
||||
[Format.SUBSCRIPT]: Decoration.mark({
|
||||
class: "cm-sub",
|
||||
type: Format.SUBSCRIPT,
|
||||
inclusive: true
|
||||
}),
|
||||
[Format.HIGHLIGHT]: Decoration.mark({
|
||||
class: "cm-highlight",
|
||||
type: Format.HIGHLIGHT,
|
||||
inclusive: true
|
||||
})
|
||||
};
|
||||
|
||||
export const ColorTagDeco = Decoration.mark({
|
||||
class: "cm-color-tag",
|
||||
type: Format.COLOR_TAG,
|
||||
omitted: true
|
||||
});
|
||||
|
||||
export const RevealedSpoiler = Decoration.mark({
|
||||
export const REVEALED_SPOILER_DECO = Decoration.mark({
|
||||
class: "cm-spoiler-revealed",
|
||||
type: Format.SPOILER
|
||||
});
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
import { Format } from "src/enums";
|
||||
|
||||
export function createHlDeco(color: string, data?: { [P in string]: unknown }) {
|
||||
export function createHlDeco(color: string, data?: Record<string, unknown>) {
|
||||
let spec: Parameters<typeof Decoration.mark>[0] = {
|
||||
class: "cm-custom-highlight cm-custom-highlight-" + (color || "default"),
|
||||
type: Format.HIGHLIGHT,
|
||||
color,
|
||||
inclusive: true
|
||||
inclusive: false
|
||||
};
|
||||
if (data) {
|
||||
for (let prop in data) { spec[prop] = data[prop] }
|
||||
|
|
|
|||
8
src/editor-mode/decorator/utils/createInlineDecoRange.ts
Normal file
8
src/editor-mode/decorator/utils/createInlineDecoRange.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { Decoration } from "@codemirror/view";
|
||||
import { Token, TokenDecoration } from "src/types";
|
||||
|
||||
export function createInlineDecoRange(token: Token, cls: string) {
|
||||
return (Decoration
|
||||
.mark({ class: cls, token }) as TokenDecoration)
|
||||
.range(token.from, token.to);
|
||||
}
|
||||
9
src/editor-mode/decorator/utils/createLineDecoRange.ts
Normal file
9
src/editor-mode/decorator/utils/createLineDecoRange.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Line } from "@codemirror/state";
|
||||
import { Decoration } from "@codemirror/view";
|
||||
import { Token, TokenDecoration } from "src/types";
|
||||
|
||||
export function createLineDecoRange(token: Token, cls: string, line: Line) {
|
||||
return (Decoration
|
||||
.line({ class: cls, token }) as TokenDecoration)
|
||||
.range(line.from);
|
||||
}
|
||||
18
src/editor-mode/decorator/utils/getLineAt.ts
Normal file
18
src/editor-mode/decorator/utils/getLineAt.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { IndexCache } from "src/types";
|
||||
import { Text } from "@codemirror/state"
|
||||
|
||||
export function getLineAt(doc: Text, offset: number, linePosCache: IndexCache) {
|
||||
if (linePosCache.number > doc.lines) { linePosCache.number = doc.lines }
|
||||
let curLine = doc.line(linePosCache.number);
|
||||
if (offset < curLine.from) {
|
||||
do {
|
||||
curLine = doc.line(curLine.number - 1);
|
||||
} while (offset < curLine.from)
|
||||
} else if (offset > curLine.to) {
|
||||
do {
|
||||
curLine = doc.line(curLine.number + 1);
|
||||
} while (offset > curLine.to)
|
||||
}
|
||||
linePosCache.number = curLine.number;
|
||||
return curLine;
|
||||
}
|
||||
7
src/editor-mode/decorator/utils/getTagRange.ts
Normal file
7
src/editor-mode/decorator/utils/getTagRange.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Token } from "src/types";
|
||||
|
||||
export function getTagRange(token: Token) {
|
||||
let from = token.from + token.openLen,
|
||||
to = from + token.tagLen;
|
||||
return { from, to };
|
||||
}
|
||||
|
|
@ -1 +1,10 @@
|
|||
export * from "./createHlDeco";
|
||||
export * from "./createHlDeco";
|
||||
export * from "./createInlineDecoRange";
|
||||
export * from "./getLineAt";
|
||||
export * from "./sliceStrFromLine";
|
||||
export * from "./getTagRange";
|
||||
export * from "./createLineDecoRange";
|
||||
export * from "./iterLine";
|
||||
export * from "./iterTokenGroup";
|
||||
export * from "./moveTokenIndexCache";
|
||||
export * from "./isEditorModeChanged";
|
||||
8
src/editor-mode/decorator/utils/isEditorModeChanged.ts
Normal file
8
src/editor-mode/decorator/utils/isEditorModeChanged.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { EditorState } from "@codemirror/state";
|
||||
import { editorLivePreviewField } from "obsidian";
|
||||
|
||||
export function isEditorModeChanged(curState: EditorState, prevState: EditorState) {
|
||||
let isLivePreviewCurrently = curState.field(editorLivePreviewField),
|
||||
isLivePreviewPreviously = prevState.field(editorLivePreviewField);
|
||||
return isLivePreviewCurrently != isLivePreviewPreviously;
|
||||
}
|
||||
8
src/editor-mode/decorator/utils/iterLine.ts
Normal file
8
src/editor-mode/decorator/utils/iterLine.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { IterLineSpec } from "src/types";
|
||||
|
||||
export function iterLine(spec: IterLineSpec) {
|
||||
for (let i = spec.fromLn; i <= (spec.toLn ?? spec.doc.lines); i++) {
|
||||
let curLine = spec.doc.line(i);
|
||||
if (spec.callback(curLine, spec.doc) === false) { break }
|
||||
}
|
||||
}
|
||||
17
src/editor-mode/decorator/utils/iterTokenGroup.ts
Normal file
17
src/editor-mode/decorator/utils/iterTokenGroup.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { IterTokenGroupSpec } from "src/types";
|
||||
import { moveTokenIndexCache } from "src/editor-mode/decorator/utils";
|
||||
|
||||
export function iterTokenGroup(spec: IterTokenGroupSpec) {
|
||||
let { tokens, ranges, callback, indexCache } = spec;
|
||||
moveTokenIndexCache(tokens, ranges[0]?.from ?? 0, indexCache);
|
||||
for (
|
||||
let i = indexCache.number, j = 0;
|
||||
i < tokens.length && j < ranges.length;
|
||||
) {
|
||||
if (ranges[j].to <= tokens[i].from) { j++; continue }
|
||||
if (tokens[i].from < ranges[j].to && tokens[i].to > ranges[j].from) {
|
||||
callback(tokens[i]);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
23
src/editor-mode/decorator/utils/moveTokenIndexCache.ts
Normal file
23
src/editor-mode/decorator/utils/moveTokenIndexCache.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { IndexCache, TokenGroup } from "src/types";
|
||||
|
||||
export function moveTokenIndexCache(tokens: TokenGroup, offset: number, indexCache: IndexCache) {
|
||||
if (tokens.length == 0) {
|
||||
indexCache.number = 0;
|
||||
return;
|
||||
}
|
||||
if (indexCache.number >= tokens.length) {
|
||||
indexCache.number = tokens.length - 1;
|
||||
}
|
||||
let curIndex = indexCache.number,
|
||||
curToken = tokens[curIndex];
|
||||
if (offset < curToken.from && curIndex != 0) {
|
||||
do {
|
||||
curToken = tokens[--curIndex];
|
||||
} while (offset < curToken.from && curIndex != 0)
|
||||
} else if (offset > curToken.to && curIndex != tokens.length - 1) {
|
||||
do {
|
||||
curToken = tokens[++curIndex];
|
||||
} while (offset > curToken.to && curIndex != tokens.length - 1)
|
||||
}
|
||||
indexCache.number = curIndex;
|
||||
}
|
||||
7
src/editor-mode/decorator/utils/sliceStrFromLine.ts
Normal file
7
src/editor-mode/decorator/utils/sliceStrFromLine.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Line } from "@codemirror/state";
|
||||
|
||||
export function sliceStrFromLine(line: Line, from: number, to: number) {
|
||||
from -= line.from;
|
||||
to -= line.from;
|
||||
return line.text.slice(from, to);
|
||||
}
|
||||
|
|
@ -1,21 +1,8 @@
|
|||
import { WidgetType, EditorView, Decoration } from "@codemirror/view"
|
||||
import { MarkdownView, Menu } from "obsidian";
|
||||
import { appFacet } from "src/editor-mode/facets";
|
||||
import { CharPos } from "src/types";
|
||||
|
||||
// first is the text in the color tag, second is displayed text on menu
|
||||
const PREDEFINED_COLORS = [
|
||||
["red", "Red"],
|
||||
["orange", "Orange"],
|
||||
["yellow", "Yellow"],
|
||||
["green", "Green"],
|
||||
["cyan", "Cyan"],
|
||||
["blue", "Blue"],
|
||||
["purple", "Purple"],
|
||||
["pink", "Pink"],
|
||||
["accent", "Accent"],
|
||||
["default", "Default"],
|
||||
];
|
||||
import { Menu } from "obsidian";
|
||||
import { appFacet, settingsFacet } from "src/editor-mode/facets";
|
||||
import { getActiveCanvasNodeCoords } from "src/editor-mode/utils";
|
||||
import { PlainRange, Token } from "src/types";
|
||||
|
||||
/**
|
||||
* These code snippets are taken from
|
||||
|
|
@ -24,98 +11,134 @@ const PREDEFINED_COLORS = [
|
|||
*/
|
||||
export class ColorButton extends WidgetType {
|
||||
menu: Menu;
|
||||
colorTag: CharPos;
|
||||
open: CharPos;
|
||||
close: CharPos;
|
||||
constructor(color: string, open: CharPos, close: CharPos) {
|
||||
tagRange: PlainRange;
|
||||
openRange: PlainRange;
|
||||
closeRange: PlainRange;
|
||||
constructor(token: Token) {
|
||||
super();
|
||||
this.open = open;
|
||||
this.close = close;
|
||||
this.colorTag = color ?
|
||||
{ from: open.to, to: open.to + color.length + 2 } :
|
||||
{ from: open.to, to: open.to };
|
||||
this.openRange = { from: token.from, to: token.from + token.openLen };
|
||||
this.tagRange = { from: this.openRange.to, to: this.openRange.to + token.tagLen };
|
||||
this.closeRange = { from: token.to - token.closeLen, to: token.to };
|
||||
}
|
||||
get openLen() {
|
||||
return this.openRange.to - this.openRange.from;
|
||||
}
|
||||
get closeLen() {
|
||||
return this.closeRange.to - this.closeRange.from;
|
||||
}
|
||||
get tagLen() {
|
||||
return this.tagRange.to - this.tagRange.from;
|
||||
}
|
||||
eq(other: ColorButton) {
|
||||
return (
|
||||
other.colorTag.from == this.colorTag.from &&
|
||||
other.colorTag.to == this.colorTag.to
|
||||
);
|
||||
other.tagRange.from == this.tagRange.from &&
|
||||
other.tagRange.to == this.tagRange.to
|
||||
);
|
||||
}
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
let btn = document.createElement("span");
|
||||
let btn = document.createElement("span"),
|
||||
settings = view.state.facet(settingsFacet),
|
||||
colorConfigs = settings.colorConfigs;
|
||||
btn.setAttribute("aria-hidden", "true");
|
||||
btn.className = "cm-highlight-color-btn";
|
||||
btn.onclick = (evt) => {
|
||||
view.dispatch({
|
||||
selection: {
|
||||
anchor: this.colorTag.from,
|
||||
head: this.colorTag.to
|
||||
}
|
||||
});
|
||||
this.menu = new Menu();
|
||||
btn.onclick = () => {
|
||||
view.dispatch({
|
||||
selection: {
|
||||
anchor: this.tagRange.from,
|
||||
head: this.tagRange.to
|
||||
}
|
||||
});
|
||||
this.menu = new Menu();
|
||||
this.menu.dom.addClass("highlight-colors-modal");
|
||||
PREDEFINED_COLORS.forEach((color) => {
|
||||
this.menu.addItem((item) => {
|
||||
item.setTitle(color[1]);
|
||||
item.setIcon("palette");
|
||||
item.dom.addClass(`menu-item-${color[0] || "default"}`);
|
||||
item.onClick(() => { this.changeColor(view, color[0]) });
|
||||
});
|
||||
});
|
||||
this.menu.addItem(item => {
|
||||
item.setTitle("Remove");
|
||||
item.setIcon("eraser");
|
||||
item.dom.addClass("menu-item-remove-highlight");
|
||||
item.onClick(() => {
|
||||
view.dispatch({
|
||||
changes: [{
|
||||
from: this.open.from,
|
||||
to: this.colorTag.to,
|
||||
insert: ""
|
||||
}, {
|
||||
from: this.close.from,
|
||||
to: this.close.to,
|
||||
insert: ""
|
||||
}]
|
||||
});
|
||||
});
|
||||
});
|
||||
let app = view.state.facet(appFacet.reader),
|
||||
menuCoord = { x: evt.clientX, y: evt.clientY + 10 };
|
||||
if (app.workspace.getMostRecentLeaf()?.view.getViewType() == "canvas") {
|
||||
let containerEl = (app.workspace.activeEditor as MarkdownView)?.containerEl;
|
||||
if (containerEl) {
|
||||
let containerCoord = containerEl.getBoundingClientRect();
|
||||
menuCoord.x += containerCoord.x;
|
||||
menuCoord.y += containerCoord.y;
|
||||
}
|
||||
}
|
||||
this.menu.showAtPosition(menuCoord);
|
||||
}
|
||||
colorConfigs.forEach(({ name, tag, showInMenu }) => {
|
||||
if (!showInMenu) { return }
|
||||
this.addItem(name, "palette", "menu-item-" + tag, () => { this.changeColor(view, tag) });
|
||||
});
|
||||
this.addItem("Accent", "palette", "menu-item-accent", () => { this.changeColor(view, "accent") });
|
||||
this.addItem("Default", "palette", "menu-item-default", () => { this.toDefault(view) });
|
||||
this.addItem("Remove", "eraser", "menu-item-remove-highlight", () => { this.removeColor(view) });
|
||||
this.showMenu(view);
|
||||
}
|
||||
return btn;
|
||||
}
|
||||
adjustPos(length: number) {
|
||||
this.colorTag.to += length;
|
||||
this.close.from += length;
|
||||
this.close.to += length;
|
||||
}
|
||||
changeColor(view: EditorView, color: string) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: this.colorTag.from,
|
||||
to: this.colorTag.to,
|
||||
insert: color != "default" ? `{${color}}` : ""
|
||||
}
|
||||
});
|
||||
let differ = color == "default" ?
|
||||
this.colorTag.from - this.colorTag.to :
|
||||
color.length + 2 - (this.colorTag.to - this.colorTag.from);
|
||||
this.adjustPos(differ);
|
||||
}
|
||||
showMenu(view: EditorView) {
|
||||
let charPos = this.tagRange.from,
|
||||
menu = this.menu;
|
||||
view.requestMeasure({
|
||||
read(view) {
|
||||
let app = view.state.facet(appFacet.reader),
|
||||
canvasNodeCoords = getActiveCanvasNodeCoords(app),
|
||||
charCoords = view.coordsForChar(charPos);
|
||||
return { charCoords, canvasNodeCoords };
|
||||
},
|
||||
write(measure) {
|
||||
let { charCoords, canvasNodeCoords } = measure;
|
||||
if (charCoords) {
|
||||
let menuCoords = { x: charCoords.left, y: charCoords.bottom };
|
||||
if (canvasNodeCoords) {
|
||||
menuCoords.x += canvasNodeCoords.x;
|
||||
menuCoords.y += canvasNodeCoords.y;
|
||||
}
|
||||
menu.showAtPosition(menuCoords);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
adjustPos(differ: number) {
|
||||
this.tagRange.to += differ;
|
||||
this.closeRange.from += differ;
|
||||
this.closeRange.to += differ;
|
||||
}
|
||||
changeColor(view: EditorView, color: string) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: this.tagRange.from,
|
||||
to: this.tagRange.to,
|
||||
insert: `{${color}}`
|
||||
}
|
||||
});
|
||||
let differ = color.length + 2 - (this.tagRange.to - this.tagRange.from);
|
||||
this.adjustPos(differ);
|
||||
}
|
||||
removeColor(view: EditorView) {
|
||||
view.dispatch({
|
||||
changes: [{
|
||||
from: this.openRange.from,
|
||||
to: this.tagRange.to,
|
||||
insert: ""
|
||||
}, {
|
||||
from: this.closeRange.from,
|
||||
to: this.closeRange.to,
|
||||
insert: ""
|
||||
}]
|
||||
});
|
||||
}
|
||||
toDefault(view: EditorView) {
|
||||
view.dispatch({
|
||||
changes: {
|
||||
from: this.tagRange.from,
|
||||
to: this.tagRange.to,
|
||||
insert: ""
|
||||
}
|
||||
});
|
||||
let tagLen = this.tagLen;
|
||||
this.adjustPos(-tagLen);
|
||||
}
|
||||
addItem(title: string, icon: string, cls: string, callback: (evt: MouseEvent | KeyboardEvent) => unknown) {
|
||||
this.menu.addItem(item => {
|
||||
item.setTitle(title);
|
||||
item.setIcon(icon);
|
||||
item.dom.addClass(cls);
|
||||
item.onClick(callback);
|
||||
});
|
||||
}
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
static of(color: string, open: CharPos, close: CharPos) {
|
||||
return Decoration.widget({ widget: new ColorButton(color, open, close) }).range(open.to);
|
||||
}
|
||||
static of(hlToken: Token) {
|
||||
let btnOffset = hlToken.from + hlToken.openLen;
|
||||
return Decoration
|
||||
.widget({ widget: new ColorButton(hlToken), side: 1 })
|
||||
.range(btnOffset);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,23 @@
|
|||
import { Decoration, WidgetType } from "@codemirror/view";
|
||||
import { Token } from "src/types";
|
||||
|
||||
export class HiddenWidget extends WidgetType {
|
||||
replaced: Decoration;
|
||||
constructor(replaced: Decoration) {
|
||||
token: Token;
|
||||
constructor(replaced: Token) {
|
||||
super();
|
||||
this.replaced = replaced;
|
||||
this.token = replaced;
|
||||
}
|
||||
eq(other: HiddenWidget) {
|
||||
return other.replaced == this.replaced;
|
||||
return other.token == this.token;
|
||||
}
|
||||
toDOM(): HTMLElement {
|
||||
return document.createElement("span");
|
||||
}
|
||||
static of(from: number, to: number, replaced: Decoration) {
|
||||
static of(from: number, to: number, token: Token, isBlock = false) {
|
||||
return Decoration.replace({
|
||||
widget: new HiddenWidget(replaced)
|
||||
widget: new HiddenWidget(token),
|
||||
block: isBlock,
|
||||
inclusiveEnd: false
|
||||
}).range(from, to);
|
||||
}
|
||||
}
|
||||
20
src/editor-mode/decorator/widgets/LineBreak.ts
Normal file
20
src/editor-mode/decorator/widgets/LineBreak.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { Decoration, WidgetType } from "@codemirror/view";
|
||||
|
||||
export class LineBreak extends WidgetType {
|
||||
offset: number;
|
||||
constructor(offset: number) {
|
||||
super();
|
||||
this.offset = offset;
|
||||
}
|
||||
eq(other: LineBreak) {
|
||||
return other.offset === this.offset;
|
||||
}
|
||||
toDOM(): HTMLElement {
|
||||
return document.createElement("br");
|
||||
}
|
||||
static of(offset: number) {
|
||||
return Decoration.replace({
|
||||
widget: new LineBreak(offset),
|
||||
}).range(offset - 1, offset);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./HiddenWidget";
|
||||
export * from "./ColorButton";
|
||||
export * from "./ColorButton";
|
||||
export * from "./LineBreak";
|
||||
|
|
@ -1,6 +1,19 @@
|
|||
import { ViewPlugin } from "@codemirror/view";
|
||||
import { ExtendedSyntax } from "src/editor-mode/decorator/builder";
|
||||
import { Extension, RangeSet } from "@codemirror/state";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import { builderField, decoSetField, parserField, selectionObserverField } from "src/editor-mode/state-fields";
|
||||
import { BuilderPlugin } from "src/editor-mode/view-plugin";
|
||||
|
||||
export const editorExtendedSyntax = ViewPlugin.fromClass(ExtendedSyntax, {
|
||||
decorations: value => value.combinedSet,
|
||||
});
|
||||
const builderPlugin = ViewPlugin.fromClass(BuilderPlugin);
|
||||
|
||||
export const editorExtendedSyntax: Extension = [
|
||||
parserField,
|
||||
selectionObserverField,
|
||||
builderField,
|
||||
decoSetField,
|
||||
builderPlugin,
|
||||
EditorView.outerDecorations.of(view => view.plugin(builderPlugin)?.builder.holder.inlineSet ?? RangeSet.empty),
|
||||
EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.blockSet ?? RangeSet.empty),
|
||||
EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.inlineOmittedSet ?? RangeSet.empty),
|
||||
EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.colorBtnSet ?? RangeSet.empty),
|
||||
EditorView.decorations.of(view => view.plugin(builderPlugin)?.builder.holder.revealedSpoilerSet ?? RangeSet.empty)
|
||||
];
|
||||
13
src/editor-mode/utils/checkRefreshed.ts
Normal file
13
src/editor-mode/utils/checkRefreshed.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Transaction } from "@codemirror/state";
|
||||
import { refresherAnnot } from "src/editor-mode/annotations";
|
||||
|
||||
export function checkRefreshed(transactions: Transaction[] | readonly Transaction[] | Transaction) {
|
||||
if (transactions instanceof Array) {
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
if (transactions[i].annotation(refresherAnnot)) { return true }
|
||||
}
|
||||
} else {
|
||||
if (transactions.annotation(refresherAnnot)) { return true }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
9
src/editor-mode/utils/getActiveCanvasEditor.ts
Normal file
9
src/editor-mode/utils/getActiveCanvasEditor.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { App } from "obsidian";
|
||||
import { isCanvas } from "src/editor-mode/utils";
|
||||
|
||||
export function getActiveCanvasEditor(app: App) {
|
||||
if (isCanvas(app)) {
|
||||
return app.workspace.activeEditor;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
11
src/editor-mode/utils/getActiveCanvasNodeCoords.ts
Normal file
11
src/editor-mode/utils/getActiveCanvasNodeCoords.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { App, MarkdownView } from "obsidian";
|
||||
import { getActiveCanvasEditor } from "./getActiveCanvasEditor";
|
||||
|
||||
export function getActiveCanvasNodeCoords(app: App) {
|
||||
let canvasEditor = getActiveCanvasEditor(app),
|
||||
containerEl = (canvasEditor as MarkdownView)?.containerEl;
|
||||
if (containerEl) {
|
||||
return containerEl.getBoundingClientRect();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
4
src/editor-mode/utils/index.ts
Normal file
4
src/editor-mode/utils/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./checkRefreshed";
|
||||
export * from "./getActiveCanvasNodeCoords";
|
||||
export * from "./getActiveCanvasEditor";
|
||||
export * from "./isCanvas";
|
||||
5
src/editor-mode/utils/isCanvas.ts
Normal file
5
src/editor-mode/utils/isCanvas.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
export function isCanvas(app: App) {
|
||||
return app.workspace.getMostRecentLeaf()?.view.getViewType() == "canvas";
|
||||
}
|
||||
15
src/editor-mode/view-plugin/BuilderPlugin.ts
Normal file
15
src/editor-mode/view-plugin/BuilderPlugin.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { EditorView, PluginValue, ViewUpdate } from "@codemirror/view";
|
||||
import { DecorationBuilder } from "src/editor-mode/decorator/builder";
|
||||
import { builderField } from "src/editor-mode/state-fields";
|
||||
|
||||
export class BuilderPlugin implements PluginValue {
|
||||
builder: DecorationBuilder;
|
||||
constructor(view: EditorView) {
|
||||
let state = view.state;
|
||||
this.builder = state.field(builderField);
|
||||
this.builder.onViewInit(view);
|
||||
}
|
||||
update(update: ViewUpdate) {
|
||||
this.builder.onViewUpdate(update);
|
||||
}
|
||||
}
|
||||
1
src/editor-mode/view-plugin/index.ts
Normal file
1
src/editor-mode/view-plugin/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./BuilderPlugin";
|
||||
Loading…
Reference in a new issue