UBERF-4795 Make editor extensions kit configurable (#4329)

Signed-off-by: Alexander Onnikov <alexander.onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-01-10 17:51:05 +07:00 committed by GitHub
parent e6ae8703b3
commit b84aa01534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 388 additions and 221 deletions

1
.vscode/launch.json vendored
View File

@ -90,6 +90,7 @@
"SECRET": "secret",
"METRICS_CONSOLE": "true",
"TRANSACTOR_URL": "ws://localhost:3333",
"UPLOAD_URL": "/files",
"MONGO_URL": "mongodb://localhost:27017",
"MINIO_ACCESS_KEY": "minioadmin",
"MINIO_SECRET_KEY": "minioadmin",

View File

@ -707,6 +707,9 @@ dependencies:
'@tiptap/extension-highlight':
specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history':
specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link':
specifier: ^2.1.12
version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
@ -23650,7 +23653,7 @@ packages:
dev: false
file:projects/text-editor.tgz(@types/node@16.11.68)(bufferutil@4.0.7)(esbuild@0.16.17)(postcss-load-config@4.0.1)(postcss@8.4.31)(prosemirror-model@1.19.3)(ts-node@10.9.1):
resolution: {integrity: sha512-OTmONKS1vIIvQHrDVFHNsaVZopEiXGbimLMlkHpMj1sznAdusJITPsPbj9Q3YLvIc8VXNSlQeRzN2QlkwW2IYg==, tarball: file:projects/text-editor.tgz}
resolution: {integrity: sha512-QgUDUW3v5zIVHETcfC/UsoZ8EPeauNcSyGO+I/J6rXLG2ndtNKIWRdl7Herj480/Q6JjPjWmm5RTIRAnz8lZXQ==, tarball: file:projects/text-editor.tgz}
id: file:projects/text-editor.tgz
name: '@rush-temp/text-editor'
version: 0.0.0
@ -23665,6 +23668,7 @@ packages:
'@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-list-keymap': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12)
@ -23735,7 +23739,7 @@ packages:
dev: false
file:projects/text.tgz(@types/node@16.11.68)(esbuild@0.16.17)(svelte@4.2.5)(ts-node@10.9.1):
resolution: {integrity: sha512-mAnt4uQfrjjGcqAXJI8y+sOq5joLxJAU4l8shfLkgdv0FJY+JcR7/lnT0cg79pAEySJ3WWb3RnXlyH+TtGejJQ==, tarball: file:projects/text.tgz}
resolution: {integrity: sha512-yBBmdVZ6t7OiE7WauaYvqDsgVsOUzS8W4sKslQ6EXa/gvDvnV3F/cjAMIvbH6B4w1bFOWoqgC25sqxO8HUaEOQ==, tarball: file:projects/text.tgz}
id: file:projects/text.tgz
name: '@rush-temp/text'
version: 0.0.0
@ -23744,6 +23748,7 @@ packages:
'@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-highlight': 2.1.12(@tiptap/core@2.1.12)
'@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-link': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)
'@tiptap/extension-mention': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(@tiptap/suggestion@2.1.12)
'@tiptap/extension-table': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)

View File

@ -103,6 +103,7 @@ services:
- COLLABORATOR_PORT=3078
- SECRET=secret
- TRANSACTOR_URL=ws://localhost:3333
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27017
- MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin

View File

@ -60,6 +60,7 @@
"@tiptap/extension-code-block": "^2.1.12",
"@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-table": "^2.1.12",
"@tiptap/extension-table-cell": "^2.1.12",
"@tiptap/extension-table-header": "^2.1.12",

View File

@ -25,7 +25,7 @@
import { calculateDecorations, createYdocDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions'
import { EditorKit } from '../kits/editor-kit'
export let ydoc: Ydoc
export let field: string | undefined = undefined
@ -79,7 +79,7 @@
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, { class: 'flex-grow' }) },
element,
editable: false,
extensions: [...defaultExtensions, DecorationExtension, Collaboration.configure({ document: ydoc, field })]
extensions: [EditorKit, DecorationExtension, Collaboration.configure({ document: ydoc, field })]
})
})

View File

@ -100,4 +100,5 @@
on:focus
on:blur
on:update
on:open-document
/>

View File

