obsidian-visualiser/shared/grammar/callout.extension.ts

127 lines
4.8 KiB
TypeScript

import type { MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
import type { Decoration, EditorView, KeyBinding } from '@codemirror/view';
import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state';
import { BlockDecorator } from '../editor.util';
export const callout: MarkdownConfig = {
defineNodes: [
'CalloutBlock',
'CalloutMarker',
'CalloutMark',
'CalloutType',
'CalloutTitle',
'CalloutLine',
'CalloutContent',
],
parseBlock: [{
name: 'Callout',
before: 'Blockquote',
parse(cx, line) {
const match = /^>\s*\[!(\w+)\](?:\s+(.*))?/.exec(line.text);
if (!match || !match[1]) return false; //No match
const start = cx.lineStart, children = [];
let continued = false;
const quoteEnd = start + line.text.indexOf('[!');
const typeStart = quoteEnd + 2;
const typeEnd = typeStart + match[1].length;
const bracketEnd = typeEnd + 1;
children.push(cx.elt('CalloutMarker', start, bracketEnd, [ cx.elt('CalloutMark', start, quoteEnd), cx.elt('CalloutType', typeStart, typeEnd) ]));
if(match[2]) children.push(cx.elt('CalloutTitle', bracketEnd + 1, start + line.text.length));
while ((continued = cx.nextLine()) && line.text.startsWith('>'))
{
const pos = line.text.substring(1).search(/\S/) + 1;
children.push(cx.elt('CalloutLine', cx.lineStart, cx.lineStart + line.text.length, [
cx.elt('CalloutMark', cx.lineStart, cx.lineStart + pos),
cx.elt('CalloutContent', cx.lineStart + pos, cx.lineStart + line.text.length),
]));
}
cx.addElement(cx.elt('Blockquote', start, continued ? cx.lineStart - 1 : cx.lineStart, [cx.elt('CalloutBlock', start, continued ? cx.lineStart - 1 : cx.lineStart, children)]));
return true;
}
}],
props: [
styleTags({
'CalloutBlock': tags.special(tags.quote),
'CalloutMarker': tags.meta,
'CalloutMark': tags.meta,
'CalloutType': tags.keyword,
'CalloutTitle': tags.heading,
'CalloutLine': tags.content,
'CalloutContent': tags.content,
})
]
};
//Use the BlockDecorator to fetch every built block widgets and try to check if the future line should be positionned inside a block
function fetchBlockLine(state: EditorState, range: SelectionRange, forward: boolean): SelectionRange | null
{
const start = state.doc.lineAt(range.head), next = start.number + (forward ? 1 : -1);
if (next < 1 || next > state.doc.lines)
return null;
const nextLine = state.doc.line(next);
let matched = !0, current: Decoration | null = null;
state.field(BlockDecorator, false)?.between(nextLine.from, nextLine.to, (from, to, value) => {
if (value.spec.block)
{
if(current || from > nextLine.from || to < nextLine.to)
return (matched = false);
else
{
if(!current) current = value;
return;
}
}
});
if (!matched || !current)
return null;
if (!(current as Decoration).spec.block)
return null;
const position = range.head - start.from;
return EditorSelection.cursor(nextLine.from + Math.min(nextLine.length, position), range.assoc)
}
function moveCursor(view: EditorView, select: boolean, forward: boolean)
{
const state = view.state, selection = state.selection;
const range = EditorSelection.create(selection.ranges.map((range) => {
if(!select && !range.empty) //If I have already selected something and I stop holding Shift, I just deselect
return EditorSelection.cursor(forward ? range.to : range.from);
let target = view.moveVertically(range, forward);
const blockTarget = fetchBlockLine(state, range, forward);
target = blockTarget && Math.abs(target.head - range.head) > Math.abs(blockTarget.head - range.head) ? blockTarget : target.head != range.head ? target : view.moveToLineBoundary(range, forward);
return select ? EditorSelection.range(range.anchor, target.head, target.goalColumn) : target;
}, selection.mainIndex));
view.dispatch({
userEvent: 'select',
selection: range,
scrollIntoView: true,
});
return true;
}
export const calloutKeymap: KeyBinding[] = [
{
key: "ArrowUp",
run: (view) => moveCursor(view, false, false),
shift: (view) => moveCursor(view, true, false),
},
{
key: "ArrowDown",
run: (view) => moveCursor(view, false, true),
shift: (view) => moveCursor(view, true, true),
}
]