prosemirror-docx-web
Version:
Export from a prosemirror document to Microsoft word forked from curvenote/prosemirror-docx
715 lines • 26.6 kB
JavaScript
"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