typesxml
Version:
Open source XML library written in TypeScript
433 lines • 15.6 kB
JavaScript
"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