diff --git a/packages/super-editor/src/core/helpers/list-numbering-helpers.js b/packages/super-editor/src/core/helpers/list-numbering-helpers.js index f0dba8bea9..c837184550 100644 --- a/packages/super-editor/src/core/helpers/list-numbering-helpers.js +++ b/packages/super-editor/src/core/helpers/list-numbering-helpers.js @@ -17,24 +17,70 @@ import { findParentNode } from '@helpers/index.js'; * @param {Editor} param0.editor - The editor instance where the list definition will be added. * @returns {Object} The new abstract and num definitions. */ -export const generateNewListDefinition = ({ numId, listType, editor }) => { +export const generateNewListDefinition = ({ numId, listType, level, start, text, fmt, editor }) => { // Generate a new numId to add to numbering.xml if (typeof listType === 'string') listType = editor.schema.nodes[listType]; const definition = listType.name === 'orderedList' ? baseOrderedListDef : baseBulletList; const numbering = editor.converter.numbering; const newNumbering = { ...numbering }; + let skipAddingNewAbstract = false; // Generate the new abstractNum definition - const newAbstractId = getNewListId(editor, 'abstracts'); - const newAbstractDef = { - ...definition, - attributes: { - ...definition.attributes, - 'w:abstractNumId': String(newAbstractId), - }, - }; - newNumbering.abstracts[newAbstractId] = newAbstractDef; + let newAbstractId = getNewListId(editor, 'abstracts'); + let newAbstractDef = JSON.parse( + JSON.stringify({ + ...definition, + attributes: { + ...definition.attributes, + 'w:abstractNumId': String(newAbstractId), + }, + }), + ); + + // Generate the new abstractNum definition for copy/paste lists + if (level && start && text && fmt) { + if (newNumbering.definitions[numId]) { + const abstractId = newNumbering.definitions[numId]?.elements[0]?.attributes['w:val']; + newAbstractId = abstractId; + const abstract = editor.converter.numbering.abstracts[abstractId]; + newAbstractDef = { ...abstract }; + skipAddingNewAbstract = true; + } + + const levelDefIndex = newAbstractDef.elements.findIndex( + (el) => el.name === 'w:lvl' && el.attributes['w:ilvl'] === level, + ); + const levelProps = newAbstractDef.elements[levelDefIndex]; + const elToFilter = ['w:numFmt', 'w:lvlText', 'w:start']; + const oldElements = levelProps.elements.filter((el) => !elToFilter.includes(el.name)); + levelProps.elements = [ + ...oldElements, + { + type: 'element', + name: 'w:start', + attributes: { + 'w:val': start, + }, + }, + { + type: 'element', + name: 'w:numFmt', + attributes: { + 'w:val': fmt, + }, + }, + { + type: 'element', + name: 'w:lvlText', + attributes: { + 'w:val': text, + }, + }, + ]; + } + + if (!skipAddingNewAbstract) newNumbering.abstracts[newAbstractId] = newAbstractDef; // Generate the new numId definition const newNumDef = getBasicNumIdTag(numId, newAbstractId); diff --git a/packages/super-editor/src/core/helpers/pasteListHelpers.js b/packages/super-editor/src/core/helpers/pasteListHelpers.js new file mode 100644 index 0000000000..2ba0bd4a88 --- /dev/null +++ b/packages/super-editor/src/core/helpers/pasteListHelpers.js @@ -0,0 +1,79 @@ +export const extractListLevelStyles = (cssText, listId, level) => { + const pattern = new RegExp(`@list\\s+l${listId}:level${level}\\s*\\{([^}]+)\\}`, 'i'); + const match = cssText.match(pattern); + if (!match) return null; + + const rawStyles = match[1] + .split(';') + .map((line) => line.trim()) + .filter(Boolean); + + const styleMap = {}; + for (const style of rawStyles) { + const [key, value] = style.split(':').map((s) => s.trim()); + styleMap[key] = value; + } + + return styleMap; +}; + +export const numDefMap = new Map([ + ['decimal', 'decimal'], + ['alpha-lower', 'lowerLetter'], + ['alpha-upper', 'upperLetter'], + ['roman-lower', 'lowerRoman'], + ['roman-upper', 'upperRoman'], + ['bullet', 'bullet'], +]); + +export const numDefByTypeMap = new Map([ + ['1', 'decimal'], + ['a', 'lowerLetter'], + ['A', 'upperLetter'], + ['I', 'upperRoman'], + ['i', 'lowerRoman'], +]); + +function getStartNumber(lvlText) { + const match = lvlText.match(/^(\d+)/); + if (match) return parseInt(match[1], 10); + return null; +} + +function letterToNumber(letter) { + return letter.toLowerCase().charCodeAt(0) - 'a'.charCodeAt(0) + 1; +} + +function getStartNumberFromAlpha(lvlText) { + const match = lvlText.match(/^([a-zA-Z])/); + if (match) return letterToNumber(match[1]); + return null; +} + +function romanToNumber(roman) { + const map = { I: 1, V: 5, X: 10, L: 50, C: 100, D: 500, M: 1000 }; + let num = 0, + prev = 0; + for (let i = roman.length - 1; i >= 0; i--) { + const curr = map[roman[i].toUpperCase()] || 0; + if (curr < prev) num -= curr; + else num += curr; + prev = curr; + } + return num; +} + +function getStartNumberFromRoman(lvlText) { + const match = lvlText.match(/^([ivxlcdmIVXLCDM]+)/); + if (match) return romanToNumber(match[1]); + return null; +} + +export const startHelperMap = new Map([ + ['decimal', getStartNumber], + ['lowerLetter', getStartNumberFromAlpha], + ['upperLetter', getStartNumberFromAlpha], + ['lowerRoman', getStartNumberFromRoman], + ['upperRoman', getStartNumberFromRoman], + ['bullet', () => 1], +]); diff --git a/packages/super-editor/src/core/inputRules/docx-paste/docx-paste.js b/packages/super-editor/src/core/inputRules/docx-paste/docx-paste.js index 8569ba45b2..03599c95d2 100644 --- a/packages/super-editor/src/core/inputRules/docx-paste/docx-paste.js +++ b/packages/super-editor/src/core/inputRules/docx-paste/docx-paste.js @@ -1,34 +1,8 @@ import { DOMParser } from 'prosemirror-model'; import { cleanHtmlUnnecessaryTags, convertEmToPt, handleHtmlPaste } from '../../InputRule.js'; import { ListHelpers } from '@helpers/list-numbering-helpers.js'; - -function extractListLevelStyles(cssText, listId, level) { - const pattern = new RegExp(`@list\\s+l${listId}:level${level}\\s*\\{([^}]+)\\}`, 'i'); - const match = cssText.match(pattern); - if (!match) return null; - - const rawStyles = match[1] - .split(';') - .map((line) => line.trim()) - .filter(Boolean); - - const styleMap = {}; - for (const style of rawStyles) { - const [key, value] = style.split(':').map((s) => s.trim()); - styleMap[key] = value; - } - - return styleMap; -} - -const numDefMap = new Map([ - ['decimal', { def: 'decimal', abstractNum: 1 }], - ['alpha-lower', { def: 'lowerLetter', abstractNum: 1 }], - ['alpha-upper', { def: 'upperLetter', abstractNum: 1 }], - ['roman-lower', { def: 'lowerRoman', abstractNum: 1 }], - ['roman-upper', { def: 'upperRoman', abstractNum: 1 }], - ['bullet', { def: 'bullet', abstractNum: 0 }], -]); +import { extractListLevelStyles, numDefByTypeMap, numDefMap, startHelperMap } from '@helpers/pasteListHelpers.js'; +import { normalizeLvlTextChar } from '../../super-converter/v2/importer/listImporter.js'; /** * Main handler for pasted DOCX content. @@ -49,49 +23,62 @@ export const handleDocxPaste = (html, editor, view, plugin) => { const tempDiv = document.createElement('div'); tempDiv.innerHTML = cleanedHtml; - const paragraphs = tempDiv.querySelectorAll('p'); - paragraphs.forEach((p) => { - const innerHTML = p.innerHTML; + const data = tempDiv.querySelectorAll('p, li'); + + const startMap = {}; - // Looking only for lists to extract list info - if (!innerHTML.includes('