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 {nodes: [], consumed: 0};
}
const node = nodes[0];

const handleStandardNode = nodeListHandler.handlerEntities.find(e => e.handlerName === 'standardNodeHandler')?.handler;
if (!handleStandardNode) {
console.error('Standard node handler not found');
return {nodes: [], consumed: 0};
}
const result = handleStandardNode([node], docx, nodeListHandler, insideTrackChange);
if(result.nodes.length === 1) {
result.nodes[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,33 @@
import {SuperConverter} from "../../SuperConverter.js";
import {handleBookmarkNode} from "./bookmarkNodeImporter.js";
import {createNodeListHandlerMock} from "./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.nodes.length).toBe(0);
expect(result.consumed).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.nodes.length).toBe(1);
expect(result.consumed).toBe(1);
expect(result.nodes[0].type).toBe('standardNodeHandler');
expect(result.nodes[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.nodes.length).toBe(0);
expect(result.consumed).toBe(0);
expect(consoleMock).toHaveBeenCalledOnce();
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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 {
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} 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) => {nodes: PmNodeJson[], consumed: number}} NodeHandler
* @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;
}

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 processedElements = [];

for (let index = 0; index < elements.length; index++) {
const {nodes, consumed} = nodeHandlers.reduce((res, handler) => {
if(res.consumed > 0) return res;
const nodesToHandle = elements.slice(index);
if(!nodesToHandle || nodesToHandle.length === 0) return res;
return handler.handler(nodesToHandle, docx, {handler: nodeListHandlerFn, handlerEntities: nodeHandlers}, insideTrackChange);
}, {nodes: [], consumed: 0});
index += consumed-1;
if(consumed === 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);
}
}
}
return processedElements;
}

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