typesxml
Version:
Open source XML library written in TypeScript
206 lines • 7.53 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.DTDContentModelParser = void 0;
const XMLUtils_js_1 = require("../XMLUtils.js");
const DTDChoiceModel_js_1 = require("./DTDChoiceModel.js");
const DTDContentModelTokenizer_js_1 = require("./DTDContentModelTokenizer.js");
const DTDElementNameParticle_js_1 = require("./DTDElementNameParticle.js");
const DTDSequenceModel_js_1 = require("./DTDSequenceModel.js");
class DTDContentModelParser {
tokens;
pos = 0;
constructor(input) {
const tokenizer = new DTDContentModelTokenizer_js_1.DTDContentModelTokenizer(input);
this.tokens = tokenizer.tokenize();
}
parseParticle() {
// Parse a particle: group, mixed, or name
const token = this.peek();
if (!token) {
throw new Error('Unexpected end of content model');
}
if (token.type === '(') {
this.next(); // always consume '('
const nextToken = this.peek();
if (nextToken && nextToken.type === 'PCDATA') {
return this.parseMixed();
}
else {
return this.parseGroup();
}
}
else if (token.type === 'NAME') {
return this.parseNameParticle();
}
else {
throw new Error('Expected (, or NAME in particle');
}
}
parse() {
if (this.tokens.length === 0) {
throw new Error('Empty content model');
}
return this.parseParticle();
}
parseGroup() {
// Assumes '(' already consumed
let particles = [];
let separator = null;
let expectParticle = true;
let lastWasSeparator = false;
while (true) {
const token = this.peek();
if (!token) {
throw new Error('Unexpected end of content model');
}
if (token.type === ')') {
// Allow single particle followed by ')'
this.next(); // consume ')'
break;
}
if (expectParticle) {
// Handle mixed content: (#PCDATA|foo|bar)*
if (token.type === 'PCDATA') {
return this.parseMixed();
}
let particle = this.parseParticle();
if (!particle) {
throw new Error('Expected particle in group');
}
particles.push(particle);
expectParticle = false;
lastWasSeparator = false;
}
else {
if (token.type === ',' || token.type === '|') {
if (separator && separator !== token.type) {
throw new Error('Mixed separators in group');
}
separator = token.type;
this.next(); // consume separator
expectParticle = true;
lastWasSeparator = true;
}
else if (token.type === ')') {
this.next(); // consume ')'
break;
}
else {
throw new Error('Expected , | or ) in group');
}
}
}
if (particles.length === 0) {
throw new Error('Empty group not allowed');
}
if (lastWasSeparator) {
throw new Error('Trailing separator in group');
}
if (!separator && particles.length > 1) {
throw new Error('Missing separator in group');
}
let cardinality = this.parseCardinality();
if (separator === '|') {
const model = new DTDChoiceModel_js_1.DTDChoiceModel(particles);
model.cardinality = cardinality;
return model;
}
else if (separator === ',') {
const model = new DTDSequenceModel_js_1.DTDSequenceModel(particles);
model.cardinality = cardinality;
return model;
}
else if (particles.length === 1) {
if (cardinality && 'cardinality' in particles[0]) {
particles[0].cardinality = cardinality;
}
return particles[0];
}
else {
throw new Error('Invalid group structure');
}
}
parseMixed() {
this.expect('PCDATA');
let choice = new DTDChoiceModel_js_1.DTDChoiceModel();
choice.addChoice(new DTDElementNameParticle_js_1.DTDElementNameParticle('#PCDATA'));
while (this.match('|')) {
this.expect('|');
if (this.match('NAME')) {
let name = this.expect('NAME').value;
choice.addChoice(new DTDElementNameParticle_js_1.DTDElementNameParticle(name));
}
else if (this.match('PCDATA')) {
// Allow repeated #PCDATA in mixed content
this.expect('PCDATA');
choice.addChoice(new DTDElementNameParticle_js_1.DTDElementNameParticle('#PCDATA'));
}
else {
throw new Error('Expected NAME or #PCDATA in mixed content');
}
}
this.expect(')');
let cardinality = this.parseCardinality();
// Accept (#PCDATA) and (#PCDATA|name|...)* as valid
if (cardinality && cardinality !== '*') {
throw new Error('Mixed content must end with )* or be (#PCDATA)');
}
choice.cardinality = cardinality;
return choice;
}
parseNameParticle() {
let name = this.expect('NAME').value;
if (!XMLUtils_js_1.XMLUtils.isValidXMLName(name)) {
throw new Error('Invalid XML name in content model: ' + name);
}
let cardinality = this.parseCardinality();
return new DTDElementNameParticle_js_1.DTDElementNameParticle(name, cardinality);
}
parseCardinality() {
if (this.match('*'))
return this.expect('*').value;
if (this.match('+'))
return this.expect('+').value;
if (this.match('?'))
return this.expect('?').value;
return '';
}
match(type) {
return this.peek()?.type === type;
}
peek() {
if (this.pos >= this.tokens.length) {
return undefined;
}
return this.tokens[this.pos];
}
expect(type) {
if (!this.match(type)) {
throw new Error('Expected ' + type + ', got ' + (this.peek()?.type || 'EOF'));
}
// check bounds
if (this.pos >= this.tokens.length) {
throw new Error('Unexpected end of input');
}
return this.tokens[this.pos++];
}
next() {
if (this.pos >= this.tokens.length) {
return undefined;
}
return this.tokens[this.pos++];
}
}
exports.DTDContentModelParser = DTDContentModelParser;
//# sourceMappingURL=DTDContentModelParser.js.map