@ -27,6 +27,7 @@
import { Completion } from '../Completion'
import { textEditorCommandHandler } from '../commands'
import { EditorKit } from '../kits/editor-kit'
import textEditorPlugin from '../plugin'
import { DocumentId, TiptapCollabProvider } from '../provider'
import {
@ -47,7 +48,7 @@
import { FileAttachFunction, ImageExtension } from './extension/imageExt'
import { InlinePopupExtension } from './extension/inlinePopup'
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { completionConfig, defaultExtensions } from './extensions'
import { completionConfig } from './extensions'
export let documentId: DocumentId
export let field: string | undefined = undefined
@ -207,7 +208,8 @@
optionalExtensions.push(
ImageExtension.configure({
inline: true,
attachFile
attachFile,
uploadUrl: getMetadata(presentation.metadata.UploadURL)
})
)
}
@ -220,7 +222,7 @@
element,
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes, { class: 'flex-grow' }) },
extensions: [
...defaultExtensions,
EditorKit.configure({ history: false }),
...optionalExtensions,
Placeholder.configure({ placeholder: placeHolderStr }),
InlineStyleToolbarExtension.configure({
@ -259,7 +261,7 @@
dispatch('open-document', { event, _id, _class })
}
}),
EmojiExtension.configure(),
EmojiExtension,
...extensions
],
parseOptions: {

View File

@ -20,10 +20,13 @@
import { DecorationSet } from '@tiptap/pm/view'
import { onDestroy, onMount } from 'svelte'
import { Markup } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { calculateDecorations, createMarkupDocument } from './diff/decorations'
import { defaultEditorAttributes } from './editor/editorProps'
import { defaultExtensions } from './extensions'
import { ImageExtension } from './extension/imageExt'
import { EditorKit } from '../kits/editor-kit'
export let content: Markup
export let comparedVersion: Markup | undefined = undefined
@ -78,7 +81,13 @@
element,
content,
editable: false,
extensions: [...defaultExtensions, DecorationExtension],
extensions: [
EditorKit,
ImageExtension.configure({
uploadUrl: getMetadata(presentation.metadata.UploadURL)
}),
DecorationExtension
],
onTransaction: () => {
// force re-render so `editor.isActive` works as expected
editor = editor

View File

@ -1,5 +1,5 @@
<script lang="ts">
import { IntlString } from '@hcengineering/platform'
import { IntlString, getMetadata } from '@hcengineering/platform'
import presentation, { MessageViewer } from '@hcengineering/presentation'
import {
ActionIcon,
@ -175,7 +175,8 @@
attachFile,
reportNode: (id, node) => {
attachments.set(id, node)
}
},
uploadUrl: getMetadata(presentation.metadata.UploadURL)
})
const completionPlugin = Completion.configure({

View File

@ -31,7 +31,7 @@
import { InlinePopupExtension } from './extension/inlinePopup'
import { InlineStyleToolbarExtension } from './extension/inlineStyleToolbar'
import { SubmitExtension } from './extension/submit'
import { defaultExtensions } from './extensions'
import { EditorKit } from '../kits/editor-kit'
export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder
@ -124,7 +124,7 @@
editorProps: { attributes: mergeAttributes(defaultEditorAttributes, editorAttributes) },
content,
extensions: [
...defaultExtensions,
EditorKit,
...(supportSubmit ? [Handle] : []), // order important
Placeholder.configure({ placeholder: placeHolderStr }),
...extensions,

View File

@ -12,8 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { getMetadata } from '@hcengineering/platform'
import presentation, { PDFViewer, getFileUrl } from '@hcengineering/presentation'
import { PDFViewer } from '@hcengineering/presentation'
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
import { mergeAttributes, nodeInputRule } from '@tiptap/core'
@ -37,6 +36,7 @@ export type ImageAlignment = 'center' | 'left' | 'right'
export interface ImageOptions extends ImageNodeOptions {
attachFile?: FileAttachFunction
reportNode?: (id: string, node: ProseMirrorNode) => void
uploadUrl: string
}
export interface ImageAlignmentOptions {
@ -80,6 +80,11 @@ function getType (type: string): 'image' | 'other' {
return 'other'
}
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
function getFileUrl (fileId: string, size: IconSize = 'full', uploadUrl: string): string {
return `${uploadUrl}?file=${fileId}&size=${size as string}`
}
/**
* @public
*/
@ -87,7 +92,8 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
addOptions () {
return {
inline: true,
HTMLAttributes: {}
HTMLAttributes: {},
uploadUrl: ''
}
},
@ -117,9 +123,11 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
HTMLAttributes
)
const uploadUrl = this.options.uploadUrl ?? ''
const id = imgAttributes['file-id']
if (id != null) {
imgAttributes.src = getFileUrl(id, 'full')
imgAttributes.src = getFileUrl(id, 'full', uploadUrl)
let width: IconSize | undefined
switch (imgAttributes.width) {
case '32px':
@ -137,8 +145,9 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
break
}
if (width !== undefined) {
imgAttributes.src = getFileUrl(id, width)
imgAttributes.srcset = getFileUrl(id, width) + ' 1x,' + getFileUrl(id, getIconSize2x(width)) + ' 2x'
imgAttributes.src = getFileUrl(id, width, uploadUrl)
imgAttributes.srcset =
getFileUrl(id, width, uploadUrl) + ' 1x,' + getFileUrl(id, getIconSize2x(width), uploadUrl) + ' 2x'
}
imgAttributes.class = 'text-editor-image'
imgAttributes.contentEditable = false
@ -207,8 +216,7 @@ export const ImageExtension = ImageNode.extend<ImageOptions>({
for (const uri of uris) {
if (uri !== '') {
const url = new URL(uri)
const uploadUrl = getMetadata(presentation.metadata.UploadURL)
if (uploadUrl === undefined || !url.href.includes(uploadUrl)) {
if (opt.uploadUrl === undefined || !url.href.includes(opt.uploadUrl)) {
continue
}

View File

@ -1,91 +1,23 @@
import TableHeader from '@tiptap/extension-table-header'
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import { type Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import StarterKit from '@tiptap/starter-kit'
import Underline from '@tiptap/extension-underline'
import Gapcursor from '@tiptap/extension-gapcursor'
import ListKeymap from '@tiptap/extension-list-keymap'
import { type AnyExtension } from '@tiptap/core'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
import { type CompletionOptions } from '../Completion'
import MentionList from './MentionList.svelte'
import { NodeUuidExtension } from './extension/nodeUuid'
import { SvelteRenderer } from './node-view'
import { CodemarkExtension } from './extension/codemark'
import type { SuggestionKeyDownProps, SuggestionProps } from './extension/suggestion'
import { Table, TableCell, TableRow } from './extension/table'
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export const supportedHeadingLevels: Level[] = [1, 2, 3]
export const defaultExtensions: AnyExtension[] = [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: {
levels: supportedHeadingLevels,
HTMLAttributes: {
class: 'proseHeading'
}
}
}),
CodemarkExtension,
Highlight.configure({
multicolor: false
}),
Underline.configure({}),
Typography.configure({}),
Gapcursor,
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
ListKeymap.configure({}),
NodeUuidExtension,
...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now.
]
export const mInsertTable = [
{
label: '2x2',

View File

@ -0,0 +1,63 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Extension } from '@tiptap/core'
import type { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
import StarterKit from '@tiptap/starter-kit'
export interface DefaultKitOptions {
heading?: {
levels?: Level[]
}
history?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: this.options.heading,
history: this.options.history
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
})
]
}
})

View File

@ -0,0 +1,77 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Extension } from '@tiptap/core'
import { type Level } from '@tiptap/extension-heading'
import ListKeymap from '@tiptap/extension-list-keymap'
import TableHeader from '@tiptap/extension-table-header'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import Underline from '@tiptap/extension-underline'
import { DefaultKit, type DefaultKitOptions } from './default-kit'
import { CodemarkExtension } from '../components/extension/codemark'
import { NodeUuidExtension } from '../components/extension/nodeUuid'
import { Table, TableCell, TableRow } from '../components/extension/table'
const headingLevels: Level[] = [1, 2, 3]
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export interface EditorKitOptions extends DefaultKitOptions {
history?: false
}
export const EditorKit = Extension.create<EditorKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
DefaultKit.configure({
...this.options,
heading: {
levels: headingLevels
}
}),
CodemarkExtension,
Underline,
ListKeymap,
NodeUuidExtension,
...tableExtensions
// ...taskListExtensions // Disable since tasks are not working properly now.
]
}
})

