myst-to-docx
Version:
Export from a MyST Markdown document to Microsoft Word (*.docx)
481 lines (480 loc) • 14.8 kB
JavaScript
import { FootnoteReferenceRun, TabStopPosition, TabStopType, TextRun, AlignmentType, BorderStyle, convertInchesToTwip, ExternalHyperlink, HeadingLevel, ImageRun, ShadingType, Math, MathRun, TableRow, Table, TableCell, Paragraph, PageBreak, } from 'docx';
import { RuleId, fileError, getMetadataTags } from 'myst-common';
import { createReference, createReferenceBookmark, createShortId, getImageWidth, MAX_DOCX_IMAGE_WIDTH, } from './utils.js';
import { createNumbering } from './numbering.js';
import sizeOf from 'buffer-image-size';
const text = (state, node) => {
var _a;
state.text((_a = node.value) !== null && _a !== void 0 ? _a : '');
};
const paragraph = (state, node) => {
state.renderChildren(node);
state.closeBlock();
};
const block = (state, node) => {
if (node.visibility === 'remove' || node.visibility === 'hide')
return;
const metadataTags = getMetadataTags(node);
if (metadataTags.includes('page-break') || metadataTags.includes('new-page')) {
state.current.push(new PageBreak());
}
state.renderChildren(node);
};
const heading = (state, node) => {
if (!state.options.useFieldsForCrossReferences && node.enumerator) {
state.text(`${node.enumerator}\t`);
}
else {
// some way to number the headings?
}
state.renderChildren(node);
const headingLevel = [
HeadingLevel.HEADING_1,
HeadingLevel.HEADING_2,
HeadingLevel.HEADING_3,
HeadingLevel.HEADING_4,
HeadingLevel.HEADING_5,
HeadingLevel.HEADING_6,
][node.depth - 1];
state.closeBlock({ heading: headingLevel });
};
const emphasis = (state, node) => {
state.addRunOptions({ italics: true });
state.renderChildren(node);
};
const strong = (state, node) => {
state.addRunOptions({ bold: true });
state.renderChildren(node);
};
const list = (state, node) => {
const style = node.ordered ? 'numbered' : 'bullets';
if (!state.data.currentNumbering) {
const nextId = createShortId();
state.numbering.push(createNumbering(nextId, style));
state.data.currentNumbering = { reference: nextId, level: 0 };
}
else {
const { reference, level } = state.data.currentNumbering;
state.data.currentNumbering = { reference, level: level + 1 };
}
state.renderChildren(node);
if (state.data.currentNumbering.level === 0) {
delete state.data.currentNumbering;
}
else {
const { reference, level } = state.data.currentNumbering;
state.data.currentNumbering = { reference, level: level - 1 };
}
};
const listItem = (state, node, parent) => {
if (!state.data.currentNumbering)
throw new Error('Trying to create a list item without a list?');
if (state.current.length > 0) {
// This is a list within a list
state.closeBlock();
}
state.addParagraphOptions({ numbering: state.data.currentNumbering });
state.renderChildren(node);
if (parent.type !== 'paragraph') {
state.closeBlock();
}
};
const link = (state, node) => {
// Pop the stack when we encounter a link
const stack = state.current;
state.addRunOptions({ style: 'Hyperlink' });
state.current = [];
state.renderChildren(node);
const hyperlink = new ExternalHyperlink({
link: node.url,
children: state.current,
});
state.current = [...stack, hyperlink];
};
const inlineCode = (state, node) => {
state.text(node.value, {
font: {
name: 'Monospace',
},
color: '000000',
shading: {
type: ShadingType.SOLID,
color: 'D2D3D2',
fill: 'D2D3D2',
},
});
};
const _break = (state) => {
state.addRunOptions({ break: 1 });
};
const thematicBreak = (state) => {
// Kinda hacky, but this works to insert two paragraphs, the first with a break
state.closeBlock({ thematicBreak: true });
state.blankLine();
};
const abbreviation = (state, node) => {
// TODO: handle abbreviation title
state.renderChildren(node);
};
const subscript = (state, node) => {
state.addRunOptions({ subScript: true });
state.renderChildren(node);
};
const superscript = (state, node) => {
state.addRunOptions({ superScript: true });
state.renderChildren(node);
};
const _delete = (state, node) => {
state.addRunOptions({ strike: true });
state.renderChildren(node);
};
const underline = (state, node) => {
state.addRunOptions({ underline: {} });
state.renderChildren(node);
};
const smallcaps = (state, node) => {
state.addRunOptions({ smallCaps: true });
state.renderChildren(node);
};
const blockquote = (state, node) => {
state.renderChildren(node, { style: 'IntenseQuote' });
};
const code = (state, node) => {
// TODO: render with color etc.
// put each line in a new paragraph
node.value.split('\n').forEach((line) => {
state.text(line, {
font: {
name: 'Monospace',
},
});
state.closeBlock();
});
};
function getAspect(buffer, size) {
if (size)
return size.height / size.width;
try {
// This does not run client side
const dimensions = sizeOf(buffer);
return dimensions.height / dimensions.width;
}
catch (error) {
return undefined;
}
}
const image = (state, node) => {
var _a, _b, _c;
const buffer = state.options.getImageBuffer(node.url);
const dimensions = (_b = (_a = state.options).getImageDimensions) === null || _b === void 0 ? void 0 : _b.call(_a, node.url);
const width = getImageWidth(node.width, (_c = state.data.maxImageWidth) !== null && _c !== void 0 ? _c : state.options.maxImageWidth);
const aspect = getAspect(buffer, dimensions);
if (!aspect) {
fileError(state.file, `Error with checking image aspect ratio for "${node.url}".`, {
node,
source: 'myst-to-docx:image',
note: 'Either provide dimensions of the image with "getImageDimensions" or ensure that the result is a Buffer.',
ruleId: RuleId.docxRenders,
});
}
state.current.push(new ImageRun({
data: buffer,
transformation: {
width,
height: width * (aspect !== null && aspect !== void 0 ? aspect : 1),
},
}));
let alignment;
switch (node.align) {
case 'right':
alignment = AlignmentType.RIGHT;
break;
case 'left':
alignment = AlignmentType.LEFT;
break;
default:
alignment = AlignmentType.CENTER;
}
state.addParagraphOptions({
alignment,
});
state.closeBlock();
};
const admonitionStyle = {
border: {
left: {
style: BorderStyle.DOUBLE,
color: '5D85D0',
},
},
indent: { left: convertInchesToTwip(0.2), right: convertInchesToTwip(0.2) },
};
const admonition = (state, node) => {
state.blankLine();
state.renderChildren(node, admonitionStyle);
state.closeBlock();
state.blankLine();
};
const admonitionTitle = (state, node) => {
state.renderChildren(node, {
...admonitionStyle,
shading: {
type: ShadingType.SOLID,
color: '5D85D0',
},
}, { bold: true, color: 'FFFFFF' });
state.closeBlock();
};
const definitionStyle = {
border: {
left: {
style: BorderStyle.THICK,
color: 'D2D3D2',
},
},
indent: { left: convertInchesToTwip(0.2), right: convertInchesToTwip(0.2) },
};
const definitionList = (state, node) => {
state.renderChildren(node, definitionStyle);
state.closeBlock();
state.blankLine();
};
const definitionTerm = (state, node) => {
state.renderChildren(node, {
...definitionStyle,
shading: {
type: ShadingType.SOLID,
color: 'D2D3D2',
},
});
state.closeBlock();
};
const definitionDescription = (state, node) => {
state.text('\t');
state.renderChildren(node, definitionStyle);
state.closeBlock();
};
const inlineMath = (state, node) => {
const latex = node.value;
state.current.push(new Math({ children: [new MathRun(latex)] }));
};
const math = (state, node) => {
state.blankLine();
const latex = node.value;
state.current = [
new TextRun('\t'),
new Math({
children: [new MathRun(latex)],
}),
];
// Add the number at the end of the field
if (node.enumerator && node.identifier && state.options.useFieldsForCrossReferences) {
state.current.push(new TextRun('\t('), createReferenceBookmark(node.identifier, 'Equation'), new TextRun(')'));
}
else if (node.enumerator) {
state.current.push(new TextRun(`\t(${node.enumerator})`));
}
state.closeBlock({
tabStops: [
{
type: TabStopType.CENTER,
position: TabStopPosition.MAX / 2,
},
{
type: TabStopType.RIGHT,
position: TabStopPosition.MAX,
},
],
});
state.blankLine();
};
const crossReference = (state, node) => {
if (state.options.useFieldsForCrossReferences && node.identifier) {
state.current.push(createReference(node.identifier));
}
else {
state.renderChildren(node);
}
};
const container = (state, node) => {
state.renderChildren(node);
};
function figCaptionToWordCaption(file, kind) {
switch (kind.toLowerCase()) {
case 'figure':
return 'Figure';
case 'table':
return 'Table';
case 'equation':
return 'Equation';
case 'code':
// This is a hack, I don't think word knows about other things!
return 'Figure';
default:
fileError(file, `Unknown figure caption of kind ${kind}`, {
ruleId: RuleId.docxRenders,
});
return 'Figure';
}
}
const captionNumber = (state, node) => {
if (state.options.useFieldsForCrossReferences) {
const bookmarkKind = figCaptionToWordCaption(state.file, node.kind);
state.current.push(createReferenceBookmark(node.identifier, bookmarkKind, `${bookmarkKind} `, ': '));
}
else {
state.renderChildren(node, undefined, { bold: true });
state.text(' ');
}
};
const caption = (state, node) => {
state.renderChildren(node, { style: 'Caption' });
};
function getFootnoteNumber(node) {
var _a;
return (_a = Number.parseInt(node.enumerator, 10)) !== null && _a !== void 0 ? _a : Number(node.identifier);
}
const footnoteDefinition = (state, node) => {
const { children, current } = state;
const number = getFootnoteNumber(node);
// Delete everything and work with the footnote definition as children
state.children = [];
state.current = [];
state.renderChildren(node);
// TODO: a problem here if there are numberings or images
state.footnotes[number] = { children: state.children };
// Put the children back, and continue
state.children = children;
state.current = current;
};
const footnoteReference = (state, node) => {
const number = getFootnoteNumber(node);
state.current.push(new FootnoteReferenceRun(number));
};
const table = (state, node) => {
var _a, _b;
const actualChildren = state.children;
const rows = [];
const imageWidth = (_b = (_a = state.data.maxImageWidth) !== null && _a !== void 0 ? _a : state.options.maxImageWidth) !== null && _b !== void 0 ? _b : MAX_DOCX_IMAGE_WIDTH;
node.children.forEach(({ children }) => {
const rowContent = children;
const cells = [];
// Check if all cells are headers in this row
let tableHeader = true;
rowContent.forEach((cell) => {
if (cell.header) {
tableHeader = false;
}
});
// This scales images inside of tables
state.data.maxImageWidth = imageWidth / rowContent.length;
rowContent.forEach((cell) => {
var _a, _b;
state.children = [];
state.renderChildren(cell);
state.closeBlock();
const tableCellOpts = { children: state.children };
const colspan = (_a = cell.colspan) !== null && _a !== void 0 ? _a : 1;
const rowspan = (_b = cell.rowspan) !== null && _b !== void 0 ? _b : 1;
if (colspan > 1)
tableCellOpts.columnSpan = colspan;
if (rowspan > 1)
tableCellOpts.rowSpan = rowspan;
cells.push(new TableCell(tableCellOpts));
});
rows.push(new TableRow({ children: cells, tableHeader }));
});
state.data.maxImageWidth = imageWidth;
const tableNode = new Table({ rows });
actualChildren.push(tableNode);
// If there are multiple tables, this seperates them
actualChildren.push(new Paragraph(''));
state.children = actualChildren;
};
const cite = (state, node) => {
state.renderChildren(node);
};
const citeGroup = (state, node) => {
if (node.kind === 'narrative') {
state.renderChildren(node);
}
else {
state.text('(');
node.children.forEach((child, ind) => {
state.render(child);
if (ind < node.children.length - 1)
state.text('; ');
});
state.text(')');
}
};
const embed = (state, node) => {
state.renderChildren(node);
};
const include = (state, node) => {
state.renderChildren(node);
};
const comment = () => {
// Do nothing!
return;
};
const mystDirective = (state, node) => {
state.renderChildren(node);
};
const mystRole = (state, node) => {
state.renderChildren(node);
};
const inlineExpression = (state, node, parent) => {
var _a;
if ((_a = node.children) === null || _a === void 0 ? void 0 : _a.length) {
state.renderChildren(node);
}
else {
inlineCode(state, { ...node, type: 'inlineCode' }, parent);
}
};
export const defaultHandlers = {
text,
paragraph,
heading,
emphasis,
strong,
inlineCode,
link,
break: _break,
thematicBreak,
list,
listItem,
abbreviation,
subscript,
superscript,
delete: _delete,
underline,
smallcaps,
blockquote,
code,
image,
block,
comment,
mystDirective,
mystRole,
admonition,
admonitionTitle,
definitionList,
definitionTerm,
definitionDescription,
math,
inlineMath,
crossReference,
container,
caption,
captionNumber,
footnoteReference,
footnoteDefinition,
table,
cite,
citeGroup,
embed,
include,
inlineExpression,
};