Skip to content

Commit 80d0aeb

Browse files
authored
Merge pull request #725 from Harbour-Enterprises/feature/supervalidator
feature: super validator infrastructure and first two validators
2 parents e040d05 + c337647 commit 80d0aeb

30 files changed

Lines changed: 1727 additions & 15 deletions

packages/super-editor/src/core/Editor.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
} from '@core/migrations/0.14-listsv2/listsv2migration.js';
3737
import { createLinkedChildEditor } from '@core/child-editor/index.js';
3838
import { unflattenListsInHtml } from './inputRules/html/html-helpers.js';
39+
import { SuperValidator } from '@core/super-validator/index.js';
3940

4041
/**
4142
* @typedef {Object} FieldValue
@@ -363,6 +364,8 @@ export class Editor extends EventEmitter {
363364
if (!this.options.isChildEditor) {
364365
this.initPagination();
365366
this.#initComments();
367+
368+
this.#validateDocumentInit();
366369
}
367370
}
368371
}
@@ -1232,6 +1235,8 @@ export class Editor extends EventEmitter {
12321235
if (this.options.collaborationIsReady) return;
12331236
console.debug('🔗 [super-editor] Collaboration ready');
12341237

1238+
this.#validateDocumentInit();
1239+
12351240
this.options.onCollaborationReady({ editor, ydoc });
12361241
this.options.collaborationIsReady = true;
12371242
this.options.initialState = this.state;
@@ -1889,4 +1894,16 @@ export class Editor extends EventEmitter {
18891894
if (!this.originalState) return;
18901895
this.view.updateState(this.originalState);
18911896
}
1897+
1898+
/**
1899+
* Run the SuperValidator's active document validation to check and fix potential known issues.
1900+
* @returns {void}
1901+
*/
1902+
#validateDocumentInit() {
1903+
if (this.options.isHeaderOrFooter || this.options.isChildEditor) return;
1904+
1905+
/** @type {import('./super-validator/index.js').SuperValidator} */
1906+
const validator = new SuperValidator({ editor: this, dryRun: false, debug: false });
1907+
validator.validateActiveDocument();
1908+
}
18921909
}

packages/super-editor/src/core/super-converter/SuperConverter.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
prepareCommentsXmlFilesForExport,
1313
} from './v2/exporter/commentsExporter.js';
1414
import { FOOTER_RELATIONSHIP_TYPE, HEADER_RELATIONSHIP_TYPE, HYPERLINK_RELATIONSHIP_TYPE } from './constants.js';
15+
import { DocxHelpers } from './docx-helpers/index.js';
1516

