New CM6 live edition components and floating cache and persistance.

This commit is contained in:
Clément Pons 2025-10-28 16:23:45 +01:00
parent b9970ccdf8
commit ab36eec4de
12 changed files with 383 additions and 65 deletions

View File

@ -33,6 +33,13 @@ onBeforeMount(() => {
</script>
<style>
iconify-icon
{
display: inline-block;
width: attr(width px, 1rem);
height: attr(height px, 1rem);
box-sizing: content-box;
}
.ToastRoot[data-type='error'] {
@apply border-light-red;
@apply dark:border-dark-red;

BIN
db.sqlite

Binary file not shown.

View File

@ -61,8 +61,6 @@ import { link } from '#shared/components.util';
const open = ref(false);
const { loggedIn, user } = useUserSession();
await fetch(false);
const route = useRouter().currentRoute;
const path = computed(() => route.value.params.path ? decodeURIComponent(unifySlug(route.value.params.path)) : undefined);

View File

@ -2,31 +2,43 @@
<Head>
<Title>d[any] - Modification</Title>
</Head>
<div class="flex flex-1 flex-col xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3 overflow-hidden">
<div class="z-30 flex w-full items-center justify-between border-b border-light-35 dark:border-dark-35 px-2">
<div class="flex items-center px-2 gap-4">
<!-- <CollapsibleTrigger asChild>
<Button icon class="!bg-transparent group md:hidden">
<Icon class="group-data-[state=open]:hidden" icon="radix-icons:hamburger-menu" />
<Icon class="group-data-[state=closed]:hidden" icon="radix-icons:cross-1" />
</Button>
</CollapsibleTrigger> -->
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-opacity-70 m-2 flex items-center gap-4" aria-label="Accueil" :to="{ path: '/', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl max-md:hidden">d[any]</span>
</NuxtLink>
</div>
<div class="flex items-center px-2 gap-4">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
<div class="flex flex-row w-full max-w-full h-full max-h-full xl:-mx-12 xl:-my-8 lg:-mx-8 lg:-my-6 -mx-6 -my-3" style="--sidebar-width: 300px">
<div class="bg-light-0 dark:bg-dark-0 w-[var(--sidebar-width)] border-r border-light-30 dark:border-dark-30 flex flex-col gap-2">
<NuxtLink class="flex flex-row items-center justify-center group gap-2 my-2" aria-label="Accueil" :to="{ name: 'index', force: true }">
<Avatar src="/logo.dark.svg" class="dark:block hidden" />
<Avatar src="/logo.light.svg" class="block dark:hidden" />
<span class="text-xl font-semibold group-hover:text-light-70 dark:group-hover:text-dark-70">d[any]</span>
</NuxtLink>
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
<div class="flex flex-col my-4 items-center justify-center gap-1 text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<NuxtLink class="hover:underline" :to="{ name: 'usage' }">Conditions d'utilisations</NuxtLink>
Copyright Peaceultime - 2025
</div>
</div>
<div class="flex flex-1 flex-row relative h-screen overflow-hidden">
<div class="bg-light-0 dark:bg-dark-0 z-40 w-screen md:w-[18rem] border-r border-light-30 dark:border-dark-30 flex flex-col justify-between my-2 max-md:data-[state=closed]:hidden">
<div class="flex-1 px-2 max-w-full max-h-full overflow-y-auto overflow-x-hidden" ref="tree"></div>
<div class="xl:px-12 px-6 pt-4 pb-2 text-center text-xs text-light-60 dark:text-dark-60">
<NuxtLink class="hover:underline italic" :to="{ name: 'roadmap' }">Roadmap</NuxtLink> - <NuxtLink class="hover:underline italic" :to="{ name: 'legal' }">Mentions légales</NuxtLink>
<p>Copyright Peaceultime - 2025</p>
<div class="flex flex-col flex-1 h-full w-[calc(100vw-var(--sidebar-width))]">
<div class="flex flex-row border-b border-light-30 dark:border-dark-30 justify-between px-8">
<div class="flex flex-row gap-16 items-center">
<NavigationMenuRoot class="relative">
<NavigationMenuList class="flex items-center gap-8 max-md:hidden">
<NavigationMenuItem>
<NavigationMenuTrigger>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Personnages</span><Icon icon="radix-icons:caret-down" /></NuxtLink>
</NavigationMenuTrigger>
<NavigationMenuContent class="absolute top-0 w-full sm:w-auto bg-light-0 dark:bg-dark-0 border border-light-30 dark:border-dark-30 py-2 z-20 flex flex-col">
<NuxtLink :href="{ name: 'character-list' }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Personnages publics</span></NuxtLink>
<NuxtLink :href="{ name: 'character-id-edit', params: { id: 'new' } }" class="hover:bg-light-30 dark:hover:bg-dark-30 px-4 py-2 select-none" active-class="!text-accent-blue"><span class="flex-1 truncate">Nouveau personnage</span></NuxtLink>
</NavigationMenuContent>
</NavigationMenuItem>
</NavigationMenuList>
<div class="absolute top-full left-0 flex w-full justify-center">
<NavigationMenuViewport class="h-[var(--radix-navigation-menu-viewport-height)] w-full origin-[top_center] flex justify-center overflow-hidden sm:w-[var(--radix-navigation-menu-viewport-width)]" />
</div>
</NavigationMenuRoot>
<NuxtLink :href="{ name: 'character' }" class="flex flex-row gap-2 items-center border-b-2 border-transparent hover:border-accent-blue py-4 select-none" active-class="!text-accent-blue"><span class="px-3 flex-1 truncate">Campagnes</span></NuxtLink>
</div>
<div class="flex flex-row gap-16 items-center">
<NuxtLink class="text-light-100 dark:text-dark-100 hover:text-light-70 dark:hover:text-dark-70" :to="{ name: 'user-login' }">{{ user!.username }}</NuxtLink>
</div>
</div>
<div class="flex flex-1 flex-row max-h-full overflow-hidden" ref="container"></div>
@ -40,6 +52,7 @@ import { button, loading } from '#shared/components.util';
import { dom, icon } from '#shared/dom.util';
import { modal, tooltip } from '#shared/floating.util';
import { Toaster } from '#shared/components.util';
import { Icon } from '@iconify/vue/dist/iconify.js';
definePageMeta({
rights: ['admin', 'editor'],

View File

@ -1,6 +1,6 @@
import type { RouteLocationAsRelativeTyped, RouteLocationRaw, RouteMapGeneric } from "vue-router";
import { type NodeProperties, type Class, type NodeChildren, dom, mergeClasses, text, div, icon, type Node } from "./dom.util";
import { contextmenu, followermenu, popper, tooltip, type FloatState } from "./floating.util";
import { contextmenu, followermenu, minimizeBox, popper, teleport, tooltip, type FloatState } from "./floating.util";
import { clamp } from "./general.util";
import { Tree } from "./tree";
import type { Placement } from "@floating-ui/dom";
@ -526,16 +526,16 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content:
})
return container as HTMLDivElement & { refresh: () => void };
}
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { href?: RouteLocationRaw, class?: Class, style?: Record<string, string | undefined | boolean | number> | string, position?: Placement, pinned?: boolean, minimizable?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (state: FloatState) => boolean, onhide?: (state: FloatState) => boolean }, title?: string })
{
let viewport = document.getElementById('mainContainer') ?? undefined;
let diffX, diffY;
let minimizeBox: DOMRect, minimized = false;
let minimizeRect: DOMRect, minimized = false;
const events: { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap>, onshow?: (this: HTMLElement, state: FloatState) => boolean, onhide?: (this: HTMLElement, state: FloatState) => boolean } = Object.assign({
show: ['mouseenter', 'mousemove', 'focus'],
hide: ['mouseleave', 'blur'],
}, settings?.events ?? {});
} as { show: Array<keyof HTMLElementEventMap>, hide: Array<keyof HTMLElementEventMap> }, settings?.events ?? {});
if(settings?.pinned)
{
@ -631,23 +631,30 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
floating.content.toggleAttribute('data-minimized', minimized);
if(minimized)
{
minimizeBox = floating.content.getBoundingClientRect();
minimizeRect = floating.content.getBoundingClientRect();
Object.assign(floating.content.style, {
left: `0px`,
top: `initial`,
bottom: `0px`,
width: `150px`,
height: `21px`,
position: 'initial',
});
floating.content.style.setProperty('top', null);
floating.content.style.setProperty('left', null);
floating.content.style.setProperty('bottom', null);
floating.content.style.setProperty('right', null);
minimizeBox.appendChild(floating.content);
}
else
{
Object.assign(floating.content.style, {
left: `${minimizeBox.left}px`,
top: `${minimizeBox.top}px`,
width: `${minimizeBox.width}px`,
height: `${minimizeBox.height}px`,
left: `${minimizeRect.left}px`,
top: `${minimizeRect.top}px`,
width: `${minimizeRect.width}px`,
height: `${minimizeRect.height}px`,
});
floating.content.style.setProperty('position', null);
teleport.appendChild(floating.content);
}
};
@ -657,10 +664,11 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
offset: 12,
cover: settings?.cover,
placement: settings?.position,
style: settings?.style,
class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full',
content: () => [
settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row items-center border-b border-light-35 dark:border-dark-35', [
dom('span', { class: 'flex-1 w-full h-full cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
dom('span', { class: 'flex-1 w-full h-5 cursor-move group-data-[minimized]:cursor-default text-xs px-2', listeners: { mousedown: dragstart }, text: (settings?.title?.substring(0, 1)?.toUpperCase() ?? '') + (settings?.title?.substring(1)?.toLowerCase() ?? '') }),
settings?.title ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { click: minimize } }, [icon('radix-icons:minus', { width: 12, height: 12, class: 'p-1' })]), text('Réduire'), 'top') : undefined,
settings?.href ? tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { ((e.ctrlKey || e.button === 1) ? window.open : useRouter().push)(useRouter().resolve(settings.href!).href); floating.hide(); } } }, [icon('radix-icons:external-link', { width: 12, height: 12, class: 'p-1' })]), 'Ouvrir', 'top') : undefined,
tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => {
@ -669,10 +677,10 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N
floating.content.toggleAttribute('data-minimized', false);
minimized && Object.assign(floating.content.style, {
left: `${minimizeBox.left}px`,
top: `${minimizeBox.top}px`,
width: `${minimizeBox.width}px`,
height: `${minimizeBox.height}px`,
left: `${minimizeRect.left}px`,
top: `${minimizeRect.top}px`,
width: `${minimizeRect.width}px`,
height: `${minimizeRect.height}px`,
});
minimized = false;
} } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'top') ]) : undefined,