View File

@ -37,6 +37,7 @@
"@tiptap/extension-gapcursor": "^2.1.12",
"@tiptap/extension-heading": "^2.1.12",
"@tiptap/extension-highlight": "^2.1.12",
"@tiptap/extension-history": "^2.1.12",
"@tiptap/extension-link": "^2.1.12",
"@tiptap/extension-mention": "^2.1.12",
"@tiptap/extension-table": "^2.1.12",

View File

@ -1,106 +0,0 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { AnyExtension } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import Typography from '@tiptap/extension-typography'
import StarterKit from '@tiptap/starter-kit'
import { ImageNode, ReferenceNode, TodoItemNode, TodoListNode } from './nodes'
/**
* @public
*/
export const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
/**
* @public
*/
export const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
/**
* @public
*/
export const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
/**
* @public
*/
export const defaultExtensions: AnyExtension[] = [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: {
levels: headingLevels
}
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
}),
...tableExtensions,
...taskListExtensions
]
/**
* @public
*/
export const serverExtensions: AnyExtension[] = [
...defaultExtensions,
ImageNode,
ReferenceNode,
TodoItemNode,
TodoListNode
]

View File

@ -17,7 +17,10 @@ import { Extensions, getSchema } from '@tiptap/core'
import { generateJSON, generateHTML } from '@tiptap/html'
import { Node as ProseMirrorNode } from '@tiptap/pm/model'
import { defaultExtensions } from './extensions'
import { ServerKit } from './kits/server-kit'
const defaultExtensions = [ServerKit]
/**
* @public
*/

