diff --git a/packages/super-editor/src/core/super-converter/SuperConverter.js b/packages/super-editor/src/core/super-converter/SuperConverter.js index 6e1bc77640..1dd3632f12 100644 --- a/packages/super-editor/src/core/super-converter/SuperConverter.js +++ b/packages/super-editor/src/core/super-converter/SuperConverter.js @@ -11,6 +11,7 @@ import { prepareCommentParaIds, prepareCommentsXmlFilesForExport, } from './v2/exporter/commentsExporter.js'; +import { HYPERLINK_RELATIONSHIP_TYPE } from './constants.js'; class SuperConverter { static allowedElements = Object.freeze({ @@ -504,15 +505,16 @@ class SuperConverter { const existingId = rel.attributes.Id; const existingTarget = relationships.elements.find((el) => el.attributes.Target === rel.attributes.Target); const isNewMedia = rel.attributes.Target?.startsWith('media/') && existingId.length > 6; - - if (existingTarget && !isNewMedia) { + const isNewHyperlink = rel.attributes.Type === HYPERLINK_RELATIONSHIP_TYPE && existingId.length > 6; + + if (existingTarget && !isNewMedia && !isNewHyperlink) { return; } // Update the target to escape ampersands rel.attributes.Target = rel.attributes?.Target?.replace(/&/g, '&'); - // Update the ID. If we've assigned a long ID (ie: images) we leave it alone + // Update the ID. If we've assigned a long ID (ie: images, links) we leave it alone rel.attributes.Id = existingId.length > 6 ? existingId : `rId${++largestId}`; newRels.push(rel); diff --git a/packages/super-editor/src/core/super-converter/constants.js b/packages/super-editor/src/core/super-converter/constants.js new file mode 100644 index 0000000000..81bb420195 --- /dev/null +++ b/packages/super-editor/src/core/super-converter/constants.js @@ -0,0 +1 @@ +export const HYPERLINK_RELATIONSHIP_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink'; diff --git a/packages/super-editor/src/core/super-converter/exporter.js b/packages/super-editor/src/core/super-converter/exporter.js index 861e5858de..2f99831767 100644 --- a/packages/super-editor/src/core/super-converter/exporter.js +++ b/packages/super-editor/src/core/super-converter/exporter.js @@ -601,25 +601,69 @@ function translateLinkNode(params) { const linkMark = node.marks.find((m) => m.type === 'link'); const link = linkMark.attrs.href; + let rId = linkMark.attrs.rId; if (!rId) { rId = addNewLinkRelationship(params, link); } node.marks = node.marks.filter((m) => m.type !== 'link'); + const outputNode = exportSchemaToJson({ ...params, node }); + const contentNode = processLinkContentNode(outputNode); + const newNode = { name: 'w:hyperlink', type: 'element', attributes: { 'r:id': rId, }, - elements: [outputNode], + elements: [contentNode], }; return newNode; } +function processLinkContentNode(node) { + if (!node) return node; + + const contentNode = carbonCopy(node); + if (!contentNode) return contentNode; + + const hyperlinkStyle = { + name: 'w:rStyle', + attributes: { 'w:val': 'Hyperlink' }, + }; + const color = { + name: 'w:color', + attributes: { 'w:val': '467886' }, + }; + + if (contentNode.name === 'w:r') { + const runProps = contentNode.elements.find((el) => el.name === 'w:rPr'); + + if (runProps) { + const foundColor = runProps.elements.find((el) => el.name === 'w:color'); + const foundHyperlinkStyle = runProps.elements.find((el) => el.name === 'w:rStyle'); + if (!foundColor) runProps.elements.unshift(color); + if (!foundHyperlinkStyle) runProps.elements.unshift(hyperlinkStyle); + } else { + // we don't add underline by default + const runProps = { + name: 'w:rPr', + elements: [ + hyperlinkStyle, + color, + ], + }; + + contentNode.elements.unshift(runProps); + } + } + + return contentNode; +} + /** * Create a new link relationship and add it to the relationships array * @@ -630,7 +674,10 @@ function translateLinkNode(params) { function addNewLinkRelationship(params, link) { const newId = 'rId' + generateDocxRandomId(); - if (!params.relationships || !Array.isArray(params.relationships)) params.relationships = []; + if (!params.relationships || !Array.isArray(params.relationships)) { + params.relationships = []; + } + params.relationships.push({ type: 'element', name: 'Relationship', @@ -641,6 +688,7 @@ function addNewLinkRelationship(params, link) { TargetMode: 'External', }, }); + return newId; } @@ -1817,6 +1865,7 @@ function prepareUrlAnnotation(params) { const newId = addNewLinkRelationship(params, attrs.linkUrl); const linkTextNode = getTextNodeForExport(attrs.linkUrl, marks, params); + const contentNode = processLinkContentNode(linkTextNode); return { name: 'w:hyperlink', @@ -1825,7 +1874,7 @@ function prepareUrlAnnotation(params) { 'r:id': newId, 'w:history': 1, }, - elements: [linkTextNode], + elements: [contentNode], }; } diff --git a/packages/super-editor/src/tests/export/annotations/fieldAnnotationExporter.test.js b/packages/super-editor/src/tests/export/annotations/fieldAnnotationExporter.test.js index 9b8db153ab..c6e648a162 100644 --- a/packages/super-editor/src/tests/export/annotations/fieldAnnotationExporter.test.js +++ b/packages/super-editor/src/tests/export/annotations/fieldAnnotationExporter.test.js @@ -66,7 +66,10 @@ describe('AnnotationNodeExporter', async () => { const shortFieldType = fieldElements.find((f) => f.name === 'w:fieldTypeShort'); expect(shortFieldType.attributes['w:val']).toBe('link'); - const text = getTextFromNode(body.elements[10].elements[1].elements[1].elements[0]); + const node = body.elements[10].elements[1].elements[1].elements[0]; + const run = node.elements.find((el) => el.name === 'w:r'); + const text = run?.elements[1].elements[0].text; + expect(text).toEqual('https://vitest.dev/guide/coverage'); expect(params.relationships[2].attributes.Target).toBe('https://vitest.dev/guide/coverage'); }); diff --git a/packages/super-editor/src/tests/export/annotations/fieldAnnotationFinalDoc.test.js b/packages/super-editor/src/tests/export/annotations/fieldAnnotationFinalDoc.test.js index 756e5e196d..f64e755fd8 100644 --- a/packages/super-editor/src/tests/export/annotations/fieldAnnotationFinalDoc.test.js +++ b/packages/super-editor/src/tests/export/annotations/fieldAnnotationFinalDoc.test.js @@ -46,7 +46,10 @@ describe('AnnotationNodeExporter for final doc', async () => { it('export url annotation correctly', async() => { const hyperLinkNode = body.elements[10].elements[1]; - const text = getTextFromNode(hyperLinkNode); + + const run = hyperLinkNode.elements.find((el) => el.name === 'w:r'); + const text = run?.elements[1].elements[0].text; + expect(text).toBe('https://vitest.dev/guide/coverage'); expect(hyperLinkNode.attributes['r:id']).toBe(params.relationships[2].attributes.Id); });