1617
class SuperConverter {
1718
static allowedElements = Object.freeze({
@@ -133,6 +134,14 @@ class SuperConverter {
133134
if (this.docx.length || this.xml) this.parseFromXml();
134135
}
135136

137+
/**
138+
* Get the DocxHelpers object that contains utility functions for working with docx files.
139+
* @returns {import('./docx-helpers/docx-helpers.js').DocxHelpers} The DocxHelpers object.
140+
*/
141+
get docxHelpers() {
142+
return DocxHelpers;
143+
}
144+
136145
parseFromXml() {
137146
this.docx?.forEach((file) => {
138147
this.convertedXml[file.name] = this.parseXmlToJson(file.content);
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// @ts-check
2+
import { RELATIONSHIP_TYPES } from './docx-constants.js';
3+
4+
/** @typedef {import('../types.js').Editor} Editor */
5+
/** @typedef {import('../types.js').XmlRelationshipElement} XmlRelationshipElement */
6+
/** @typedef {import('../types.js').RelationshipType} RelationshipType */
7+
8+
/**
9+
* Get all relationship elements from the document.xml.rels.
10+
* @param {Editor} editor The editor instance
11+
* @returns {XmlRelationshipElement[]} An array of relationship elements
12+
*/
13+
export const getDocumentRelationshipElements = (editor) => {
14+
const docx = editor.converter?.convertedXml;
15+
if (!docx) return [];
16+
17+
const documentRels = docx['word/_rels/document.xml.rels'];
18+
const elements = documentRels?.elements;
19+
if (!Array.isArray(elements)) return [];
20+
21+
const relationshipTag = elements.find((el) => el.name === 'Relationships');
22+
return relationshipTag?.elements || [];
23+
};
24+
25+
/**
26+
* Get the maximum relationship ID from existing relationships.
27+
* @param {XmlRelationshipElement[]} relationships The array of relationship elements
28+
* @returns {number} The maximum relationship ID integer
29+
*/
30+
export const getMaxRelationshipIdInt = (relationships) => {
31+
const ids = [];
32+
relationships.forEach((rel) => {
33+
const splitId = rel.attributes.Id.split('rId');
34+
const parsedInt = parseInt(splitId[1], 10);
35+
if (Number.isInteger(parsedInt)) {
36+
ids.push(parsedInt);
37+
}
38+
});
39+
40+
if (ids.length === 0) return 0;
41+
return Math.max(...ids);
42+
};
43+
44+
/**
45+
* Find an existing relationship ID based on the target path.
46+
* @param {string} target The target path to search for
47+
* @param {Editor} editor The editor instance
48+
* @returns {string|null} The relationship ID if found, otherwise null
49+
*/
50+
export const findRelationshipIdFromTarget = (target, editor) => {
51+
if (!target) return null;
52+
53+
if (target.startsWith('word/')) target = target.replace('word/', '');
54+
const relationships = getDocumentRelationshipElements(editor);
55+
const existingLinkRel = relationships?.find((rel) => rel.attributes.Target === target);
56+
if (existingLinkRel) {
57+
return existingLinkRel.attributes.Id;
58+
}
59+
};
60+
61+
/**
62+
* Insert a new relationship into the document.xml.rels.
63+
* This will verify that we do not already have a relationship for the target.
64+
* If a relationship already exists, it will not create a new one.
65+
* @param {string} target The target path for the relationship
66+
* @param {RelationshipType} type The type of the relationship
67+
* @param {Editor} editor The editor instance
68+
* @returns {string|null} The new or existing relationship ID or null if it could not be created
69+
* @throws {Error} When required parameters are missing or invalid
70+
*/
71+
export const insertNewRelationship = (target, type, editor) => {
72+
// Input validation
73+
if (!target || typeof target !== 'string') {
74+
throw new Error('Target must be a non-empty string');
75+
}
76+
if (!type || typeof type !== 'string') {
77+
throw new Error('Type must be a non-empty string');
78+
}
79+
if (!editor) {
80+
throw new Error('Editor instance is required');
81+
}
82+
83+
// Check if relationship type is supported
84+
const mappedType = RELATIONSHIP_TYPES[type];
85+
if (!mappedType) {
86+
console.warn(
87+
`Unsupported relationship type: ${type}. Available types: ${Object.keys(RELATIONSHIP_TYPES).join(', ')}`,
88+
);
89+
return null;
90+
}
91+
92+
// Check for existing relationship
93+
const existingRelId = findRelationshipIdFromTarget(target, editor);
94+
if (existingRelId) {
95+
console.info(`Reusing existing relationship for target: ${target} (ID: ${existingRelId})`);
96+
return existingRelId;
97+
}
98+
99+
// Validate document structure
100+
const docx = editor.converter?.convertedXml;
101+
if (!docx) {
102+
console.error('No converted XML found in editor');
103+
return null;
104+
}
105+
106+
const documentRels = docx['word/_rels/document.xml.rels'];
107+
if (!documentRels) {
108+
console.error('No document relationships found in the docx');
109+
return null;
110+
}
111+
112+
const relationshipsTag = documentRels.elements?.find((el) => el.name === 'Relationships');
113+
if (!relationshipsTag) {
114+
console.error('No Relationships tag found in document relationships');
115+
return null;
116+
}
117+
118+
// Ensure elements array exists
119+
if (!relationshipsTag.elements) {
120+
relationshipsTag.elements = [];
121+
}
122+
123+
// Generate new relationship ID
124+
const newId = getNewRelationshipId(editor);
125+
if (!newId) {
126+
console.error('Failed to generate new relationship ID');
127+
return null;
128+
}
129+
130+
// Create new relationship element
131+
const newRel = {
132+
type: 'element',
133+
name: 'Relationship',
134+
attributes: {
135+
Id: newId,
136+
Type: mappedType,
137+
Target: target,
138+
},
139+
};
140+
141+
// Insert the new relationship
142+
relationshipsTag.elements.push(newRel);
143+
144+
return newId;
145+
};
146+
147+
/**
148+
* Generate a new relationship ID for the document.
149+
* This will be in the format rIdX where X is the next available integer.
150+
* @param {Editor} editor The editor instance
151+
* @returns {string} The new relationship ID
152+
*/
153+
export const getNewRelationshipId = (editor) => {
154+
const relationships = getDocumentRelationshipElements(editor);
155+
const maxIdInt = getMaxRelationshipIdInt(relationships);
156+
return `rId${maxIdInt + 1}`;
157+
};

0 commit comments

Comments
 (0)