UNPKG

typesxml

Version:

Open source XML library written in TypeScript

206 lines 7.53 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.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