View File

@ -1,14 +1,18 @@
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, type DecorationSet } from '@codemirror/view';
import { Annotation, EditorState, SelectionRange, type Range } from '@codemirror/state';
import { crosshairCursor, Decoration, dropCursor, EditorView, keymap, ViewPlugin, ViewUpdate, WidgetType, type DecorationSet } from '@codemirror/view';
import { Annotation, EditorState, SelectionRange, StateField, type Range } from '@codemirror/state';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, foldKeymap, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { lintKeymap } from '@codemirror/lint';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree } from '@lezer/common';
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
import { tags } from '@lezer/highlight';
import { dom } from './dom.util';
import { dom } from '#shared/dom.util';
import { callout as calloutExtension } from '#shared/grammar/callout.extension';
import { wikilink as wikilinkExtension } from '#shared/grammar/wikilink.extension';
import { renderMarkdown } from '#shared/markdown.util';
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
import { tagTag, tag as tagExtension } from './grammar/tag.extension';
const External = Annotation.define<boolean>();
const Hidden = Decoration.mark({ class: 'hidden' });
@ -28,15 +32,88 @@ const highlight = HighlightStyle.define([
{ tag: tags.heading2, class: 'text-4xl pt-4 pb-2 ps-1 leading-loose after:hidden' },
{ tag: tags.heading3, class: 'text-2xl font-bold pt-1 after:hidden' },
{ tag: tags.heading4, class: 'text-xl font-semibold pt-1 after:hidden variant-cap' },
{ tag: tags.meta, color: "#404740" },
{ tag: tags.link, textDecoration: "underline" },
{ tag: tags.meta, class: 'text-light-60 dark:text-dark-60' },
{ tag: tags.link, class: 'text-accent-blue hover:underline' },
{ tag: tags.special(tags.link), class: 'text-accent-blue font-semibold' },
{ tag: tags.heading, textDecoration: "underline", fontWeight: "bold" },
{ tag: tags.emphasis, fontStyle: "italic" },
{ tag: tags.strong, fontWeight: "bold" },
{ tag: tags.strikethrough, textDecoration: "line-through" },
{ tag: tags.keyword, color: "#708" },
{ tag: tags.keyword, class: "text-accent-blue" },
{ tag: tags.monospace, class: "border border-light-35 dark:border-dark-35 px-2 py-px rounded-sm bg-light-20 dark:bg-dark-20" },
{ tag: tagTag, class: "cursor-default bg-accent-blue bg-opacity-10 text-accent-blue text-sm px-1 ms-1 pb-0.5 rounded-full rounded-se-none border border-accent-blue border-opacity-30" },
]);
class CalloutWidget extends WidgetType
{
from: number;
to: number;
title: string;
type: string;
foldable?: boolean;
content: string;
contentMD: HTMLElement;
static create(node: SyntaxNodeRef, state: EditorState): CalloutWidget | undefined
{
let type = '';
let title = '';
const content: string[] = [];
let cursor = node.node.cursor();
if (!cursor.firstChild()) return undefined;
do
{
if (cursor.name === 'CalloutMarker')
{
const _cursor = cursor.node.cursor();
_cursor.lastChild();
type = state.doc.sliceString(_cursor.from, _cursor.to).toLowerCase();
}
else if (cursor.name === 'CalloutTitle')
{
title = state.doc.sliceString(cursor.from, cursor.to).trim();
}
else if (cursor.name === 'CalloutLine')
{
const _cursor = cursor.node.cursor();
_cursor.lastChild();
content.push(state.doc.sliceString(_cursor.from, _cursor.to));
}
}
while (cursor.nextSibling());
return new CalloutWidget(node.from, node.to, title || (type.substring(0, 1).toUpperCase() + type.substring(1).toLowerCase()), type, content.join('\n'));
}
constructor(from: number, to: number, title: string, type: string, content: string, foldable?: boolean)
{
super();
this.from = from;
this.to = to;
this.title = title;
this.type = type;
this.content = content;
this.foldable = foldable;
this.contentMD = renderMarkdown(useMarkdown().parseSync(content), { a, blockquote, tag, callout: callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th });
}
override eq(other: CalloutWidget)
{
return this.from === other.from && this.to === other.to;
}
toDOM(view: EditorView)
{
return dom('div', { class: 'flex cm-line', listeners: { click: e => view.dispatch({ selection: { anchor: this.from, head: this.to } }) } }, [prose('blockquote', callout, [ this.contentMD ], { title: this.title, type: this.type, fold: this.foldable, class: '!m-px ' }) as HTMLElement | undefined]);
}
override ignoreEvent(event: Event)
{
return false;
}
}
class Decorator
{
static hiddenNodes: string[] = [
@ -46,11 +123,15 @@ class Decorator
'CodeMark',
'CodeInfo',
'URL',
'CalloutMark',
'WikilinkMeta',
'WikilinkHref',
'TagMeta'
]
decorations: DecorationSet;
constructor(view: EditorView)
{
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, []), true);
this.decorations = Decoration.set(this.iterate(syntaxTree(view.state), view.visibleRanges, [], view.state), true);
}
update(update: ViewUpdate)
{
@ -59,14 +140,14 @@ class Decorator
this.decorations = this.decorations.update({
filter: (f, t, v) => false,
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges),
add: this.iterate(syntaxTree(update.state), update.view.visibleRanges, update.state.selection.ranges, update.state),
sort: true,
});
}
iterate(tree: Tree, visible: readonly {
from: number;
to: number;
}[], selection: readonly SelectionRange[]): Range<Decoration>[]
}[], selection: readonly SelectionRange[], state: EditorState): Range<Decoration>[]
{
const decorations: Range<Decoration>[] = [];
@ -74,7 +155,7 @@ class Decorator
tree.iterate({
from, to, mode: IterMode.IgnoreMounts,
enter: node => {
if(node.node.parent && selection.some(e => intersects(e, node.node.parent!)))
if(node.node.parent && node.node.parent.name !== 'Document' && selection.some(e => intersects(e, node.node.parent!)))
return true;
else if(node.name === 'HeaderMark')
@ -97,20 +178,56 @@ class Decorator
return decorations;
}
}
function blockIterate(tree: Tree, state: EditorState): Range<Decoration>[]
{
const decorations: Range<Decoration>[] = [];
const selection = state.selection.ranges;
tree.iterate({
mode: IterMode.IgnoreMounts,
enter: node => {
if(selection.some(e => intersects(e, node)))
return true;
else if(node.name === 'CalloutBlock')
return decorations.push(Decoration.replace({ widget: CalloutWidget.create(node, state), block: true, }).range(node.from, node.to)), false;
return true;
},
});
return decorations;
}
const BlockDecorator = StateField.define<DecorationSet>({
create(state)
{
return Decoration.set(blockIterate(syntaxTree(state), state), true);
},
update(decorations, transaction)
{
if(transaction.docChanged || transaction.selection)
return Decoration.set(blockIterate(syntaxTree(transaction.state), transaction.state), true);
return decorations.map(transaction.changes);
},
provide: f => EditorView.decorations.from(f),
})
export class MarkdownEditor
{
private static _singleton: MarkdownEditor;
private view: EditorView;
private viewer: 'read' | 'live' | 'edit' = 'live';
onChange?: (content: string) => void;
constructor()
{
this.view = new EditorView({
extensions: [
markdown({
base: markdownLanguage
base: markdownLanguage,
extensions: [ calloutExtension, wikilinkExtension, tagExtension ]
}),
BlockDecorator,
history(),
search(),
dropCursor(),
@ -126,9 +243,7 @@ export class MarkdownEditor
...defaultKeymap,
...searchKeymap,
...historyKeymap,
...foldKeymap,
...completionKeymap,
...lintKeymap
...completionKeymap
]),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))

View File

@ -11,6 +11,7 @@ export interface FloatingProperties
style?: Record<string, string | undefined | boolean | number> | string;
viewport?: HTMLElement;
cover?: 'width' | 'height' | 'all' | 'none';
persistant?: boolean;
}
export interface FollowerProperties extends FloatingProperties
{
@ -36,17 +37,35 @@ export interface ModalProperties
closeWhenOutside?: boolean;
onClose?: () => boolean | void;
}
type ModalInternals = {
container: HTMLElement;
content: HTMLElement;
stop: Function;
start: Function;
show: Function;
hide: Function;
persistant: boolean;
};
let teleport: HTMLDivElement;
export let teleport: HTMLDivElement, minimizeBox: HTMLDivElement, cache: ModalInternals[] = [];
export function init()
{
teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' });
minimizeBox = dom('div', { attributes: { id: 'minimize-container' }, class: 'absolute bottom-0 left-0 flex flex-row px-4 gap-4 z-40 h-[21px]' });
cache = [];
document.body.appendChild(teleport);
document.body.appendChild(minimizeBox);
useRouter().afterEach(clear);
}
function clear()
{
cache = cache.filter(e => !(!e.persistant && e.content.remove()));
}
export function popper(container: HTMLElement, properties?: PopperProperties)
{
let state: FloatState = 'hidden', manualStop = false, timeout: Timer;
let state: FloatState = 'hidden', timeout: Timer;
const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]);
const content = dom('div', { class: properties?.class, style: properties?.style });
const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]);
@ -201,7 +220,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
link(container);
link(floater);
return { container, content: floater, stop, start, show: () => {
const result = { container, content: floater, stop, start, show: () => {
if(typeof properties?.content === 'function')
properties.content = properties.content();
@ -228,11 +247,13 @@ export function popper(container: HTMLElement, properties?: PopperProperties)
floater.setAttribute('data-state', 'closed');
floater.classList.toggle('hidden', true);
manualStop = false;
floater.toggleAttribute('data-pinned', false);
state = 'hidden';
} };
cache.push({ ...result, persistant: properties?.persistant ?? false });
return result;
}
export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties)
{

View File

@ -0,0 +1,57 @@
import type { MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
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 = [];
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 (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));
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,
})
]
};

