UNPKG

lexical-vue

Version:

An extensible Vue 3 web text-editor based on Lexical.

319 lines (318 loc) 14.1 kB
import { $generateHtmlFromNodes } from "@lexical/html"; import { $isLinkNode } from "@lexical/link"; import { $isMarkNode } from "@lexical/mark"; import { $isTableSelection } from "@lexical/table"; import { $getRoot, $getSelection, $isElementNode, $isNodeSelection, $isParagraphNode, $isRangeSelection, $isTextNode } from "lexical"; const NON_SINGLE_WIDTH_CHARS_REPLACEMENT = Object.freeze({ '\t': '\\t', '\n': '\\n' }); const NON_SINGLE_WIDTH_CHARS_REGEX = new RegExp(Object.keys(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).join('|'), 'g'); const SYMBOLS = Object.freeze({ ancestorHasNextSibling: '|', ancestorIsLastChild: ' ', hasNextSibling: '├', isLastChild: '└', selectedChar: '^', selectedLine: '>' }); const FORMAT_PREDICATES = [ (node)=>node.hasFormat('bold') && 'Bold', (node)=>node.hasFormat('code') && 'Code', (node)=>node.hasFormat('italic') && 'Italic', (node)=>node.hasFormat('strikethrough') && 'Strikethrough', (node)=>node.hasFormat("subscript") && "Subscript", (node)=>node.hasFormat("superscript") && "Superscript", (node)=>node.hasFormat('underline') && 'Underline', (node)=>node.hasFormat('highlight') && 'Highlight' ]; const FORMAT_PREDICATES_PARAGRAPH = [ (node)=>node.hasTextFormat('bold') && 'Bold', (node)=>node.hasTextFormat('code') && 'Code', (node)=>node.hasTextFormat('italic') && 'Italic', (node)=>node.hasTextFormat('strikethrough') && 'Strikethrough', (node)=>node.hasTextFormat("subscript") && "Subscript", (node)=>node.hasTextFormat("superscript") && "Superscript", (node)=>node.hasTextFormat('underline') && 'Underline', (node)=>node.hasTextFormat('highlight') && 'Highlight' ]; const DETAIL_PREDICATES = [ (node)=>node.isDirectionless() && 'Directionless', (node)=>node.isUnmergeable() && 'Unmergeable' ]; const MODE_PREDICATES = [ (node)=>node.isToken() && 'Token', (node)=>node.isSegmented() && 'Segmented' ]; function generateContent(editor, commandsLog, exportDOM, customPrintNode) { let obfuscateText = arguments.length > 4 && void 0 !== arguments[4] ? arguments[4] : false; const editorState = editor.getEditorState(); const editorConfig = editor._config; const compositionKey = editor._compositionKey; const editable = editor._editable; if (exportDOM) { let htmlString = ''; editorState.read(()=>{ htmlString = printPrettyHTML($generateHtmlFromNodes(editor)); }); return htmlString; } let res = ' root\n'; const selectionString = editorState.read(()=>{ const selection = $getSelection(); visitTree($getRoot(), (node, indent)=>{ const nodeKey = node.getKey(); const nodeKeyDisplay = "(".concat(nodeKey, ")"); const typeDisplay = node.getType() || ''; const isSelected = node.isSelected(); res += "".concat(isSelected ? SYMBOLS.selectedLine : ' ', " ").concat(indent.join(' '), " ").concat(nodeKeyDisplay, " ").concat(typeDisplay, " ").concat(printNode(node, customPrintNode, obfuscateText), "\n"); res += $printSelectedCharsLine({ indent, isSelected, node, nodeKeyDisplay, selection, typeDisplay }); }); return null === selection ? ': null' : $isRangeSelection(selection) ? printRangeSelection(selection) : $isTableSelection(selection) ? printTableSelection(selection) : printNodeSelection(selection); }); res += "\n selection".concat(selectionString); res += '\n\n commands:'; if (commandsLog.length) for (const { index, type, payload } of commandsLog)res += "\n └ ".concat(index, ". { type: ").concat(type, ", payload: ").concat(payload instanceof Event ? payload.constructor.name : payload, " }"); else res += '\n └ None dispatched.'; const { version } = editor.constructor; res += "\n\n editor".concat(version ? " (v".concat(version, ")") : '', ":"); res += "\n └ namespace ".concat(editorConfig.namespace); if (null !== compositionKey) res += "\n └ compositionKey ".concat(compositionKey); res += "\n └ editable ".concat(String(editable)); return res; } function printRangeSelection(selection) { let res = ''; const formatText = printFormatProperties(selection); res += ": range ".concat('' !== formatText ? "{ ".concat(formatText, " }") : '', " ").concat('' !== selection.style ? "{ style: ".concat(selection.style, " } ") : ''); const anchor = selection.anchor; const focus = selection.focus; const anchorOffset = anchor.offset; const focusOffset = focus.offset; res += "\n ├ anchor { key: ".concat(anchor.key, ", offset: ").concat(null === anchorOffset ? 'null' : anchorOffset, ", type: ").concat(anchor.type, " }"); res += "\n └ focus { key: ".concat(focus.key, ", offset: ").concat(null === focusOffset ? 'null' : focusOffset, ", type: ").concat(focus.type, " }"); return res; } function printNodeSelection(selection) { if (!$isNodeSelection(selection)) return ''; return ": node\n └ [".concat(Array.from(selection._nodes).join(', '), "]"); } function printTableSelection(selection) { return ": table\n └ { table: ".concat(selection.tableKey, ", anchorCell: ").concat(selection.anchor.key, ", focusCell: ").concat(selection.focus.key, " }"); } function visitTree(currentNode, visitor) { let indent = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : []; const childNodes = currentNode.getChildren(); const childNodesLength = childNodes.length; childNodes.forEach((childNode, i)=>{ visitor(childNode, indent.concat(i === childNodesLength - 1 ? SYMBOLS.isLastChild : SYMBOLS.hasNextSibling)); if ($isElementNode(childNode)) visitTree(childNode, visitor, indent.concat(i === childNodesLength - 1 ? SYMBOLS.ancestorIsLastChild : SYMBOLS.ancestorHasNextSibling)); }); } function normalize(text) { let obfuscateText = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : false; const textToPrint = Object.entries(NON_SINGLE_WIDTH_CHARS_REPLACEMENT).reduce((acc, param)=>{ let [key, value] = param; return acc.replace(new RegExp(key, 'g'), String(value)); }, text); if (obfuscateText) return textToPrint.replace(/\S/g, '*'); return textToPrint; } function printNode(node, customPrintNode) { let obfuscateText = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : false; const customPrint = customPrintNode ? customPrintNode(node, obfuscateText) : void 0; if (void 0 !== customPrint && customPrint.length > 0) return customPrint; if ($isTextNode(node)) { const text = node.getTextContent(); const title = 0 === text.length ? '(empty)' : '"'.concat(normalize(text, obfuscateText), '"'); const properties = printAllTextNodeProperties(node); return [ title, 0 !== properties.length ? "{ ".concat(properties, " }") : null ].filter(Boolean).join(' ').trim(); } if ($isLinkNode(node)) { const link = node.getURL(); const title = 0 === link.length ? '(empty)' : '"'.concat(normalize(link, obfuscateText), '"'); const properties = printAllLinkNodeProperties(node); return [ title, 0 !== properties.length ? "{ ".concat(properties, " }") : null ].filter(Boolean).join(' ').trim(); } if ($isMarkNode(node)) return "ids: [ ".concat(node.getIDs().join(', '), " ]"); { if (!$isParagraphNode(node)) return ''; const formatText = printTextFormatProperties(node); let paragraphData = '' !== formatText ? "{ ".concat(formatText, " }") : ''; paragraphData += node.__style ? "(".concat(node.__style, ")") : ''; return paragraphData; } } function printTextFormatProperties(nodeOrSelection) { let str = FORMAT_PREDICATES_PARAGRAPH.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase(); if ('' !== str) str = "format: ".concat(str); return str; } function printAllTextNodeProperties(node) { return [ printFormatProperties(node), printDetailProperties(node), printModeProperties(node), printStateProperties(node) ].filter(Boolean).join(', '); } function printAllLinkNodeProperties(node) { return [ printTargetProperties(node), printRelProperties(node), printTitleProperties(node), printStateProperties(node) ].filter(Boolean).join(', '); } function printDetailProperties(nodeOrSelection) { let str = DETAIL_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase(); if ('' !== str) str = "detail: ".concat(str); return str; } function printModeProperties(nodeOrSelection) { let str = MODE_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase(); if ('' !== str) str = "mode: ".concat(str); return str; } function printFormatProperties(nodeOrSelection) { let str = FORMAT_PREDICATES.map((predicate)=>predicate(nodeOrSelection)).filter(Boolean).join(', ').toLocaleLowerCase(); if ('' !== str) str = "format: ".concat(str); return str; } function printTargetProperties(node) { let str = node.getTarget(); if (null != str) str = "target: ".concat(str); return str; } function printRelProperties(node) { let str = node.getRel(); if (null != str) str = "rel: ".concat(str); return str; } function printTitleProperties(node) { let str = node.getTitle(); if (null != str) str = "title: ".concat(str); return str; } function printStateProperties(node) { if (!node.__state) return false; const states = []; for (const [stateType, value] of node.__state.knownState.entries()){ if (stateType.isEqual(value, stateType.defaultValue)) continue; const textValue = JSON.stringify(stateType.unparse(value)); states.push("[".concat(stateType.key, ": ").concat(textValue, "]")); } let str = states.join(','); if ('' !== str) str = "state: ".concat(str); return str; } function $printSelectedCharsLine(param) { let { indent, isSelected, node, nodeKeyDisplay, selection, typeDisplay } = param; if (!$isTextNode(node) || !$isRangeSelection(selection) || !isSelected || $isElementNode(node)) return ''; const anchor = selection.anchor; const focus = selection.focus; if ('' === node.getTextContent() || anchor.getNode() === selection.focus.getNode() && anchor.offset === focus.offset) return ''; const [start, end] = $getSelectionStartEnd(node, selection); if (start === end) return ''; const selectionLastIndent = indent[indent.length - 1] === SYMBOLS.hasNextSibling ? SYMBOLS.ancestorHasNextSibling : SYMBOLS.ancestorIsLastChild; const indentionChars = [ ...indent.slice(0, indent.length - 1), selectionLastIndent ]; const unselectedChars = new Array(start + 1).fill(' '); const selectedChars = new Array(end - start).fill(SYMBOLS.selectedChar); const paddingLength = typeDisplay.length + 2; const nodePrintSpaces = new Array(nodeKeyDisplay.length + paddingLength).fill(' '); return "".concat([ SYMBOLS.selectedLine, indentionChars.join(' '), [ ...nodePrintSpaces, ...unselectedChars, ...selectedChars ].join('') ].join(' '), "\n"); } function printPrettyHTML(str) { const div = document.createElement('div'); div.innerHTML = str.trim(); return prettifyHTML(div, 0).innerHTML; } function prettifyHTML(node, level) { const indentBefore = new Array(level++ + 1).join(' '); const indentAfter = Array.from({ length: level - 1 }).join(' '); let textNode; for(let i = 0; i < node.children.length; i++){ textNode = document.createTextNode("\n".concat(indentBefore)); node.insertBefore(textNode, node.children[i]); prettifyHTML(node.children[i], level); if (node.lastElementChild === node.children[i]) { textNode = document.createTextNode("\n".concat(indentAfter)); node.appendChild(textNode); } } return node; } function $getSelectionStartEnd(node, selection) { const anchorAndFocus = selection.getStartEndPoints(); if ($isNodeSelection(selection) || null === anchorAndFocus) return [ -1, -1 ]; const [anchor, focus] = anchorAndFocus; const textContent = node.getTextContent(); const textLength = textContent.length; let start = -1; let end = -1; if ('text' === anchor.type && 'text' === focus.type) { const anchorNode = anchor.getNode(); const focusNode = focus.getNode(); if (anchorNode === focusNode && node === anchorNode && anchor.offset !== focus.offset) [start, end] = anchor.offset < focus.offset ? [ anchor.offset, focus.offset ] : [ focus.offset, anchor.offset ]; else if (node === anchorNode) [start, end] = anchorNode.isBefore(focusNode) ? [ anchor.offset, textLength ] : [ 0, anchor.offset ]; else if (node === focusNode) [start, end] = focusNode.isBefore(anchorNode) ? [ focus.offset, textLength ] : [ 0, focus.offset ]; else [start, end] = [ 0, textLength ]; } const numNonSingleWidthCharBeforeSelection = (textContent.slice(0, start).match(NON_SINGLE_WIDTH_CHARS_REGEX) || []).length; const numNonSingleWidthCharInSelection = (textContent.slice(start, end).match(NON_SINGLE_WIDTH_CHARS_REGEX) || []).length; return [ start + numNonSingleWidthCharBeforeSelection, end + numNonSingleWidthCharBeforeSelection + numNonSingleWidthCharInSelection ]; } export { generateContent };