UNPKG

prosemirror-docx-web

Version:

Export from a prosemirror document to Microsoft word forked from curvenote/prosemirror-docx

715 lines 26.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DocxSerializer = exports.DocxSerializerState = void 0; const docx_1 = require("docx"); const numbering_1 = require("./numbering"); const utils_1 = require("./utils"); function normalizeText(text) { return ((text || '') // eslint-disable-next-line no-misleading-character-class .replace(/[\u200B\u200C\u200D\uFEFF]/g, '') // eslint-disable-next-line no-control-regex .replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F]/g, '')); } const MAX_IMAGE_WIDTH = 600; let currentLink; class DocxSerializerState { numberingStyles; cslFormatService; bibliographyTitle; nodes; options; marks; children; numbering; nextRunOpts; current = []; currentBlockNode = ''; currentLink; comments = []; pageBreak = 'hr'; // Optionally add options nextParentParagraphOpts; currentNumbering; footnoteIdx; footnoteState = ''; footnoteIds; maxImageWidth = 600; fullCiteContents; transformHtmlToNode; constructor(nodes, marks, options, fullCiteContents, pageBreak = 'hr', numberingStyles = null, cslFormatService = null, bibliographyTitle = 'Bibliography', footnoteState = 'disable', transformHtmlToNode = (html) => null) { this.numberingStyles = numberingStyles; this.cslFormatService = cslFormatService; this.bibliographyTitle = bibliographyTitle; this.nodes = nodes; this.marks = marks; this.options = options ?? {}; this.children = []; this.numbering = []; this.footnoteIdx = 0; this.footnoteIds = []; this.fullCiteContents = fullCiteContents; this.pageBreak = pageBreak; this.footnoteState = footnoteState; this.transformHtmlToNode = transformHtmlToNode; } renderContent(parent, opts) { parent.forEach((node, _, i) => { if (opts) this.addParagraphOptions(opts); this.render(node, parent, i); }); } render(node, parent, index) { const target = this.nodes[node.type.name] || this.nodes.default; if (!target) throw new Error(`Token type \`${node.type.name}\` not supported by Word renderer`); target(this, node, parent, index); } renderMarks(node, marks) { return marks .map((mark) => { return this.marks[mark.type.name]?.(this, node, mark); }) .reduce((a, b) => ({ ...a, ...b }), {}); } renderParagraphHtml(html) { // eslint-disable-next-line no-param-reassign html = normalizeText(html); const node = this.transformHtmlToNode(html); if (node) { // this.render(node, node, 0); const cache = this.current; this.current = []; this.renderInline(node); const res = this.current; this.current = cache; return res; } return [new docx_1.TextRun(html)]; } openLink(href) { this.addRunOptions({ style: 'Hyperlink' }); // TODO: https://github.com/dolanmiu/docx/issues/1119 // Remove the if statement here and oneLink! // const oneLink = true; // if (!oneLink) { // closeLink(); // } else { // if (currentLink && sameLink) return; // if (currentLink && !sameLink) { // // Close previous, and open a new one // closeLink(); // } // } currentLink = { link: href, stack: this.current, }; this.current = []; } curIdx = 0; wrapComment(node) { if (node.type.name === 'comment') { let time = Date.now(); try { time = parseInt(node.attrs.createDate, 10); } catch (e) { // eslint-disable-next-line no-console console.error('Error parsing comment time: ', e); } this.comments.push({ id: this.curIdx, children: [ new docx_1.Paragraph({ children: [ new docx_1.TextRun({ text: node.attrs.comment, }), ], }), ], date: new Date(time), }); this.current.push(new docx_1.CommentRangeStart(this.curIdx)); this.renderInline(node); this.current.push(new docx_1.CommentRangeEnd(this.curIdx), new docx_1.TextRun({ children: [new docx_1.CommentReference(this.curIdx)], })); this.curIdx += 1; } } closeLink() { if (!currentLink) return; const hyperlink = new docx_1.ExternalHyperlink({ link: currentLink.link, // child: this.current[0], children: this.current, }); this.current = [...currentLink.stack, hyperlink]; currentLink = undefined; } openTable() { } closeTable() { } hierarchy_title(node) { if (this.pageBreak === 'page' && this.children.length > 0) { this.addParagraphOptions({ pageBreakBefore: true }); } if (node.content.size > 0) { this.renderInline(node); const heading = [ docx_1.HeadingLevel.HEADING_1, docx_1.HeadingLevel.HEADING_2, docx_1.HeadingLevel.HEADING_3, docx_1.HeadingLevel.HEADING_4, docx_1.HeadingLevel.HEADING_5, docx_1.HeadingLevel.HEADING_6, ][node.attrs.level - 1]; this.closeBlock(node, { heading, style: `heading${node.attrs.level}` }); } } horizontal_rule(node) { if (this.pageBreak === 'hr') { this.addParagraphOptions({ pageBreakBefore: true }); } else { // Kinda hacky, but this works to insert two paragraphs, the first with a break this.closeBlock(node, { thematicBreak: true }); this.closeBlock(node); } } renderInline(parent) { // Pop the stack over to this object when we encounter a link, and closeLink restores it // let currentLink: { link: string; stack: ParagraphChild[] } | undefined; // const closeLink = () => { // if (!currentLink) return; // const hyperlink = new ExternalHyperlink({ // link: currentLink.link, // // child: this.current[0], // children: this.current, // }); // this.current = [...currentLink.stack, hyperlink]; // currentLink = undefined; // }; // const openLink = (href: string) => { // const sameLink = href === currentLink?.link; // this.addRunOptions({ style: 'Hyperlink' }); // // TODO: https://github.com/dolanmiu/docx/issues/1119 // // Remove the if statement here and oneLink! // const oneLink = true; // if (!oneLink) { // closeLink(); // } else { // if (currentLink && sameLink) return; // if (currentLink && !sameLink) { // // Close previous, and open a new one // closeLink(); // } // } // currentLink = { // link: href, // stack: this.current, // }; // this.current = []; // }; const progress = (node, offset, index) => { // const links: ProsemirrorNode[] = []; // node.forEach((child) => { // if (child.type.name === 'link') { // links.push(child); // return false; // } // return true; // }); // const hasLink = links.length > 0; // if (hasLink) { // openLink(links[0].attrs.href); // } else if (!hasLink && currentLink) { // closeLink(); // } if (node.isText) { this.text(node.text, this.renderMarks(node, node.marks)); } else { this.render(node, parent, index); } }; parent.forEach(progress); // Must call close at the end of everything, just in case // closeLink(); } renderList(node, style) { if (!this.currentNumbering) { const nextId = (0, utils_1.createShortId)(); this.numbering.push((0, numbering_1.createNumbering)(nextId, style, this.numberingStyles?.[style] || null)); this.currentNumbering = { reference: nextId, level: 0 }; } else { const { reference, level } = this.currentNumbering; this.currentNumbering = { reference, level: level + 1 }; } this.renderContent(node, { style: `${style}list`, }); if (this.currentNumbering.level === 0) { delete this.currentNumbering; } else { const { reference, level } = this.currentNumbering; this.currentNumbering = { reference, level: level - 1 }; } } // This is a pass through to the paragraphs, etc. underneath they will close the block renderListItem(node) { if (!this.currentNumbering) throw new Error('Trying to create a list item without a list?'); this.addParagraphOptions({ numbering: this.currentNumbering }); // this.renderContent(node); let onlyParagraph = true; node.forEach((n, _, i) => { if (n.type.name === 'paragraph' && onlyParagraph) { if (this.current.length > 0) { // add a break between paragraphs this.current.push(new docx_1.TextRun({ break: 1 })); } this.renderInline(n); } else { if (this.current.length > 0) { this.closeBlock(n); } onlyParagraph = false; this.render(n, node, i); } }); if (onlyParagraph) { this.closeBlock(node); } } addParagraphOptions(opts) { this.nextParentParagraphOpts = { ...this.nextParentParagraphOpts, ...opts }; } addRunOptions(opts) { this.nextRunOpts = { ...this.nextRunOpts, ...opts }; } text(text, opts) { const textNormalized = normalizeText(text || ''); if (!text) return; this.current.push(new docx_1.TextRun({ text: textNormalized, ...(currentLink ? { style: 'Hyperlink' } : {}), ...this.nextRunOpts, ...opts, })); delete this.nextRunOpts; } math(latex, opts = { inline: true }) { if (opts.inline || !opts.numbered) { this.current.push(new docx_1.Math({ children: [new docx_1.MathRun(latex)] })); return; } const id = opts.id ?? (0, utils_1.createShortId)(); this.current = [ new docx_1.TextRun('\t'), new docx_1.Math({ children: [new docx_1.MathRun(latex)], }), new docx_1.TextRun('\t('), new docx_1.Bookmark({ id, children: [new docx_1.SequentialIdentifier('Equation')], }), new docx_1.TextRun(')'), ]; this.addParagraphOptions({ tabStops: [ { type: docx_1.TabStopType.CENTER, position: docx_1.TabStopPosition.MAX / 2, }, { type: docx_1.TabStopType.RIGHT, position: docx_1.TabStopPosition.MAX, }, ], }); } bib_cite(node) { try { if (node.type.name === 'group_bio_citation') { this.current.push(new docx_1.TextRun(node.attrs.cache)); } if (this.cslFormatService) { const cite = this.cslFormatService.getCitationByIdSync(node.attrs.reference || node.attrs.metadataId, 'text'); this.current.push(new docx_1.TextRun(cite)); } } catch (error) { // eslint-disable-next-line no-console console.error(error); } } bibliography(node) { try { if (node.type.name === 'bibliography') { if (this.cslFormatService) { const bib = this.cslFormatService?.getBibliographyByIdSync(undefined, 'text', true); this.closeBlock(node); this.current.push(new docx_1.TextRun(this.bibliographyTitle)); this.closeBlock(node, { style: 'BibliographyTitle' }); bib.forEach(([_, bibliography]) => { this.current.push(new docx_1.TextRun(bibliography)); this.closeBlock(node, { style: 'Bibliography' }); }); } } else { this.closeBlock(node); this.current.push(new docx_1.TextRun(this.bibliographyTitle)); this.closeBlock(node, { style: 'BibliographyTitle' }); node.content.forEach((n) => { this.current.push(new docx_1.TextRun(n.textContent)); this.addParagraphOptions({ style: 'Bibliography' }); this.closeBlock(node); }); } } catch (error) { // eslint-disable-next-line no-console console.error(error); } } footnoteRef(id) { if (this.footnoteState === 'disable') return; this.footnoteIds.push(id); this.footnoteIdx += 1; this.current.push(this.footnoteState === 'endnotes' ? new docx_1.TextRun({ text: `${this.footnoteIdx}`, style: 'FootnoteReference', }) : new docx_1.FootnoteReferenceRun(this.footnoteIdx)); } imageWithType() { } image(src, align = 'center', widthPercent = 90) { const { arrayBuffer, width: rawW, height: rawH } = this.options.getImageBuffer(src); const aspect = rawH / rawW; const width = this.maxImageWidth * (widthPercent / 100); this.current.push(new docx_1.ImageRun({ data: arrayBuffer, transformation: { width, height: width * aspect, }, // floating: { // horizontalPosition: { // offset: 0, // }, // verticalPosition: { // offset: 0, // }, // wrap: { // type: TextWrappingType.TOP_AND_BOTTOM, // }, // }, })); let alignment; switch (align) { case 'right': alignment = docx_1.AlignmentType.RIGHT; break; case 'left': alignment = docx_1.AlignmentType.LEFT; break; default: alignment = docx_1.AlignmentType.CENTER; } this.addParagraphOptions({ alignment, style: 'Normal', }); } imageInline(src, maxHeight = 0) { const { arrayBuffer, width, height } = this.options.getImageBuffer(src); if (maxHeight) { const aspect = height / width; const newWidth = maxHeight / aspect; this.current.push(new docx_1.ImageRun({ data: arrayBuffer, transformation: { width: newWidth, height: maxHeight, }, })); return; } this.current.push(new docx_1.ImageRun({ data: arrayBuffer, transformation: { width, height, }, })); } addAside(text = '') { this.children.push(new docx_1.Paragraph({ text, style: 'Aside', })); } addCodeBlock(node) { if (node.textContent) { // this.children.push(new Paragraph({ text: '' })); // node.textContent.split('\n').forEach((text) => { // this.children.push( // new Paragraph({ // text, // style: 'BlockCode', // }), // ); // }); this.children.push(new docx_1.Paragraph({ children: normalizeText(node.textContent) .split('\n') .map((text, idx) => new docx_1.TextRun({ text, break: idx > 0 ? 1 : undefined })), style: 'BlockCode', })); // this.children.push(new Paragraph({ text: '' })); } } captionLabel(id, kind) { this.current.push(new docx_1.Bookmark({ id, children: [new docx_1.TextRun(`${kind} `), new docx_1.SequentialIdentifier(kind)], })); } table(node) { const actualChildren = this.children; const rows = []; let percent = 0; let colCount = 0; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore node.content.forEach(({ content: rowContent }) => { const cells = []; // Check if all cells are headers in this row let tableHeader = true; rowContent.forEach((cell) => { if (cell.type.name !== 'table_header') { tableHeader = false; } }); // This scales images inside of tables this.maxImageWidth = MAX_IMAGE_WIDTH / rowContent.childCount; percent = percent || 100 / (rowContent.childCount || 1); colCount = rowContent.childCount; rowContent.forEach((cell) => { this.children = []; this.renderContent(cell, { style: 'TableCell' }); const tableCellOpts = { children: this.children, width: { type: docx_1.WidthType.PERCENTAGE, size: percent, }, }; const colspan = cell.attrs.colspan ?? 1; const rowspan = cell.attrs.rowspan ?? 1; if (colspan > 1) tableCellOpts.columnSpan = colspan; if (rowspan > 1) tableCellOpts.rowSpan = rowspan; cells.push(new docx_1.TableCell(tableCellOpts)); }); rows.push(new docx_1.TableRow({ children: cells, tableHeader })); }); this.maxImageWidth = MAX_IMAGE_WIDTH; const table = new docx_1.Table({ rows, columnWidths: new Array(colCount).fill(0).map(() => 9010 / (colCount || 1)), // width: { // type: WidthType.DXA, // size: 9010, // }, }); // if (table instanceof Paragraph) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore actualChildren.push(table); // } // If there are multiple tables, this seperates them actualChildren.push(new docx_1.Paragraph('')); this.children = actualChildren; } columns(node) { if (node.childCount < 1) return; const actualChildren = this.children; const columnsItems = []; const columnsWidth = []; node.content.forEach((column, _, idx) => { this.children = []; columnsWidth.push(new docx_1.Column({ width: (parseFloat(column.attrs.basis) / 100) * 9010 })); this.maxImageWidth = (MAX_IMAGE_WIDTH * parseFloat(column.attrs.basis)) / 100; this.renderContent(column); // column.content.forEach((child) => { // this.renderContent(child); // }); columnsItems.push(...this.children); // Add column break except for last column if (idx < node.childCount - 1) { columnsItems.push(new docx_1.Paragraph({ children: [ new docx_1.TextRun({ children: [new docx_1.ColumnBreak()], }), ], spacing: { after: 0, before: 0, lineRule: docx_1.LineRuleType.EXACT, line: 1, }, })); } }); actualChildren.push({ properties: { type: docx_1.SectionType.CONTINUOUS, column: { space: 708, count: node.childCount, equalWidth: false, children: columnsWidth, }, }, children: columnsItems, }); actualChildren.push(new docx_1.Paragraph('')); this.children = actualChildren; this.maxImageWidth = MAX_IMAGE_WIDTH; } equalColumns(node, colCount = 0, colGap = 0) { if (node.childCount < 1) return; const actualChildren = this.children; const columnsItems = []; const columns = colCount || node.attrs.columns; const columnGap = colGap || +node.attrs.columnGap || 1; // 等宽多栏 this.maxImageWidth = MAX_IMAGE_WIDTH / columns; this.children = []; this.renderContent(node); columnsItems.push(...this.children); actualChildren.push({ properties: { type: docx_1.SectionType.CONTINUOUS, column: { space: columnGap * 20, count: columns, equalWidth: true, }, }, children: columnsItems, }); this.children = actualChildren; this.maxImageWidth = MAX_IMAGE_WIDTH; } closeBlock(node, props) { const paragraph = new docx_1.Paragraph({ children: this.current, style: 'NormalPara', ...this.nextParentParagraphOpts, ...props, }); this.current = []; delete this.nextParentParagraphOpts; this.children.push(paragraph); } } exports.DocxSerializerState = DocxSerializerState; class DocxSerializer { nodes; marks; constructor(nodes, marks) { this.nodes = nodes; this.marks = marks; } serialize(content, options, footerText = '', footnotes = [], pageOptions, fullCiteContents, externalStyles = null, numberingStyles = null, cslFormatService = null, bibliographyTitle = 'Bibliography', footnoteTitle = 'Footnotes', transformHtmlToNode, log) { const enableFootnotes = !!pageOptions?.footnotes; const isEndNotes = enableFootnotes && (pageOptions?.footnotesPosition === 'page' || pageOptions?.footnotesPosition === 'before_bib'); let footnoteState = enableFootnotes ? 'enable' : 'disable'; if (isEndNotes) { footnoteState = 'endnotes'; } if (log) { log('isEndNotes: ', isEndNotes); } const state = new DocxSerializerState(this.nodes, this.marks, options, fullCiteContents, pageOptions?.splitPage || 'hr', numberingStyles, cslFormatService, bibliographyTitle, footnoteState, transformHtmlToNode); // eslint-disable-next-line no-param-reassign footnotes = footnotes.map((f) => state.renderParagraphHtml(f)); state.renderContent(content); const f = footnotes.reduce((acc, cur, idx) => { acc[idx + 1] = { children: [ new docx_1.Paragraph({ style: 'FootnoteList', children: (Array.isArray(cur) ? cur : [cur]), }), ], }; return acc; }, {}); try { if (isEndNotes && footnotes.length > 0) { const idx = state.children.findIndex((c) => c === '[[THIS_IS_A_FOOTNOTES_HOLE]]'); if (idx > -1 && pageOptions?.footnotesPosition === 'before_bib') { if (log) { log('Bibliography is Last'); } state.children.splice(idx, 1, new docx_1.Paragraph({ children: [] }), new docx_1.Paragraph({ text: footnoteTitle, style: 'BibliographyTitle', }), ...footnotes.map((footnote, i) => new docx_1.Paragraph({ style: 'Bibliography', children: [ new docx_1.TextRun(`${i + 1}. `), ...(Array.isArray(footnote) ? footnote : [footnote]), ], }))); } else { state.children.push(new docx_1.Paragraph({ children: [] }), new docx_1.Paragraph({ text: footnoteTitle, style: 'BibliographyTitle', }), ...footnotes.map((footnote, i) => new docx_1.Paragraph({ style: 'Bibliography', children: [ new docx_1.TextRun(`${i + 1}. `), ...(Array.isArray(footnote) ? footnote : [footnote]), ], }))); } } } catch (e) { if (log) { log('Error adding footnotes: ', e); } } finally { const index = state.children.findIndex((c) => c === '[[THIS_IS_A_FOOTNOTES_HOLE]]'); if (index > -1) { state.children.splice(index, 1); } } return (0, utils_1.createDocFromState)(state, footerText, isEndNotes || !enableFootnotes ? {} : f, pageOptions, options.getImageBuffer, externalStyles); } } exports.DocxSerializer = DocxSerializer; //# sourceMappingURL=serializer.js.map