UNPKG

typesxml

Version:

Open source XML library written in TypeScript

433 lines 15.6 kB
"use strict"; /******************************************************************************* * Copyright (c) 2023-2026 Maxprograms. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/org/documents/epl-v10.html * * Contributors: * Maxprograms - initial API and implementation *******************************************************************************/ Object.defineProperty(exports, "__esModule", { value: true }); exports.ContentModel = exports.ContentModelType = exports.Cardinality = void 0; const dtdChoice_js_1 = require("./dtdChoice.js"); const dtdName_js_1 = require("./dtdName.js"); const dtdPCData_js_1 = require("./dtdPCData.js"); const dtdSequence_js_1 = require("./dtdSequence.js"); exports.Cardinality = { NONE: 0, // (exactly one) OPTIONAL: 1, // ? ZEROMANY: 2, // * ONEMANY: 3, // + }; exports.ContentModelType = { EMPTY: 'EMPTY', ANY: 'ANY', MIXED: 'Mixed', PCDATA: '#PCDATA', CHILDREN: 'Children' }; class ContentModel { content; type = exports.ContentModelType.EMPTY; constructor(content, type) { this.content = content; this.type = type; } static parse(modelString) { const model = new ContentModel([], exports.ContentModelType.EMPTY); return model.parseSpec(modelString); } getContent() { return this.content; } getType() { return this.type; } validateParentheses(contentString) { let balance = 0; for (let c of contentString) { if (c == '(') balance++; else if (c == ')') balance--; if (balance < 0) { throw new Error('Unbalanced parentheses in content model: ' + contentString); } } if (balance != 0) { throw new Error('Unbalanced parentheses in content model: ' + contentString); } } parseSpec(modelString) { // Normalize whitespace so multi-line mixed content declarations parse correctly let contentString = modelString.replaceAll(/\s+/g, ""); const invalidWhitespace = modelString.match(/\S\s+([*+?])/); if (invalidWhitespace) { const operator = invalidWhitespace[1]; throw new Error('Invalid content model: whitespace not allowed before "' + operator + '"'); } try { this.validateParentheses(contentString); } catch (e) { throw e; } const pcdataIndex = contentString.indexOf(exports.ContentModelType.PCDATA); if (pcdataIndex !== -1 && !contentString.startsWith('(#PCDATA')) { throw new Error('Invalid mixed content model: #PCDATA must be the first token'); } let particles = new Array(); let type = exports.ContentModelType.CHILDREN; // Handle EMPTY and ANY if (contentString === exports.ContentModelType.EMPTY) { type = exports.ContentModelType.EMPTY; return new ContentModel(particles, type); } if (contentString === exports.ContentModelType.ANY) { type = exports.ContentModelType.ANY; return new ContentModel(particles, type); } // Handle pure PCDATA if (contentString === "(#PCDATA)") { particles.push(new dtdPCData_js_1.DTDPCData()); return new ContentModel(particles, exports.ContentModelType.MIXED); } // Handle mixed content if (contentString.startsWith("(#PCDATA")) { type = exports.ContentModelType.MIXED; if (!contentString.endsWith(")*")) { throw new Error('Invalid mixed content model: ' + modelString); } } // Handle element content (sequence/choice/groups) const tokens = []; let buffer = ''; for (let i = 0; i < contentString.length; i++) { const c = contentString[i]; if ('()|,?*+'.includes(c)) { if (buffer.trim().length > 0) { tokens.push(buffer.trim()); buffer = ''; } tokens.push(c); } else { buffer += c; } } if (buffer.trim().length > 0) { tokens.push(buffer.trim()); } const stack = []; let current = []; for (let token of tokens) { if (token === "(") { stack.push(current); current = []; } else if (token === ")") { const groupParticle = this.processGroup(current); current = stack.pop(); current.push(groupParticle); } else if (token === "*" || token === "+" || token === "?") { if (current.length === 0) { throw new Error('Cardinality operator "' + token + '" must follow a valid particle'); } const lastObject = current[current.length - 1]; if (!(this.isContentParticle(lastObject))) { throw new Error('Cardinality operator "' + token + '" must follow a valid particle'); } const cardinality = token === "?" ? exports.Cardinality.OPTIONAL : (token === "*" ? exports.Cardinality.ZEROMANY : exports.Cardinality.ONEMANY); lastObject.setCardinality(cardinality); } else if (token === "|" || token === ",") { current.push(token); } else if (token === exports.ContentModelType.PCDATA) { current.push(new dtdPCData_js_1.DTDPCData()); } else { current.push(new dtdName_js_1.DTDName(token)); } } for (const obj of current) { if (!this.isContentParticle(obj)) { throw new Error('Invalid object in content model: ' + obj); } particles.push(obj); } return new ContentModel(particles, type); } isContentParticle(obj) { return obj instanceof dtdName_js_1.DTDName || obj instanceof dtdChoice_js_1.DTDChoice || obj instanceof dtdSequence_js_1.DTDSequence || obj instanceof dtdPCData_js_1.DTDPCData; } processGroup(group) { if (group.length === 0) { throw new Error('Empty group found in content model'); } if (group.length === 1) { let obj = group[0]; if (typeof obj === "string") { return new dtdName_js_1.DTDName(obj); } if (obj instanceof dtdChoice_js_1.DTDChoice) { return obj; } if (obj instanceof dtdSequence_js_1.DTDSequence) { return obj; } if (obj instanceof dtdName_js_1.DTDName) { return obj; } if (obj instanceof dtdPCData_js_1.DTDPCData) { return obj; } throw new Error('Invalid object in content model group: ' + obj); } let sep = null; for (let obj of group) { if (typeof obj === "string") { if (obj === "|" || obj === ",") { sep = obj; break; } } } if (sep === null) { throw new Error('No separator found when parsing group'); } let result = sep === "|" ? new dtdChoice_js_1.DTDChoice() : new dtdSequence_js_1.DTDSequence(); for (let obj of group) { if (obj === "|" || obj === ",") { continue; } if (typeof obj === "string") { result.addParticle(new dtdName_js_1.DTDName(obj)); } else if (obj instanceof dtdChoice_js_1.DTDChoice) { result.addParticle(obj); } else if (obj instanceof dtdSequence_js_1.DTDSequence) { result.addParticle(obj); } else if (obj instanceof dtdName_js_1.DTDName) { result.addParticle(obj); } else if (obj instanceof dtdPCData_js_1.DTDPCData) { result.addParticle(obj); } else { throw new Error('Invalid object in content model group: ' + obj); } } return result; } toString() { if (this.type === exports.ContentModelType.EMPTY) { return exports.ContentModelType.EMPTY; } if (this.type === exports.ContentModelType.ANY) { return exports.ContentModelType.ANY; } if (this.content.length === 0) { return ""; } let sb = ''; // For MIXED content, handle specially if (this.type === exports.ContentModelType.MIXED) { sb += "("; for (let i = 0; i < this.content.length; i++) { let particle = this.content[i]; sb += particle.toString(); if (i < this.content.length - 1) { sb += "|"; } } sb += ")*"; return sb; } // For CHILDREN content, the particles themselves determine the structure for (let i = 0; i < this.content.length; i++) { let particle = this.content[i]; sb += particle.toString(); if (i < this.content.length - 1) { // This should not happen as CHILDREN content typically has a single root particle sb += ","; // Default to sequence if multiple top-level particles } } return sb; } isMixed() { return this.type === exports.ContentModelType.MIXED; } getChildren() { if (this.type === exports.ContentModelType.EMPTY) { return new Set(); } const children = new Set(); for (const particle of this.content) { if (particle instanceof dtdName_js_1.DTDName) { children.add(particle.getName()); } if (particle instanceof dtdChoice_js_1.DTDChoice) { const choice = particle; for (const child of choice.getChildren()) { children.add(child); } } if (particle instanceof dtdSequence_js_1.DTDSequence) { const sequence = particle; for (const child of sequence.getChildren()) { children.add(child); } } } return children; } validateChildren(children) { if (this.type === exports.ContentModelType.MIXED) { const allowed = this.getChildren(); for (const child of children) { if (!allowed.has(child)) { return false; } } return true; } if (this.type !== exports.ContentModelType.CHILDREN) { return true; } if (this.content.length === 0) { return children.length === 0; } let index = 0; for (const particle of this.content) { const nextIndex = this.matchParticle(particle, children, index); if (nextIndex === -1) { return false; } index = nextIndex; } return index === children.length; } matchParticle(particle, children, start) { if (particle instanceof dtdName_js_1.DTDName) { return this.matchNameParticle(particle, children, start); } if (particle instanceof dtdSequence_js_1.DTDSequence) { return this.matchSequenceParticle(particle, children, start); } if (particle instanceof dtdChoice_js_1.DTDChoice) { return this.matchChoiceParticle(particle, children, start); } if (particle instanceof dtdPCData_js_1.DTDPCData) { return start; } return -1; } matchNameParticle(particle, children, start) { const min = this.getMinOccurs(particle.getCardinality()); const max = this.getMaxOccurs(particle.getCardinality()); let index = start; let count = 0; while (index < children.length && count < max && children[index] === particle.getName()) { index++; count++; } if (count < min) { return -1; } return index; } matchSequenceParticle(sequence, children, start) { const min = this.getMinOccurs(sequence.getCardinality()); const max = this.getMaxOccurs(sequence.getCardinality()); let index = start; let repetitions = 0; while (repetitions < max) { const iterationStart = index; const result = this.matchSequenceOnce(sequence, children, iterationStart); if (result === -1) { break; } index = result; repetitions++; if (result === iterationStart) { break; } } if (repetitions < min) { return -1; } return index; } matchChoiceParticle(choice, children, start) { const min = this.getMinOccurs(choice.getCardinality()); const max = this.getMaxOccurs(choice.getCardinality()); let index = start; let repetitions = 0; while (repetitions < max) { const iterationStart = index; let matched = false; for (const option of choice.getParticles()) { const nextIndex = this.matchParticle(option, children, iterationStart); if (nextIndex !== -1) { index = nextIndex; matched = true; break; } } if (!matched) { break; } repetitions++; if (index === iterationStart) { break; } } if (repetitions < min) { return -1; } return index; } matchSequenceOnce(sequence, children, start) { let index = start; for (const subParticle of sequence.getParticles()) { const nextIndex = this.matchParticle(subParticle, children, index); if (nextIndex === -1) { return -1; } index = nextIndex; } return index; } getMinOccurs(cardinality) { switch (cardinality) { case exports.Cardinality.OPTIONAL: case exports.Cardinality.ZEROMANY: return 0; case exports.Cardinality.ONEMANY: case exports.Cardinality.NONE: default: return 1; } } getMaxOccurs(cardinality) { switch (cardinality) { case exports.Cardinality.ZEROMANY: case exports.Cardinality.ONEMANY: return Number.MAX_SAFE_INTEGER; case exports.Cardinality.OPTIONAL: return 1; case exports.Cardinality.NONE: default: return 1; } } } exports.ContentModel = ContentModel; //# sourceMappingURL=ContentModel.js.map