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

120 lines
5.0 KiB
TypeScript

import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import type { Element, MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
import { Content } from '../content.util';
function fuzzyMatch(text: string, search: string): number {
const textLower = text.toLowerCase().normalize('NFC');
const searchLower = search.toLowerCase().normalize('NFC');
let searchIndex = 0;
let score = 0;
for (let i = 0; i < textLower.length && searchIndex < searchLower.length; i++) {
if (textLower[i] === searchLower[searchIndex]) {
score += 1;
if (i === searchIndex) score += 2; // Bonus for sequential match
searchIndex++;
}
}
return searchIndex === searchLower.length ? score : 0;
}
export const wikilink: MarkdownConfig = {
defineNodes: [
'Wikilink',
'WikilinkMeta',
'WikilinkHref',
'WikilinkTitle',
],
parseInline: [{
name: 'Wikilink',
before: 'Link',
parse(cx, next, pos)
{
// 91 == '['
if (next !== 91 || cx.slice(pos, pos + 1).charCodeAt(0) !== 91) return -1;
const match = /!?\[\[([^\[\]\|\#]+)?(#+[^\[\]\|\#]+)?(\|[^\[\]\|\#]+)?\]\]/.exec(cx.slice(pos, cx.end));
if(!match) return -1;
const start = pos, children: Element[] = [], end = start + match[0].length;
children.push(cx.elt('WikilinkMeta', start, start + 2));
if(match[1] && !match[2] && !match[3]) //Link only
{
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
}
else if(!match[1] && match[2] && match[3]) //Hash and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[2].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[2].length, start + 2 + match[2].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[2].length + 1, start + 2 + match[2].length + match[3].length));
}
else if(!match[1] && !match[2] && match[3]) //Hash only
{
children.push(cx.elt('WikilinkTitle', start + 2, end - 2));
}
else if(match[1] && match[2] && !match[3]) //Link and hash
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length, start + 2 + match[1].length + match[2].length));
}
else if(match[1] && !match[2] && match[3]) //Link and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length, start + 2 + match[1].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + 1, start + 2 + match[1].length + match[3].length));
}
else if(match[1] && match[2] && match[3]) //Link, hash and title
{
children.push(cx.elt('WikilinkHref', start + 2, start + 2 + match[1].length + match[2].length));
children.push(cx.elt('WikilinkMeta', start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1));
children.push(cx.elt('WikilinkTitle', start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length));
}
children.push(cx.elt('WikilinkMeta', end - 2, end));
return cx.addElement(cx.elt('Wikilink', start, end, children));
},
}],
props: [
styleTags({
'Wikilink': tags.special(tags.content),
'WikilinkMeta': tags.meta,
'WikilinkHref': tags.link,
'WikilinkTitle': tags.special(tags.link),
})
]
};
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();
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);
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}]]`
},
selection: { anchor: word.from + e.path.length + 2 }
});
},
type: 'text'
})),
validFor: /^[\[\w\s-]*$/,
}
};