View File

@ -13,8 +13,10 @@
// limitations under the License.
//
export * from './extensions'
export * from './html'
export * from './node'
export * from './nodes'
export * from './text'
export * from './kits/default-kit'
export * from './kits/server-kit'

View File

@ -0,0 +1,63 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Extension } from '@tiptap/core'
import { Level } from '@tiptap/extension-heading'
import Highlight from '@tiptap/extension-highlight'
import Link from '@tiptap/extension-link'
import Typography from '@tiptap/extension-typography'
import StarterKit from '@tiptap/starter-kit'
export interface DefaultKitOptions {
heading?: {
levels?: Level[]
}
history?: false
}
export const DefaultKit = Extension.create<DefaultKitOptions>({
name: 'defaultKit',
addExtensions () {
return [
StarterKit.configure({
code: {
HTMLAttributes: {
class: 'proseCode'
}
},
codeBlock: {
languageClassPrefix: 'language-',
exitOnArrowDown: true,
exitOnTripleEnter: true,
HTMLAttributes: {
class: 'proseCodeBlock'
}
},
heading: this.options.heading,
history: this.options.history
}),
Highlight.configure({
multicolor: false
}),
Typography.configure({}),
Link.configure({
openOnClick: true,
HTMLAttributes: { class: 'cursor-pointer', rel: 'noopener noreferrer', target: '_blank' }
})
]
}
})

View File

@ -0,0 +1,78 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { Extension } from '@tiptap/core'
import Table from '@tiptap/extension-table'
import TableCell from '@tiptap/extension-table-cell'
import TableHeader from '@tiptap/extension-table-header'
import TableRow from '@tiptap/extension-table-row'
import TaskItem from '@tiptap/extension-task-item'
import TaskList from '@tiptap/extension-task-list'
import { ImageNode, ImageOptions } from '../nodes/image'
import { ReferenceNode } from '../nodes/reference'
import { TodoItemNode, TodoListNode } from '../nodes/todo'
import { DefaultKit, DefaultKitOptions } from './default-kit'
import { Level } from '@tiptap/extension-heading'
const headingLevels: Level[] = [1, 2, 3, 4, 5, 6]
const tableExtensions = [
Table.configure({
resizable: false,
HTMLAttributes: {
class: 'proseTable'
}
}),
TableRow.configure({}),
TableHeader.configure({}),
TableCell.configure({})
]
const taskListExtensions = [
TaskList,
TaskItem.configure({
nested: true,
HTMLAttributes: {
class: 'flex flex-grow gap-1 checkbox_style'
}
})
]
export interface ServerKitOptions extends DefaultKitOptions {
image: Partial<ImageOptions>
}
export const ServerKit = Extension.create<ServerKitOptions>({
name: 'serverKit',
addExtensions () {
return [
DefaultKit.configure({
...this.options,
heading: {
levels: headingLevels
}
}),
...tableExtensions,
...taskListExtensions,
ImageNode.configure(this.options.image),
TodoItemNode,
TodoListNode,
ReferenceNode
]
}
})

View File