View File

@ -0,0 +1,27 @@
import type { MarkdownConfig } from '@lezer/markdown';
import { styleTags, Tag, tags } from '@lezer/highlight';
export const tagTag = Tag.define('tag');
export const tag: MarkdownConfig = {
defineNodes: [
'Tag',
'TagMeta',
],
parseInline: [{
name: 'Tag',
parse(cx, next, pos)
{
//35 == '#'
if (cx.slice(pos, pos + 1).charCodeAt(0) !== 35 || String.fromCharCode(next).trim() === '') return -1;
const end = cx.slice(pos, cx.end).search(/\s/);
return cx.addElement(cx.elt('Tag', pos, end === -1 ? cx.end : end, [ cx.elt('TagMeta', pos, pos + 1) ]));
},
}],
props: [
styleTags({
'Tag': tagTag,
'TagMeta': tags.meta,
})
]
};

View File

@ -0,0 +1,72 @@
import type { Element, MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight';
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
{
//console.log(cx.slice(pos, end), '/', cx.slice(start + 2, start + 2 + match[1].length + match[2].length), '/', cx.slice(start + 2 + match[1].length + match[2].length, start + 2 + match[1].length + match[2].length + 1), cx.slice(start + 2 + match[1].length + match[2].length + 1, start + 2 + match[1].length + match[2].length + match[3].length))
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),
})
]
};

View File

@ -64,7 +64,7 @@ export function markdownReference(content: string, filter?: string, properties?:
}
}
const el = renderMarkdown(data, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th, ...properties?.tags });
const el = renderMarkdown(data, Object.assign({}, { a, blockquote, tag, callout, h1, h2, h3, h4, h5, hr, li, small, table, td, th }, properties?.tags));
if(properties) styling(el, properties);

View File

@ -109,7 +109,7 @@ export const callout: Prose = {
} = properties;
let open = fold;
const container = dom('div', { class: 'callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
const container = dom('div', { class: ['callout group overflow-hidden my-4 p-3 ps-4 bg-blend-lighten !bg-opacity-25 border-l-4 inline-block pe-8 bg-light-blue dark:bg-dark-blue', properties?.class], attributes: { 'data-state': fold !== false ? 'closed' : 'open', 'data-type': type } }, [
dom('div', { class: [{'cursor-pointer': fold !== undefined}, 'flex flex-row items-center justify-start ps-2'], listeners: { click: e => {
container.setAttribute('data-state', open ? 'open' : 'closed');
open = !open;