Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions packages/super-editor/src/core/super-converter/SuperConverter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import xmljs from 'xml-js';
import { getNodeNumberingDefinition } from './numbering';
import { toKebabCase } from '@harbour-enterprises/common';

import { DocxExporter, exportSchemaToJson } from './exporter';
import { DocxImporter } from './importer';
import {createDocumentJson} from "./v2/importer/docxImporter.js";
import {getInitialJSON} from "./v2/docxHelper.js";


class SuperConverter {
Expand All @@ -14,7 +15,7 @@ class SuperConverter {
'w:p': 'paragraph',
'w:r': 'run',
'w:t': 'text',
'w:delText': 'deletedText',
'w:delText': 'text',
'w:br': 'lineBreak',
'w:tbl': 'table',
'w:tr': 'tableRow',
Expand Down Expand Up @@ -158,8 +159,14 @@ class SuperConverter {
}

getSchema() {
const importer = new DocxImporter(this);
return importer.getSchema();
const result = createDocumentJson({...this.convertedXml, media: this.media});
if (result) {
this.savedTagsToRestore.push({...result.savedTagsToRestore});
this.pageStyles = result.pageStyles;
return result.pmDoc;
} else {
return null;
}
}

schemaToXml(data) {
Expand Down
92 changes: 92 additions & 0 deletions packages/super-editor/src/core/super-converter/v2/docxHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import xmljs from 'xml-js';
/**
* @typedef {Object.<string, XmlNode>} ParsedDocx
* this should have the `media` as a key for image handling
*/

/**
* Convert docx to our inner representation
* @param {{name: string, content: string}[]} docxAsXmlFileList
* @returns {ParsedDocx}
*/
export const docxAsXmlFileList2ParsedDocx = (docxAsXmlFileList) => {
const convertedXml = {}
docxAsXmlFileList.forEach(file => {
convertedXml[file.name] = parseXmlToJson(file.content);
});
return convertedXml;
}

const defaultInitialXml = `<w:document xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh wp14"><w:body></w:body></w:document>`

export const parseXmlToJson = (xml) => {
return JSON.parse(xmljs.xml2json(xml, null, 2))
}

/**
*
* @param {ParsedDocx} parsedDocx
* @param {string} [fallbackXml]
*/
export const getInitialJSON = (parsedDocx, fallbackXml = defaultInitialXml) => {
return parsedDocx['word/document.xml'] || parseXmlToJson(fallbackXml);
}
/**
*
* @param {ParsedDocx} parsedDocx
* @returns *
*/
const getDeclaration = (parsedDocx) => {
return getInitialJSON(parsedDocx).declaration;
}

const getThemeInfo = (themeName) =>{
themeName = themeName.toLowerCase();
const theme1 = this.convertedXml['word/theme/theme1.xml'];
const themeData = theme1.elements.find((el) => el.name === 'a:theme');
const themeElements = themeData.elements.find((el) => el.name === "a:themeElements");
const fontScheme = themeElements.elements.find((el) => el.name === 'a:fontScheme');
let fonts;

if (themeName.startsWith('major')) {
fonts = fontScheme.elements.find((el) => el.name === 'a:majorFont').elements[0];
} else if (themeName.startsWith('minor')) {
fonts = fontScheme.elements.find((el) => el.name === 'a:minorFont').elements[0];
}

const { typeface, panose } = fonts.attributes;
return { typeface, panose };
}

/**
*
* @param {ParsedDocx} parsedDocx
* @returns {{} | {fontSizePt: number, kern: string, typeface: string, panose: string}}
*/
const getDocumentDefaultStyles = (parsedDocx) => {
const styles = parsedDocx['word/styles.xml'];
if (!styles) return {};

const defaults = styles.elements[0].elements.find((el) => el.name === 'w:docDefaults');

// TODO: Check if we need this
// const pDefault = defaults.elements.find((el) => el.name === 'w:pPrDefault');

// Get the run defaults for this document - this will include font, theme etc.
const rDefault = defaults.elements.find((el) => el.name === 'w:rPrDefault');
if ('elements' in rDefault) {
const rElements = rDefault.elements[0].elements
const fontThemeName = rElements.find((el) => el.name === 'w:rFonts')?.attributes['w:asciiTheme'];
let typeface, panose;
if (fontThemeName) {
const fontInfo = getThemeInfo(fontThemeName);
typeface = fontInfo.typeface;
panose = fontInfo.panose;
}

const fontSizePt = Number(rElements.find((el) => el.name === 'w:sz')?.attributes['w:val']) / 2;
const kern = rElements.find((el) => el.name === 'w:kern')?.attributes['w:val'];
return { fontSizePt, kern, typeface, panose };
}
return {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @type {import("docxImporter").NodeHandler}
*/
export const handleBookmarkNode = (nodes, docx, nodeListHandler, insideTrackChange) => {
if(nodes.length === 0 || nodes[0].name !== 'w:bookmarkStart') {
return [];
}
const node = nodes[0];

const handleStandardNode = nodeListHandler.handlerEntities.find(e => e.handlerName === 'standardNodeHandler')?.handler;
if (!handleStandardNode) {
console.error('Standard node handler not found');
return [];
}
const result = handleStandardNode([node], docx, nodeListHandler, insideTrackChange);
if(result.length === 1) {
result[0].attrs.name = node.attributes['w:name'];
}
return result;
}

/**
* @type {import("docxImporter").NodeHandlerEntry}
*/
export const bookmarkNodeHandlerEntity = {
handlerName: 'bookmarkNodeHandler',
handler: handleBookmarkNode
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {SuperConverter} from "../../SuperConverter.js";
import {handleBookmarkNode} from "./bookmarkNodeImporter.js";
import {createNodeListHandlerMock} from "./test-helpers/testUtils.test.js";


describe('BookmarkNodeImporter', () => {
it("parses only bookmark nodes", () => {
const names = Object.keys(SuperConverter.allowedElements).filter((name) => name !== 'w:bookmarkStart');
const nodesOfNodes = names.map((name) => ([{name}]));
for(const nodes of nodesOfNodes) {
const result = handleBookmarkNode(nodes, null, null, false);
expect(result.length).toBe(0);
}
})
it("parses bookmark nodes and w:name attributes", () => {
const nodes = [{name: 'w:bookmarkStart', attributes: {'w:name': 'bookmarkName'}}];
const result = handleBookmarkNode(nodes, null, createNodeListHandlerMock(), false);
expect(result.length).toBe(1);
expect(result[0].type).toBe('standardNodeHandler');
expect(result[0].attrs.name).toBe("bookmarkName");
})
it("parser relies on handleStandardNode", () => {
const consoleMock = vi.spyOn(console, 'error').mockImplementation(() => undefined);

const nodes = [{name: 'w:bookmarkStart', attributes: {'w:name': 'bookmarkName'}}];
const result = handleBookmarkNode(nodes, null, {handlerEntities: []}, false);
expect(result.length).toBe(0);
expect(consoleMock).toHaveBeenCalledOnce();
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import {getInitialJSON} from "../docxHelper.js";
import {carbonCopy} from "../../../utilities/cabonCopy.js";
import {twipsToInches} from "../../helpers.js";
import {tableNodeHandlerEntity} from "./tableImporter.js";
import {drawingNodeHandlerEntity} from "./imageImporter.js";
import {
handleParagraphNodeChange,
nodeTypeChangeAttributeChanger,
trackChangeNodeHandlerEntity
} from "./trackChangesImporter.js";
import {hyperlinkNodeHandlerEntity} from "./hyperlinkImporter.js";
import {runNodeHandlerEntity} from "./runNodeImporter.js";
import {textNodeHandlerEntity} from "./textNodeImporter.js";
import {paragraphNodeHandlerEntity} from "./paragraphNodeImporter.js";
import {standardNodeHandlerEntity} from "./standardNodeImporter.js";
import {lineBreakNodeHandlerEntity} from "./lineBreakImporter.js";
import {bookmarkNodeHandlerEntity} from "./bookmarkNodeImporter.js";
import {listHandlerEntity, listNodeAggregator} from "./listImporter.js";

/**
* @typedef {import()} XmlNode
* @typedef {{type: string, content: *, attrs: {}}} PmNodeJson
* @typedef {{type: string, attrs: {}}} PmMarkJson
*
* @typedef {(nodes: XmlNode[], docx: ParsedDocx, insideTrackCahange: boolean) => PmNodeJson[]} NodeListHandlerFn
* @typedef {{handler: NodeListHandlerFn, handlerEntities: NodeHandlerEntry[]}} NodeListHandler
*
* @typedef {(nodes: XmlNode[], docx: ParsedDocx, nodeListHandler: NodeListHandler, insideTrackCahange: boolean) => PmNodeJson[]} NodeHandler

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be refactored to node: XmlNode bcs we removed the n to n mapping, and we did an 1 to n which is way easier to think about. (This is really just refactoring the array to a single element.)

* @typedef {{handlerName: string, handler: NodeHandler}} NodeHandlerEntry
*/

/**
*
* @param {ParsedDocx} docx
* @returns {{pmDoc: PmNodeJson, savedTagsToRestore: XmlNode, pageStyles: *}|null}
*/
export const createDocumentJson = (docx) => {
const json = carbonCopy(getInitialJSON(docx));
if (!json) return null;

console.debug('\n\n JSON', json,)
const nodeListHandler = defaultNodeListHandler();
if(json.elements[0].elements[0].name === 'w:body') {
const node = json.elements[0].elements[0];
const ignoreNodes = ['w:sectPr'];
const content = node.elements?.filter((n) => !ignoreNodes.includes(n.name)) ?? [];

const result = {
type: 'doc',
content: nodeListHandler.handler(content, docx, false),
attrs: {
attributes: json.elements[0].attributes,
}
}
return {
pmDoc: result,
savedTagsToRestore: node,
pageStyles: getDocumentStyles(node),
};
}
return null;
}

/**
* @returns {NodeListHandler}
*/
export const defaultNodeListHandler = () => {
const entities = [
runNodeHandlerEntity,
paragraphNodeHandlerEntity,
listHandlerEntity,
textNodeHandlerEntity,
lineBreakNodeHandlerEntity,
bookmarkNodeHandlerEntity,
hyperlinkNodeHandlerEntity,
drawingNodeHandlerEntity,
trackChangeNodeHandlerEntity,
tableNodeHandlerEntity,
standardNodeHandlerEntity, //this should be the last one, bcs this parses everything!!!
]
const handler = createNodeListHandler(entities);
return {
handler,
handlerEntities: entities
}
}

/**
*
* @param {NodeHandlerEntry[]} nodeHandlers
*/
const createNodeListHandler = (nodeHandlers) => {
/**
* @param {XmlNode[]} elements
* @param {ParsedDocx} docx
* @param {boolean} insideTrackChange
* @return {{type: string, content: *, attrs: {attributes}}[]}
*/
const nodeListHandlerFn = (elements, docx, insideTrackChange) => {
if (!elements || !elements.length) return [];

const handlerObj = {handler: nodeListHandlerFn, handlerEntities: nodeHandlers}
const processedElements = [];

for (let index = 0; index < elements.length; index++) {
const nodes = nodeHandlers.reduce((res, handler) => {
if(res.length > 0) return res;
const nodesToHandle = elements.slice(index);
if(!nodesToHandle || nodesToHandle.length === 0) return res;
const handlerResponse = handler.handler(nodesToHandle, docx, handlerObj, insideTrackChange);
if(handlerResponse.length > 0) {
handleParagraphNodeChange(nodesToHandle[0], handlerResponse[0], docx, handlerObj)
return handlerResponse;
}
return res
}, []);
if(nodes.length === 0) {
console.warn("We have a node that we can't handle!", elements[index])
}
for(let node of nodes) {
if (node?.type) {
const ignore = ['runProperties'];
if (!ignore.includes(node.type)) processedElements.push(node);
}
}
}

const aggregatedNodes = listNodeAggregator(processedElements, docx, handlerObj);

return aggregatedNodes;
}

return nodeListHandlerFn;
}

/**
*
* @param {XmlNode} node
* @returns {*}
*/
function getDocumentStyles(node) {
const sectPr = node.elements.find((n) => n.name === 'w:sectPr');
const styles = {};

sectPr.elements.forEach((el) => {
const { name, attributes } = el;
switch (name) {
case 'w:pgSz':
styles['pageSize'] = {
width: twipsToInches(attributes['w:w']),
height: twipsToInches(attributes['w:h']),
}
break;
case 'w:pgMar':
styles['pageMargins'] = {
top: twipsToInches(attributes['w:top']),
right: twipsToInches(attributes['w:right']),
bottom: twipsToInches(attributes['w:bottom']),
left: twipsToInches(attributes['w:left']),
header: twipsToInches(attributes['w:header']),
footer: twipsToInches(attributes['w:footer']),
gutter: twipsToInches(attributes['w:gutter']),
}
break;
case 'w:cols':
styles['columns'] = {
space: twipsToInches(attributes['w:space']),
num: attributes['w:num'],
equalWidth: attributes['w:equalWidth'],
}
break;
case 'w:docGrid':
styles['docGrid'] = {
linePitch: twipsToInches(attributes['w:linePitch']),
type: attributes['w:type'],
}
break;
}
});
return styles;
}
Loading