@ -21,6 +21,12 @@ import { getDataAttribute } from './utils'
export interface ImageOptions {
inline: boolean
HTMLAttributes: Record<string, any>
uploadUrl?: string
}
// This is a simplified version of getFileUrl from presentation plugin, which we cannot use
function getFileUrl (uploadUrl: string, fileId: string, size: string = 'full'): string {
return `${uploadUrl}?file=${fileId}&size=${size}`
}
/**
@ -32,7 +38,8 @@ export const ImageNode = Node.create<ImageOptions>({
addOptions () {
return {
inline: true,
HTMLAttributes: {}
HTMLAttributes: {},
uploadUrl: ''
}
},
@ -98,6 +105,12 @@ export const ImageNode = Node.create<ImageOptions>({
HTMLAttributes
)
const fileId = imgAttributes['file-id']
if (fileId != null) {
const uploadUrl = this.options.uploadUrl ?? ''
imgAttributes.src = getFileUrl(uploadUrl, fileId)
}
return ['div', divAttributes, ['img', imgAttributes]]
}
})

View File

@ -15,9 +15,9 @@
import chunter, { Backlink } from '@hcengineering/chunter'
import { Class, Data, Doc, Ref, Tx, TxFactory } from '@hcengineering/core'
import { extractReferences, getHTML, parseHTML, serverExtensions } from '@hcengineering/text'
import { ServerKit, extractReferences, getHTML, parseHTML } from '@hcengineering/text'
const extensions = serverExtensions
const extensions = [ServerKit]
export function getBacklinks (
backlinkId: Ref<Doc>,

View File

@ -26,6 +26,7 @@ export interface Config {
TransactorUrl: string
MongoUrl: string
UploadUrl: string
MinioEndpoint: string
MinioAccessKey: string
@ -39,6 +40,7 @@ const envMap: { [key in keyof Config]: string } = {
Port: 'COLLABORATOR_PORT',
TransactorUrl: 'TRANSACTOR_URL',
MongoUrl: 'MONGO_URL',
UploadUrl: 'UPLOAD_URL',
MinioEndpoint: 'MINIO_ENDPOINT',
MinioAccessKey: 'MINIO_ACCESS_KEY',
MinioSecretKey: 'MINIO_SECRET_KEY'
@ -63,6 +65,7 @@ const config: Config = (() => {
Port: parseInt(process.env[envMap.Port] ?? '3078'),
TransactorUrl: process.env[envMap.TransactorUrl],
MongoUrl: process.env[envMap.MongoUrl],
UploadUrl: process.env[envMap.UploadUrl] ?? '/files',
MinioEndpoint: process.env[envMap.MinioEndpoint],
MinioAccessKey: process.env[envMap.MinioAccessKey],
MinioSecretKey: process.env[envMap.MinioSecretKey]

View File

@ -15,7 +15,7 @@
import { MeasureContext } from '@hcengineering/core'
import { MinioService } from '@hcengineering/minio'
import { serverExtensions } from '@hcengineering/text'
import { ServerKit } from '@hcengineering/text'
import { Hocuspocus, onAuthenticatePayload } from '@hocuspocus/server'
import bp from 'body-parser'
import compression from 'compression'
@ -72,6 +72,14 @@ export async function start (
})
)
const extensions = [
ServerKit.configure({
image: {
uploadUrl: config.UploadUrl
}
})
]
const extensionsCtx = ctx.newChild('extensions', {})
const storageCtx = ctx.newChild('storage', {})
@ -111,7 +119,7 @@ export async function start (
extensions: [
new ActionsExtension({
ctx: extensionsCtx.newChild('actions', {}),
transformer: new HtmlTransformer(serverExtensions)
transformer: new HtmlTransformer(extensions)
}),
new StorageExtension({
ctx: extensionsCtx.newChild('storage', {}),
@ -121,12 +129,12 @@ export async function start (
mongodb: new MongodbStorageAdapter(
storageCtx.newChild('mongodb', {}),
mongo,
new HtmlTransformer(serverExtensions)
new HtmlTransformer(extensions)
),
platform: new PlatformStorageAdapter(
storageCtx.newChild('platform', {}),
config.TransactorUrl,
new HtmlTransformer(serverExtensions)
new HtmlTransformer(extensions)
)
},
'minio'

View File

@ -1,9 +1,9 @@
import { MeasureContext, WorkspaceId } from '@hcengineering/core'
import { ContentTextAdapter } from '@hcengineering/server-core'
import { getText, serverExtensions, yDocContentToNodes } from '@hcengineering/text'
import { ServerKit, getText, yDocContentToNodes } from '@hcengineering/text'
import { Readable } from 'stream'
const extensions = serverExtensions
const extensions = [ServerKit]
/**
* @public

View File

@ -111,6 +111,7 @@ services:
- COLLABORATOR_PORT=3078
- SECRET=secret
- TRANSACTOR_URL=ws://localhost:3334
- UPLOAD_URL=/files
- MONGO_URL=mongodb://mongodb:27018
- MINIO_ENDPOINT=minio
- MINIO_ACCESS_KEY=minioadmin