Mass updates

This commit is contained in:
2026-01-05 11:33:32 +01:00
parent 32b6cf4af7
commit 04534b2530
36 changed files with 1886 additions and 12036 deletions

View File

@@ -1,5 +1,8 @@
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: [
@@ -19,6 +22,7 @@ export const callout: MarkdownConfig = {
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;
@@ -29,16 +33,16 @@ export const callout: MarkdownConfig = {
if(match[2]) children.push(cx.elt('CalloutTitle', bracketEnd + 1, start + line.text.length));
while (cx.nextLine() && line.text.startsWith('>'))
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('CalloutBlock', start, cx.lineStart - 1, children));
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;
}
@@ -55,3 +59,69 @@ export const callout: MarkdownConfig = {
})
]
};
//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),
}
]

View File

@@ -9,19 +9,20 @@ export const tag: MarkdownConfig = {
],
parseInline: [{
name: 'Tag',
after: 'Wikilink',
parse(cx, next, pos)
{
//35 == '#'
if (cx.slice(pos, pos + 1).charCodeAt(0) !== 35 || String.fromCharCode(next).trim() === '') return -1;
if(pos !== 0 && cx.slice(pos - 1, pos).match(/\w/)) return -1;
const end = cx.slice(pos, cx.end).search(/\s/);
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : pos + end, [ cx.elt('TagMeta', pos, pos + 1) ]));
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : pos + end));
},
}],
props: [
styleTags({
'Tag': tagTag,
'TagMeta': tags.meta,
})
]
};

View File

@@ -1,7 +1,9 @@
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import type { EditorView } from '@codemirror/view';
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import type { Element, MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
import { Content } from '../content.util';
import { selectAll } from 'hast-util-select';
function fuzzyMatch(text: string, search: string): number {
const textLower = text.toLowerCase().normalize('NFC');
@@ -23,10 +25,10 @@ function fuzzyMatch(text: string, search: string): number {
export const wikilink: MarkdownConfig = {
defineNodes: [
'Wikilink',
'WikilinkMeta',
'WikilinkHref',
'WikilinkTitle',
'Wikilink', //Whole group
'WikilinkMeta', //Meta characters ([[ & ]])
'WikilinkHref', //Link
'WikilinkTitle', //Title (always visible)
],
parseInline: [{
name: 'Wikilink',
@@ -41,6 +43,8 @@ export const wikilink: MarkdownConfig = {
const start = pos, children: Element[] = [], end = start + match[0].length;
if(match[0] === '[[]]') return end;
children.push(cx.elt('WikilinkMeta', start, start + 2));
if(match[1] && !match[2] && !match[3]) //Link only
@@ -89,32 +93,69 @@ export const wikilink: MarkdownConfig = {
})
]
};
export const autocompletion = (context: CompletionContext): CompletionResult | null => {
const word = context.matchBefore(/\[\[[\w\s-]*/);
if (!word || (word.from === word.to && !context.explicit))
return null;
const searchTerm = word.text.slice(2).toLowerCase();
export const autocompletion = (context: CompletionContext): CompletionResult | Promise<CompletionResult | null> | null => {
const header = context.matchBefore(/\[\[[^\[\]\|\#]+#[^\[\]\|\#]*/);
if(!header || (header.from === header.to && !context.explicit))
{
const word = context.matchBefore(/\[\[[\w\s-]*/);
if (!word || (word.from === word.to && !context.explicit)) return null;
const options = Object.values(Content.files).filter(e => e.type !== 'folder').map(e => ({ ...e, score: fuzzyMatch(e.title, searchTerm) })).filter(e => e.score > 0).sort((a, b) => b.score - a.score).slice(0, 50);
const options = Object.values(Content.files).filter(e => e.type !== 'folder');
return {
from: word.from + 2,
options: options.map(e => ({
label: e.title,
detail: e.path,
apply: (view, completion, from, to) => {
view.dispatch({
changes: {
from: word.from,
to: word.to,
insert: `[[${e.path}]]`
return {
from: word.from + 2,
options: options.map(e => ({
label: e.title,
detail: e.path,
apply: (view, completion, from, to) => {
const closed = view.state.sliceDoc(from, to + 2).endsWith(']]');
view.dispatch({
changes: {
from: from - 2,
to: to,
insert: closed ? `[[${completion.detail}` : `[[${completion.detail}]]`
},
selection: { anchor: from + (completion.detail?.length ?? 0) }
});
},
type: 'text',
})),
commitCharacters: ['#', '|'],
validFor: /^[\[\w\s-]*$/,
}
}
else
{
const path = header.text.match(/^\[\[([^\[\]\|\#]+)#/);
if(!path || !path[1]) return null;
const content = Content.getFromPath(path[1]);
if(!content || content.type !== 'markdown') return null;
return (async () => {
const headers = selectAll('h1, h2, h3, h4, h5, h6', await useMarkdown().parse((await Content.getContent(content.id))!.content as string));
return {
from: header.from + path[1]!.length + 3,
options: headers.map(e => ({
label: e.properties.id as string,
apply: (view, completion, from, to) => {
const closed = view.state.sliceDoc(from, to + 2).endsWith(']]');
view.dispatch({
changes: {
from: from,
to: to,
insert: closed ? `${completion.label}` : `${completion.label}]]`
},
selection: { anchor: from + (completion.label?.length ?? 0) }
});
},
selection: { anchor: word.from + e.path.length + 2 }
});
},
type: 'text'
})),
validFor: /^[\[\w\s-]*$/,
type: 'text',
})),
commitCharacters: ['#', '|'],
validFor: new RegExp(`\\[\\[${path[1]}#[^\[\]\|\#]*`),
};
})();
}
};