chore: upgrade lexical (#2889)
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { $createParagraphNode, $getRoot, $insertNodes, $nodesOfType, LexicalNode } from 'lexical'
|
||||
import { $createParagraphNode, $getRoot, $insertNodes, LexicalNode } from 'lexical'
|
||||
import { $generateNodesFromDOM } from '@lexical/html'
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
import { BlockEditorNodes } from '../SuperEditor/Lexical/Nodes/AllNodes'
|
||||
import BlocksEditorTheme from '../SuperEditor/Lexical/Theme/Theme'
|
||||
import { ClipPayload } from '@standardnotes/clipper/src/types/message'
|
||||
import { LinkNode } from '@lexical/link'
|
||||
import { $isLinkNode } from '@lexical/link'
|
||||
import { $dfs } from '@lexical/utils'
|
||||
|
||||
const AbsoluteLinkRegExp = new RegExp('^(?:[a-z+]+:)?//', 'i')
|
||||
|
||||
@@ -66,15 +67,18 @@ export const getSuperJSONFromClipPayload = async (clipPayload: ClipPayload) => {
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
editor.update(() => {
|
||||
$nodesOfType(LinkNode).forEach((linkNode) => {
|
||||
const url = linkNode.getURL()
|
||||
for (const { node } of $dfs()) {
|
||||
if (!$isLinkNode(node)) {
|
||||
continue
|
||||
}
|
||||
const url = node.getURL()
|
||||
const isAbsoluteLink = AbsoluteLinkRegExp.test(url)
|
||||
|
||||
if (!isAbsoluteLink) {
|
||||
const fixedURL = new URL(url, clipURL)
|
||||
linkNode.setURL(fixedURL.toString())
|
||||
node.setURL(fixedURL.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resolve()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { parseAndCreateZippableFileName } from '@standardnotes/utils'
|
||||
import { DOMExportOutput, Spread } from 'lexical'
|
||||
import { DOMExportOutput, ElementFormatType, NodeKey, Spread } from 'lexical'
|
||||
|
||||
type SerializedFileExportNode = Spread<
|
||||
{
|
||||
@@ -20,14 +20,14 @@ export class FileExportNode extends DecoratorBlockNode {
|
||||
return 'file-export'
|
||||
}
|
||||
|
||||
constructor(name: string, mimeType: string) {
|
||||
super()
|
||||
constructor(name: string, mimeType: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__name = name
|
||||
this.__mimeType = mimeType
|
||||
}
|
||||
|
||||
static clone(node: FileExportNode): FileExportNode {
|
||||
return new FileExportNode(node.__name, node.__mimeType)
|
||||
return new FileExportNode(node.__name, node.__mimeType, node.__format, node.__key)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedFileExportNode): FileExportNode {
|
||||
|
||||
@@ -127,6 +127,11 @@ export class TweetNode extends DecoratorBlockNode {
|
||||
return 'tweet'
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
static override clone(node: TweetNode): TweetNode {
|
||||
return new TweetNode(node.__id, node.__format, node.__key)
|
||||
}
|
||||
@@ -168,11 +173,6 @@ export class TweetNode extends DecoratorBlockNode {
|
||||
return { element }
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.__id
|
||||
}
|
||||
|
||||
@@ -72,6 +72,11 @@ export class YouTubeNode extends DecoratorBlockNode {
|
||||
return 'youtube'
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
static clone(node: YouTubeNode): YouTubeNode {
|
||||
return new YouTubeNode(node.__id, node.__format, node.__key)
|
||||
}
|
||||
@@ -121,11 +126,6 @@ export class YouTubeNode extends DecoratorBlockNode {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
}
|
||||
|
||||
updateDOM(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,12 +7,9 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Taken from https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownExport.ts
|
||||
* but modified using changes from https://github.com/facebook/lexical/pull/4957 to make nested elements work
|
||||
* better when exporting to markdown.
|
||||
*/
|
||||
* Taken from https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownExport.ts but modified using changes from https://github.com/facebook/lexical/pull/4957 to make nested elements work better when exporting to markdown.
|
||||
* */
|
||||
|
||||
import type { ElementTransformer, TextFormatTransformer, TextMatchTransformer, Transformer } from '@lexical/markdown'
|
||||
import {
|
||||
ElementNode,
|
||||
LexicalNode,
|
||||
@@ -24,10 +21,27 @@ import {
|
||||
$isLineBreakNode,
|
||||
$isTextNode,
|
||||
} from 'lexical'
|
||||
import { TRANSFORMERS, transformersByType } from './MarkdownImportExportUtils'
|
||||
|
||||
export function createMarkdownExport(transformers: Array<Transformer>): (node?: ElementNode) => string {
|
||||
import {
|
||||
ElementTransformer,
|
||||
MultilineElementTransformer,
|
||||
TextFormatTransformer,
|
||||
TextMatchTransformer,
|
||||
Transformer,
|
||||
} from '@lexical/markdown'
|
||||
|
||||
import { isEmptyParagraph, TRANSFORMERS, transformersByType } from './MarkdownImportExportUtils'
|
||||
|
||||
/**
|
||||
* Renders string from markdown. The selection is moved to the start after the operation.
|
||||
*/
|
||||
function createMarkdownExport(
|
||||
transformers: Array<Transformer>,
|
||||
shouldPreserveNewLines: boolean = false,
|
||||
): (node?: ElementNode) => string {
|
||||
const byType = transformersByType(transformers)
|
||||
const elementTransformers = [...byType.multilineElement, ...byType.element]
|
||||
const isNewlineDelimited = !shouldPreserveNewLines
|
||||
|
||||
// Export only uses text formats that are responsible for single format
|
||||
// e.g. it will filter out *** (bold, italic) and instead use separate ** and *
|
||||
@@ -37,25 +51,35 @@ export function createMarkdownExport(transformers: Array<Transformer>): (node?:
|
||||
const output = []
|
||||
const children = (node || $getRoot()).getChildren()
|
||||
|
||||
for (const child of children) {
|
||||
const result = exportTopLevelElements(child, byType.element, textFormatTransformers, byType.textMatch)
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i]
|
||||
const result = exportTopLevelElements(child, elementTransformers, textFormatTransformers, byType.textMatch)
|
||||
|
||||
if (result != null) {
|
||||
output.push(result)
|
||||
output.push(
|
||||
// separate consecutive group of texts with a line break: eg. ["hello", "world"] -> ["hello", "/nworld"]
|
||||
isNewlineDelimited && i > 0 && !isEmptyParagraph(child) && !isEmptyParagraph(children[i - 1])
|
||||
? '\n'.concat(result)
|
||||
: result,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return output.join('\n\n')
|
||||
// Ensure consecutive groups of texts are at least \n\n apart while each empty paragraph render as a newline.
|
||||
// Eg. ["hello", "", "", "hi", "\nworld"] -> "hello\n\n\nhi\n\nworld"
|
||||
return output.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
function exportTopLevelElements(
|
||||
node: LexicalNode,
|
||||
elementTransformers: Array<ElementTransformer>,
|
||||
elementTransformers: Array<ElementTransformer | MultilineElementTransformer>,
|
||||
textTransformersIndex: Array<TextFormatTransformer>,
|
||||
textMatchTransformers: Array<TextMatchTransformer>,
|
||||
): string | null {
|
||||
for (const transformer of elementTransformers) {
|
||||
if (!transformer.export) {
|
||||
continue
|
||||
}
|
||||
const result = transformer.export(node, (_node) =>
|
||||
exportChildren(_node, elementTransformers, textTransformersIndex, textMatchTransformers),
|
||||
)
|
||||
@@ -76,23 +100,33 @@ function exportTopLevelElements(
|
||||
|
||||
function exportChildren(
|
||||
node: ElementNode,
|
||||
elementTransformers: Array<ElementTransformer>,
|
||||
elementTransformers: Array<ElementTransformer | MultilineElementTransformer>,
|
||||
textTransformersIndex: Array<TextFormatTransformer>,
|
||||
textMatchTransformers: Array<TextMatchTransformer>,
|
||||
): string {
|
||||
const output = []
|
||||
const children = node.getChildren()
|
||||
const childrenLength = children.length
|
||||
// keep track of unclosed tags from the very beginning
|
||||
const unclosedTags: { format: TextFormatType; tag: string }[] = []
|
||||
|
||||
mainLoop: for (let childIndex = 0; childIndex < childrenLength; childIndex++) {
|
||||
const child = children[childIndex]
|
||||
const isLastChild = childIndex === childrenLength - 1
|
||||
|
||||
mainLoop: for (const child of children) {
|
||||
if ($isElementNode(child)) {
|
||||
for (const transformer of elementTransformers) {
|
||||
if (!transformer.export) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = transformer.export(child, (_node) =>
|
||||
exportChildren(_node, elementTransformers, textTransformersIndex, textMatchTransformers),
|
||||
)
|
||||
|
||||
if (result != null) {
|
||||
output.push(result)
|
||||
if (children.indexOf(child) !== children.length - 1) {
|
||||
if (!isLastChild) {
|
||||
output.push('\n')
|
||||
}
|
||||
continue mainLoop
|
||||
@@ -101,10 +135,14 @@ function exportChildren(
|
||||
}
|
||||
|
||||
for (const transformer of textMatchTransformers) {
|
||||
if (!transformer.export) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = transformer.export(
|
||||
child,
|
||||
(parentNode) => exportChildren(parentNode, elementTransformers, textTransformersIndex, textMatchTransformers),
|
||||
(textNode, textContent) => exportTextFormat(textNode, textContent, textTransformersIndex),
|
||||
(textNode, textContent) => exportTextFormat(textNode, textContent, textTransformersIndex, unclosedTags),
|
||||
)
|
||||
|
||||
if (result != null) {
|
||||
@@ -116,9 +154,15 @@ function exportChildren(
|
||||
if ($isLineBreakNode(child)) {
|
||||
output.push('\n')
|
||||
} else if ($isTextNode(child)) {
|
||||
output.push(exportTextFormat(child, child.getTextContent(), textTransformersIndex))
|
||||
output.push(exportTextFormat(child, child.getTextContent(), textTransformersIndex, unclosedTags))
|
||||
} else if ($isElementNode(child)) {
|
||||
output.push(exportChildren(child, elementTransformers, textTransformersIndex, textMatchTransformers), '\n')
|
||||
// empty paragraph returns ""
|
||||
output.push(exportChildren(child, elementTransformers, textTransformersIndex, textMatchTransformers))
|
||||
// Don't insert linebreak after last child
|
||||
if (!isLastChild) {
|
||||
// Insert two line breaks to create a space between two paragraphs or other elements, as required by Markdown syntax.
|
||||
output.push('\n', '\n')
|
||||
}
|
||||
} else if ($isDecoratorNode(child)) {
|
||||
output.push(child.getTextContent())
|
||||
}
|
||||
@@ -127,13 +171,26 @@ function exportChildren(
|
||||
return output.join('')
|
||||
}
|
||||
|
||||
function exportTextFormat(node: TextNode, textContent: string, textTransformers: Array<TextFormatTransformer>): string {
|
||||
function exportTextFormat(
|
||||
node: TextNode,
|
||||
textContent: string,
|
||||
textTransformers: Array<TextFormatTransformer>,
|
||||
// unclosed tags include the markdown tags that haven't been closed yet, and their associated formats
|
||||
unclosedTags: Array<{ format: TextFormatType; tag: string }>,
|
||||
): string {
|
||||
// This function handles the case of a string looking like this: " foo "
|
||||
// Where it would be invalid markdown to generate: "** foo **"
|
||||
// We instead want to trim the whitespace out, apply formatting, and then
|
||||
// bring the whitespace back. So our returned string looks like this: " **foo** "
|
||||
const frozenString = textContent.trim()
|
||||
let output = frozenString
|
||||
// the opening tags to be added to the result
|
||||
let openingTags = ''
|
||||
// the closing tags to be added to the result
|
||||
let closingTags = ''
|
||||
|
||||
const prevNode = getTextSibling(node, true)
|
||||
const nextNode = getTextSibling(node, false)
|
||||
|
||||
const applied = new Set()
|
||||
|
||||
@@ -141,27 +198,39 @@ function exportTextFormat(node: TextNode, textContent: string, textTransformers:
|
||||
const format = transformer.format[0]
|
||||
const tag = transformer.tag
|
||||
|
||||
// dedup applied formats
|
||||
if (hasFormat(node, format) && !applied.has(format)) {
|
||||
// Multiple tags might be used for the same format (*, _)
|
||||
applied.add(format)
|
||||
// Prevent adding opening tag is already opened by the previous sibling
|
||||
const previousNode = getTextSibling(node, true)
|
||||
|
||||
if (!hasFormat(previousNode, format)) {
|
||||
output = tag + output
|
||||
}
|
||||
|
||||
// Prevent adding closing tag if next sibling will do it
|
||||
const nextNode = getTextSibling(node, false)
|
||||
|
||||
if (!hasFormat(nextNode, format)) {
|
||||
output += tag
|
||||
// append the tag to openningTags, if it's not applied to the previous nodes,
|
||||
// or the nodes before that (which would result in an unclosed tag)
|
||||
if (!hasFormat(prevNode, format) || !unclosedTags.find((element) => element.tag === tag)) {
|
||||
unclosedTags.push({ format, tag })
|
||||
openingTags += tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// close any tags in the same order they were applied, if necessary
|
||||
for (let i = 0; i < unclosedTags.length; i++) {
|
||||
// prevent adding closing tag if next sibling will do it
|
||||
if (hasFormat(nextNode, unclosedTags[i].format)) {
|
||||
continue
|
||||
}
|
||||
|
||||
while (unclosedTags.length > i) {
|
||||
const unclosedTag = unclosedTags.pop()
|
||||
if (unclosedTag && typeof unclosedTag.tag === 'string') {
|
||||
closingTags += unclosedTag.tag
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
output = openingTags + output + closingTags
|
||||
// Replace trimmed version of textContent ensuring surrounding whitespace is not modified
|
||||
return textContent.replace(frozenString, output)
|
||||
return textContent.replace(frozenString, () => output)
|
||||
}
|
||||
|
||||
// Get next or previous text sibling a text node, including cases
|
||||
@@ -208,7 +277,11 @@ function hasFormat(node: LexicalNode | null | undefined, format: TextFormatType)
|
||||
return $isTextNode(node) && node.hasFormat(format)
|
||||
}
|
||||
|
||||
export function $convertToMarkdownString(transformers: Array<Transformer> = TRANSFORMERS, node?: ElementNode): string {
|
||||
const exportMarkdown = createMarkdownExport(transformers)
|
||||
export function $convertToMarkdownString(
|
||||
transformers: Array<Transformer> = TRANSFORMERS,
|
||||
node?: ElementNode,
|
||||
shouldPreserveNewLines: boolean = false,
|
||||
): string {
|
||||
const exportMarkdown = createMarkdownExport(transformers, shouldPreserveNewLines)
|
||||
return exportMarkdown(node)
|
||||
}
|
||||
|
||||
@@ -1,368 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Taken from https://github.com/facebook/lexical/blob/main/packages/lexical-markdown/src/MarkdownImport.ts
|
||||
* but modified to allow keeping new lines when importing markdown.
|
||||
*/
|
||||
|
||||
import { CodeNode, $createCodeNode } from '@lexical/code'
|
||||
import { ElementTransformer, TextFormatTransformer, TextMatchTransformer, Transformer } from '@lexical/markdown'
|
||||
|
||||
import { $isListItemNode, $isListNode, ListItemNode } from '@lexical/list'
|
||||
import { $isQuoteNode } from '@lexical/rich-text'
|
||||
import { $findMatchingParent } from '@lexical/utils'
|
||||
import {
|
||||
LexicalNode,
|
||||
TextNode,
|
||||
$createLineBreakNode,
|
||||
$createParagraphNode,
|
||||
$createTextNode,
|
||||
$getRoot,
|
||||
$getSelection,
|
||||
$isParagraphNode,
|
||||
$isTextNode,
|
||||
ElementNode,
|
||||
} from 'lexical'
|
||||
import { IS_APPLE_WEBKIT, IS_IOS, IS_SAFARI } from '../Shared/environment'
|
||||
import { TRANSFORMERS, transformersByType } from './MarkdownImportExportUtils'
|
||||
|
||||
const PUNCTUATION_OR_SPACE = /[!-/:-@[-`{-~\s]/
|
||||
|
||||
const MARKDOWN_EMPTY_LINE_REG_EXP = /^\s{0,3}$/
|
||||
const CODE_BLOCK_REG_EXP = /^```(\w{1,10})?\s?$/
|
||||
type TextFormatTransformersIndex = Readonly<{
|
||||
fullMatchRegExpByTag: Readonly<Record<string, RegExp>>
|
||||
openTagsRegExp: RegExp
|
||||
transformersByTag: Readonly<Record<string, TextFormatTransformer>>
|
||||
}>
|
||||
|
||||
function createMarkdownImport(
|
||||
transformers: Array<Transformer>,
|
||||
): (markdownString: string, node?: ElementNode, keepNewLines?: boolean) => void {
|
||||
const byType = transformersByType(transformers)
|
||||
const textFormatTransformersIndex = createTextFormatTransformersIndex(byType.textFormat)
|
||||
|
||||
return (markdownString, node, keepNewLines = false) => {
|
||||
const lines = markdownString.split('\n')
|
||||
const linesLength = lines.length
|
||||
const root = node || $getRoot()
|
||||
root.clear()
|
||||
|
||||
for (let i = 0; i < linesLength; i++) {
|
||||
const lineText = lines[i]
|
||||
// Codeblocks are processed first as anything inside such block
|
||||
// is ignored for further processing
|
||||
// TODO:
|
||||
// Abstract it to be dynamic as other transformers (add multiline match option)
|
||||
const [codeBlockNode, shiftedIndex] = importCodeBlock(lines, i, root)
|
||||
|
||||
if (codeBlockNode != null) {
|
||||
i = shiftedIndex
|
||||
continue
|
||||
}
|
||||
|
||||
importBlocks(lineText, root, byType.element, textFormatTransformersIndex, byType.textMatch)
|
||||
}
|
||||
|
||||
if (!keepNewLines) {
|
||||
// Removing empty paragraphs as md does not really
|
||||
// allow empty lines and uses them as dilimiter
|
||||
const children = root.getChildren()
|
||||
for (const child of children) {
|
||||
if (isEmptyParagraph(child)) {
|
||||
child.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($getSelection() !== null) {
|
||||
root.selectEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isEmptyParagraph(node: LexicalNode): boolean {
|
||||
if (!$isParagraphNode(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const firstChild = node.getFirstChild()
|
||||
return (
|
||||
firstChild == null ||
|
||||
(node.getChildrenSize() === 1 &&
|
||||
$isTextNode(firstChild) &&
|
||||
MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent()))
|
||||
)
|
||||
}
|
||||
|
||||
function importBlocks(
|
||||
lineText: string,
|
||||
rootNode: ElementNode,
|
||||
elementTransformers: Array<ElementTransformer>,
|
||||
textFormatTransformersIndex: TextFormatTransformersIndex,
|
||||
textMatchTransformers: Array<TextMatchTransformer>,
|
||||
) {
|
||||
const lineTextTrimmed = lineText.trim()
|
||||
const textNode = $createTextNode(lineTextTrimmed)
|
||||
const elementNode = $createParagraphNode()
|
||||
elementNode.append(textNode)
|
||||
rootNode.append(elementNode)
|
||||
|
||||
for (const { regExp, replace } of elementTransformers) {
|
||||
const match = lineText.match(regExp)
|
||||
|
||||
if (match) {
|
||||
textNode.setTextContent(lineText.slice(match[0].length))
|
||||
replace(elementNode, [textNode], match, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
importTextFormatTransformers(textNode, textFormatTransformersIndex, textMatchTransformers)
|
||||
|
||||
// If no transformer found and we left with original paragraph node
|
||||
// can check if its content can be appended to the previous node
|
||||
// if it's a paragraph, quote or list
|
||||
if (elementNode.isAttached() && lineTextTrimmed.length > 0) {
|
||||
const previousNode = elementNode.getPreviousSibling()
|
||||
if ($isParagraphNode(previousNode) || $isQuoteNode(previousNode) || $isListNode(previousNode)) {
|
||||
let targetNode: typeof previousNode | ListItemNode | null = previousNode
|
||||
|
||||
if ($isListNode(previousNode)) {
|
||||
const lastDescendant = previousNode.getLastDescendant()
|
||||
if (lastDescendant == null) {
|
||||
targetNode = null
|
||||
} else {
|
||||
targetNode = $findMatchingParent(lastDescendant, $isListItemNode)
|
||||
}
|
||||
}
|
||||
|
||||
if (targetNode != null && targetNode.getTextContentSize() > 0) {
|
||||
targetNode.splice(targetNode.getChildrenSize(), 0, [$createLineBreakNode(), ...elementNode.getChildren()])
|
||||
elementNode.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function importCodeBlock(
|
||||
lines: Array<string>,
|
||||
startLineIndex: number,
|
||||
rootNode: ElementNode,
|
||||
): [CodeNode | null, number] {
|
||||
const openMatch = lines[startLineIndex].match(CODE_BLOCK_REG_EXP)
|
||||
|
||||
if (openMatch) {
|
||||
let endLineIndex = startLineIndex
|
||||
const linesLength = lines.length
|
||||
|
||||
while (++endLineIndex < linesLength) {
|
||||
const closeMatch = lines[endLineIndex].match(CODE_BLOCK_REG_EXP)
|
||||
|
||||
if (closeMatch) {
|
||||
const codeBlockNode = $createCodeNode(openMatch[1])
|
||||
const textNode = $createTextNode(lines.slice(startLineIndex + 1, endLineIndex).join('\n'))
|
||||
codeBlockNode.append(textNode)
|
||||
rootNode.append(codeBlockNode)
|
||||
return [codeBlockNode, endLineIndex]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [null, startLineIndex]
|
||||
}
|
||||
|
||||
// Processing text content and replaces text format tags.
|
||||
// It takes outermost tag match and its content, creates text node with
|
||||
// format based on tag and then recursively executed over node's content
|
||||
//
|
||||
// E.g. for "*Hello **world**!*" string it will create text node with
|
||||
// "Hello **world**!" content and italic format and run recursively over
|
||||
// its content to transform "**world**" part
|
||||
function importTextFormatTransformers(
|
||||
textNode: TextNode,
|
||||
textFormatTransformersIndex: TextFormatTransformersIndex,
|
||||
textMatchTransformers: Array<TextMatchTransformer>,
|
||||
) {
|
||||
const textContent = textNode.getTextContent()
|
||||
const match = findOutermostMatch(textContent, textFormatTransformersIndex)
|
||||
|
||||
if (!match) {
|
||||
// Once text format processing is done run text match transformers, as it
|
||||
// only can span within single text node (unline formats that can cover multiple nodes)
|
||||
importTextMatchTransformers(textNode, textMatchTransformers)
|
||||
return
|
||||
}
|
||||
|
||||
let currentNode, remainderNode, leadingNode
|
||||
|
||||
// If matching full content there's no need to run splitText and can reuse existing textNode
|
||||
// to update its content and apply format. E.g. for **_Hello_** string after applying bold
|
||||
// format (**) it will reuse the same text node to apply italic (_)
|
||||
if (match[0] === textContent) {
|
||||
currentNode = textNode
|
||||
} else {
|
||||
const startIndex = match.index || 0
|
||||
const endIndex = startIndex + match[0].length
|
||||
|
||||
if (startIndex === 0) {
|
||||
;[currentNode, remainderNode] = textNode.splitText(endIndex)
|
||||
} else {
|
||||
;[leadingNode, currentNode, remainderNode] = textNode.splitText(startIndex, endIndex)
|
||||
}
|
||||
}
|
||||
|
||||
currentNode.setTextContent(match[2])
|
||||
const transformer = textFormatTransformersIndex.transformersByTag[match[1]]
|
||||
|
||||
if (transformer) {
|
||||
for (const format of transformer.format) {
|
||||
if (!currentNode.hasFormat(format)) {
|
||||
currentNode.toggleFormat(format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively run over inner text if it's not inline code
|
||||
if (!currentNode.hasFormat('code')) {
|
||||
importTextFormatTransformers(currentNode, textFormatTransformersIndex, textMatchTransformers)
|
||||
}
|
||||
|
||||
// Run over leading/remaining text if any
|
||||
if (leadingNode) {
|
||||
importTextFormatTransformers(leadingNode, textFormatTransformersIndex, textMatchTransformers)
|
||||
}
|
||||
|
||||
if (remainderNode) {
|
||||
importTextFormatTransformers(remainderNode, textFormatTransformersIndex, textMatchTransformers)
|
||||
}
|
||||
}
|
||||
|
||||
function importTextMatchTransformers(textNode_: TextNode, textMatchTransformers: Array<TextMatchTransformer>) {
|
||||
let textNode = textNode_
|
||||
|
||||
mainLoop: while (textNode) {
|
||||
for (const transformer of textMatchTransformers) {
|
||||
const match = textNode.getTextContent().match(transformer.importRegExp)
|
||||
|
||||
if (!match) {
|
||||
continue
|
||||
}
|
||||
|
||||
const startIndex = match.index || 0
|
||||
const endIndex = startIndex + match[0].length
|
||||
let replaceNode, newTextNode
|
||||
|
||||
if (startIndex === 0) {
|
||||
;[replaceNode, textNode] = textNode.splitText(endIndex)
|
||||
} else {
|
||||
;[, replaceNode, newTextNode] = textNode.splitText(startIndex, endIndex)
|
||||
}
|
||||
if (newTextNode) {
|
||||
importTextMatchTransformers(newTextNode, textMatchTransformers)
|
||||
}
|
||||
transformer.replace(replaceNode, match)
|
||||
continue mainLoop
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Finds first "<tag>content<tag>" match that is not nested into another tag
|
||||
function findOutermostMatch(
|
||||
textContent: string,
|
||||
textTransformersIndex: TextFormatTransformersIndex,
|
||||
): RegExpMatchArray | null {
|
||||
const openTagsMatch = textContent.match(textTransformersIndex.openTagsRegExp)
|
||||
|
||||
if (openTagsMatch == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const match of openTagsMatch) {
|
||||
// Open tags reg exp might capture leading space so removing it
|
||||
// before using match to find transformer
|
||||
const tag = match.replace(/^\s/, '')
|
||||
const fullMatchRegExp = textTransformersIndex.fullMatchRegExpByTag[tag]
|
||||
if (fullMatchRegExp == null) {
|
||||
continue
|
||||
}
|
||||
|
||||
const fullMatch = textContent.match(fullMatchRegExp)
|
||||
const transformer = textTransformersIndex.transformersByTag[tag]
|
||||
if (fullMatch != null && transformer != null) {
|
||||
if (transformer.intraword !== false) {
|
||||
return fullMatch
|
||||
}
|
||||
|
||||
// For non-intraword transformers checking if it's within a word
|
||||
// or surrounded with space/punctuation/newline
|
||||
const { index = 0 } = fullMatch
|
||||
const beforeChar = textContent[index - 1]
|
||||
const afterChar = textContent[index + fullMatch[0].length]
|
||||
|
||||
if (
|
||||
(!beforeChar || PUNCTUATION_OR_SPACE.test(beforeChar)) &&
|
||||
(!afterChar || PUNCTUATION_OR_SPACE.test(afterChar))
|
||||
) {
|
||||
return fullMatch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function createTextFormatTransformersIndex(
|
||||
textTransformers: Array<TextFormatTransformer>,
|
||||
): TextFormatTransformersIndex {
|
||||
const transformersByTag: Record<string, TextFormatTransformer> = {}
|
||||
const fullMatchRegExpByTag: Record<string, RegExp> = {}
|
||||
const openTagsRegExp = []
|
||||
const escapeRegExp = '(?<![\\\\])'
|
||||
|
||||
for (const transformer of textTransformers) {
|
||||
const { tag } = transformer
|
||||
transformersByTag[tag] = transformer
|
||||
const tagRegExp = tag.replace(/(\*|\^|\+)/g, '\\$1')
|
||||
openTagsRegExp.push(tagRegExp)
|
||||
|
||||
if (IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT) {
|
||||
fullMatchRegExpByTag[tag] = new RegExp(
|
||||
`(${tagRegExp})(?![${tagRegExp}\\s])(.*?[^${tagRegExp}\\s])${tagRegExp}(?!${tagRegExp})`,
|
||||
)
|
||||
} else {
|
||||
fullMatchRegExpByTag[tag] = new RegExp(
|
||||
`(?<![\\\\${tagRegExp}])(${tagRegExp})((\\\\${tagRegExp})?.*?[^${tagRegExp}\\s](\\\\${tagRegExp})?)((?<!\\\\)|(?<=\\\\\\\\))(${tagRegExp})(?![\\\\${tagRegExp}])`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// Reg exp to find open tag + content + close tag
|
||||
fullMatchRegExpByTag,
|
||||
// Reg exp to find opening tags
|
||||
openTagsRegExp: new RegExp(
|
||||
(IS_SAFARI || IS_IOS || IS_APPLE_WEBKIT ? '' : `${escapeRegExp}`) + '(' + openTagsRegExp.join('|') + ')',
|
||||
'g',
|
||||
),
|
||||
transformersByTag,
|
||||
}
|
||||
}
|
||||
|
||||
export function $convertFromMarkdownString(
|
||||
markdown: string,
|
||||
transformers: Array<Transformer> = TRANSFORMERS,
|
||||
node?: ElementNode,
|
||||
keepNewLines = false,
|
||||
): void {
|
||||
const importMarkdown = createMarkdownImport(transformers)
|
||||
return importMarkdown(markdown, node, keepNewLines)
|
||||
}
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
ELEMENT_TRANSFORMERS,
|
||||
TEXT_FORMAT_TRANSFORMERS,
|
||||
TEXT_MATCH_TRANSFORMERS,
|
||||
MultilineElementTransformer,
|
||||
MULTILINE_ELEMENT_TRANSFORMERS,
|
||||
} from '@lexical/markdown'
|
||||
import { LexicalNode, $isParagraphNode, $isTextNode } from 'lexical'
|
||||
|
||||
function indexBy<T>(list: Array<T>, callback: (arg0: T) => string): Readonly<Record<string, Array<T>>> {
|
||||
const index: Record<string, Array<T>> = {}
|
||||
@@ -26,6 +29,7 @@ function indexBy<T>(list: Array<T>, callback: (arg0: T) => string): Readonly<Rec
|
||||
|
||||
export function transformersByType(transformers: Array<Transformer>): Readonly<{
|
||||
element: Array<ElementTransformer>
|
||||
multilineElement: Array<MultilineElementTransformer>
|
||||
textFormat: Array<TextFormatTransformer>
|
||||
textMatch: Array<TextMatchTransformer>
|
||||
}> {
|
||||
@@ -33,13 +37,31 @@ export function transformersByType(transformers: Array<Transformer>): Readonly<{
|
||||
|
||||
return {
|
||||
element: (byType.element || []) as Array<ElementTransformer>,
|
||||
multilineElement: (byType['multiline-element'] || []) as Array<MultilineElementTransformer>,
|
||||
textFormat: (byType['text-format'] || []) as Array<TextFormatTransformer>,
|
||||
textMatch: (byType['text-match'] || []) as Array<TextMatchTransformer>,
|
||||
}
|
||||
}
|
||||
|
||||
const MARKDOWN_EMPTY_LINE_REG_EXP = /^\s{0,3}$/
|
||||
|
||||
export function isEmptyParagraph(node: LexicalNode): boolean {
|
||||
if (!$isParagraphNode(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const firstChild = node.getFirstChild()
|
||||
return (
|
||||
firstChild == null ||
|
||||
(node.getChildrenSize() === 1 &&
|
||||
$isTextNode(firstChild) &&
|
||||
MARKDOWN_EMPTY_LINE_REG_EXP.test(firstChild.getTextContent()))
|
||||
)
|
||||
}
|
||||
|
||||
export const TRANSFORMERS: Array<Transformer> = [
|
||||
...ELEMENT_TRANSFORMERS,
|
||||
...MULTILINE_ELEMENT_TRANSFORMERS,
|
||||
...TEXT_FORMAT_TRANSFORMERS,
|
||||
...TEXT_MATCH_TRANSFORMERS,
|
||||
]
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TextMatchTransformer,
|
||||
$convertToMarkdownString,
|
||||
$convertFromMarkdownString,
|
||||
MULTILINE_ELEMENT_TRANSFORMERS,
|
||||
} from '@lexical/markdown'
|
||||
import {
|
||||
$createTableCellNode,
|
||||
@@ -247,6 +248,7 @@ export const MarkdownTransformers = [
|
||||
IMAGE,
|
||||
INLINE_FILE,
|
||||
...ELEMENT_TRANSFORMERS,
|
||||
...MULTILINE_ELEMENT_TRANSFORMERS,
|
||||
...TEXT_FORMAT_TRANSFORMERS,
|
||||
...TEXT_MATCH_TRANSFORMERS,
|
||||
HorizontalRule,
|
||||
|
||||
@@ -44,14 +44,14 @@ export class CollapsibleContainerNode extends ElementNode {
|
||||
this.__open = open ?? false
|
||||
}
|
||||
|
||||
static override getType(): string {
|
||||
return 'collapsible-container'
|
||||
}
|
||||
|
||||
static override clone(node: CollapsibleContainerNode): CollapsibleContainerNode {
|
||||
return new CollapsibleContainerNode(node.__open, node.__key)
|
||||
}
|
||||
|
||||
static override getType(): string {
|
||||
return 'collapsible-container'
|
||||
}
|
||||
|
||||
override createDOM(_: EditorConfig, editor: LexicalEditor): HTMLElement {
|
||||
const dom = document.createElement('details')
|
||||
dom.classList.add('Collapsible__container')
|
||||
|
||||
@@ -13,6 +13,12 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface {
|
||||
return 'snfile'
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey, zoomLevel?: number) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
this.__zoomLevel = zoomLevel || 100
|
||||
}
|
||||
|
||||
static clone(node: FileNode): FileNode {
|
||||
return new FileNode(node.__id, node.__format, node.__key, node.__zoomLevel)
|
||||
}
|
||||
@@ -56,12 +62,6 @@ export class FileNode extends DecoratorBlockNode implements ItemNodeInterface {
|
||||
return { element }
|
||||
}
|
||||
|
||||
constructor(id: string, format?: ElementFormatType, key?: NodeKey, zoomLevel?: number) {
|
||||
super(format, key)
|
||||
this.__id = id
|
||||
this.__zoomLevel = zoomLevel || 100
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this.__id
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { handleEditorChange } from '../../Utils'
|
||||
import { SuperNotePreviewCharLimit } from '../../SuperEditor'
|
||||
import { $generateNodesFromDOM } from '@lexical/html'
|
||||
import { MarkdownTransformers } from '../../MarkdownTransformers'
|
||||
import { $convertFromMarkdownString } from '../../Lexical/Utils/MarkdownImport'
|
||||
import { $convertFromMarkdownString } from '@lexical/markdown'
|
||||
|
||||
/** Note that markdown conversion does not insert new lines. See: https://github.com/facebook/lexical/issues/2815 */
|
||||
export default function ImportPlugin({
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, Spread } from 'lexical'
|
||||
import {
|
||||
DOMConversionMap,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
ElementFormatType,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
import InlineFileComponent from './InlineFileComponent'
|
||||
|
||||
type SerializedInlineFileNode = Spread<
|
||||
@@ -22,15 +31,15 @@ export class InlineFileNode extends DecoratorBlockNode {
|
||||
return 'inline-file'
|
||||
}
|
||||
|
||||
constructor(src: string, mimeType: string, fileName: string | undefined) {
|
||||
super()
|
||||
constructor(src: string, mimeType: string, fileName: string | undefined, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__src = src
|
||||
this.__mimeType = mimeType
|
||||
this.__fileName = fileName
|
||||
}
|
||||
|
||||
static clone(node: InlineFileNode): InlineFileNode {
|
||||
return new InlineFileNode(node.__src, node.__mimeType, node.__fileName)
|
||||
return new InlineFileNode(node.__src, node.__mimeType, node.__fileName, node.__format, node.__key)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedInlineFileNode): InlineFileNode {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { useEffect } from 'react'
|
||||
import { $createCodeNode } from '@lexical/code'
|
||||
import { $createTextNode, $getRoot, $nodesOfType, ParagraphNode } from 'lexical'
|
||||
import { $convertToMarkdownString } from '@lexical/markdown'
|
||||
import { $createTextNode, $getRoot, $isParagraphNode } from 'lexical'
|
||||
import { MarkdownTransformers } from '../../MarkdownTransformers'
|
||||
import { $dfs } from '@lexical/utils'
|
||||
import { $convertToMarkdownString } from '../../Lexical/Utils/MarkdownExport'
|
||||
|
||||
type Props = {
|
||||
onMarkdown: (markdown: string) => void
|
||||
@@ -15,10 +16,12 @@ export default function MarkdownPreviewPlugin({ onMarkdown }: Props): JSX.Elemen
|
||||
useEffect(() => {
|
||||
editor.update(() => {
|
||||
const root = $getRoot()
|
||||
const paragraphs = $nodesOfType(ParagraphNode)
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.isEmpty()) {
|
||||
paragraph.remove()
|
||||
for (const { node } of $dfs()) {
|
||||
if (!$isParagraphNode(node)) {
|
||||
continue
|
||||
}
|
||||
if (node.isEmpty()) {
|
||||
node.remove()
|
||||
}
|
||||
}
|
||||
const markdown = $convertToMarkdownString(MarkdownTransformers)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { DecoratorBlockNode, SerializedDecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode'
|
||||
import { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, Spread } from 'lexical'
|
||||
import {
|
||||
DOMConversionMap,
|
||||
DOMExportOutput,
|
||||
EditorConfig,
|
||||
ElementFormatType,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
NodeKey,
|
||||
Spread,
|
||||
} from 'lexical'
|
||||
import RemoteImageComponent from './RemoteImageComponent'
|
||||
|
||||
type SerializedRemoteImageNode = Spread<
|
||||
@@ -20,14 +29,14 @@ export class RemoteImageNode extends DecoratorBlockNode {
|
||||
return 'unencrypted-image'
|
||||
}
|
||||
|
||||
constructor(src: string, alt?: string) {
|
||||
super()
|
||||
constructor(src: string, alt?: string, format?: ElementFormatType, key?: NodeKey) {
|
||||
super(format, key)
|
||||
this.__src = src
|
||||
this.__alt = alt
|
||||
}
|
||||
|
||||
static clone(node: RemoteImageNode): RemoteImageNode {
|
||||
return new RemoteImageNode(node.__src, node.__alt)
|
||||
return new RemoteImageNode(node.__src, node.__alt, node.__format, node.__key)
|
||||
}
|
||||
|
||||
static importJSON(serializedNode: SerializedRemoteImageNode): RemoteImageNode {
|
||||
|
||||
@@ -89,9 +89,10 @@ export const SearchPlugin = () => {
|
||||
|
||||
const handleSearch = useCallback(
|
||||
(query: string, isCaseSensitive: boolean) => {
|
||||
document.querySelectorAll('.search-highlight').forEach((element) => {
|
||||
const currentHighlights = document.querySelectorAll('.search-highlight')
|
||||
for (const element of currentHighlights) {
|
||||
element.remove()
|
||||
})
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
dispatch({ type: 'clear-results' })
|
||||
@@ -109,7 +110,7 @@ export const SearchPlugin = () => {
|
||||
|
||||
const results: SuperSearchResult[] = []
|
||||
|
||||
textNodes.forEach((node) => {
|
||||
for (const node of textNodes) {
|
||||
const text = node.textContent || ''
|
||||
|
||||
const indices: number[] = []
|
||||
@@ -122,7 +123,7 @@ export const SearchPlugin = () => {
|
||||
indices.push(index)
|
||||
}
|
||||
|
||||
indices.forEach((index) => {
|
||||
for (const index of indices) {
|
||||
const startIndex = index
|
||||
const endIndex = startIndex + query.length
|
||||
|
||||
@@ -131,8 +132,8 @@ export const SearchPlugin = () => {
|
||||
startIndex,
|
||||
endIndex,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'set-results',
|
||||
@@ -205,7 +206,10 @@ export const SearchPlugin = () => {
|
||||
}
|
||||
replaceResult(result, true)
|
||||
} else if (type === 'all') {
|
||||
resultsRef.current.forEach((result) => replaceResult(result))
|
||||
const results = resultsRef.current
|
||||
for (const result of results) {
|
||||
replaceResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
void handleSearch(queryRef.current, isCaseSensitiveRef.current)
|
||||
@@ -214,9 +218,10 @@ export const SearchPlugin = () => {
|
||||
}, [addReplaceEventListener, currentResultIndexRef, editor, handleSearch, isCaseSensitiveRef, queryRef, resultsRef])
|
||||
|
||||
useEffect(() => {
|
||||
document.querySelectorAll('.search-highlight').forEach((element) => {
|
||||
const currentHighlights = document.querySelectorAll('.search-highlight')
|
||||
for (const element of currentHighlights) {
|
||||
element.remove()
|
||||
})
|
||||
}
|
||||
if (currentResultIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ function TableActionMenu({ onClose, tableCellNode: _tableCellNode, cellMerge }:
|
||||
|
||||
const tableSelection = getTableObserverFromTableElement(tableElement)
|
||||
if (tableSelection !== null) {
|
||||
tableSelection.clearHighlight()
|
||||
tableSelection.$clearHighlight()
|
||||
}
|
||||
|
||||
tableNode.markDirty()
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
import { createHeadlessEditor } from '@lexical/headless'
|
||||
import { FileItem, PrefKey, PrefValue, SuperConverterServiceInterface } from '@standardnotes/snjs'
|
||||
import {
|
||||
$createParagraphNode,
|
||||
$getRoot,
|
||||
$insertNodes,
|
||||
$nodesOfType,
|
||||
LexicalEditor,
|
||||
LexicalNode,
|
||||
ParagraphNode,
|
||||
} from 'lexical'
|
||||
import { $createParagraphNode, $getRoot, $insertNodes, $isParagraphNode, LexicalEditor, LexicalNode } from 'lexical'
|
||||
import BlocksEditorTheme from '../Lexical/Theme/Theme'
|
||||
import { BlockEditorNodes, SuperExportNodes } from '../Lexical/Nodes/AllNodes'
|
||||
import { MarkdownTransformers } from '../MarkdownTransformers'
|
||||
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html'
|
||||
import { FileNode } from '../Plugins/EncryptedFilePlugin/Nodes/FileNode'
|
||||
import { $createFileExportNode } from '../Lexical/Nodes/FileExportNode'
|
||||
import { $createInlineFileNode } from '../Plugins/InlineFilePlugin/InlineFileNode'
|
||||
import { $convertFromMarkdownString } from '../Lexical/Utils/MarkdownImport'
|
||||
import { $convertFromMarkdownString } from '@lexical/markdown'
|
||||
import { $convertToMarkdownString } from '../Lexical/Utils/MarkdownExport'
|
||||
import { parseFileName } from '@standardnotes/utils'
|
||||
import { $dfs } from '@lexical/utils'
|
||||
import { $isFileNode } from '../Plugins/EncryptedFilePlugin/Nodes/FileUtils'
|
||||
|
||||
export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
private importEditor: LexicalEditor
|
||||
@@ -80,103 +73,103 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
let content: string | undefined
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
if (embedBehavior === 'reference') {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (!getFileItem) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const fileNodes = $nodesOfType(FileNode)
|
||||
const filenameCounts: Record<string, number> = {}
|
||||
Promise.all(
|
||||
fileNodes.map(async (fileNode) => {
|
||||
const fileItem = getFileItem(fileNode.getId())
|
||||
if (!fileItem) {
|
||||
const handleFileNodes = () => {
|
||||
if (embedBehavior === 'reference') {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
if (!getFileItem) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const filenameCounts: Record<string, number> = {}
|
||||
Promise.all(
|
||||
$dfs().map(async ({ node: fileNode }) => {
|
||||
if (!$isFileNode(fileNode)) {
|
||||
return
|
||||
}
|
||||
const fileItem = getFileItem(fileNode.getId())
|
||||
if (!fileItem) {
|
||||
return
|
||||
}
|
||||
const canInlineFileType = toFormat === 'pdf' ? fileItem.mimeType.startsWith('image/') : true
|
||||
if (embedBehavior === 'inline' && getFileBase64 && canInlineFileType) {
|
||||
const fileBase64 = await getFileBase64(fileNode.getId())
|
||||
if (!fileBase64) {
|
||||
return
|
||||
}
|
||||
const canInlineFileType = toFormat === 'pdf' ? fileItem.mimeType.startsWith('image/') : true
|
||||
if (embedBehavior === 'inline' && getFileBase64 && canInlineFileType) {
|
||||
const fileBase64 = await getFileBase64(fileNode.getId())
|
||||
if (!fileBase64) {
|
||||
return
|
||||
}
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
const inlineFileNode = $createInlineFileNode(fileBase64, fileItem.mimeType, fileItem.name)
|
||||
fileNode.replace(inlineFileNode)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
} else {
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
filenameCounts[fileItem.name] =
|
||||
filenameCounts[fileItem.name] == undefined ? 0 : filenameCounts[fileItem.name] + 1
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
const inlineFileNode = $createInlineFileNode(fileBase64, fileItem.mimeType, fileItem.name)
|
||||
fileNode.replace(inlineFileNode)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
} else {
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
filenameCounts[fileItem.name] =
|
||||
filenameCounts[fileItem.name] == undefined ? 0 : filenameCounts[fileItem.name] + 1
|
||||
|
||||
let name = fileItem.name
|
||||
let name = fileItem.name
|
||||
|
||||
if (filenameCounts[name] > 0) {
|
||||
const { name: _name, ext } = parseFileName(name)
|
||||
name = `${_name}-${fileItem.uuid}.${ext}`
|
||||
}
|
||||
if (filenameCounts[name] > 0) {
|
||||
const { name: _name, ext } = parseFileName(name)
|
||||
name = `${_name}-${fileItem.uuid}.${ext}`
|
||||
}
|
||||
|
||||
const fileExportNode = $createFileExportNode(name, fileItem.mimeType)
|
||||
fileNode.replace(fileExportNode)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(() => resolve())
|
||||
.catch(console.error)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
const fileExportNode = $createFileExportNode(name, fileItem.mimeType)
|
||||
fileNode.replace(fileExportNode)
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.then(() => resolve())
|
||||
.catch(console.error)
|
||||
}
|
||||
this.exportEditor.update(handleFileNodes, { discrete: true })
|
||||
})
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
this.exportEditor.update(
|
||||
() => {
|
||||
switch (toFormat) {
|
||||
case 'txt':
|
||||
case 'md': {
|
||||
const paragraphs = $nodesOfType(ParagraphNode)
|
||||
for (const paragraph of paragraphs) {
|
||||
if (paragraph.isEmpty()) {
|
||||
paragraph.remove()
|
||||
}
|
||||
const convertToFormat = () => {
|
||||
switch (toFormat) {
|
||||
case 'txt':
|
||||
case 'md': {
|
||||
for (const { node: paragraph } of $dfs()) {
|
||||
if (!$isParagraphNode(paragraph)) {
|
||||
continue
|
||||
}
|
||||
if (paragraph.isEmpty()) {
|
||||
paragraph.remove()
|
||||
}
|
||||
content = $convertToMarkdownString(MarkdownTransformers)
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
case 'html':
|
||||
content = $generateHtmlFromNodes(this.exportEditor)
|
||||
resolve()
|
||||
break
|
||||
case 'pdf': {
|
||||
void import('../Lexical/Utils/PDFExport/PDFExport').then(({ $generatePDFFromNodes }): void => {
|
||||
void $generatePDFFromNodes(this.exportEditor, config?.pdf?.pageSize || 'A4').then((pdf) => {
|
||||
content = pdf
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'json':
|
||||
default:
|
||||
content = superString
|
||||
resolve()
|
||||
break
|
||||
content = $convertToMarkdownString(MarkdownTransformers)
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
},
|
||||
{ discrete: true },
|
||||
)
|
||||
case 'html':
|
||||
content = $generateHtmlFromNodes(this.exportEditor)
|
||||
resolve()
|
||||
break
|
||||
case 'pdf': {
|
||||
void import('../Lexical/Utils/PDFExport/PDFExport').then(({ $generatePDFFromNodes }): void => {
|
||||
void $generatePDFFromNodes(this.exportEditor, config?.pdf?.pageSize || 'A4').then((pdf) => {
|
||||
content = pdf
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'json':
|
||||
default:
|
||||
content = superString
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
}
|
||||
this.exportEditor.update(convertToFormat, { discrete: true })
|
||||
})
|
||||
|
||||
if (typeof content !== 'string') {
|
||||
@@ -288,14 +281,16 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
|
||||
const ids: string[] = []
|
||||
|
||||
this.exportEditor.getEditorState().read(() => {
|
||||
const fileNodes = $nodesOfType(FileNode)
|
||||
fileNodes.forEach((fileNode) => {
|
||||
for (const { node: fileNode } of $dfs()) {
|
||||
if (!$isFileNode(fileNode)) {
|
||||
continue
|
||||
}
|
||||
const nodeId = fileNode.getId()
|
||||
if (ids.includes(nodeId)) {
|
||||
return
|
||||
continue
|
||||
}
|
||||
ids.push(nodeId)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return ids
|
||||
|
||||
Reference in New Issue
Block a user