diff --git a/packages/super-editor/src/core/super-converter/v2/importer/docxImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/docxImporter.js index ced8a614bf..f5f3da25b4 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/docxImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/docxImporter.js @@ -451,6 +451,8 @@ const importHeadersFooters = (docx, converter, editor) => { filename: currentFileName, }); + if (!converter.headerIds.ids) converter.headerIds.ids = []; + converter.headerIds.ids.push(rId); converter.headers[rId] = { type: 'doc', content: [...schema] }; converter.headerIds[sectionType] = rId; }); @@ -470,6 +472,8 @@ const importHeadersFooters = (docx, converter, editor) => { filename: currentFileName, }); + if (!converter.footerIds.ids) converter.footerIds.ids = []; + converter.footerIds.ids.push(rId); converter.footers[rId] = { type: 'doc', content: [...schema] }; converter.footerIds[sectionType] = rId; }); diff --git a/packages/super-editor/src/core/super-converter/v2/importer/paragraphNodeImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/paragraphNodeImporter.js index ad8b979acb..5f66da1725 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/paragraphNodeImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/paragraphNodeImporter.js @@ -65,13 +65,15 @@ export const handleParagraphNode = (params) => { schemaNode.attrs.marksAttrs = marks; } - + + let styleId; if (styleTag) { - schemaNode.attrs['styleId'] = styleTag.attributes['w:val']; + styleId = styleTag.attributes['w:val']; + schemaNode.attrs['styleId'] = styleId; } - + if (docx) { - const indent = getParagraphIndent(node, docx, schemaNode.attrs['styleId']); + const indent = getParagraphIndent(node, docx, styleId); if (!schemaNode.attrs.indent) { schemaNode.attrs.indent = {}; @@ -86,10 +88,10 @@ export const handleParagraphNode = (params) => { if (indent.firstLine || indent.firstLine === 0) { schemaNode.attrs.indent.firstLine = indent.firstLine; } - if (indent.hanging) { + if (indent.hanging || indent.hanging === 0) { schemaNode.attrs.indent.hanging = indent.hanging; } - if (indent.textIndent) { + if (indent.textIndent || indent.textIndent === 0) { schemaNode.attrs.textIndent = `${indent.textIndent}in`; } } @@ -111,10 +113,10 @@ export const handleParagraphNode = (params) => { if (docx) { const defaultStyleId = node.attributes?.['w:rsidRDefault']; - schemaNode.attrs['spacing'] = getParagraphSpacing(node, docx, schemaNode.attrs['styleId'], schemaNode.attrs.marksAttrs); + schemaNode.attrs['spacing'] = getParagraphSpacing(node, docx, styleId, schemaNode.attrs.marksAttrs); schemaNode.attrs['rsidRDefault'] = defaultStyleId; } - + if (framePr && framePr.attributes['w:dropCap']) { schemaNode.attrs.dropcap = { type: framePr.attributes['w:dropCap'], @@ -135,6 +137,14 @@ export const handleParagraphNode = (params) => { }; } + // Pass through this paragraph's sectPr, if any + const sectPr = pPr?.elements?.find((el) => el.name === 'w:sectPr'); + if (sectPr) { + if (!schemaNode.attrs.paragraphProperties) schemaNode.attrs.paragraphProperties = {}; + schemaNode.attrs.paragraphProperties.sectPr = sectPr; + schemaNode.attrs.pageBreakSource = 'sectPr'; + }; + return { nodes: schemaNode ? [schemaNode] : [], consumed: 1 }; }; @@ -252,16 +262,20 @@ const getDefaultParagraphStyle = (docx, styleId = '') => { // Styles based on styleId let pPrStyleIdSpacingTag = {}; let pPrStyleIdIndentTag = {}; + let pPrStyleJc = {}; if (styleId) { const stylesById = styles.elements[0].elements?.find((el) => el.name === 'w:style' && el.attributes['w:styleId'] === styleId); const pPrById = stylesById?.elements?.find((el) => el.name === 'w:pPr'); + pPrStyleIdSpacingTag = pPrById?.elements?.find((el) => el.name === 'w:spacing') || {}; pPrStyleIdIndentTag = pPrById?.elements?.find((el) => el.name === 'w:ind') || {}; + pPrStyleJc = pPrById?.elements?.find((el) => el.name === 'w:jc') || {}; } const { attributes: pPrDefaultSpacingAttr } = pPrDefaultSpacingTag; const { attributes: pPrNormalSpacingAttr } = pPrNormalSpacingTag; const { attributes: pPrByIdSpacingAttr } = pPrStyleIdSpacingTag; + const { attributes: pPrByIdJcAttr } = pPrStyleJc; const { attributes: pPrDefaultIndentAttr } = pPrDefaultIndentTag; const { attributes: pPrNormalIndentAttr } = pPrNormalIndentTag; @@ -270,6 +284,7 @@ const getDefaultParagraphStyle = (docx, styleId = '') => { return { spacing: pPrByIdSpacingAttr || pPrDefaultSpacingAttr || pPrNormalSpacingAttr, indent: pPrByIdIndentAttr || pPrDefaultIndentAttr || pPrNormalIndentAttr, + justify: pPrByIdJcAttr, }; }; diff --git a/packages/super-editor/src/core/super-converter/v2/importer/tabImporter.js b/packages/super-editor/src/core/super-converter/v2/importer/tabImporter.js index 0f044aa838..c02e4ceb1b 100644 --- a/packages/super-editor/src/core/super-converter/v2/importer/tabImporter.js +++ b/packages/super-editor/src/core/super-converter/v2/importer/tabImporter.js @@ -1,14 +1,25 @@ -import { parseProperties } from './importerHelpers.js'; +import { twipsToPixels } from '../../helpers.js'; /** * @type {import("docxImporter").NodeHandler} */ const handleTabNode = (params) => { - const { nodes } = params; + const { nodes, docx, parentStyleId } = params; if (nodes.length === 0 || nodes[0].name !== 'w:tab') { return { nodes: [], consumed: 0 }; } const node = nodes[0]; + + const styles = docx['word/styles.xml']; + let stylePos; + if (styles && styles.elements?.length) { + const style = styles.elements[0]?.elements?.find((s) => s.attributes?.['w:styleId'] === parentStyleId); + const pPr = style?.elements?.find((s) => s.name === 'w:pPr'); + const tabsDef = pPr?.elements?.find((s) => s.name === 'w:tabs'); + const firstTab = tabsDef?.elements?.find((s) => s.name === 'w:tab'); + stylePos = twipsToPixels(firstTab?.attributes?.['w:pos']); + } + const { attributes = {} } = node; const processedNode = { type: 'tab', diff --git a/packages/super-editor/src/extensions/line-break/line-break.js b/packages/super-editor/src/extensions/line-break/line-break.js index 01f722001e..2dcdc6795e 100644 --- a/packages/super-editor/src/extensions/line-break/line-break.js +++ b/packages/super-editor/src/extensions/line-break/line-break.js @@ -35,6 +35,19 @@ export const HardBreak = Node.create({ } }, + addAttributes() { + return { + pageBreakSource: { + rendered: false, + default: null, + }, + pageBreakType: { + default: null, + rendered: false, + } + } + }, + parseDOM() { return [{ tag: 'span' }]; }, diff --git a/packages/super-editor/src/extensions/pagination/pagination-helpers.js b/packages/super-editor/src/extensions/pagination/pagination-helpers.js index 964596b7bd..3438cc099b 100644 --- a/packages/super-editor/src/extensions/pagination/pagination-helpers.js +++ b/packages/super-editor/src/extensions/pagination/pagination-helpers.js @@ -14,8 +14,8 @@ export const initPaginationData = async (editor) => { if (!editor.converter) return; const sectionData = { headers: {}, footers: {} }; - const headerIds = editor.converter.headerIds; - const footerIds = editor.converter.footerIds; + const headerIds = editor.converter.headerIds.ids; + const footerIds = editor.converter.footerIds.ids; for (let key in headerIds) { const sectionId = headerIds[key]; diff --git a/packages/super-editor/src/extensions/pagination/pagination.js b/packages/super-editor/src/extensions/pagination/pagination.js index 4bfa07b75f..dfbd06f6bd 100644 --- a/packages/super-editor/src/extensions/pagination/pagination.js +++ b/packages/super-editor/src/extensions/pagination/pagination.js @@ -165,10 +165,26 @@ export const Pagination = Extension.create({ * @param {Editor} editor * @returns {String|null} The header or footer ID */ -const getHeaderFooterId = (currentPageNumber, sectionType, editor) => { +const getHeaderFooterId = (currentPageNumber, sectionType, editor, node = null) => { const { alternateHeaders } = editor.converter.pageStyles; const sectionIds = editor.converter[sectionType]; + if (node && node.attrs?.paragraphProperties?.sectPr) { + const sectPr = node.attrs?.paragraphProperties?.sectPr; + + if (currentPageNumber === 1) { + if (sectionType === 'headerIds') { + const sectionData = sectPr?.elements?.find((el) => el.name === 'w:headerReference' && el.attributes?.['w:type'] === 'first'); + const newId = sectionData?.attributes?.['r:id']; + return newId; + } else if (sectionType === 'footerIds') { + const sectionData = sectPr?.elements?.find((el) => el.name === 'w:footerReference' && el.attributes?.['w:type'] === 'first'); + const newId = sectionData?.attributes?.['r:id']; + return newId; + } + } + } + if (sectionIds?.titlePg && !sectionIds.first && currentPageNumber === 1) return null; const even = sectionIds.even; @@ -179,9 +195,11 @@ const getHeaderFooterId = (currentPageNumber, sectionType, editor) => { if (sectionIds?.titlePg && first && currentPageNumber === 1) return first; let sectionId = sectionIds.default; + if (currentPageNumber === 1) sectionId = first || defaultHeader; + if (alternateHeaders) { if (currentPageNumber === 1) sectionId = first; - else if (currentPageNumber % 2 === 0) sectionId = even || defaultHeader; + if (currentPageNumber % 2 === 0) sectionId = even || defaultHeader; else sectionId = odd || defaultHeader; } @@ -319,7 +337,22 @@ function generateInternalPageBreaks(doc, view, editor, sectionData) { if (!coords) return; let shouldAddPageBreak = coords.bottom > pageHeightThreshold; - const isHardBreakNode = currentNode.type.name === 'hardBreak'; + let isHardBreakNode = currentNode.type.name === 'hardBreak'; + + const paragraphSectPrBreak = currentNode.attrs?.pageBreakSource; + if (paragraphSectPrBreak === 'sectPr') { + const nextNode = doc.nodeAt(currentPos + currentNode.nodeSize); + const nextNodeSectPr = nextNode?.attrs?.pageBreakSource === 'sectPr'; + if (!nextNodeSectPr) isHardBreakNode = true; + + if (currentPageNumber === 1) { + const headerId = getHeaderFooterId(currentPageNumber, 'headerIds', editor, currentNode); + decorations.pop(); // Remove the first header and replace with sectPr header + const newFirstHeader = createHeader(pageMargins, pageSize, sectionData, headerId); + const pageBreak = createPageBreak({ editor, header: newFirstHeader, isFirstHeader: true }); + decorations.push(Decoration.widget(0, pageBreak, { key: 'stable-key' })); + }; + }; if (currentNode.type.name === 'paragraph' && currentNode.attrs.styleId) { const linkedStyles = LinkedStylesPluginKey.getState(editor.state)?.styles; @@ -359,9 +392,10 @@ function generateInternalPageBreaks(doc, view, editor, sectionData) { }; // Update the header and footer based on the current page number + const footerId = getHeaderFooterId(currentPageNumber, 'footerIds', editor, currentNode); + currentPageNumber++; const headerId = getHeaderFooterId(currentPageNumber, 'headerIds', editor); - const footerId = getHeaderFooterId(currentPageNumber, 'footerIds', editor); header = createHeader(pageMargins, pageSize, sectionData, headerId); footer = createFooter(pageMargins, pageSize, sectionData, footerId, currentPageNumber - 1); diff --git a/packages/super-editor/src/extensions/paragraph/paragraph.js b/packages/super-editor/src/extensions/paragraph/paragraph.js index 4c31cbc723..132e3b17b2 100644 --- a/packages/super-editor/src/extensions/paragraph/paragraph.js +++ b/packages/super-editor/src/extensions/paragraph/paragraph.js @@ -88,6 +88,21 @@ export const Paragraph = Node.create({ keepNext: { rendered: false }, paragraphProperties: { rendered: false }, dropcap: { rendered: false }, + pageBreakSource: { rendered: false }, + justify: { + renderDOM: ({ justify }) => { + const { val: jc } = justify || {}; + if (!jc) return {}; + + let style = ''; + if (jc === 'left') style += 'text-align: left;'; + else if (jc === 'right') style += 'text-align: right;'; + else if (jc === 'center') style += 'text-align: center;'; + else if (jc === 'both') style += 'text-align: justify;'; + + return { style }; + }, + } }; }, diff --git a/packages/super-editor/src/extensions/tab/tab.js b/packages/super-editor/src/extensions/tab/tab.js index 34687983d1..637db5a052 100644 --- a/packages/super-editor/src/extensions/tab/tab.js +++ b/packages/super-editor/src/extensions/tab/tab.js @@ -36,7 +36,7 @@ export const TabNode = Node.create({ tabSize: { renderDOM: ({ tabSize }) => { if (!tabSize) return {}; - const style = `width: ${tabSize}px;`; + const style = `width: ${tabSize}px; min-width: ${tabSize}px;`; return { style }; }, },