From b12c6cc4282d5777118c9d2e177f0198af048989 Mon Sep 17 00:00:00 2001 From: Hufe921 Date: Tue, 25 Jul 2023 22:13:17 +0800 Subject: [PATCH] feat: add getHTML api #218 --- docs/guide/command-get.md | 14 +- src/editor/core/command/Command.ts | 2 + src/editor/core/command/CommandAdapt.ts | 17 +- src/editor/core/cursor/CursorAgent.ts | 3 +- src/editor/interface/Editor.ts | 6 + src/editor/utils/clipboard.ts | 417 +---------------------- src/editor/utils/element.ts | 421 +++++++++++++++++++++++- 7 files changed, 460 insertions(+), 420 deletions(-) diff --git a/docs/guide/command-get.md b/docs/guide/command-get.md index 6824d9f..741c82f 100644 --- a/docs/guide/command-get.md +++ b/docs/guide/command-get.md @@ -81,4 +81,16 @@ const { 用法: ```javascript const catalog = await instance.command.getCatalog() -``` \ No newline at end of file +``` + +## getHTML +功能:获取HTML + +用法: +```javascript +const { + header: string + main: string + footer: string +} = await instance.command.getHTML() +``` diff --git a/src/editor/core/command/Command.ts b/src/editor/core/command/Command.ts index a00ff73..aab6c47 100644 --- a/src/editor/core/command/Command.ts +++ b/src/editor/core/command/Command.ts @@ -77,6 +77,7 @@ export class Command { public getCatalog: CommandAdapt['getCatalog'] public getImage: CommandAdapt['getImage'] public getValue: CommandAdapt['getValue'] + public getHTML: CommandAdapt['getHTML'] public getWordCount: CommandAdapt['getWordCount'] public getRangeText: CommandAdapt['getRangeText'] public getRangeContext: CommandAdapt['getRangeContext'] @@ -167,6 +168,7 @@ export class Command { // 获取 this.getImage = adapt.getImage.bind(adapt) this.getValue = adapt.getValue.bind(adapt) + this.getHTML = adapt.getHTML.bind(adapt) this.getWordCount = adapt.getWordCount.bind(adapt) this.getRangeText = adapt.getRangeText.bind(adapt) this.getRangeContext = adapt.getRangeContext.bind(adapt) diff --git a/src/editor/core/command/CommandAdapt.ts b/src/editor/core/command/CommandAdapt.ts index 6a221a2..e32a44d 100644 --- a/src/editor/core/command/CommandAdapt.ts +++ b/src/editor/core/command/CommandAdapt.ts @@ -17,6 +17,7 @@ import { TableBorder } from '../../dataset/enum/table/Table' import { TitleLevel } from '../../dataset/enum/Title' import { VerticalAlign } from '../../dataset/enum/VerticalAlign' import { ICatalog } from '../../interface/Catalog' +import { DeepRequired } from '../../interface/Common' import { IAppendElementListOption, IDrawImagePayload, @@ -25,6 +26,7 @@ import { } from '../../interface/Draw' import { IEditorData, + IEditorHTML, IEditorOption, IEditorResult } from '../../interface/Editor' @@ -37,6 +39,7 @@ import { ITr } from '../../interface/table/Tr' import { IWatermark } from '../../interface/Watermark' import { deepClone, downloadFile, getUUID } from '../../utils' import { + createDomFromElementList, formatElementContext, formatElementList, isTextLikeElement, @@ -61,7 +64,7 @@ export class CommandAdapt { private historyManager: HistoryManager private canvasEvent: CanvasEvent private tableTool: TableTool - private options: Required + private options: DeepRequired private control: Control private workerManager: WorkerManager private searchManager: Search @@ -1680,6 +1683,18 @@ export class CommandAdapt { return this.draw.getValue(options) } + public getHTML(): IEditorHTML { + const options = this.options + const headerElementList = this.draw.getHeaderElementList() + const mainElementList = this.draw.getOriginalMainElementList() + const footerElementList = this.draw.getFooterElementList() + return { + header: createDomFromElementList(headerElementList, options).innerHTML, + main: createDomFromElementList(mainElementList, options).innerHTML, + footer: createDomFromElementList(footerElementList, options).innerHTML + } + } + public getWordCount(): Promise { return this.workerManager.getWordCount() } diff --git a/src/editor/core/cursor/CursorAgent.ts b/src/editor/core/cursor/CursorAgent.ts index 33a6cb8..e8e8e60 100644 --- a/src/editor/core/cursor/CursorAgent.ts +++ b/src/editor/core/cursor/CursorAgent.ts @@ -4,8 +4,7 @@ import { VIRTUAL_ELEMENT_TYPE } from '../../dataset/constant/Element' import { ElementType } from '../../dataset/enum/Element' import { IElement } from '../../interface/Element' import { debounce } from '../../utils' -import { getElementListByHTML } from '../../utils/clipboard' -import { formatElementContext } from '../../utils/element' +import { formatElementContext, getElementListByHTML } from '../../utils/element' import { Draw } from '../draw/Draw' import { CanvasEvent } from '../event/CanvasEvent' diff --git a/src/editor/interface/Editor.ts b/src/editor/interface/Editor.ts index 4e76ad7..0f64663 100644 --- a/src/editor/interface/Editor.ts +++ b/src/editor/interface/Editor.ts @@ -79,3 +79,9 @@ export interface IEditorResult { watermark?: IWatermark data: IEditorData } + +export interface IEditorHTML { + header: string + main: string + footer: string +} diff --git a/src/editor/utils/clipboard.ts b/src/editor/utils/clipboard.ts index d08d618..24d3853 100644 --- a/src/editor/utils/clipboard.ts +++ b/src/editor/utils/clipboard.ts @@ -1,23 +1,6 @@ -import { IEditorOption, IElement, ListStyle, ListType, RowFlex } from '..' -import { ZERO } from '../dataset/constant/Common' -import { - INLINE_NODE_NAME, - TEXTLIKE_ELEMENT_TYPE -} from '../dataset/constant/Element' -import { - listStyleCSSMapping, - listTypeElementMapping -} from '../dataset/constant/List' -import { - titleNodeNameMapping, - titleOrderNumberMapping -} from '../dataset/constant/Title' -import { ControlComponent } from '../dataset/enum/Control' -import { ElementType } from '../dataset/enum/Element' +import { IEditorOption, IElement } from '..' import { DeepRequired } from '../interface/Common' -import { ITd } from '../interface/table/Td' -import { ITr } from '../interface/table/Tr' -import { getElementRowFlex, zipElementList } from './element' +import { createDomFromElementList } from './element' export function writeClipboardItem(text: string, html: string) { if (!text || !html) return @@ -46,175 +29,11 @@ export function writeClipboardItem(text: string, html: string) { } } -export function convertElementToDom( - element: IElement, - options: DeepRequired -): HTMLElement { - let tagName: keyof HTMLElementTagNameMap = 'span' - if (element.type === ElementType.SUPERSCRIPT) { - tagName = 'sup' - } else if (element.type === ElementType.SUBSCRIPT) { - tagName = 'sub' - } else if ( - element.rowFlex === RowFlex.CENTER || - element.rowFlex === RowFlex.RIGHT - ) { - tagName = 'p' - } - const dom = document.createElement(tagName) - dom.style.fontFamily = element.font || options.defaultFont - if (element.rowFlex) { - const isAlignment = element.rowFlex === RowFlex.ALIGNMENT - dom.style.textAlign = isAlignment ? 'justify' : element.rowFlex - } - if (element.color) { - dom.style.color = element.color - } - if (element.bold) { - dom.style.fontWeight = '600' - } - if (element.italic) { - dom.style.fontStyle = 'italic' - } - dom.style.fontSize = `${element.size || options.defaultSize}px` - if (element.highlight) { - dom.style.backgroundColor = element.highlight - } - dom.innerText = element.value.replace(new RegExp(`${ZERO}`, 'g'), '\n') - return dom -} - export function writeElementList( elementList: IElement[], options: DeepRequired ) { - function buildDomFromElementList(payload: IElement[]): HTMLDivElement { - const clipboardDom = document.createElement('div') - for (let e = 0; e < payload.length; e++) { - const element = payload[e] - // 构造表格 - if (element.type === ElementType.TABLE) { - const tableDom: HTMLTableElement = document.createElement('table') - const trList = element.trList! - for (let t = 0; t < trList.length; t++) { - const trDom = document.createElement('tr') - const tr = trList[t] - for (let d = 0; d < tr.tdList.length; d++) { - const tdDom = document.createElement('td') - tdDom.style.border = '1px solid' - const td = tr.tdList[d] - tdDom.colSpan = td.colspan - tdDom.rowSpan = td.rowspan - const childDom = buildDomFromElementList(zipElementList(td.value!)) - tdDom.innerHTML = childDom.innerHTML - if (td.backgroundColor) { - tdDom.style.backgroundColor = td.backgroundColor - } - trDom.append(tdDom) - } - tableDom.append(trDom) - } - clipboardDom.append(tableDom) - } else if (element.type === ElementType.HYPERLINK) { - const a = document.createElement('a') - a.innerText = element.valueList!.map(v => v.value).join('') - if (element.url) { - a.href = element.url - } - clipboardDom.append(a) - } else if (element.type === ElementType.TITLE) { - const h = document.createElement( - `h${titleOrderNumberMapping[element.level!]}` - ) - const childDom = buildDomFromElementList( - zipElementList(element.valueList!) - ) - h.innerHTML = childDom.innerHTML - clipboardDom.append(h) - } else if (element.type === ElementType.LIST) { - const list = document.createElement( - listTypeElementMapping[element.listType!] - ) - if (element.listStyle) { - list.style.listStyleType = listStyleCSSMapping[element.listStyle] - } - // 按照换行符拆分 - let curListIndex = 0 - const listElementListMap: Map = new Map() - const zipList = zipElementList(element.valueList!) - for (let z = 0; z < zipList.length; z++) { - const zipElement = zipList[z] - if (zipElement.listWrap) { - const listElementList = listElementListMap.get(curListIndex) || [] - listElementList.push(zipElement) - listElementListMap.set(curListIndex, listElementList) - } else { - const zipValueList = zipElement.value.split('\n') - for (let c = 0; c < zipValueList.length; c++) { - if (c > 0) { - curListIndex += 1 - } - const value = zipValueList[c] - const listElementList = listElementListMap.get(curListIndex) || [] - listElementList.push({ - ...zipElement, - value - }) - listElementListMap.set(curListIndex, listElementList) - } - } - } - listElementListMap.forEach(listElementList => { - const li = document.createElement('li') - const childDom = buildDomFromElementList(listElementList) - li.innerHTML = childDom.innerHTML - list.append(li) - }) - clipboardDom.append(list) - } else if (element.type === ElementType.IMAGE) { - const img = document.createElement('img') - if (element.value) { - img.src = element.value - img.width = element.width! - img.height = element.height! - } - clipboardDom.append(img) - } else if (element.type === ElementType.SEPARATOR) { - const hr = document.createElement('hr') - clipboardDom.append(hr) - } else if (element.type === ElementType.CHECKBOX) { - const checkbox = document.createElement('input') - checkbox.type = 'checkbox' - if (element.checkbox?.value) { - checkbox.setAttribute('checked', 'true') - } - clipboardDom.append(checkbox) - } else if ( - !element.type || - element.type === ElementType.LATEX || - TEXTLIKE_ELEMENT_TYPE.includes(element.type) - ) { - let text = '' - if (element.type === ElementType.CONTROL) { - text = element.control!.value?.[0]?.value || '' - } else if (element.type === ElementType.DATE) { - text = element.valueList?.map(v => v.value).join('') || '' - } else { - text = element.value - } - if (!text) continue - // 前一个元素是标题,移除首行换行符 - if (payload[e - 1]?.type === ElementType.TITLE) { - text = text.replace(/^\n/, '') - } - const dom = convertElementToDom(element, options) - dom.innerText = text.replace(new RegExp(`${ZERO}`, 'g'), '\n') - clipboardDom.append(dom) - } - } - return clipboardDom - } - const clipboardDom = buildDomFromElementList(zipElementList(elementList)) + const clipboardDom = createDomFromElementList(elementList, options) // 写入剪贴板 document.body.append(clipboardDom) const text = clipboardDom.innerText @@ -224,233 +43,3 @@ export function writeElementList( if (!text || !html) return writeClipboardItem(text, html) } - -export function convertTextNodeToElement( - textNode: Element | Node -): IElement | null { - if (!textNode || textNode.nodeType !== 3) return null - const parentNode = textNode.parentNode - const anchorNode = - parentNode.nodeName === 'FONT' - ? parentNode.parentNode - : parentNode - const rowFlex = getElementRowFlex(anchorNode) - const value = textNode.textContent - const style = window.getComputedStyle(anchorNode) - if (!value || anchorNode.nodeName === 'STYLE') return null - const element: IElement = { - value, - color: style.color, - bold: Number(style.fontWeight) > 500, - italic: style.fontStyle.includes('italic'), - size: Math.floor(parseFloat(style.fontSize)) - } - // 元素类型-默认文本 - if (anchorNode.nodeName === 'SUB' || style.verticalAlign === 'sub') { - element.type = ElementType.SUBSCRIPT - } else if (anchorNode.nodeName === 'SUP' || style.verticalAlign === 'super') { - element.type = ElementType.SUPERSCRIPT - } - // 行对齐 - if (rowFlex !== RowFlex.LEFT) { - element.rowFlex = rowFlex - } - // 高亮色 - if (style.backgroundColor !== 'rgba(0, 0, 0, 0)') { - element.highlight = style.backgroundColor - } - return element -} - -interface IGetElementListByHTMLOption { - innerWidth: number -} - -export function getElementListByHTML( - htmlText: string, - options: IGetElementListByHTMLOption -): IElement[] { - const elementList: IElement[] = [] - function findTextNode(dom: Element | Node) { - if (dom.nodeType === 3) { - const element = convertTextNodeToElement(dom) - if (element) { - elementList.push(element) - } - } else if (dom.nodeType === 1) { - const childNodes = dom.childNodes - for (let n = 0; n < childNodes.length; n++) { - const node = childNodes[n] - // br元素与display:block元素需换行 - if (node.nodeName === 'BR') { - elementList.push({ - value: '\n' - }) - } else if (node.nodeName === 'A') { - const aElement = node as HTMLLinkElement - const value = aElement.innerText - if (value) { - elementList.push({ - type: ElementType.HYPERLINK, - value: '', - valueList: [ - { - value - } - ], - url: aElement.href - }) - } - } else if (/H[1-6]/.test(node.nodeName)) { - const hElement = node as HTMLTitleElement - const valueList = getElementListByHTML(hElement.innerHTML, options) - elementList.push({ - value: '', - type: ElementType.TITLE, - level: titleNodeNameMapping[node.nodeName], - valueList - }) - if ( - node.nextSibling && - !INLINE_NODE_NAME.includes(node.nextSibling.nodeName) - ) { - elementList.push({ - value: '\n' - }) - } - } else if (node.nodeName === 'UL' || node.nodeName === 'OL') { - const listNode = node as HTMLOListElement | HTMLUListElement - const listElement: IElement = { - value: '', - type: ElementType.LIST, - valueList: [] - } - if (node.nodeName === 'OL') { - listElement.listType = ListType.OL - } else { - listElement.listType = ListType.UL - listElement.listStyle = ( - (listNode.style.listStyleType) - ) - } - listNode.querySelectorAll('li').forEach(li => { - const liValueList = getElementListByHTML(li.innerHTML, options) - liValueList.forEach(list => { - if (list.value === '\n') { - list.listWrap = true - } - }) - liValueList.unshift({ - value: '\n' - }) - listElement.valueList!.push(...liValueList) - }) - elementList.push(listElement) - } else if (node.nodeName === 'HR') { - elementList.push({ - value: '\n', - type: ElementType.SEPARATOR - }) - } else if (node.nodeName === 'IMG') { - const { src, width, height } = node as HTMLImageElement - if (src && width && height) { - elementList.push({ - width, - height, - value: src, - type: ElementType.IMAGE - }) - } - } else if (node.nodeName === 'TABLE') { - const tableElement = node as HTMLTableElement - const element: IElement = { - type: ElementType.TABLE, - value: '\n', - colgroup: [], - trList: [] - } - // 基础数据 - tableElement.querySelectorAll('tr').forEach(trElement => { - const trHeightStr = window - .getComputedStyle(trElement) - .height.replace('px', '') - const tr: ITr = { - height: Number(trHeightStr), - tdList: [] - } - trElement.querySelectorAll('th,td').forEach(tdElement => { - const tableCell = tdElement - const valueList = getElementListByHTML( - tableCell.innerHTML, - options - ) - const td: ITd = { - colspan: tableCell.colSpan, - rowspan: tableCell.rowSpan, - value: valueList - } - if (tableCell.style.backgroundColor) { - td.backgroundColor = tableCell.style.backgroundColor - } - tr.tdList.push(td) - }) - if (tr.tdList.length) { - element.trList!.push(tr) - } - }) - if (element.trList!.length) { - // 列选项数据 - const tdCount = element.trList![0].tdList.reduce( - (pre, cur) => pre + cur.colspan, - 0 - ) - const width = Math.ceil(options.innerWidth / tdCount) - for (let i = 0; i < tdCount; i++) { - element.colgroup!.push({ - width - }) - } - elementList.push(element) - } - } else if ( - node.nodeName === 'INPUT' && - (node).type === ControlComponent.CHECKBOX - ) { - elementList.push({ - type: ElementType.CHECKBOX, - value: '', - checkbox: { - value: (node).checked - } - }) - } else { - findTextNode(node) - if (node.nodeType === 1 && n !== childNodes.length - 1) { - const display = window.getComputedStyle(node as Element).display - if (display === 'block') { - elementList.push({ - value: '\n' - }) - } - } - } - } - } - } - // 追加dom - const clipboardDom = document.createElement('div') - clipboardDom.innerHTML = htmlText - document.body.appendChild(clipboardDom) - const deleteNodes: ChildNode[] = [] - clipboardDom.childNodes.forEach(child => { - if (child.nodeType !== 1 && !child.textContent?.trim()) { - deleteNodes.push(child) - } - }) - deleteNodes.forEach(node => node.remove()) - // 搜索文本节点 - findTextNode(clipboardDom) - // 移除dom - clipboardDom.remove() - return elementList -} diff --git a/src/editor/utils/element.ts b/src/editor/utils/element.ts index 62f8068..dbefe5b 100644 --- a/src/editor/utils/element.ts +++ b/src/editor/utils/element.ts @@ -1,5 +1,12 @@ import { cloneProperty, deepClone, getUUID, splitText } from '.' -import { ElementType, IEditorOption, IElement, RowFlex } from '..' +import { + ElementType, + IEditorOption, + IElement, + ListStyle, + ListType, + RowFlex +} from '..' import { LaTexParticle } from '../core/draw/particle/latex/LaTexParticle' import { defaultCheckboxOption } from '../dataset/constant/Checkbox' import { ZERO } from '../dataset/constant/Common' @@ -7,12 +14,23 @@ import { defaultControlOption } from '../dataset/constant/Control' import { EDITOR_ELEMENT_CONTEXT_ATTR, EDITOR_ELEMENT_ZIP_ATTR, + INLINE_NODE_NAME, TABLE_CONTEXT_ATTR, TEXTLIKE_ELEMENT_TYPE } from '../dataset/constant/Element' -import { titleSizeMapping } from '../dataset/constant/Title' +import { + listStyleCSSMapping, + listTypeElementMapping +} from '../dataset/constant/List' +import { + titleNodeNameMapping, + titleOrderNumberMapping, + titleSizeMapping +} from '../dataset/constant/Title' import { ControlComponent, ControlType } from '../dataset/enum/Control' +import { DeepRequired } from '../interface/Common' import { ITd } from '../interface/table/Td' +import { ITr } from '../interface/table/Tr' export function unzipElementList(elementList: IElement[]): IElement[] { const result: IElement[] = [] @@ -644,3 +662,402 @@ export function formatElementContext( ) } } + +export function convertElementToDom( + element: IElement, + options: DeepRequired +): HTMLElement { + let tagName: keyof HTMLElementTagNameMap = 'span' + if (element.type === ElementType.SUPERSCRIPT) { + tagName = 'sup' + } else if (element.type === ElementType.SUBSCRIPT) { + tagName = 'sub' + } else if ( + element.rowFlex === RowFlex.CENTER || + element.rowFlex === RowFlex.RIGHT + ) { + tagName = 'p' + } + const dom = document.createElement(tagName) + dom.style.fontFamily = element.font || options.defaultFont + if (element.rowFlex) { + const isAlignment = element.rowFlex === RowFlex.ALIGNMENT + dom.style.textAlign = isAlignment ? 'justify' : element.rowFlex + } + if (element.color) { + dom.style.color = element.color + } + if (element.bold) { + dom.style.fontWeight = '600' + } + if (element.italic) { + dom.style.fontStyle = 'italic' + } + dom.style.fontSize = `${element.size || options.defaultSize}px` + if (element.highlight) { + dom.style.backgroundColor = element.highlight + } + dom.innerText = element.value.replace(new RegExp(`${ZERO}`, 'g'), '\n') + return dom +} + +export function createDomFromElementList( + elementList: IElement[], + options: DeepRequired +) { + function buildDom(payload: IElement[]): HTMLDivElement { + const clipboardDom = document.createElement('div') + for (let e = 0; e < payload.length; e++) { + const element = payload[e] + // 构造表格 + if (element.type === ElementType.TABLE) { + const tableDom: HTMLTableElement = document.createElement('table') + const trList = element.trList! + for (let t = 0; t < trList.length; t++) { + const trDom = document.createElement('tr') + const tr = trList[t] + for (let d = 0; d < tr.tdList.length; d++) { + const tdDom = document.createElement('td') + tdDom.style.border = '1px solid' + const td = tr.tdList[d] + tdDom.colSpan = td.colspan + tdDom.rowSpan = td.rowspan + const childDom = buildDom(zipElementList(td.value!)) + tdDom.innerHTML = childDom.innerHTML + if (td.backgroundColor) { + tdDom.style.backgroundColor = td.backgroundColor + } + trDom.append(tdDom) + } + tableDom.append(trDom) + } + clipboardDom.append(tableDom) + } else if (element.type === ElementType.HYPERLINK) { + const a = document.createElement('a') + a.innerText = element.valueList!.map(v => v.value).join('') + if (element.url) { + a.href = element.url + } + clipboardDom.append(a) + } else if (element.type === ElementType.TITLE) { + const h = document.createElement( + `h${titleOrderNumberMapping[element.level!]}` + ) + const childDom = buildDom(zipElementList(element.valueList!)) + h.innerHTML = childDom.innerHTML + clipboardDom.append(h) + } else if (element.type === ElementType.LIST) { + const list = document.createElement( + listTypeElementMapping[element.listType!] + ) + if (element.listStyle) { + list.style.listStyleType = listStyleCSSMapping[element.listStyle] + } + // 按照换行符拆分 + let curListIndex = 0 + const listElementListMap: Map = new Map() + const zipList = zipElementList(element.valueList!) + for (let z = 0; z < zipList.length; z++) { + const zipElement = zipList[z] + if (zipElement.listWrap) { + const listElementList = listElementListMap.get(curListIndex) || [] + listElementList.push(zipElement) + listElementListMap.set(curListIndex, listElementList) + } else { + const zipValueList = zipElement.value.split('\n') + for (let c = 0; c < zipValueList.length; c++) { + if (c > 0) { + curListIndex += 1 + } + const value = zipValueList[c] + const listElementList = listElementListMap.get(curListIndex) || [] + listElementList.push({ + ...zipElement, + value + }) + listElementListMap.set(curListIndex, listElementList) + } + } + } + listElementListMap.forEach(listElementList => { + const li = document.createElement('li') + const childDom = buildDom(listElementList) + li.innerHTML = childDom.innerHTML + list.append(li) + }) + clipboardDom.append(list) + } else if (element.type === ElementType.IMAGE) { + const img = document.createElement('img') + if (element.value) { + img.src = element.value + img.width = element.width! + img.height = element.height! + } + clipboardDom.append(img) + } else if (element.type === ElementType.SEPARATOR) { + const hr = document.createElement('hr') + clipboardDom.append(hr) + } else if (element.type === ElementType.CHECKBOX) { + const checkbox = document.createElement('input') + checkbox.type = 'checkbox' + if (element.checkbox?.value) { + checkbox.setAttribute('checked', 'true') + } + clipboardDom.append(checkbox) + } else if ( + !element.type || + element.type === ElementType.LATEX || + TEXTLIKE_ELEMENT_TYPE.includes(element.type) + ) { + let text = '' + if (element.type === ElementType.CONTROL) { + text = element.control!.value?.[0]?.value || '' + } else if (element.type === ElementType.DATE) { + text = element.valueList?.map(v => v.value).join('') || '' + } else { + text = element.value + } + if (!text) continue + // 前一个元素是标题,移除首行换行符 + if (payload[e - 1]?.type === ElementType.TITLE) { + text = text.replace(/^\n/, '') + } + const dom = convertElementToDom(element, options) + dom.innerText = text.replace(new RegExp(`${ZERO}`, 'g'), '\n') + clipboardDom.append(dom) + } + } + return clipboardDom + } + return buildDom(zipElementList(elementList)) +} + +export function convertTextNodeToElement( + textNode: Element | Node +): IElement | null { + if (!textNode || textNode.nodeType !== 3) return null + const parentNode = textNode.parentNode + const anchorNode = + parentNode.nodeName === 'FONT' + ? parentNode.parentNode + : parentNode + const rowFlex = getElementRowFlex(anchorNode) + const value = textNode.textContent + const style = window.getComputedStyle(anchorNode) + if (!value || anchorNode.nodeName === 'STYLE') return null + const element: IElement = { + value, + color: style.color, + bold: Number(style.fontWeight) > 500, + italic: style.fontStyle.includes('italic'), + size: Math.floor(parseFloat(style.fontSize)) + } + // 元素类型-默认文本 + if (anchorNode.nodeName === 'SUB' || style.verticalAlign === 'sub') { + element.type = ElementType.SUBSCRIPT + } else if (anchorNode.nodeName === 'SUP' || style.verticalAlign === 'super') { + element.type = ElementType.SUPERSCRIPT + } + // 行对齐 + if (rowFlex !== RowFlex.LEFT) { + element.rowFlex = rowFlex + } + // 高亮色 + if (style.backgroundColor !== 'rgba(0, 0, 0, 0)') { + element.highlight = style.backgroundColor + } + return element +} + +interface IGetElementListByHTMLOption { + innerWidth: number +} + +export function getElementListByHTML( + htmlText: string, + options: IGetElementListByHTMLOption +): IElement[] { + const elementList: IElement[] = [] + function findTextNode(dom: Element | Node) { + if (dom.nodeType === 3) { + const element = convertTextNodeToElement(dom) + if (element) { + elementList.push(element) + } + } else if (dom.nodeType === 1) { + const childNodes = dom.childNodes + for (let n = 0; n < childNodes.length; n++) { + const node = childNodes[n] + // br元素与display:block元素需换行 + if (node.nodeName === 'BR') { + elementList.push({ + value: '\n' + }) + } else if (node.nodeName === 'A') { + const aElement = node as HTMLLinkElement + const value = aElement.innerText + if (value) { + elementList.push({ + type: ElementType.HYPERLINK, + value: '', + valueList: [ + { + value + } + ], + url: aElement.href + }) + } + } else if (/H[1-6]/.test(node.nodeName)) { + const hElement = node as HTMLTitleElement + const valueList = getElementListByHTML(hElement.innerHTML, options) + elementList.push({ + value: '', + type: ElementType.TITLE, + level: titleNodeNameMapping[node.nodeName], + valueList + }) + if ( + node.nextSibling && + !INLINE_NODE_NAME.includes(node.nextSibling.nodeName) + ) { + elementList.push({ + value: '\n' + }) + } + } else if (node.nodeName === 'UL' || node.nodeName === 'OL') { + const listNode = node as HTMLOListElement | HTMLUListElement + const listElement: IElement = { + value: '', + type: ElementType.LIST, + valueList: [] + } + if (node.nodeName === 'OL') { + listElement.listType = ListType.OL + } else { + listElement.listType = ListType.UL + listElement.listStyle = ( + (listNode.style.listStyleType) + ) + } + listNode.querySelectorAll('li').forEach(li => { + const liValueList = getElementListByHTML(li.innerHTML, options) + liValueList.forEach(list => { + if (list.value === '\n') { + list.listWrap = true + } + }) + liValueList.unshift({ + value: '\n' + }) + listElement.valueList!.push(...liValueList) + }) + elementList.push(listElement) + } else if (node.nodeName === 'HR') { + elementList.push({ + value: '\n', + type: ElementType.SEPARATOR + }) + } else if (node.nodeName === 'IMG') { + const { src, width, height } = node as HTMLImageElement + if (src && width && height) { + elementList.push({ + width, + height, + value: src, + type: ElementType.IMAGE + }) + } + } else if (node.nodeName === 'TABLE') { + const tableElement = node as HTMLTableElement + const element: IElement = { + type: ElementType.TABLE, + value: '\n', + colgroup: [], + trList: [] + } + // 基础数据 + tableElement.querySelectorAll('tr').forEach(trElement => { + const trHeightStr = window + .getComputedStyle(trElement) + .height.replace('px', '') + const tr: ITr = { + height: Number(trHeightStr), + tdList: [] + } + trElement.querySelectorAll('th,td').forEach(tdElement => { + const tableCell = tdElement + const valueList = getElementListByHTML( + tableCell.innerHTML, + options + ) + const td: ITd = { + colspan: tableCell.colSpan, + rowspan: tableCell.rowSpan, + value: valueList + } + if (tableCell.style.backgroundColor) { + td.backgroundColor = tableCell.style.backgroundColor + } + tr.tdList.push(td) + }) + if (tr.tdList.length) { + element.trList!.push(tr) + } + }) + if (element.trList!.length) { + // 列选项数据 + const tdCount = element.trList![0].tdList.reduce( + (pre, cur) => pre + cur.colspan, + 0 + ) + const width = Math.ceil(options.innerWidth / tdCount) + for (let i = 0; i < tdCount; i++) { + element.colgroup!.push({ + width + }) + } + elementList.push(element) + } + } else if ( + node.nodeName === 'INPUT' && + (node).type === ControlComponent.CHECKBOX + ) { + elementList.push({ + type: ElementType.CHECKBOX, + value: '', + checkbox: { + value: (node).checked + } + }) + } else { + findTextNode(node) + if (node.nodeType === 1 && n !== childNodes.length - 1) { + const display = window.getComputedStyle(node as Element).display + if (display === 'block') { + elementList.push({ + value: '\n' + }) + } + } + } + } + } + } + // 追加dom + const clipboardDom = document.createElement('div') + clipboardDom.innerHTML = htmlText + document.body.appendChild(clipboardDom) + const deleteNodes: ChildNode[] = [] + clipboardDom.childNodes.forEach(child => { + if (child.nodeType !== 1 && !child.textContent?.trim()) { + deleteNodes.push(child) + } + }) + deleteNodes.forEach(node => node.remove()) + // 搜索文本节点 + findTextNode(clipboardDom) + // 移除dom + clipboardDom.remove() + return elementList +}