Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion packages/super-editor/src/core/Editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,8 @@ export class Editor extends EventEmitter {

const dom = this.view.dom;
dom.editor = this;

this.options.telemetry?.sendReport();
}

/**
Expand Down Expand Up @@ -912,7 +914,7 @@ export class Editor extends EventEmitter {
isHeadless: this.options.isHeadless,
});

this.telemetry?.trackUsage('document_export', {
this.options.telemetry?.trackUsage('document_export', {
documentType: 'docx',
timestamp: new Date().toISOString()
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class SuperConverter {
'w:rPr': 'runProperties',
'w:sectPr': 'sectionProperties',
'w:numPr': 'numberingProperties',
'w:tcPr': 'tableCellProperties',
});

static elements = new Set(['w:document', 'w:body', 'w:p', 'w:r', 'w:t', 'w:delText']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,36 +38,63 @@ export const createDocumentJson = (docx, converter, editor) => {
const json = carbonCopy(getInitialJSON(docx));
if (!json) return null;

const nodeListHandler = defaultNodeListHandler();
// Track initial document structure
if (converter?.telemetry) {
const files = Object.keys(docx).map((filePath) => {
const parts = filePath.split('/');
return {
filePath,
fileDepth: parts.length,
fileType: filePath.split('.').pop(),
};
});

converter.telemetry.trackFileStructure(
{
totalFiles: files.length,
maxDepth: Math.max(...files.map((f) => f.fileDepth)),
totalNodes: 0,
files,
},
converter.fileSource,
converter.documentId,
converter.documentInternalId,
);
}

const nodeListHandler = defaultNodeListHandler();
const bodyNode = json.elements[0].elements.find((el) => el.name === 'w:body');

if (bodyNode) {
const node = bodyNode;
const ignoreNodes = ['w:sectPr'];
const content = node.elements?.filter((n) => !ignoreNodes.includes(n.name)) ?? [];

const parsedContent = nodeListHandler.handler({
nodes: content, nodeListHandler, docx, converter, editor
nodes: content,
nodeListHandler,
docx,
converter,
editor,
});

const result = {
type: 'doc',
content: parsedContent,
attrs: {
attributes: json.elements[0].attributes,
},
};

// Not empty document
if (result.content.length > 1) {
converter?.telemetry?.trackUsage(
converter?.fileSource,
converter?.documentId,
'document_import',
'document_import',
{
documentType: 'docx',
internalId: converter?.documentInternalId,
timestamp: new Date().toISOString()
});
documentType: 'docx',
timestamp: new Date().toISOString()
}
);
}

return {
Expand Down Expand Up @@ -112,9 +139,11 @@ const createNodeListHandler = (nodeHandlers) => {
* Gets safe element context even if index is out of bounds
* @param {Array} elements Array of elements
* @param {number} index Index to check
* @param {Object} processedNode result node
* @param {String} path Occurrence filename
* @returns {Object} Safe context object
*/
const getSafeElementContext = (elements, index) => {
const getSafeElementContext = (elements, index, processedNode, path) => {
if (!elements || index < 0 || index >= elements.length) {
return {
elementIndex: index,
Expand All @@ -125,36 +154,25 @@ const createNodeListHandler = (nodeHandlers) => {

const element = elements[index];
return {
elementIndex: index,
elementName: element?.name,
elementAttributes: element?.attributes,
hasElements: !!element?.elements,
elementCount: element?.elements?.length
attributes: processedNode?.attrs,
marks: processedNode?.marks,
elementPath: path,
type: processedNode?.type,
content: processedNode?.content,
};
};

const nodeListHandlerFn = ({ nodes: elements, docx, insideTrackChange, converter, editor, filename }) => {
if (!elements || !elements.length) return [];

const processedElements = [];
const unhandledNodes = [];

try {
for (let index = 0; index < elements.length; index++) {
try {
const nodesToHandle = elements.slice(index);
if (!nodesToHandle || nodesToHandle.length === 0) {
converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'node',
'empty_slice',
`/word/${filename || 'document.xml'}`,
{
index,
internalId: converter?.documentInternalId,
totalElements: elements.length,
});
continue;
}

Expand All @@ -175,140 +193,74 @@ const createNodeListHandler = (nodeHandlers) => {
{ nodes: [], consumed: 0 }
);

// Track unhandled nodes with safe context
// Only track unhandled nodes that should have been handled
const context = getSafeElementContext(elements, index, nodes[0], `/word/${filename || 'document.xml'}`);
if (unhandled) {
const context = getSafeElementContext(elements, index);
if (!context.elementName) continue;

unhandledNodes.push({
name: context.elementName,
attributes: context.elementAttributes
});

converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'node',
'unhandled',
`/word/${filename || 'document.xml'}`,
{
context,
internalId: converter?.documentInternalId,
});

converter?.telemetry?.trackStatistic('unknown', context);
continue;
}

// Bounds check before incrementing index
if (consumed > 0) {
index += consumed - 1;
if (index < 0) {
converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'node',
'invalid_index',
`/word/${filename || 'document.xml'}`,
{
originalIndex: index - (consumed - 1),
consumed,
resultingIndex: index,
internalId: converter?.documentInternalId,
}
);
index = 0; // Reset to safe value
} else {
converter?.telemetry?.trackStatistic('node', context);

// Use Telemetry to track list item attributes
if (context.type === 'orderedList' || context.type === 'bulletList') {
context.content.forEach((item) => {
Comment thread
caio-pizzol marked this conversation as resolved.
const innerItemContext = getSafeElementContext([item], 0, item, `/word/${filename || 'document.xml'}`);
converter?.telemetry?.trackStatistic('attributes', innerItemContext);
})
}
}

// Process valid nodes
for (let node of (nodes || [])) {
if (node?.type) {
const ignore = ['runProperties'];

if (node.type === 'text' && Array.isArray(node.content) && !node.content.length) {

converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'node',
'empty_text',
`/word/${filename || 'document.xml'}`,
{
context: node.attrs,
internalId: converter?.documentInternalId,
});
continue;
}

if (!ignore.includes(node.type)) {
// Process and store nodes (no tracking needed for success)
if (nodes) {
nodes.forEach((node) => {
if (node?.type && !['runProperties'].includes(node.type)) {
if (node.type === 'text' && Array.isArray(node.content) && !node.content.length) {
return;
}
processedElements.push(node);
}
}
});
}
} catch (error) {
editor?.emit('exception', { error });

const context = getSafeElementContext(elements, index);
if (error.details) {
context.elementAttributes = error.details;
}

// Track individual element processing errors with safe context
converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'element',
'processing_error',
`/word/${filename || 'document.xml'}`,
{
error: {
message: error.message,
name: error.name,
stack: error.stack,
},
internalId: converter?.documentInternalId,
context
}
);

converter?.telemetry?.trackStatistic('error', {
type: 'processing_error',
message: error.message,
name: error.name,
stack: error.stack,
fileName: `/word/${filename || 'document.xml'}`,
});
}
}

return processedElements;
} catch (error) {
editor?.emit('exception', { error });

// Track only catastrophic handler failures
converter?.telemetry?.trackStatistic('error', {
type: 'fatal_error',
message: error.message,
name: error.name,
stack: error.stack,
fileName: `/word/${filename || 'document.xml'}`,
});

// Track catastrophic errors in the node list handler
converter?.telemetry?.trackParsing(
converter?.fileSource,
converter?.documentId,
'handler',
'nodeListHandler',
`/word/${filename || 'document.xml'}`,
{
status: 'error',
error: {
message: error.message,
name: error.name,
stack: error.stack
},
internalId: converter?.documentInternalId,
context: {
totalElements: elements?.length || 0,
processedCount: processedElements.length,
unhandledCount: unhandledNodes.length,
}
}
);
throw error;
}
};

return nodeListHandlerFn;
};

/**
*
* @param {XmlNode} node
* @param {ParsedDocx} docx
* @param {SuperConverter} converter instance.
* @param {Editor} editor instance.
* @returns {Object} The document styles object
*/
function getDocumentStyles(node, docx, converter, editor) {
Expand Down Expand Up @@ -397,4 +349,4 @@ function getHeaderFooter(el, elementType, docx, converter, editor) {

storage[rId] = { type: 'doc', content: [...schema] };
storageIds[sectionType] = rId;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export function getElementName(element) {
return SuperConverter.allowedElements[element.name || element.type];
}

export const isPropertiesElement = (element) => {
return !!SuperConverter.propertyTypes[element.name || element.type];
}

/**
*
* @param {XmlNode[]} elements
Expand Down
Loading