feat: add syntax highlighting and enhance document editing features
- Import GitHub dark theme for highlight.js in main.ts - Pass slug prop to VisualEditor in DocumentView.vue for version and normal editing modes - Update Vite configuration to increase maximum cache size and add media proxy - Add new dependencies for Tiptap extensions and highlight.js in package.json and package-lock.json
This commit is contained in:
@@ -40,7 +40,7 @@ Webbläsare
|
||||
| Frontend | Vue 3 (Composition API), Vite, TypeScript, Tailwind CSS |
|
||||
| State | Pinia |
|
||||
| Routing | Vue Router 4 |
|
||||
| Editor | TipTap 2 (visuell), CodeMirror 6 (AsciiDoc-källa) |
|
||||
| Editor | TipTap 2 (visuell, med custom image extension), CodeMirror 6 (AsciiDoc-källa) |
|
||||
| AsciiDoc | Asciidoctor.js (rendering i webbläsaren) |
|
||||
| PWA | vite-plugin-pwa (Service Worker, manifest) |
|
||||
| Backend | Go 1.22, HTTP-standardbibliotek |
|
||||
|
||||
153
frontend/package-lock.json
generated
153
frontend/package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@tiptap/extension-bullet-list": "^2.27.2",
|
||||
"@tiptap/extension-code": "^2.27.2",
|
||||
"@tiptap/extension-code-block": "^2.27.2",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.27.2",
|
||||
"@tiptap/extension-document": "^2.27.2",
|
||||
"@tiptap/extension-heading": "^2.27.2",
|
||||
"@tiptap/extension-horizontal-rule": "^2.27.2",
|
||||
@@ -24,6 +25,10 @@
|
||||
"@tiptap/extension-ordered-list": "^2.27.2",
|
||||
"@tiptap/extension-paragraph": "^2.27.2",
|
||||
"@tiptap/extension-strike": "^2.27.2",
|
||||
"@tiptap/extension-table": "^2.27.2",
|
||||
"@tiptap/extension-table-cell": "^2.27.2",
|
||||
"@tiptap/extension-table-header": "^2.27.2",
|
||||
"@tiptap/extension-table-row": "^2.27.2",
|
||||
"@tiptap/extension-text": "^2.27.2",
|
||||
"@tiptap/html": "^2.27.2",
|
||||
"@tiptap/pm": "^2.4.0",
|
||||
@@ -33,6 +38,8 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
@@ -40,6 +47,7 @@
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-markdown": "^6.2.5",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@tiptap/extension-image": "^2.27.2",
|
||||
"@types/node": "^20.14.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
@@ -2978,6 +2986,23 @@
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block-lowlight": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-2.27.2.tgz",
|
||||
"integrity": "sha512-v6NKStBbQ/XCc1NnCi3ObsL1DsxadSIBtUQNA/B+urkPgn5LEy72HAGlf0xwjRaNkAGSaTASLKmc84L5q5zlGQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/extension-code-block": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0",
|
||||
"highlight.js": "^11",
|
||||
"lowlight": "^2 || ^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-document": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.27.2.tgz",
|
||||
@@ -3090,6 +3115,20 @@
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-image": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.27.2.tgz",
|
||||
"integrity": "sha512-5zL/BY41FIt72azVrCrv3n+2YJ/JyO8wxCcA4Dk1eXIobcgVyIdo4rG39gCqIOiqziAsqnqoj12QHTBtHsJ6mQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-italic": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.2.tgz",
|
||||
@@ -3172,6 +3211,59 @@
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-2.27.2.tgz",
|
||||
"integrity": "sha512-pDbhOpT5phZkcsyPjGBQlXv0+0hmdrvqHJ+dJjkGcCtlfy2pHiEIhmIItOFagc7wXy8G9iUFZ9Jie4zvDf+brg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0",
|
||||
"@tiptap/pm": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-cell": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-2.27.2.tgz",
|
||||
"integrity": "sha512-9Lk46MjZMFzVZfOj9Kd7VgC6Odt6vmEhlCYVumErShUY7EkFqCw3b2IYoUtQkntfOEx/Afnhff/okNQwPsJeUA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-header": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-2.27.2.tgz",
|
||||
"integrity": "sha512-ZEb6lbG0NbbodWLV0b4BS/QrDIPlUbCcuOsUxzqVvlMUY1Vg6Fj6fKwLaBcsIUDHi8sxZDBEgYEDw3BR/zcO6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-row": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-2.27.2.tgz",
|
||||
"integrity": "sha512-Nw9+tA56Y5HtLVP01NGCZSUuTQhJPtfK9OfmDgGgcxynn2cRVdEtj+9FNZqRhQ1iRVaAI+Rd4xRvX9qYePMOxw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^2.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-text": {
|
||||
"version": "2.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.27.2.tgz",
|
||||
@@ -3304,6 +3396,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
@@ -3350,6 +3451,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz",
|
||||
@@ -4371,6 +4478,28 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/didyoumean": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -5157,6 +5286,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/idb": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
|
||||
@@ -5859,6 +5997,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"highlight.js": "~11.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@tiptap/extension-bullet-list": "^2.27.2",
|
||||
"@tiptap/extension-code": "^2.27.2",
|
||||
"@tiptap/extension-code-block": "^2.27.2",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.27.2",
|
||||
"@tiptap/extension-document": "^2.27.2",
|
||||
"@tiptap/extension-heading": "^2.27.2",
|
||||
"@tiptap/extension-horizontal-rule": "^2.27.2",
|
||||
@@ -25,6 +26,10 @@
|
||||
"@tiptap/extension-ordered-list": "^2.27.2",
|
||||
"@tiptap/extension-paragraph": "^2.27.2",
|
||||
"@tiptap/extension-strike": "^2.27.2",
|
||||
"@tiptap/extension-table": "^2.27.2",
|
||||
"@tiptap/extension-table-cell": "^2.27.2",
|
||||
"@tiptap/extension-table-header": "^2.27.2",
|
||||
"@tiptap/extension-table-row": "^2.27.2",
|
||||
"@tiptap/extension-text": "^2.27.2",
|
||||
"@tiptap/html": "^2.27.2",
|
||||
"@tiptap/pm": "^2.4.0",
|
||||
@@ -34,6 +39,8 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"graphql": "^16.9.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
@@ -41,6 +48,7 @@
|
||||
"devDependencies": {
|
||||
"@codemirror/lang-markdown": "^6.2.5",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@tiptap/extension-image": "^2.27.2",
|
||||
"@types/node": "^20.14.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"autoprefixer": "^10.4.19",
|
||||
|
||||
@@ -3,13 +3,17 @@ import { generateJSON } from '@tiptap/html'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import { Admonition, CustomCodeBlock } from './asciidoc-extensions'
|
||||
import { Table } from '@tiptap/extension-table'
|
||||
import { TableRow } from '@tiptap/extension-table-row'
|
||||
import { TableCell } from '@tiptap/extension-table-cell'
|
||||
import { TableHeader } from '@tiptap/extension-table-header'
|
||||
import { Admonition, CustomCodeBlock, AsciidocImage } from './asciidoc-extensions'
|
||||
|
||||
const asciidoctor = Asciidoctor()
|
||||
|
||||
// ── AsciiDoc → TipTap ────────────────────────────────────────────────────────
|
||||
|
||||
export function toTipTap(adoc: string): JSONContent {
|
||||
export function toTipTap(adoc: string, slug?: string): JSONContent {
|
||||
if (!adoc || !adoc.trim()) {
|
||||
return { type: 'doc', content: [{ type: 'paragraph' }] }
|
||||
}
|
||||
@@ -29,11 +33,47 @@ export function toTipTap(adoc: string): JSONContent {
|
||||
}
|
||||
)
|
||||
|
||||
// Rewrite image tags to point to backend /media/ path
|
||||
// Asciidoctor outputs: <img src="filename.png" alt="alt" width="200">
|
||||
// We add data-original-src so we can convert back properly, and rewrite src to /media/...
|
||||
if (slug) {
|
||||
const parts = slug.split('/')
|
||||
parts.pop() // remove filename
|
||||
const dir = parts.length > 0 ? parts.join('/') + '/' : ''
|
||||
|
||||
htmlContent = htmlContent.replace(
|
||||
/<img([^>]*)src="([^"]+)"([^>]*)>/g,
|
||||
(_match, before, src, after) => {
|
||||
// Only rewrite relative paths (not http/https)
|
||||
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('data:')) {
|
||||
return `<img${before}src="${src}" data-original-src="${src}"${after}>`
|
||||
}
|
||||
const backendSrc = `/media/${dir}${src}`
|
||||
return `<img${before}src="${backendSrc}" data-original-src="${src}"${after}>`
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// Om slug saknas, lägg i alla fall till data-original-src så den bevaras
|
||||
htmlContent = htmlContent.replace(
|
||||
/<img([^>]*)src="([^"]+)"([^>]*)>/g,
|
||||
(_match, before, src, after) => {
|
||||
return `<img${before}src="${src}" data-original-src="${src}"${after}>`
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const extensions = [
|
||||
StarterKit.configure({ codeBlock: false }),
|
||||
CustomCodeBlock,
|
||||
Link,
|
||||
Admonition
|
||||
Admonition,
|
||||
AsciidocImage,
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
}),
|
||||
TableRow,
|
||||
TableHeader,
|
||||
TableCell,
|
||||
]
|
||||
|
||||
return generateJSON(htmlContent, extensions)
|
||||
@@ -86,6 +126,12 @@ class TipTapToAsciidoc {
|
||||
case 'admonition':
|
||||
output += this.convertAdmonition(node)
|
||||
break
|
||||
case 'image':
|
||||
output += this.convertImage(node)
|
||||
break
|
||||
case 'table':
|
||||
output += this.convertTable(node)
|
||||
break
|
||||
case 'horizontalRule':
|
||||
output += "'''\n\n"
|
||||
break
|
||||
@@ -137,6 +183,37 @@ class TipTapToAsciidoc {
|
||||
return `[${type.toUpperCase()}]\n====\n${text}\n====\n\n`
|
||||
}
|
||||
|
||||
convertImage(node: JSONContent): string {
|
||||
const src = node.attrs?.['data-original-src'] || node.attrs?.src || ''
|
||||
const alt = node.attrs?.alt || ''
|
||||
const width = node.attrs?.width ? `, width=${node.attrs.width}` : ''
|
||||
const height = node.attrs?.height ? `, height=${node.attrs.height}` : ''
|
||||
const title = node.attrs?.title ? `title="${node.attrs.title}", ` : ''
|
||||
|
||||
if (title || width || height || alt) {
|
||||
return `image::${src}[${title}${alt}${width}${height}]\n\n`
|
||||
}
|
||||
return `image::${src}[]\n\n`
|
||||
}
|
||||
|
||||
convertTable(node: JSONContent): string {
|
||||
if (!node.content || node.content.length === 0) return ''
|
||||
|
||||
let tableOut = '|===\n'
|
||||
node.content.forEach((row) => {
|
||||
if (row.type !== 'tableRow' || !row.content) return
|
||||
|
||||
row.content.forEach((cell) => {
|
||||
const cellText = (cell.content ? this.processNodes(cell.content).trim() : '');
|
||||
tableOut += `| ${cellText} `
|
||||
})
|
||||
tableOut += '\n\n'
|
||||
})
|
||||
|
||||
tableOut += '|===\n\n'
|
||||
return tableOut
|
||||
}
|
||||
|
||||
renderTextNodes(nodes: JSONContent[]): string {
|
||||
if (!nodes || nodes.length === 0) return ''
|
||||
let textOut = ''
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
import { Node } from '@tiptap/core'
|
||||
import CodeBlock from '@tiptap/extension-code-block'
|
||||
import { CodeBlockLowlight, CodeBlockLowlightOptions } from '@tiptap/extension-code-block-lowlight'
|
||||
import { createLowlight, all } from 'lowlight'
|
||||
import Image from '@tiptap/extension-image'
|
||||
|
||||
const lowlight = createLowlight(all)
|
||||
|
||||
export const AsciidocImage = Image.extend({
|
||||
addAttributes() {
|
||||
return {
|
||||
...this.parent?.(),
|
||||
src: { default: null },
|
||||
alt: { default: null },
|
||||
title: { default: null },
|
||||
width: {
|
||||
default: null,
|
||||
parseHTML: element => element.getAttribute('width'),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.width) return {}
|
||||
return { width: attributes.width }
|
||||
}
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
parseHTML: element => element.getAttribute('height'),
|
||||
renderHTML: attributes => {
|
||||
if (!attributes.height) return {}
|
||||
return { height: attributes.height }
|
||||
}
|
||||
},
|
||||
'data-original-src': {
|
||||
default: null,
|
||||
parseHTML: element => element.getAttribute('data-original-src'),
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const Admonition = Node.create({
|
||||
name: 'admonition',
|
||||
@@ -26,7 +61,18 @@ export const Admonition = Node.create({
|
||||
},
|
||||
})
|
||||
|
||||
export const CustomCodeBlock = CodeBlock.extend({
|
||||
export const CustomCodeBlock = CodeBlockLowlight.extend<CodeBlockLowlightOptions>({
|
||||
addOptions() {
|
||||
return {
|
||||
...(this.parent?.() || {}),
|
||||
lowlight,
|
||||
defaultLanguage: null,
|
||||
languageClassPrefix: 'language-',
|
||||
HTMLAttributes: {
|
||||
class: 'code-block text-sm bg-gray-800 text-gray-100 p-3 rounded',
|
||||
},
|
||||
} as any
|
||||
},
|
||||
addAttributes() {
|
||||
return {
|
||||
language: {
|
||||
|
||||
98
frontend/src/components/editor/ImageEditDialog.vue
Normal file
98
frontend/src/components/editor/ImageEditDialog.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
initialData: {
|
||||
src: string;
|
||||
width: string;
|
||||
height: string;
|
||||
alt: string;
|
||||
title: string;
|
||||
'data-original-src': string;
|
||||
} | null;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'update', data: { src: string; width: string; height: string; alt: string; title: string, 'data-original-src': string }): void;
|
||||
}>()
|
||||
|
||||
const formData = ref({
|
||||
src: '',
|
||||
width: '',
|
||||
height: '',
|
||||
alt: '',
|
||||
title: '',
|
||||
'data-original-src': ''
|
||||
})
|
||||
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (open && props.initialData) {
|
||||
formData.value = { ...props.initialData }
|
||||
}
|
||||
})
|
||||
|
||||
function save() {
|
||||
emit('update', { ...formData.value })
|
||||
emit('close')
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-sm w-full overflow-hidden flex flex-col">
|
||||
<div class="p-4 border-b dark:border-slate-700 flex justify-between items-center bg-slate-50 dark:bg-slate-900/50">
|
||||
<h3 class="font-bold text-lg text-slate-800 dark:text-slate-100">Redigera bild</h3>
|
||||
<button @click="cancel" class="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex flex-col gap-4 overflow-y-auto">
|
||||
<!-- Källa (Bild-URL) -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Bildkälla (URL)</label>
|
||||
<input v-model="formData.src" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" />
|
||||
</div>
|
||||
|
||||
<!-- Original src (t.ex. kopplad fil) -->
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Kopplad fil (i AsciiDoc)</label>
|
||||
<input v-model="formData['data-original-src']" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" />
|
||||
<p class="text-[10px] text-slate-400 mt-1">Namnet på filen refererad i texten.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Bredd (px/% m.m.)</label>
|
||||
<input v-model="formData.width" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" placeholder="t.ex. 300" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Höjd (px/% m.m.)</label>
|
||||
<input v-model="formData.height" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" placeholder="t.ex. 200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Alt-text</label>
|
||||
<input v-model="formData.alt" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Titel</label>
|
||||
<input v-model="formData.title" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t dark:border-slate-700 flex justify-end gap-2 bg-slate-50 dark:bg-slate-900/50">
|
||||
<button @click="cancel" class="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 dark:text-slate-300 dark:hover:text-slate-100 transition-colors">Avbryt</button>
|
||||
<button @click="save" class="px-4 py-2 text-sm bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition-colors">Spara</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
211
frontend/src/components/editor/ImagePickerDialog.vue
Normal file
211
frontend/src/components/editor/ImagePickerDialog.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { gql } from '@/lib/gql'
|
||||
|
||||
const props = defineProps<{
|
||||
slug?: string;
|
||||
isOpen: boolean;
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'close'): void;
|
||||
(e: 'insert', data: { src: string; alt: string; title: string, 'data-original-src': string }): void;
|
||||
}>()
|
||||
|
||||
const activeTab = ref<'upload' | 'library' | 'url'>('library')
|
||||
const images = ref<{ name: string; url: string; size: number }[]>([])
|
||||
const isLoading = ref(false)
|
||||
|
||||
const urlInput = ref('')
|
||||
const altInput = ref('')
|
||||
const titleInput = ref('')
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const isUploading = ref(false)
|
||||
|
||||
async function fetchImages() {
|
||||
if (!props.slug) return
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await gql<{ images: { name: string; url: string; size: number }[] }>(
|
||||
`query GetImages($slug: String!) { images(slug: $slug) { name url size } }`,
|
||||
{ slug: props.slug }
|
||||
)
|
||||
images.value = data.images
|
||||
} catch (err) {
|
||||
console.error('Failed to load images:', err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.slug) fetchImages()
|
||||
})
|
||||
|
||||
function onOpen() {
|
||||
if (props.isOpen && props.slug) fetchImages()
|
||||
}
|
||||
|
||||
// Watch prop manually if mounted was called before isOpen was true
|
||||
import { watch } from 'vue'
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (open) onOpen()
|
||||
})
|
||||
|
||||
async function uploadImage() {
|
||||
const file = fileInput.value?.files?.[0]
|
||||
if (!file || !props.slug) return
|
||||
|
||||
isUploading.value = true
|
||||
const token = localStorage.getItem('token') || ''
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
formData.append('slug', props.slug)
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
if (!res.ok) throw new Error('Upload failed')
|
||||
|
||||
const data = await res.json()
|
||||
// data.url, data.name
|
||||
emit('insert', {
|
||||
src: data.url,
|
||||
alt: altInput.value || data.name,
|
||||
title: titleInput.value,
|
||||
'data-original-src': data.name
|
||||
})
|
||||
close()
|
||||
} catch (err) {
|
||||
console.error('Upload Error:', err)
|
||||
alert('Upload failed')
|
||||
} finally {
|
||||
isUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function selectImage(img: { name: string; url: string }) {
|
||||
emit('insert', {
|
||||
src: img.url,
|
||||
alt: altInput.value || img.name,
|
||||
title: titleInput.value,
|
||||
'data-original-src': img.name
|
||||
})
|
||||
close()
|
||||
}
|
||||
|
||||
function insertUrl() {
|
||||
if (!urlInput.value) return
|
||||
emit('insert', {
|
||||
src: urlInput.value,
|
||||
alt: altInput.value || 'Image',
|
||||
title: titleInput.value,
|
||||
'data-original-src': urlInput.value
|
||||
})
|
||||
close()
|
||||
}
|
||||
|
||||
function close() {
|
||||
urlInput.value = ''
|
||||
altInput.value = ''
|
||||
titleInput.value = ''
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
emit('close')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg shadow-xl max-w-lg w-full overflow-hidden flex flex-col max-h-[90vh]">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b dark:border-slate-700 flex justify-between items-center bg-slate-50 dark:bg-slate-900/50">
|
||||
<h3 class="font-bold text-lg text-slate-800 dark:text-slate-100">Infoga bild</h3>
|
||||
<button @click="close" class="text-slate-500 hover:text-slate-700 dark:hover:text-slate-300">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
|
||||
<button v-if="slug" @click="activeTab = 'library'" class="px-4 py-2 font-medium text-sm transition-colors border-b-2" :class="activeTab === 'library' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'">
|
||||
Bibliotek
|
||||
</button>
|
||||
<button v-if="slug" @click="activeTab = 'upload'" class="px-4 py-2 font-medium text-sm transition-colors border-b-2" :class="activeTab === 'upload' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'">
|
||||
Ladda upp
|
||||
</button>
|
||||
<button @click="activeTab = 'url'" class="px-4 py-2 font-medium text-sm transition-colors border-b-2" :class="activeTab === 'url' ? 'border-indigo-500 text-indigo-600 dark:text-indigo-400' : 'border-transparent text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'">
|
||||
Bild-URL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-4 overflow-y-auto flex-grow">
|
||||
|
||||
<!-- Metadata fields applied to all methods -->
|
||||
<div class="flex gap-4 mb-6 pb-4 border-b dark:border-slate-700">
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Alt-text (valfri)</label>
|
||||
<input v-model="altInput" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" placeholder="Alternativ text för skärmläsare" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="block text-xs font-medium text-slate-500 dark:text-slate-400 mb-1">Titel (valfri)</label>
|
||||
<input v-model="titleInput" type="text" class="w-full px-3 py-1.5 bg-white dark:bg-slate-900 border dark:border-slate-700 rounded-md text-sm text-slate-800 dark:text-slate-200" placeholder="Text visas vid hovring" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'url'">
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Klistra in bild-URL</label>
|
||||
<input v-model="urlInput" type="text" class="w-full px-3 py-2 bg-white dark:bg-slate-900 border border-slate-300 dark:border-slate-700 rounded-md shadow-sm mb-4" placeholder="https://example.com/image.png" @keydown.enter="insertUrl" />
|
||||
<div class="flex justify-end">
|
||||
<button @click="insertUrl" :disabled="!urlInput" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors">
|
||||
Infoga bild
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'upload'" class="flex flex-col gap-4">
|
||||
<div class="border-2 border-dashed border-slate-300 dark:border-slate-700 rounded-lg p-6 flex flex-col items-center justify-center text-center bg-slate-50 dark:bg-slate-900/30">
|
||||
<input type="file" accept="image/*" ref="fileInput" class="block w-full text-sm text-slate-500 dark:text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100 dark:file:bg-indigo-900/50 dark:file:text-indigo-400 cursor-pointer" />
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button @click="uploadImage" :disabled="isUploading || !fileInput" class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 disabled:opacity-50 transition-colors inline-flex items-center gap-2">
|
||||
<svg v-if="isUploading" class="animate-spin h-4 w-4 text-white" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path></svg>
|
||||
{{ isUploading ? 'Laddar upp...' : 'Ladda upp & infoga' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'library'">
|
||||
<div v-if="isLoading" class="flex justify-center p-8">
|
||||
<svg class="animate-spin h-6 w-6 text-indigo-500" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"></path></svg>
|
||||
</div>
|
||||
<div v-else-if="images.length === 0" class="text-center py-8 text-slate-500 dark:text-slate-400">
|
||||
<p>Inga uppladdade bilder hittades.</p>
|
||||
</div>
|
||||
<div v-else class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||
<button
|
||||
v-for="img in images"
|
||||
:key="img.name"
|
||||
@click="selectImage(img)"
|
||||
class="group relative aspect-square border border-slate-200 dark:border-slate-700 rounded-md overflow-hidden bg-slate-100 dark:bg-slate-900 hover:border-indigo-500 dark:hover:border-indigo-400 focus:outline-none transition-colors"
|
||||
:title="img.name"
|
||||
>
|
||||
<img :src="img.url" :alt="img.name" class="w-full h-full object-cover" />
|
||||
<div class="absolute inset-0 bg-indigo-500/0 group-hover:bg-indigo-500/10 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-all">
|
||||
<span class="bg-indigo-600 text-white text-xs px-2 py-1 rounded shadow-sm">Välj</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,21 +3,49 @@ import { ref, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { useEditor, EditorContent } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import { Table } from '@tiptap/extension-table'
|
||||
import { TableRow } from '@tiptap/extension-table-row'
|
||||
import { TableCell } from '@tiptap/extension-table-cell'
|
||||
import { TableHeader } from '@tiptap/extension-table-header'
|
||||
import { toTipTap, fromTipTap } from '@/bridge/asciidoc-bridge'
|
||||
import { Admonition, CustomCodeBlock } from '@/bridge/asciidoc-extensions'
|
||||
import { Admonition, CustomCodeBlock, AsciidocImage } from '@/bridge/asciidoc-extensions'
|
||||
import ImagePickerDialog from './ImagePickerDialog.vue'
|
||||
import ImageEditDialog from './ImageEditDialog.vue'
|
||||
|
||||
const model = defineModel<string>({ required: true })
|
||||
const props = defineProps<{ slug?: string }>()
|
||||
|
||||
const isEditing = ref(false)
|
||||
const showImagePicker = ref(false)
|
||||
const showImageEditor = ref(false)
|
||||
|
||||
const contextMenu = ref({ show: false, x: 0, y: 0, type: '' as 'table' | 'image' | '' })
|
||||
const activeImageData = ref<{ src: string; width: string; height: string; alt: string; title: string; 'data-original-src': string } | null>(null)
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({ codeBlock: false }),
|
||||
CustomCodeBlock,
|
||||
Link,
|
||||
Admonition
|
||||
Admonition,
|
||||
AsciidocImage,
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
HTMLAttributes: {
|
||||
class: 'border-collapse table-auto w-full text-sm',
|
||||
},
|
||||
}),
|
||||
TableRow.configure({
|
||||
HTMLAttributes: { class: 'border-b dark:border-slate-700' }
|
||||
}),
|
||||
TableHeader.configure({
|
||||
HTMLAttributes: { class: 'border p-2 bg-gray-100 dark:bg-slate-800 font-bold' }
|
||||
}),
|
||||
TableCell.configure({
|
||||
HTMLAttributes: { class: 'border p-2' }
|
||||
}),
|
||||
],
|
||||
content: toTipTap(model.value),
|
||||
content: toTipTap(model.value, props.slug),
|
||||
editable: false,
|
||||
onUpdate({ editor }) {
|
||||
model.value = fromTipTap(editor.getJSON())
|
||||
@@ -32,14 +60,124 @@ watch(isEditing, (editing) => {
|
||||
// Sync external changes (e.g. switching from SourceEditor) into TipTap.
|
||||
watch(model, (adoc) => {
|
||||
if (!editor.value) return
|
||||
const json = toTipTap(adoc)
|
||||
const json = toTipTap(adoc, props.slug)
|
||||
const current = JSON.stringify(editor.value.getJSON())
|
||||
if (JSON.stringify(json) !== current) {
|
||||
editor.value.commands.setContent(json, false)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => editor.value?.destroy())
|
||||
function onContextMenu(e: MouseEvent) {
|
||||
if (!isEditing.value || !editor.value) return
|
||||
|
||||
const target = e.target as HTMLElement
|
||||
|
||||
if (target instanceof HTMLImageElement) {
|
||||
// Om man högerklickar på en bild vill vi välja hela noden
|
||||
const pos = editor.value.view.posAtDOM(target, 0)
|
||||
if (pos >= 0) {
|
||||
editor.value.commands.setNodeSelection(pos)
|
||||
contextMenu.value = { show: true, x: e.clientX, y: e.clientY, type: 'image' }
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const view = editor.value.view
|
||||
const coords = view.posAtCoords({ left: e.clientX, top: e.clientY })
|
||||
if (!coords) {
|
||||
contextMenu.value.show = false
|
||||
return
|
||||
}
|
||||
|
||||
// För text/celler sätt markören inuti
|
||||
editor.value.commands.setTextSelection(coords.pos)
|
||||
|
||||
if (editor.value.isActive('table')) {
|
||||
contextMenu.value = { show: true, x: e.clientX, y: e.clientY, type: 'table' }
|
||||
e.preventDefault()
|
||||
} else {
|
||||
contextMenu.value.show = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleContextAction(action: string) {
|
||||
if (!editor.value) return
|
||||
const cmd = editor.value.chain().focus()
|
||||
|
||||
switch (action) {
|
||||
case 'editImage':
|
||||
openImageEditor()
|
||||
break
|
||||
case 'addRowBefore': cmd.addRowBefore().run(); break
|
||||
case 'addRowAfter': cmd.addRowAfter().run(); break
|
||||
case 'deleteRow': cmd.deleteRow().run(); break
|
||||
case 'addColumnBefore': cmd.addColumnBefore().run(); break
|
||||
case 'addColumnAfter': cmd.addColumnAfter().run(); break
|
||||
case 'deleteColumn': cmd.deleteColumn().run(); break
|
||||
case 'deleteTable': cmd.deleteTable().run(); break
|
||||
}
|
||||
|
||||
contextMenu.value.show = false
|
||||
}
|
||||
|
||||
function openImageEditor() {
|
||||
if (!editor.value) return
|
||||
const attrs = editor.value.getAttributes('image')
|
||||
activeImageData.value = {
|
||||
src: attrs.src || '',
|
||||
width: attrs.width || '',
|
||||
height: attrs.height || '',
|
||||
alt: attrs.alt || '',
|
||||
title: attrs.title || '',
|
||||
'data-original-src': attrs['data-original-src'] || ''
|
||||
}
|
||||
showImageEditor.value = true
|
||||
}
|
||||
|
||||
function handleImageUpdate(updatedData: { src: string; width: string; height: string; alt: string; title: string, 'data-original-src': string }) {
|
||||
if (!editor.value) return
|
||||
(editor.value.commands as any).updateAttributes('image', updatedData)
|
||||
showImageEditor.value = false
|
||||
}
|
||||
|
||||
function insertAdmonition(e: Event) {
|
||||
const select = e.target as HTMLSelectElement
|
||||
const type = select.value
|
||||
if (!type || !editor.value) return
|
||||
editor.value.commands.insertContent({
|
||||
type: 'admonition',
|
||||
attrs: { type: type.toLowerCase() },
|
||||
content: [{ type: 'paragraph' }]
|
||||
})
|
||||
select.value = ''
|
||||
}
|
||||
|
||||
function handleImageInsert(data: { src: string; alt: string; title: string, 'data-original-src': string }) {
|
||||
if (editor.value) {
|
||||
(editor.value.commands as any).setImage({
|
||||
src: data.src,
|
||||
alt: data.alt,
|
||||
title: data.title,
|
||||
'data-original-src': data['data-original-src']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy()
|
||||
document.removeEventListener('click', closeContextMenu)
|
||||
})
|
||||
|
||||
function closeContextMenu() {
|
||||
if (contextMenu.value.show) {
|
||||
contextMenu.value.show = false
|
||||
}
|
||||
}
|
||||
import { onMounted } from 'vue'
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', closeContextMenu)
|
||||
})
|
||||
|
||||
function btnClass(active: boolean | undefined) {
|
||||
return [
|
||||
@@ -132,11 +270,42 @@ function btnClass(active: boolean | undefined) {
|
||||
Kodblock
|
||||
</button>
|
||||
|
||||
<div class="w-px h-4 bg-gray-300 dark:bg-slate-600 mx-1"></div>
|
||||
|
||||
<select
|
||||
@change="insertAdmonition"
|
||||
class="bg-transparent text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-slate-700 px-2 py-1 rounded outline-none cursor-pointer"
|
||||
:disabled="!editor || !isEditing"
|
||||
>
|
||||
<option value="" disabled selected hidden>Info/Varning</option>
|
||||
<option value="NOTE">Information</option>
|
||||
<option value="TIP">Tips</option>
|
||||
<option value="WARNING">Varning</option>
|
||||
<option value="CAUTION">Försiktighet</option>
|
||||
<option value="IMPORTANT">Viktigt</option>
|
||||
</select>
|
||||
|
||||
<div class="w-px h-4 bg-gray-300 dark:bg-slate-600 mx-1"></div>
|
||||
|
||||
<button
|
||||
@click="(editor as any).chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()"
|
||||
:class="btnClass(editor.isActive('table'))"
|
||||
>
|
||||
Tabell
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="showImagePicker = true"
|
||||
class="px-2 py-0.5 text-sm font-medium transition-colors select-none text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-slate-700"
|
||||
>
|
||||
Bild
|
||||
</button>
|
||||
|
||||
<div class="flex-grow"></div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Area -->
|
||||
<div class="flex-grow overflow-y-auto p-4 w-full">
|
||||
<div class="flex-grow overflow-y-auto p-4 w-full" @contextmenu="onContextMenu">
|
||||
<div
|
||||
class="max-w-2xl mx-auto prose prose-indigo dark:prose-invert"
|
||||
@click="!isEditing && (isEditing = true)"
|
||||
@@ -144,6 +313,42 @@ function btnClass(active: boolean | undefined) {
|
||||
<EditorContent :editor="editor" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ImagePickerDialog
|
||||
:is-open="showImagePicker"
|
||||
:slug="props.slug"
|
||||
@close="showImagePicker = false"
|
||||
@insert="handleImageInsert"
|
||||
/>
|
||||
|
||||
<ImageEditDialog
|
||||
:is-open="showImageEditor"
|
||||
:initial-data="activeImageData"
|
||||
@close="showImageEditor = false"
|
||||
@update="handleImageUpdate"
|
||||
/>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div
|
||||
v-if="contextMenu.show"
|
||||
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
|
||||
class="fixed z-50 bg-white dark:bg-slate-800 shadow-xl border border-slate-200 dark:border-slate-700 rounded py-1 min-w-[160px] text-sm flex flex-col"
|
||||
>
|
||||
<template v-if="contextMenu.type === 'image'">
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200" @click="handleContextAction('editImage')">Redigera bild...</button>
|
||||
</template>
|
||||
<template v-if="contextMenu.type === 'table'">
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200" @click="handleContextAction('addRowBefore')">Sätt in rad ovanför</button>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200" @click="handleContextAction('addRowAfter')">Sätt in rad nedanför</button>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-red-600 dark:text-red-400" @click="handleContextAction('deleteRow')">Ta bort rad</button>
|
||||
<div class="h-px bg-slate-200 dark:bg-slate-700 my-1"></div>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200" @click="handleContextAction('addColumnBefore')">Sätt in kolumn före</button>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-200" @click="handleContextAction('addColumnAfter')">Sätt in kolumn efter</button>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-red-600 dark:text-red-400" @click="handleContextAction('deleteColumn')">Ta bort kolumn</button>
|
||||
<div class="h-px bg-slate-200 dark:bg-slate-700 my-1"></div>
|
||||
<button class="px-4 py-2 text-left hover:bg-slate-100 dark:hover:bg-slate-700 text-red-600 dark:text-red-400" @click="handleContextAction('deleteTable')">Ta bort tabell</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createPinia } from 'pinia'
|
||||
import { router } from '@/router'
|
||||
import App from '@/App.vue'
|
||||
import '@/assets/main.css'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
|
||||
@@ -311,6 +311,7 @@ function formatDate(dateStr: string) {
|
||||
<div v-if="versionLoading" class="p-6 text-slate-400 animate-pulse">Loading version…</div>
|
||||
<VisualEditor
|
||||
v-else-if="versionEditorMode === 'visual'"
|
||||
:slug="slug"
|
||||
:model-value="versionContent"
|
||||
@update:model-value="() => {}"
|
||||
/>
|
||||
@@ -324,7 +325,7 @@ function formatDate(dateStr: string) {
|
||||
<!-- Normal editor -->
|
||||
<template v-else>
|
||||
<div v-if="loading" class="p-6 text-slate-400 animate-pulse">Loading…</div>
|
||||
<VisualEditor v-else-if="editorMode === 'visual'" v-model="content" />
|
||||
<VisualEditor v-else-if="editorMode === 'visual'" v-model="content" :slug="slug" />
|
||||
<SourceEditor v-else v-model="content" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ export default defineConfig({
|
||||
// instead of waiting for all tabs to close.
|
||||
skipWaiting: true,
|
||||
clientsClaim: true,
|
||||
maximumFileSizeToCacheInBytes: 5000000,
|
||||
// Only cache these explicit file types — do NOT cache HTML so the
|
||||
// SPA shell is always fetched fresh (prevents stale SW loops).
|
||||
globPatterns: ['**/*.{js,css,woff2,png,svg,ico}'],
|
||||
@@ -50,6 +51,7 @@ export default defineConfig({
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/graphql': { target: 'http://localhost:4000', changeOrigin: true },
|
||||
'/media': { target: 'http://localhost:4000', changeOrigin: true },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
558
package-lock.json
generated
558
package-lock.json
generated
@@ -5,9 +5,23 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19"
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
||||
"@tiptap/extension-table": "^3.22.3",
|
||||
"@tiptap/extension-table-cell": "^3.22.3",
|
||||
"@tiptap/extension-table-header": "^3.22.3",
|
||||
"@tiptap/extension-table-row": "^3.22.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@remirror/core-constants": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.19",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
|
||||
@@ -20,6 +34,190 @@
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.3.tgz",
|
||||
"integrity": "sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/pm": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.3.tgz",
|
||||
"integrity": "sha512-RiQtEjDAPrHpdo6sw6b7fOw/PijqgFIsozKKkGcSeBgWHQuFg7q9OxJTj+l0e60rVwSu/5gmKEEobzM9bX+t2Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.22.3",
|
||||
"@tiptap/pm": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-code-block-lowlight": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block-lowlight/-/extension-code-block-lowlight-3.22.3.tgz",
|
||||
"integrity": "sha512-NGFuD9zb1QfnCgD2zW4XaUEdQvd/ydm9FmXXh8eawx/+C8xt3p21DIKRERvxWrCvvVNzIUEpZRRYehPOJiD1eg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.22.3",
|
||||
"@tiptap/extension-code-block": "^3.22.3",
|
||||
"@tiptap/pm": "^3.22.3",
|
||||
"highlight.js": "^11",
|
||||
"lowlight": "^2 || ^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.22.3.tgz",
|
||||
"integrity": "sha512-inbQSusJad7H0T++L1APg/anfL5d15cNGp2YG3vwo6TQr71nn2c9pepvmz3xuAIt8eygZDRba+4GT/COP1f9QA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/core": "^3.22.3",
|
||||
"@tiptap/pm": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-cell": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-cell/-/extension-table-cell-3.22.3.tgz",
|
||||
"integrity": "sha512-LgaUgNwjHe6J7dq66N7iflC9efjgygsYWkHJtfLzaSU82tXzk8HSwBrMRCaj0PU+AR967/jGixEZIxH9xfXswQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-header": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-header/-/extension-table-header-3.22.3.tgz",
|
||||
"integrity": "sha512-5wQ5rne9ccdbzeqC1AEDaUzlWjnpQNgBctgSPKEv+Da88lRRcGX21H+b/8jhL7fr4/sznr45toKGUg8NmRhboQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/extension-table-row": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-table-row/-/extension-table-row-3.22.3.tgz",
|
||||
"integrity": "sha512-dkoHnbOZgb2iQLXsp2FUu0KejAh3LOqDLMixYYU/0lnUwhqGG8kgu6+0YkCiUhedbfMFqVvemdKnRddpTQ4sqg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tiptap/extension-table": "^3.22.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/pm": {
|
||||
"version": "3.22.3",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.3.tgz",
|
||||
"integrity": "sha512-NjfWjZuvrqmpICT+GZWNIjtOdhPyqFKDMtQy7tsQ5rErM9L2ZQdy/+T/BKSO1JdTeBhdg9OP+0yfsqoYp2aT6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
"prosemirror-commands": "^1.6.2",
|
||||
"prosemirror-dropcursor": "^1.8.1",
|
||||
"prosemirror-gapcursor": "^1.3.2",
|
||||
"prosemirror-history": "^1.4.1",
|
||||
"prosemirror-inputrules": "^1.4.0",
|
||||
"prosemirror-keymap": "^1.2.2",
|
||||
"prosemirror-markdown": "^1.13.1",
|
||||
"prosemirror-menu": "^1.2.4",
|
||||
"prosemirror-model": "^1.24.1",
|
||||
"prosemirror-schema-basic": "^1.2.3",
|
||||
"prosemirror-schema-list": "^1.5.0",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
"prosemirror-tables": "^1.6.4",
|
||||
"prosemirror-trailing-node": "^3.0.0",
|
||||
"prosemirror-transform": "^1.10.2",
|
||||
"prosemirror-view": "^1.38.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "14.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "^5",
|
||||
"@types/mdurl": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/unist": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/crelt": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@@ -32,6 +230,120 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/devlop": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
|
||||
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"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/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"uc.micro": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lowlight": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
|
||||
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"highlight.js": "~11.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-it": {
|
||||
"version": "14.1.1",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "^4.4.0",
|
||||
"linkify-it": "^5.0.0",
|
||||
"mdurl": "^2.0.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"uc.micro": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"markdown-it": "bin/markdown-it.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/orderedmap": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
@@ -45,6 +357,236 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-changeset": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||
"integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-collab": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-commands": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.10.2"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-dropcursor": {
|
||||
"version": "1.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0",
|
||||
"prosemirror-view": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-gapcursor": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.0.0",
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-view": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-history": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.2.2",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.31.0",
|
||||
"rope-sequence": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-inputrules": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-keymap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"w3c-keyname": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-markdown": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/markdown-it": "^14.0.0",
|
||||
"markdown-it": "^14.0.0",
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-menu": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
|
||||
"integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crelt": "^1.0.0",
|
||||
"prosemirror-commands": "^1.0.0",
|
||||
"prosemirror-history": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-model": {
|
||||
"version": "1.25.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-basic": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.25.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-schema-list": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.7.3"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-state": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
"prosemirror-view": "^1.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-tables": {
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-keymap": "^1.2.3",
|
||||
"prosemirror-model": "^1.25.4",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.10.5",
|
||||
"prosemirror-view": "^1.41.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-trailing-node": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@remirror/core-constants": "3.0.0",
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prosemirror-model": "^1.22.1",
|
||||
"prosemirror-state": "^1.4.2",
|
||||
"prosemirror-view": "^1.33.8"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-transform": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prosemirror-view": {
|
||||
"version": "1.41.8",
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
|
||||
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
"prosemirror-transform": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode.js": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/rope-sequence": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
|
||||
@@ -52,11 +594,25 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.8",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@tailwindcss/typography": "^0.5.19"
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tiptap/extension-code-block-lowlight": "^3.22.3",
|
||||
"@tiptap/extension-table": "^3.22.3",
|
||||
"@tiptap/extension-table-cell": "^3.22.3",
|
||||
"@tiptap/extension-table-header": "^3.22.3",
|
||||
"@tiptap/extension-table-row": "^3.22.3",
|
||||
"highlight.js": "^11.11.1",
|
||||
"lowlight": "^3.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user