mirror of
https://github.com/hcengineering/platform.git
synced 2025-05-25 01:15:14 +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 { 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 {
|
||||
block: MarkupNodeType
|
||||
block: SpecRule<MarkupNodeType>
|
||||
getAttrs?: (tok: Token, state: MarkdownParseState) => Attrs
|
||||
wrapContent?: 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
|
||||
}
|
||||
|
||||
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)) {
|
||||
handlers[type] = newSimpleBlockHandler(specBlock, spec)
|
||||
} else {
|
||||
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) {
|
||||
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) => {
|
||||
state.openNode(specBlock, attrs(spec, tok, state))
|
||||
state.openNode(readSpec(specBlock, tok, state), attrs(spec, tok, state))
|
||||
state.addText(withoutTrailingNewline(tok.content))
|
||||
state.closeNode()
|
||||
}
|
||||
@ -448,14 +462,24 @@ const tokensBlock: Record<string, ParsingBlockRule> = {
|
||||
getAttrs: (tok: Token) => ({ level: Number(tok.tag.slice(1)), marker: tok.markup })
|
||||
},
|
||||
code_block: {
|
||||
block: MarkupNodeType.code_block,
|
||||
block: (tok) => {
|
||||
if (tok.info === 'mermaid') {
|
||||
return MarkupNodeType.mermaid
|
||||
}
|
||||
return MarkupNodeType.code_block
|
||||
},
|
||||
getAttrs: (tok: Token) => {
|
||||
return { language: tok.info ?? '' }
|
||||
},
|
||||
noCloseToken: true
|
||||
},
|
||||
fence: {
|
||||
block: MarkupNodeType.code_block,
|
||||
block: (tok) => {
|
||||
if (tok.info === 'mermaid') {
|
||||
return MarkupNodeType.mermaid
|
||||
}
|
||||
return MarkupNodeType.code_block
|
||||
},
|
||||
getAttrs: (tok: Token) => {
|
||||
return { language: tok.info ?? '' }
|
||||
},
|
||||
|
@ -102,6 +102,13 @@ export const storeNodes: Record<string, NodeProcessor> = {
|
||||
state.write('```')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
mermaid: (state, node) => {
|
||||
state.write('```mermaid\n')
|
||||
state.renderInline(node)
|
||||
state.ensureNewLine()
|
||||
state.write('```')
|
||||
state.closeBlock(node)
|
||||
},
|
||||
heading: (state, node) => {
|
||||
const attrs = nodeAttrs(node)
|
||||
if (attrs.marker === '=' && attrs.level === 1) {
|
||||
|
@ -52,6 +52,7 @@
|
||||
"@hcengineering/collaborator-client": "^0.6.4",
|
||||
"@hcengineering/contact": "^0.6.24",
|
||||
"@hcengineering/presence": "^0.6.0",
|
||||
"@hcengineering/text-markdown": "^0.6.0",
|
||||
"@tiptap/core": "^2.11.7",
|
||||
"@tiptap/pm": "^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'
|
||||
export { getTargetObjectFromUrl, getReferenceFromUrl, getReferenceLabel } from './components/extension/reference'
|
||||
export { TodoItemExtension, TodoListExtension } from './components/extension/todo'
|
||||
export { TransformPastedContentExtension } from './components/extension/paste'
|
||||
|
||||
export * from '@hcengineering/presentation/src/types'
|
||||
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 TextAlign, { type TextAlignOptions } from '@tiptap/extension-text-align'
|
||||
import { LinkUtilsExtension } from '../components/extension/link'
|
||||
import { TransformPastedContentExtension } from '../components/extension/paste'
|
||||
|
||||
export interface EditorKitOptions extends DefaultKitOptions {
|
||||
history?: false
|
||||
@ -333,6 +334,8 @@ async function buildEditorKit (): Promise<Extension<EditorKitOptions, any>> {
|
||||
staticKitExtensions.push([1000, NoteExtension.configure(this.options.note ?? {})])
|
||||
}
|
||||
|
||||
staticKitExtensions.push([1100, TransformPastedContentExtension.configure({})])
|
||||
|
||||
const allKitExtensions = [...tableKitExtensions, ...modelKitExtensions, ...staticKitExtensions]
|
||||
|
||||
allKitExtensions.sort((a, b) => a[0] - b[0])
|
||||
|
@ -71,8 +71,9 @@ export class S3Service implements StorageAdapter {
|
||||
expireTime: number
|
||||
client: S3
|
||||
constructor (readonly opt: S3Config) {
|
||||
const endpoint = Number.isInteger(opt.port) ? `${opt.endpoint}:${opt.port}` : opt.endpoint
|
||||
this.client = new S3({
|
||||
endpoint: opt.endpoint,
|
||||
endpoint,
|
||||
credentials: {
|
||||
accessKeyId: opt.accessKey,
|
||||
secretAccessKey: opt.secretKey
|
||||
|
Loading…
Reference in New Issue
Block a user