mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-25 09:30:27 +00:00
Merge develop into staging-new (#8622)
* fix: append port to s3 endpoint (#8601) Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Interpret plain text paste events in the text editor as an opportunity to parse and apply Markdown (#8618) Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> * Fixed Mermaid support in the Markdown renderer/parser. (#8620) Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> --------- Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Signed-off-by: Victor Ilyushchenko <alt13ri@gmail.com> Co-authored-by: Alexander Onnikov <aonnikov@hardcoreeng.com> Co-authored-by: Victor Ilyushchenko <alt13ri@gmail.com>
This commit is contained in:
parent
9e916670e1
commit
c820af080f
@ -751,6 +751,28 @@ Lorem ipsum dolor sit amet.
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'mermaid diagram',
|
||||||
|
markdown: '```mermaid\ngraph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;\n```',
|
||||||
|
markup: {
|
||||||
|
type: 'doc',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'mermaid',
|
||||||
|
attrs: {
|
||||||
|
language: 'mermaid'
|
||||||
|
},
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
marks: [],
|
||||||
|
text: 'graph TD;\n\tA-->B;\n\tA-->C;\n\tB-->D;\n\tC-->D;',
|
||||||
|
type: 'text'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -22,8 +22,17 @@ import type StateCore from 'markdown-it/lib/rules_core/state_core'
|
|||||||
import { addToSet, removeFromSet, sameSet } from './marks'
|
import { addToSet, removeFromSet, sameSet } from './marks'
|
||||||
import { nodeContent } from './node'
|
import { nodeContent } from './node'
|
||||||
|
|
||||||
|
type SpecRule<T> = T | ((tok: Token, state: MarkdownParseState) => T)
|
||||||
|
|
||||||
|
function readSpec<T> (rule: SpecRule<T>, tok: Token, state: MarkdownParseState): T {
|
||||||
|
if (typeof rule === 'function') {
|
||||||
|
return (rule as (tok: Token, state: MarkdownParseState) => T)(tok, state)
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
interface ParsingBlockRule {
|
interface ParsingBlockRule {
|
||||||
block: MarkupNodeType
|
block: SpecRule<MarkupNodeType>
|
||||||
getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs
|
getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs
|
||||||
wrapContent?: boolean
|
wrapContent?: boolean
|
||||||
noCloseToken?: boolean
|
noCloseToken?: boolean
|
||||||
@ -222,12 +231,17 @@ function withoutTrailingNewline (str: string): string {
|
|||||||
return str[str.length - 1] === '\n' ? str.slice(0, str.length - 1) : str
|
return str[str.length - 1] === '\n' ? str.slice(0, str.length - 1) : str
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSpecBlock (handlers: HandlersRecord, spec: ParsingBlockRule, type: string, specBlock: MarkupNodeType): void {
|
function addSpecBlock (
|
||||||
|
handlers: HandlersRecord,
|
||||||
|
spec: ParsingBlockRule,
|
||||||
|
type: string,
|
||||||
|
specBlock: SpecRule<MarkupNodeType>
|
||||||
|
): void {
|
||||||
if (noCloseToken(spec, type)) {
|
if (noCloseToken(spec, type)) {
|
||||||
handlers[type] = newSimpleBlockHandler(specBlock, spec)
|
handlers[type] = newSimpleBlockHandler(specBlock, spec)
|
||||||
} else {
|
} else {
|
||||||
handlers[type + '_open'] = (state, tok) => {
|
handlers[type + '_open'] = (state, tok) => {
|
||||||
state.openNode(specBlock, attrs(spec, tok, state))
|
state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state))
|
||||||
if (spec.wrapContent === true) {
|
if (spec.wrapContent === true) {
|
||||||
state.openNode(MarkupNodeType.paragraph, {})
|
state.openNode(MarkupNodeType.paragraph, {})
|
||||||
}
|
}
|
||||||
@ -240,9 +254,9 @@ function addSpecBlock (handlers: HandlersRecord, spec: ParsingBlockRule, type: s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function newSimpleBlockHandler (specBlock: MarkupNodeType, spec: ParsingBlockRule): HandlerRecord {
|
function newSimpleBlockHandler (specBlock: SpecRule<MarkupNodeType>, spec: ParsingBlockRule): HandlerRecord {
|
||||||
return (state, tok) => {
|
return (state, tok) => {
|
||||||
state.openNode(specBlock, attrs(spec, tok, state))
|
state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state))
|
||||||
state.addText(withoutTrailingNewline(tok.content))
|
state.addText(withoutTrailingNewline(tok.content))
|
||||||
state.closeNode()
|
state.closeNode()
|
||||||
}
|
}
|
||||||
@ -448,14 +462,24 @@ const tokensBlock: Record<string, ParsingBlockRule> = {
|
|||||||
getAttrs: (tok: Token) => ({ level: Number(tok.tag.slice(1)), marker: tok.markup })
|
getAttrs: (tok: Token) => ({ level: Number(tok.tag.slice(1)), marker: tok.markup })
|
||||||
},
|
},
|
||||||
code_block: {
|
code_block: {
|
||||||
block: MarkupNodeType.code_block,
|
block: (tok) => {
|
||||||
|
if (tok.info === 'mermaid') {
|
||||||
|
return MarkupNodeType.mermaid
|
||||||
|
}
|
||||||
|
return MarkupNodeType.code_block
|
||||||
|
},
|
||||||
getAttrs: (tok: Token) => {
|
getAttrs: (tok: Token) => {
|
||||||
return { language: tok.info ?? '' }
|
return { language: tok.info ?? '' }
|
||||||
},
|
},
|
||||||
noCloseToken: true
|
noCloseToken: true
|
||||||
},
|
},
|
||||||
fence: {
|
fence: {
|
||||||
block: MarkupNodeType.code_block,
|
block: (tok) => {
|
||||||
|
if (tok.info === 'mermaid') {
|
||||||
|
return MarkupNodeType.mermaid
|
||||||
|
}
|
||||||
|
return MarkupNodeType.code_block
|
||||||
|
},
|
||||||
getAttrs: (tok: Token) => {
|
getAttrs: (tok: Token) => {
|
||||||
return { language: tok.info ?? '' }
|
return { language: tok.info ?? '' }
|
||||||
},
|
},
|
||||||
|
@ -102,6 +102,13 @@ export const storeNodes: Record<string, NodeProcessor> = {
|
|||||||
state.write('```')
|
state.write('```')
|
||||||
state.closeBlock(node)
|
state.closeBlock(node)
|
||||||
},
|
},
|
||||||
|
mermaid: (state, node) => {
|
||||||
|
state.write('```mermaid\n')
|
||||||
|
state.renderInline(node)
|
||||||
|
state.ensureNewLine()
|
||||||
|
state.write('```')
|
||||||
|
state.closeBlock(node)
|
||||||
|
},
|
||||||
heading: (state, node) => {
|
heading: (state, node) => {
|
||||||
const attrs = nodeAttrs(node)
|
const attrs = nodeAttrs(node)
|
||||||
if (attrs.marker === '=' && attrs.level === 1) {
|
if (attrs.marker === '=' && attrs.level === 1) {
|
||||||
|
@ -52,6 +52,7 @@
|
|||||||
"@hcengineering/collaborator-client": "^0.6.4",
|
"@hcengineering/collaborator-client": "^0.6.4",
|
||||||
"@hcengineering/contact": "^0.6.24",
|
"@hcengineering/contact": "^0.6.24",
|
||||||
"@hcengineering/presence": "^0.6.0",
|
"@hcengineering/presence": "^0.6.0",
|
||||||
|
"@hcengineering/text-markdown": "^0.6.0",
|
||||||
"@tiptap/core": "^2.11.7",
|
"@tiptap/core": "^2.11.7",
|
||||||
"@tiptap/pm": "^2.11.7",
|
"@tiptap/pm": "^2.11.7",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.11.7",
|
"@tiptap/extension-code-block-lowlight": "^2.11.7",
|
||||||
|
121
plugins/text-editor-resources/src/components/extension/paste.ts
Normal file
121
plugins/text-editor-resources/src/components/extension/paste.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
//
|
||||||
|
// Copyright © 2025 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 { MarkupMarkType, MarkupNodeType, type MarkupNode } from '@hcengineering/text'
|
||||||
|
import { markdownToMarkup } from '@hcengineering/text-markdown'
|
||||||
|
import { Extension } from '@tiptap/core'
|
||||||
|
import { Node, type Schema } from '@tiptap/pm/model'
|
||||||
|
import { Plugin } from '@tiptap/pm/state'
|
||||||
|
|
||||||
|
export const TransformPastedContentExtension = Extension.create({
|
||||||
|
name: 'transformPastedContent',
|
||||||
|
|
||||||
|
addProseMirrorPlugins () {
|
||||||
|
return [PasteTextAsMarkdownPlugin()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function PasteTextAsMarkdownPlugin (): Plugin {
|
||||||
|
return new Plugin({
|
||||||
|
props: {
|
||||||
|
handlePaste (view, event, slice) {
|
||||||
|
const clipboardData = event.clipboardData
|
||||||
|
if (clipboardData === null) return false
|
||||||
|
|
||||||
|
const pastedText = clipboardData.getData('text/plain')
|
||||||
|
const isPlainPaste = clipboardData.types.length === 1 && clipboardData.types[0] === 'text/plain'
|
||||||
|
|
||||||
|
if (!isPlainPaste) return false
|
||||||
|
|
||||||
|
try {
|
||||||
|
const markupNode = cleanUnknownContent(view.state.schema, markdownToMarkup(pastedText))
|
||||||
|
if (shouldUseMarkdownOutput(markupNode)) {
|
||||||
|
const content = Node.fromJSON(view.state.schema, markupNode)
|
||||||
|
const transaction = view.state.tr.replaceSelectionWith(content)
|
||||||
|
view.dispatch(transaction)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Unable to convert plain text to markdown:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const importantMarkupNodeTypes = new Set<MarkupNodeType>([
|
||||||
|
MarkupNodeType.code_block,
|
||||||
|
MarkupNodeType.bullet_list,
|
||||||
|
MarkupNodeType.list_item,
|
||||||
|
MarkupNodeType.table,
|
||||||
|
MarkupNodeType.todoList,
|
||||||
|
MarkupNodeType.ordered_list,
|
||||||
|
MarkupNodeType.reference,
|
||||||
|
MarkupNodeType.image,
|
||||||
|
MarkupNodeType.heading,
|
||||||
|
MarkupNodeType.mermaid
|
||||||
|
])
|
||||||
|
|
||||||
|
const importantMarkupMarkTypes = new Set<MarkupMarkType>([
|
||||||
|
MarkupMarkType.bold,
|
||||||
|
MarkupMarkType.em,
|
||||||
|
MarkupMarkType.code,
|
||||||
|
MarkupMarkType.link
|
||||||
|
])
|
||||||
|
|
||||||
|
export function cleanUnknownContent (schema: Schema, node: MarkupNode): MarkupNode {
|
||||||
|
const traverse = (node: MarkupNode): void => {
|
||||||
|
if (node.content === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.content = node.content.filter((child) => schema.nodes[child.type] !== undefined)
|
||||||
|
node.content.forEach((child) => {
|
||||||
|
traverse(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(node)
|
||||||
|
return node
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldUseMarkdownOutput (node: MarkupNode): boolean {
|
||||||
|
const counter = {
|
||||||
|
importantNodes: 0,
|
||||||
|
importantMarks: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const traverse = (node: MarkupNode): void => {
|
||||||
|
if (importantMarkupNodeTypes.has(node.type)) {
|
||||||
|
counter.importantNodes++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === MarkupNodeType.text) {
|
||||||
|
for (const mark of node.marks ?? []) {
|
||||||
|
if (importantMarkupMarkTypes.has(mark.type)) {
|
||||||
|
counter.importantMarks++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
;(node.content ?? []).forEach((child) => {
|
||||||
|
traverse(child)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
traverse(node)
|
||||||
|
return counter.importantNodes > 0 || counter.importantMarks > 0
|
||||||
|
}
|
@ -29,6 +29,7 @@ import { createInlineComment, shouldShowCreateInlineCommentAction } from './comp
|
|||||||
import { isTextStylingEnabled, openBackgroundColorOptions, openTextColorOptions } from './components/extension/colors'
|
import { isTextStylingEnabled, openBackgroundColorOptions, openTextColorOptions } from './components/extension/colors'
|
||||||
export { getTargetObjectFromUrl, getReferenceFromUrl, getReferenceLabel } from './components/extension/reference'
|
export { getTargetObjectFromUrl, getReferenceFromUrl, getReferenceLabel } from './components/extension/reference'
|
||||||
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
||||||
|
export { TransformPastedContentExtension } from './components/extension/paste'
|
||||||
|
|
||||||
export * from '@hcengineering/presentation/src/types'
|
export * from '@hcengineering/presentation/src/types'
|
||||||
export type { EditorKitOptions } from './kits/editor-kit'
|
export type { EditorKitOptions } from './kits/editor-kit'
|
||||||
|
@ -49,6 +49,7 @@ import { DrawingBoardExtension, type DrawingBoardOptions } from '../components/e
|
|||||||
import { type IndendOptions, IndentExtension, indentExtensionOptions } from '../components/extension/indent'
|
import { type IndendOptions, IndentExtension, indentExtensionOptions } from '../components/extension/indent'
|
||||||
import TextAlign, { type TextAlignOptions } from '@tiptap/extension-text-align'
|
import TextAlign, { type TextAlignOptions } from '@tiptap/extension-text-align'
|
||||||
import { LinkUtilsExtension } from '../components/extension/link'
|
import { LinkUtilsExtension } from '../components/extension/link'
|
||||||
|
import { TransformPastedContentExtension } from '../components/extension/paste'
|
||||||
|
|
||||||
export interface EditorKitOptions extends DefaultKitOptions {
|
export interface EditorKitOptions extends DefaultKitOptions {
|
||||||
history?: false
|
history?: false
|
||||||
@ -333,6 +334,8 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
|||||||
staticKitExtensions.push([1000, NoteExtension.configure(this.options.note ?? {})])
|
staticKitExtensions.push([1000, NoteExtension.configure(this.options.note ?? {})])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
staticKitExtensions.push([1100, TransformPastedContentExtension.configure({})])
|
||||||
|
|
||||||
const allKitExtensions = [...tableKitExtensions, ...modelKitExtensions, ...staticKitExtensions]
|
const allKitExtensions = [...tableKitExtensions, ...modelKitExtensions, ...staticKitExtensions]
|
||||||
|
|
||||||
allKitExtensions.sort((a, b) => a[0] - b[0])
|
allKitExtensions.sort((a, b) => a[0] - b[0])
|
||||||
|
@ -71,8 +71,9 @@ export class S3Service implements StorageAdapter {
|
|||||||
expireTime: number
|
expireTime: number
|
||||||
client: S3
|
client: S3
|
||||||
constructor (readonly opt: S3Config) {
|
constructor (readonly opt: S3Config) {
|
||||||
|
const endpoint = Number.isInteger(opt.port) ? `${opt.endpoint}:${opt.port}` : opt.endpoint
|
||||||
this.client = new S3({
|
this.client = new S3({
|
||||||
endpoint: opt.endpoint,
|
endpoint,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: opt.accessKey,
|
accessKeyId: opt.accessKey,
|
||||||
secretAccessKey: opt.secretKey
|
secretAccessKey: opt.secretKey
|
||||||
|
Loading…
Reference in New Issue
Block a user