diff --git a/ka-note/VERSION b/ka-note/VERSION
index ccd7ac1..e0f9632 100644
--- a/ka-note/VERSION
+++ b/ka-note/VERSION
@@ -1 +1 @@
-1.1.106
\ No newline at end of file
+1.1.108
\ No newline at end of file
diff --git a/ka-note/client/src/lib/actions/mention.ts b/ka-note/client/src/lib/actions/mention.ts
index 63719fe..fa93571 100644
--- a/ka-note/client/src/lib/actions/mention.ts
+++ b/ka-note/client/src/lib/actions/mention.ts
@@ -28,7 +28,7 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) {
for (let i = pos - 1; i >= 0; i--) {
const ch = text[i];
if (ch === '@') {
- if (i === 0 || /\s/.test(text[i - 1])) {
+ if (i === 0 || /[\s\n]/.test(text[i - 1])) {
mentionStart = i;
mentionMode = '@';
return { query: text.slice(i + 1, pos), mode: '@' };
@@ -45,10 +45,9 @@ export function mention(node: HTMLInputElement | HTMLTextAreaElement) {
}
return null;
}
- // Allow spaces only when we haven't yet passed a non-word char other than space
- // (needed for "-> ANDST" where the space is between trigger and query).
- // For @-mentions, spaces are not allowed in the query (quoted names use autocomplete).
- if (/\s/.test(ch)) return null;
+ // For ->, a single space between trigger and query is valid — skip it.
+ if (ch === ' ' && i > 1 && text[i - 1] === '>' && text[i - 2] === '-') continue;
+ if (/[\s\n]/.test(ch)) return null;
}
return null;
}
diff --git a/ka-note/client/src/lib/components/DashboardView.svelte b/ka-note/client/src/lib/components/DashboardView.svelte
index d6d45ef..00a9a39 100644
--- a/ka-note/client/src/lib/components/DashboardView.svelte
+++ b/ka-note/client/src/lib/components/DashboardView.svelte
@@ -39,6 +39,7 @@
// Metadata collapsed by default
let metaOpen = $state(false);
+ let abbrError = $state('');
// --- Notes ---
let noteTitle = $state('');
@@ -424,13 +425,25 @@
{ e.currentTarget.value = e.currentTarget.value.replace(/\s/g, '').toUpperCase(); }}
- onchange={(e) => {
- const v = e.currentTarget.value.replace(/\s/g, '').toUpperCase();
- updateMeta('abbreviation', v || undefined);
+ placeholder="z.B. CHfi"
+ oninput={(e) => { e.currentTarget.value = e.currentTarget.value.replace(/\s/g, ''); }}
+ onchange={async (e) => {
+ const v = e.currentTarget.value.replace(/\s/g, '');
+ if (!v) { updateMeta('abbreviation', undefined as any); return; }
+ const conflict = await db.contexts
+ .filter(c => !c.deletedAt && c.type === 'person' && c.id !== context.id
+ && (c.meta as PersonMeta | null)?.abbreviation?.toLowerCase() === v.toLowerCase())
+ .first();
+ if (conflict) {
+ abbrError = `Kürzel bereits vergeben (${conflict.name.replace(/^Person\s+/, '')})`;
+ e.currentTarget.value = meta.abbreviation ?? '';
+ } else {
+ abbrError = '';
+ updateMeta('abbreviation', v);
+ }
}}
/>
+ {#if abbrError}{abbrError}{/if}
@@ -507,30 +520,26 @@
{@const lines = entry.text.split('\n')}
{@const title = lines[0]}
{@const body = lines.slice(1).join('\n').trim()}
- {@const isDone = !!entry.doneAt}
-
-
-
{entry.date} {formatTime(entry.updatedAt)}
-
-
- {#if body}
-
- {/if}
+
+
+
{entry.date} {formatTime(entry.updatedAt)}
+
+
+
+
-
-
+
+ {#if body}
+
+ {/if}
{/if}
{/each}
diff --git a/ka-note/client/src/lib/components/RenderedMarkdown.svelte b/ka-note/client/src/lib/components/RenderedMarkdown.svelte
index a385b9c..36f2b4b 100644
--- a/ka-note/client/src/lib/components/RenderedMarkdown.svelte
+++ b/ka-note/client/src/lib/components/RenderedMarkdown.svelte
@@ -38,7 +38,8 @@
function markUnknown(chip: HTMLElement, prefix: string) {
// Replace the chip (and its preceding arrow span) with plain text
const arrow = chip.previousElementSibling;
- const text = document.createTextNode(`${prefix}${chip.dataset.assignment ?? chip.dataset.person ?? ''}`);
+ const name = chip.dataset.assignment ?? chip.dataset.person ?? '';
+ const text = document.createTextNode(`${prefix}${name}`);
chip.parentNode?.insertBefore(text, chip);
chip.remove();
if (arrow && /→|→/.test(arrow.textContent ?? '')) arrow.remove();
@@ -49,7 +50,11 @@
await Promise.all([
...Array.from(assignmentChips).map(async chip => {
const ctx = await findContextByAbbreviation(chip.dataset.assignment!);
- if (!ctx) { markUnknown(chip, '-> '); return; }
+ if (!ctx) {
+ const showArrow = chip.dataset.showArrow === '1';
+ markUnknown(chip, showArrow ? '=> ' : '-> ');
+ return;
+ }
const subType = (ctx.meta as Record
| null)?.personSubType as PersonSubType | undefined;
applySubtypeColor(chip, subType);
}),
diff --git a/ka-note/client/src/lib/utils/extractors.ts b/ka-note/client/src/lib/utils/extractors.ts
index e0df826..7d6b1b2 100644
--- a/ka-note/client/src/lib/utils/extractors.ts
+++ b/ka-note/client/src/lib/utils/extractors.ts
@@ -1,5 +1,5 @@
export function extractAssignments(text: string): string[] {
- const regex = /->\s*([\w]+)/g;
+ const regex = /(?:->|=>)\s*([\w]+)/g;
const result: string[] = [];
let match;
while ((match = regex.exec(text)) !== null) {
diff --git a/ka-note/client/src/lib/utils/mentionReplace.ts b/ka-note/client/src/lib/utils/mentionReplace.ts
index 887afb5..50eb393 100644
--- a/ka-note/client/src/lib/utils/mentionReplace.ts
+++ b/ka-note/client/src/lib/utils/mentionReplace.ts
@@ -43,9 +43,9 @@ export function renameMentions(
return text;
}
-/** Replace -> OldAbbr with -> NewAbbr in text (exact word match). */
+/** Replace -> OldAbbr / => OldAbbr with -> NewAbbr / => NewAbbr in text (case-insensitive word match). */
export function renameAssignment(text: string, oldAbbr: string, newAbbr: string): string {
const esc = escapeRegex(oldAbbr);
- const pattern = new RegExp(`(->\\s*)${esc}(?=[^\\w]|$)`, 'g');
+ const pattern = new RegExp(`((?:->|=>)\\s*)${esc}(?=[^\\w]|$)`, 'gi');
return text.replace(pattern, `$1${newAbbr}`);
}
diff --git a/ka-note/client/src/lib/utils/renderMarkdown.ts b/ka-note/client/src/lib/utils/renderMarkdown.ts
index 965cb01..c8008da 100644
--- a/ka-note/client/src/lib/utils/renderMarkdown.ts
+++ b/ka-note/client/src/lib/utils/renderMarkdown.ts
@@ -4,6 +4,23 @@ import hljs from 'highlight.js';
// --- Custom inline extensions for Ka-Note tags ---
+const assignmentWithArrowExt: TokenizerExtension & RendererExtension = {
+ name: 'assignmentWithArrow',
+ level: 'inline',
+ start(src: string) { return src.indexOf('=>'); },
+ tokenizer(src: string) {
+ // \u200B may be prepended during preprocessing.
+ const match = /^\u200B?=>\s*"([^"]+)"/.exec(src) ?? /^\u200B?=>\s*([\w-]+)/.exec(src);
+ if (match) {
+ return { type: 'assignmentWithArrow', raw: match[0], name: match[1] };
+ }
+ },
+ renderer(token) {
+ return '→'
+ + `${token.name}`;
+ }
+};
+
const assignmentExt: TokenizerExtension & RendererExtension = {
name: 'assignment',
level: 'inline',
@@ -17,8 +34,7 @@ const assignmentExt: TokenizerExtension & RendererExtension = {
}
},
renderer(token) {
- return '→'
- + `${token.name}`;
+ return `${token.name}`;
}
};
@@ -108,7 +124,7 @@ function truncateLinkUrl(url: string): string {
const markedInstance = new Marked({
gfm: true,
breaks: true,
- extensions: [wikiLinkExt, assignmentExt, projectRefExt, companyRefExt, personMentionExt],
+ extensions: [wikiLinkExt, assignmentWithArrowExt, assignmentExt, projectRefExt, companyRefExt, personMentionExt],
renderer: {
link({ href, title, text }) {
if (!href) return text;
@@ -189,6 +205,7 @@ const PURIFY_CONFIG = {
ALLOWED_ATTR: [
'href', 'target', 'rel', 'src', 'alt', 'title', 'width', 'height',
'class', 'style', 'data-person', 'data-project', 'data-company', 'data-wiki-page',
+ 'data-assignment', 'data-show-arrow',
'type', 'checked', 'disabled' // for GFM task lists
],
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|blob):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i
@@ -202,9 +219,11 @@ export function renderMarkdown(text: string): string {
if (!text) return '';
// tiptap-markdown escapes [ as \[ — restore [[WikiLinks]] before parsing
const unescaped = text.replace(/\\\[\\\[(.+?)\\\]\\\]/g, '[[$1]]').replace(/\\?-(?:>|>)/g, '->');
- // Prevent Marked from treating -> at line start as a list item + blockquote.
- // Replace leading -> with a zero-width space prefix so it stays inline.
- const preprocessed = unescaped.replace(/(^|\n)([ \t]*)->/g, '$1$2\u200B->');
+ // Prevent Marked from treating -> / => at line start as a list item + blockquote.
+ // Replace leading -> / => with a zero-width space prefix so they stay inline.
+ const preprocessed = unescaped
+ .replace(/(^|\n)([ \t]*)->/g, '$1$2\u200B->')
+ .replace(/(^|\n)([ \t]*)=>/g, '$1$2\u200B=>');
const raw = markedInstance.parse(preprocessed) as string;
const withTables = wrapTables(raw);
const withCallouts = processCallouts(withTables);
diff --git a/ka-note/server/ka-note.db-shm b/ka-note/server/ka-note.db-shm
index c41327b..0383b4b 100644
Binary files a/ka-note/server/ka-note.db-shm and b/ka-note/server/ka-note.db-shm differ
diff --git a/ka-note/server/ka-note.db-wal b/ka-note/server/ka-note.db-wal
index 7ad21e5..7600343 100644
Binary files a/ka-note/server/ka-note.db-wal and b/ka-note/server/ka-note.db-wal differ