upd md editor
This commit is contained in:
parent
c47f288e3e
commit
d5a9f9ce00
|
|
@ -24,6 +24,7 @@
|
|||
"@tiptap/starter-kit": "^3.20.0",
|
||||
"dexie": "^4.0.11",
|
||||
"dompurify": "^3.3.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"marked": "^17.0.3",
|
||||
"tiptap-markdown": "^0.9.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import 'highlight.js/styles/github-dark-dimmed.css';
|
||||
|
||||
:root {
|
||||
--scope-color: #555566; /* updated dynamically by layout */
|
||||
--bg-color: #1a1a22;
|
||||
|
|
@ -79,6 +81,87 @@ ul.tree-list li {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Callout blocks */
|
||||
.callout {
|
||||
border-radius: 6px;
|
||||
padding: 0.6rem 0.9rem;
|
||||
margin: 0.75rem 0;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.callout-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.callout-body > :first-child { margin-top: 0; }
|
||||
.callout-body > :last-child { margin-bottom: 0; }
|
||||
|
||||
.callout-note { background: #1a2a3a; border-color: var(--accent); }
|
||||
.callout-note .callout-title { color: var(--accent); }
|
||||
.callout-info { background: #1a2a3a; border-color: var(--info); }
|
||||
.callout-info .callout-title { color: var(--info); }
|
||||
.callout-tip { background: #1a2e22; border-color: var(--success); }
|
||||
.callout-tip .callout-title { color: var(--success); }
|
||||
.callout-success { background: #1a2e22; border-color: var(--success); }
|
||||
.callout-success .callout-title { color: var(--success); }
|
||||
.callout-warning { background: #2e2510; border-color: var(--warning); }
|
||||
.callout-warning .callout-title { color: var(--warning); }
|
||||
.callout-danger { background: #2e1515; border-color: var(--danger); }
|
||||
.callout-danger .callout-title { color: var(--danger); }
|
||||
.callout-caution { background: #2e1515; border-color: var(--danger); }
|
||||
.callout-caution .callout-title { color: var(--danger); }
|
||||
|
||||
/* Tables */
|
||||
.markdown-content .table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin: 0.75rem 0;
|
||||
}
|
||||
|
||||
.markdown-content table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.markdown-content th {
|
||||
background: #2a2a38;
|
||||
color: #e0e0e0;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.markdown-content td {
|
||||
padding: 0.35rem 0.75rem;
|
||||
border: 1px solid var(--border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.markdown-content tbody tr:nth-child(even) td {
|
||||
background: #22222e;
|
||||
}
|
||||
|
||||
/* Typography tweaks */
|
||||
.markdown-content blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 0.75rem;
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.markdown-content hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, var(--border-color), transparent);
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
/* Tiptap Editor */
|
||||
.ka-editor-wrapper {
|
||||
@apply rounded border border-[#444] bg-bg font-mono text-white;
|
||||
|
|
@ -122,6 +205,38 @@ ul.tree-list li {
|
|||
padding-left: 20px;
|
||||
}
|
||||
|
||||
/* Table styles in editor */
|
||||
.ka-editor-wrapper .ProseMirror table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
font-size: 0.875rem;
|
||||
margin: 0.5rem 0;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ka-editor-wrapper .ProseMirror th,
|
||||
.ka-editor-wrapper .ProseMirror td {
|
||||
border: 1px solid #444;
|
||||
padding: 0.3rem 0.6rem;
|
||||
vertical-align: top;
|
||||
min-width: 60px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ka-editor-wrapper .ProseMirror th {
|
||||
background: #2a2a38;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ka-editor-wrapper .ProseMirror .selectedCell::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(74, 158, 255, 0.15);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Rating indicators on @NAME tags */
|
||||
.person-ref.rating-1 {
|
||||
border-bottom: 3px solid #d9534f !important;
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
|
||||
let element: HTMLDivElement;
|
||||
let bubbleMenuEl: HTMLDivElement;
|
||||
let tableMenuEl: HTMLDivElement;
|
||||
let editor: Editor | null = null;
|
||||
let skipNextUpdate = false;
|
||||
let lastExternalContent = $state(content);
|
||||
|
|
@ -103,10 +104,17 @@
|
|||
if (!view.hasFocus()) return false;
|
||||
if (from === to) return false;
|
||||
if (e.isActive('image')) return false;
|
||||
if (e.isActive('table')) return false;
|
||||
const { empty } = e.state.selection;
|
||||
return !empty;
|
||||
},
|
||||
}),
|
||||
BubbleMenu.configure({
|
||||
element: tableMenuEl,
|
||||
pluginKey: 'tableMenu',
|
||||
tippyOptions: { duration: 100, placement: 'top-start', hideOnClick: false },
|
||||
shouldShow: ({ editor: e }) => e.isActive('table'),
|
||||
}),
|
||||
],
|
||||
content: initialContent,
|
||||
editorProps: {
|
||||
|
|
@ -163,6 +171,27 @@
|
|||
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
||||
<div class="ka-editor-wrapper" onclick={(e) => e.stopPropagation()}>
|
||||
<div bind:this={element}></div>
|
||||
<div bind:this={tableMenuEl} class="ka-bubble-menu ka-table-menu">
|
||||
<span class="bubble-label">Zeile</span>
|
||||
<button class="bubble-btn" title="Zeile darunter einfügen"
|
||||
onmousedown={(e) => { e.preventDefault(); editor?.commands.addRowAfter(); }}
|
||||
>+</button>
|
||||
<button class="bubble-btn" title="Zeile löschen"
|
||||
onmousedown={(e) => { e.preventDefault(); editor?.commands.deleteRow(); }}
|
||||
>−</button>
|
||||
<span class="bubble-sep"></span>
|
||||
<span class="bubble-label">Spalte</span>
|
||||
<button class="bubble-btn" title="Spalte rechts einfügen"
|
||||
onmousedown={(e) => { e.preventDefault(); editor?.commands.addColumnAfter(); }}
|
||||
>+</button>
|
||||
<button class="bubble-btn" title="Spalte löschen"
|
||||
onmousedown={(e) => { e.preventDefault(); editor?.commands.deleteColumn(); }}
|
||||
>−</button>
|
||||
<span class="bubble-sep"></span>
|
||||
<button class="bubble-btn bubble-btn-danger" title="Tabelle löschen"
|
||||
onmousedown={(e) => { e.preventDefault(); editor?.commands.deleteTable(); }}
|
||||
>✕</button>
|
||||
</div>
|
||||
<div bind:this={bubbleMenuEl} class="ka-bubble-menu">
|
||||
<button class="bubble-btn"
|
||||
class:bubble-btn-active={editor?.isActive('bold')}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.slash-submenu-arrow {
|
||||
margin-left: auto;
|
||||
color: #777;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.slash-submenu-arrow-open {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.slash-child-item {
|
||||
padding-left: 2rem !important;
|
||||
font-size: 0.9em;
|
||||
border-left: 2px solid #444;
|
||||
}
|
||||
|
||||
.slash-command-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -59,3 +76,31 @@
|
|||
.bubble-btn-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bubble-btn-danger {
|
||||
color: #e05555;
|
||||
}
|
||||
|
||||
.bubble-btn-danger:hover {
|
||||
background: #3a1515;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.bubble-sep {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #555;
|
||||
margin: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bubble-label {
|
||||
font-size: 0.7rem;
|
||||
color: #777;
|
||||
padding: 0 2px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ka-table-menu {
|
||||
gap: 1px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,28 @@ interface SlashCommand {
|
|||
label: string;
|
||||
icon: string;
|
||||
keywords: string[];
|
||||
execute: (editor: Editor) => void;
|
||||
execute?: (editor: Editor) => void;
|
||||
children?: SlashCommand[];
|
||||
}
|
||||
|
||||
const TABLE_CHILDREN: SlashCommand[] = [
|
||||
{ label: '2 × 2', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 2, cols: 2, withHeaderRow: true }).run() },
|
||||
{ label: '3 × 3', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
||||
{ label: '4 × 4', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 4, cols: 4, withHeaderRow: true }).run() },
|
||||
{ label: '5 × 5', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 5, cols: 5, withHeaderRow: true }).run() },
|
||||
{ label: '2 × 5', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 5, cols: 2, withHeaderRow: true }).run() },
|
||||
{ label: '3 × 6', icon: '⊞', keywords: [], execute: (e) => e.chain().focus().insertTable({ rows: 6, cols: 3, withHeaderRow: true }).run() },
|
||||
];
|
||||
|
||||
const CALLOUT_CHILDREN: SlashCommand[] = [
|
||||
{ label: 'Note', icon: '📝', keywords: ['note'], execute: (e) => e.chain().focus().insertContent('> [!NOTE]\n> ').run() },
|
||||
{ label: 'Info', icon: 'ℹ️', keywords: ['info'], execute: (e) => e.chain().focus().insertContent('> [!INFO]\n> ').run() },
|
||||
{ label: 'Tip', icon: '💡', keywords: ['tip'], execute: (e) => e.chain().focus().insertContent('> [!TIP]\n> ').run() },
|
||||
{ label: 'Success', icon: '✅', keywords: ['success'], execute: (e) => e.chain().focus().insertContent('> [!SUCCESS]\n> ').run() },
|
||||
{ label: 'Warning', icon: '⚠️', keywords: ['warning'], execute: (e) => e.chain().focus().insertContent('> [!WARNING]\n> ').run() },
|
||||
{ label: 'Danger', icon: '🚨', keywords: ['danger'], execute: (e) => e.chain().focus().insertContent('> [!DANGER]\n> ').run() },
|
||||
];
|
||||
|
||||
const ALL_COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
label: 'Heading 1',
|
||||
|
|
@ -66,7 +85,7 @@ const ALL_COMMANDS: SlashCommand[] = [
|
|||
label: 'Table',
|
||||
icon: '⊞',
|
||||
keywords: ['table', 'grid', 'tabelle'],
|
||||
execute: (e) => e.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||
children: TABLE_CHILDREN,
|
||||
},
|
||||
{
|
||||
label: 'Divider',
|
||||
|
|
@ -74,6 +93,12 @@ const ALL_COMMANDS: SlashCommand[] = [
|
|||
keywords: ['hr', 'rule', 'divider', 'horizontal', 'trennlinie'],
|
||||
execute: (e) => e.chain().focus().setHorizontalRule().run(),
|
||||
},
|
||||
{
|
||||
label: 'Callout',
|
||||
icon: '⚠',
|
||||
keywords: ['callout', 'note', 'warning', 'info', 'tip', 'success', 'danger', 'hinweis'],
|
||||
children: CALLOUT_CHILDREN,
|
||||
},
|
||||
];
|
||||
|
||||
export const TiptapSlashCommand = Extension.create({
|
||||
|
|
@ -83,9 +108,12 @@ export const TiptapSlashCommand = Extension.create({
|
|||
const editor = this.editor;
|
||||
let dropdown: HTMLDivElement | null = null;
|
||||
let filtered: SlashCommand[] = [];
|
||||
// selectedIndex: index into the flat rendered list (parents + expanded children)
|
||||
let selectedIndex = 0;
|
||||
let triggerFrom = -1;
|
||||
let active = false;
|
||||
// Which parent item is expanded (by index in filtered), -1 = none
|
||||
let expandedIndex = -1;
|
||||
|
||||
function hide() {
|
||||
dropdown?.remove();
|
||||
|
|
@ -93,36 +121,80 @@ export const TiptapSlashCommand = Extension.create({
|
|||
active = false;
|
||||
filtered = [];
|
||||
triggerFrom = -1;
|
||||
expandedIndex = -1;
|
||||
selectedIndex = 0;
|
||||
}
|
||||
|
||||
/** Build the flat list of items currently rendered: parents, and if one is expanded, its children inline */
|
||||
function flatList(): Array<{ cmd: SlashCommand; isChild: boolean; parentIdx: number }> {
|
||||
const list: Array<{ cmd: SlashCommand; isChild: boolean; parentIdx: number }> = [];
|
||||
filtered.forEach((cmd, i) => {
|
||||
list.push({ cmd, isChild: false, parentIdx: i });
|
||||
if (i === expandedIndex && cmd.children) {
|
||||
cmd.children.forEach(child => list.push({ cmd: child, isChild: true, parentIdx: i }));
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!dropdown) return;
|
||||
if (filtered.length === 0) { hide(); return; }
|
||||
dropdown.innerHTML = '';
|
||||
filtered.forEach((cmd, i) => {
|
||||
const items = flatList();
|
||||
items.forEach((item, i) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'mention-item slash-command-item'
|
||||
+ (i === selectedIndex ? ' mention-item-active' : '');
|
||||
+ (i === selectedIndex ? ' mention-item-active' : '')
|
||||
+ (item.isChild ? ' slash-child-item' : '');
|
||||
|
||||
const iconSpan = document.createElement('span');
|
||||
iconSpan.className = 'slash-command-icon';
|
||||
iconSpan.textContent = cmd.icon;
|
||||
iconSpan.textContent = item.cmd.icon;
|
||||
|
||||
const labelSpan = document.createElement('span');
|
||||
labelSpan.textContent = cmd.label;
|
||||
labelSpan.textContent = item.cmd.label;
|
||||
|
||||
row.appendChild(iconSpan);
|
||||
row.appendChild(labelSpan);
|
||||
row.addEventListener('pointerdown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
selectItem(i);
|
||||
|
||||
if (!item.isChild && item.cmd.children) {
|
||||
const arrowSpan = document.createElement('span');
|
||||
arrowSpan.className = 'slash-submenu-arrow'
|
||||
+ (expandedIndex === item.parentIdx ? ' slash-submenu-arrow-open' : '');
|
||||
arrowSpan.textContent = expandedIndex === item.parentIdx ? '∨' : '›';
|
||||
row.appendChild(arrowSpan);
|
||||
}
|
||||
|
||||
row.addEventListener('pointerdown', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
selectFlat(i);
|
||||
});
|
||||
row.addEventListener('mouseenter', () => { selectedIndex = i; render(); });
|
||||
dropdown!.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function selectFlat(flatIdx: number) {
|
||||
const items = flatList();
|
||||
const item = items[flatIdx];
|
||||
if (!item) return;
|
||||
if (!item.isChild && item.cmd.children) {
|
||||
// Toggle expand
|
||||
expandedIndex = expandedIndex === item.parentIdx ? -1 : item.parentIdx;
|
||||
selectedIndex = flatIdx;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
if (!item.cmd.execute) return;
|
||||
const savedFrom = triggerFrom;
|
||||
const savedTo = editor.state.selection.from;
|
||||
editor.chain().focus().deleteRange({ from: savedFrom, to: savedTo }).run();
|
||||
item.cmd.execute(editor);
|
||||
hide();
|
||||
}
|
||||
|
||||
function show(view: EditorView) {
|
||||
if (!dropdown) {
|
||||
dropdown = document.createElement('div');
|
||||
|
|
@ -146,15 +218,6 @@ export const TiptapSlashCommand = Extension.create({
|
|||
render();
|
||||
}
|
||||
|
||||
function selectItem(index: number) {
|
||||
const cmd = filtered[index];
|
||||
if (!cmd) return;
|
||||
const savedFrom = triggerFrom;
|
||||
const savedTo = editor.state.selection.from;
|
||||
editor.chain().focus().deleteRange({ from: savedFrom, to: savedTo }).run();
|
||||
cmd.execute(editor);
|
||||
hide();
|
||||
}
|
||||
|
||||
function getSlashState(view: EditorView): { query: string; from: number } | null {
|
||||
const { state } = view;
|
||||
|
|
@ -219,7 +282,9 @@ export const TiptapSlashCommand = Extension.create({
|
|||
props: {
|
||||
handleKeyDown(view, event) {
|
||||
if (!active) return false;
|
||||
const count = filtered.length;
|
||||
|
||||
const items = flatList();
|
||||
const count = items.length;
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
|
|
@ -235,7 +300,7 @@ export const TiptapSlashCommand = Extension.create({
|
|||
}
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
selectItem(selectedIndex);
|
||||
selectFlat(selectedIndex);
|
||||
return true;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Marked, type TokenizerExtension, type RendererExtension } from 'marked';
|
||||
import DOMPurify from 'dompurify';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
// --- Custom inline extensions for Ka-Note tags ---
|
||||
|
||||
|
|
@ -113,10 +114,49 @@ const markedInstance = new Marked({
|
|||
const display = isRawUrl ? truncateLinkUrl(href) : text;
|
||||
const t = title ? ` title="${title}"` : '';
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer"${t}>${display}</a>`;
|
||||
},
|
||||
code({ text, lang }) {
|
||||
const language = lang && hljs.getLanguage(lang) ? lang : undefined;
|
||||
const highlighted = language
|
||||
? hljs.highlight(text, { language }).value
|
||||
: hljs.highlightAuto(text).value;
|
||||
return `<pre><code class="hljs${language ? ` language-${language}` : ''}">${highlighted}</code></pre>`;
|
||||
},
|
||||
table() {
|
||||
return false; // use default renderer; table-wrapper added in post-processing
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- Table wrapper post-processing ---
|
||||
|
||||
function wrapTables(html: string): string {
|
||||
return html.replace(/<table>/g, '<div class="table-wrapper"><table>').replace(/<\/table>/g, '</table></div>');
|
||||
}
|
||||
|
||||
// --- Callout blocks post-processing ---
|
||||
|
||||
const CALLOUT_ICONS: Record<string, string> = {
|
||||
note: '📝', info: 'ℹ️', tip: '💡', success: '✅',
|
||||
warning: '⚠️', danger: '🚨', caution: '⚠️'
|
||||
};
|
||||
|
||||
function processCallouts(html: string): string {
|
||||
return html.replace(
|
||||
/<blockquote>\s*<p>\[!(NOTE|INFO|TIP|SUCCESS|WARNING|DANGER|CAUTION)\](.*?)<\/p>([\s\S]*?)<\/blockquote>/gi,
|
||||
(_, type: string, title: string, body: string) => {
|
||||
const t = type.toLowerCase();
|
||||
const icon = CALLOUT_ICONS[t] ?? '';
|
||||
const label = title.trim() || type.toUpperCase();
|
||||
const content = body.trim();
|
||||
return `<div class="callout callout-${t}">`
|
||||
+ `<div class="callout-title">${icon} ${label}</div>`
|
||||
+ (content ? `<div class="callout-body">${content}</div>` : '')
|
||||
+ `</div>`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// --- Collapsible list post-processing ---
|
||||
|
||||
function addCollapsibleToggles(html: string): string {
|
||||
|
|
@ -161,6 +201,8 @@ export function renderMarkdown(text: string): string {
|
|||
// tiptap-markdown escapes [ as \[ — restore [[WikiLinks]] before parsing
|
||||
const unescaped = text.replace(/\\\[\\\[(.+?)\\\]\\\]/g, '[[$1]]');
|
||||
const raw = markedInstance.parse(unescaped) as string;
|
||||
const sanitized = DOMPurify.sanitize(raw, PURIFY_CONFIG);
|
||||
const withTables = wrapTables(raw);
|
||||
const withCallouts = processCallouts(withTables);
|
||||
const sanitized = DOMPurify.sanitize(withCallouts, PURIFY_CONFIG);
|
||||
return addCollapsibleToggles(sanitized);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"@tiptap/starter-kit": "^3.20.0",
|
||||
"dexie": "^4.0.11",
|
||||
"dompurify": "^3.3.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"marked": "^17.0.3",
|
||||
"tiptap-markdown": "^0.9.0"
|
||||
},
|
||||
|
|
@ -6595,6 +6596,15 @@
|
|||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue