diff --git a/examples/vue-custom-mark/src/App.vue b/examples/vue-custom-mark/src/App.vue index d9ad688dd9..8299820cb8 100644 --- a/examples/vue-custom-mark/src/App.vue +++ b/examples/vue-custom-mark/src/App.vue @@ -20,7 +20,6 @@ const init = () => { editorExtensions: [CustomMark], onReady: myCustomOnReady, }); - }; @@ -53,6 +52,12 @@ const insertCustomMark = () => { superdoc.value?.activeEditor?.commands.setMyCustomMark(randomId); }; +const exportDocx = () => { + superdoc.value?.export({ + exportType: ['docx'] + }); +}; + onMounted(() => init()); @@ -65,8 +70,9 @@ onMounted(() => init());
-
+
+
@@ -85,6 +91,13 @@ onMounted(() => init()); align-items: flex-start; justify-content: center; } +.editor-buttons { + display: flex; + flex-direction: column; +} +.editor-buttons button { + margin-bottom: 10px; +} .insert-mark { padding: 8px 12px; border-radius: 8px; diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index 0c008bfd4b..e9a976e217 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -1013,6 +1013,7 @@ export class Editor extends EventEmitter { isFinalDoc, commentsType, comments, + this, ); const customXml = this.converter.schemaToXml(this.converter.convertedXml['docProps/custom.xml'].elements[0]); diff --git a/packages/super-editor/src/core/ExtensionService.js b/packages/super-editor/src/core/ExtensionService.js index 271d8f595d..a7f93312fc 100644 --- a/packages/super-editor/src/core/ExtensionService.js +++ b/packages/super-editor/src/core/ExtensionService.js @@ -26,6 +26,14 @@ export class ExtensionService { this.editor = editor; this.externalExtensions = userExtensions || []; + + this.externalExtensions = this.externalExtensions.map((extension) => { + return { + ...extension, + isExternal: true, + }; + }); + this.extensions = ExtensionService.getResolvedExtensions([...extensions, ...this.externalExtensions]); this.schema = Schema.createSchemaByExtensions(this.extensions, editor); this.#setupExtensions(); diff --git a/packages/super-editor/src/core/Mark.js b/packages/super-editor/src/core/Mark.js index 04ecdd2918..388ba7fc40 100644 --- a/packages/super-editor/src/core/Mark.js +++ b/packages/super-editor/src/core/Mark.js @@ -13,6 +13,8 @@ export class Mark { storage; + isExternal; + config = { name: this.name, }; @@ -25,6 +27,8 @@ export class Mark { this.name = this.config.name; + this.isExternal = Boolean(this.config.isExternal) + if (this.config.addOptions) { this.options = callOrGet( getExtensionConfigField(this, 'addOptions', { diff --git a/packages/super-editor/src/core/super-converter/SuperConverter.js b/packages/super-editor/src/core/super-converter/SuperConverter.js index 91f544b630..3146373f3b 100644 --- a/packages/super-editor/src/core/super-converter/SuperConverter.js +++ b/packages/super-editor/src/core/super-converter/SuperConverter.js @@ -321,6 +321,7 @@ class SuperConverter { isFinalDoc = false, commentsExportType, comments = [], + editor, ) { const commentsWithParaIds = comments.map((c) => prepareCommentParaIds(c)); const commentDefinitions = commentsWithParaIds @@ -332,7 +333,8 @@ class SuperConverter { comments, commentDefinitions, commentsExportType, - isFinalDoc + isFinalDoc, + editor, }); const exporter = new DocxExporter(this); @@ -378,7 +380,8 @@ class SuperConverter { comments, commentDefinitions, commentsExportType = 'clean', - isFinalDoc = false + isFinalDoc = false, + editor, }) { const bodyNode = this.savedTagsToRestore.find((el) => el.name === 'w:body'); @@ -395,6 +398,7 @@ class SuperConverter { comments, commentsExportType, exportedCommentDefs: commentDefinitions, + editor, }); return { result, params }; diff --git a/packages/super-editor/src/core/super-converter/exporter.js b/packages/super-editor/src/core/super-converter/exporter.js index 7a98d78aa3..bb2a92e128 100644 --- a/packages/super-editor/src/core/super-converter/exporter.js +++ b/packages/super-editor/src/core/super-converter/exporter.js @@ -344,19 +344,61 @@ function translateChildNodes(params) { * @returns {XmlReadyNode} The translated text node */ -function getTextNodeForExport(text, marks) { +function getTextNodeForExport(text, marks, params) { const hasLeadingOrTrailingSpace = /^\s|\s$/.test(text); const space = hasLeadingOrTrailingSpace ? 'preserve' : null; const nodeAttrs = space ? { 'xml:space': space } : null; - + const textNodes = []; + const outputMarks = processOutputMarks(marks); - const textNode = { + textNodes.push({ name: 'w:t', elements: [{ text, type: 'text' }], attributes: nodeAttrs, - }; + }); + + // For custom mark export, we need to add a bookmark start and end tag + // And store attributes in the bookmark name + if (params) { + const { editor } = params; + const customMarks = editor.extensionService.extensions.filter((e) => e.isExternal === true); + + marks.forEach((mark) => { + const isCustomMark = customMarks.some((customMark) => { + const customMarkName = customMark.name; + return mark.type === customMarkName; + }); + + if (!isCustomMark) return; - return wrapTextInRun(textNode, outputMarks); + let attrsString = ''; + Object.entries(mark.attrs).forEach(([key, value]) => { + if (value) { + attrsString += `${key}=${value};`; + } + }); + + if (isCustomMark) { + textNodes.unshift({ + type: 'element', + name: 'w:bookmarkStart', + attributes: { + 'w:id': '5000', + 'w:name': mark.type + ';' + attrsString, + } + }); + textNodes.push({ + type: 'element', + name: 'w:bookmarkEnd', + attributes: { + 'w:id': '5000', + } + }); + }; + }); + } + + return wrapTextInRun(textNodes, outputMarks); } /** @@ -383,7 +425,7 @@ function translateTextNode(params) { const { text, marks = [] } = node; - return getTextNodeForExport(text, marks); + return getTextNodeForExport(text, marks, params); } function createTrackStyleMark(marks) { @@ -448,8 +490,11 @@ function translateTrackedNode(params) { * @param {XmlReadyNode} node * @returns {XmlReadyNode} The wrapped run node */ -function wrapTextInRun(node, marks) { - const elements = [node]; +function wrapTextInRun(nodeOrNodes, marks) { + let elements = []; + if (Array.isArray(nodeOrNodes)) elements = nodeOrNodes; + else elements = [nodeOrNodes]; + if (marks && marks.length) elements.unshift(generateRunProps(marks)); return { name: 'w:r', @@ -1546,7 +1591,7 @@ function prepareTextAnnotation(params) { } = params; const marksFromAttrs = translateFieldAttrsToMarks(attrs); - return getTextNodeForExport(attrs.displayLabel, [...marks, ...marksFromAttrs]); + return getTextNodeForExport(attrs.displayLabel, [...marks, ...marksFromAttrs], params); } /** @@ -1560,7 +1605,7 @@ function prepareCheckboxAnnotation(params) { node: { attrs = {}, marks = [] }, } = params; const content = he.decode(attrs.displayLabel); - return getTextNodeForExport(content, marks); + return getTextNodeForExport(content, marks, params); } /** @@ -1613,7 +1658,7 @@ function prepareUrlAnnotation(params) { } = params; const newId = addNewLinkRelationship(params, attrs.linkUrl); - const linkTextNode = getTextNodeForExport(attrs.linkUrl, marks); + const linkTextNode = getTextNodeForExport(attrs.linkUrl, marks, params); return { name: 'w:hyperlink', diff --git a/packages/super-editor/src/core/super-converter/v2/importer/bookmarkNodeImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/bookmarkNodeImporter.js index 9d9e2a27bb..997cc5ca75 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/bookmarkNodeImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/bookmarkNodeImporter.js @@ -2,12 +2,11 @@ * @type {import("docxImporter").NodeHandler} */ export const handleBookmarkNode = (params) => { - const { nodes, nodeListHandler } = params; + const { nodes, nodeListHandler, editor } = params; if (nodes.length === 0 || nodes[0].name !== 'w:bookmarkStart') { return { nodes: [], consumed: 0 }; } const node = nodes[0]; - const handleStandardNode = nodeListHandler.handlerEntities.find( (e) => e.handlerName === 'standardNodeHandler', )?.handler; @@ -16,6 +15,36 @@ export const handleBookmarkNode = (params) => { return { nodes: [], consumed: 0 }; }; + // Check if this bookmark is a custom mark + const customMarks = editor?.extensionService?.extensions?.filter((e) => e.isExternal === true) || []; + const bookmarkName = node.attributes['w:name']?.split(';')[0]; + const customMark = customMarks.find((mark) => mark.name === bookmarkName); + if (customMark) { + const bookmarkEndIndex = nodes.findIndex((n) => n.name === 'w:bookmarkEnd' && n.attributes['w:id'] === node.attributes['w:id']); + const textNodes = nodes.slice(1, bookmarkEndIndex); + + const nodeListHandler = params.nodeListHandler; + const attrs = {}; + node.attributes['w:name'].split(';').forEach((name) => { + const [key, value] = name.split('='); + if (key && value) { + attrs[key] = value; + } + }); + + const translatedText = nodeListHandler.handler({ ...params, nodes: textNodes }); + translatedText.forEach((n) => { + n.marks.push({ + type: customMark.name, + attrs, + }) + }); + return { + nodes: translatedText, + consumed: translatedText.length + 2, + }; + } + const updatedParams = {...params, nodes: [node]}; const result = handleStandardNode(updatedParams); if (result.nodes.length === 1) { diff --git a/packages/super-editor/src/core/super-converter/v2/importer/runNodeImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/runNodeImporter.js index f3412a8157..53ce565938 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/runNodeImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/runNodeImporter.js @@ -26,7 +26,11 @@ const handleRunNode = (params) => { } if (node.marks) marks.push(...node.marks); - processedRun = processedRun.map((n) => ({ ...n, marks: createImportMarks(marks), attributes })); + const newMarks = createImportMarks(marks); + processedRun = processedRun.map((n) => { + const existingMarks = n.marks || []; + return { ...n, marks: [...newMarks, ...existingMarks], attributes } + }); } else if (defaultNodeStyles.marks) { processedRun = processedRun.map((n) => ({ ...n, marks: createImportMarks(defaultNodeStyles.marks) })); }