UNPKG

typesxml

Version:

Open source XML library written in TypeScript

393 lines 17 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.DTDGrammar = void 0; const Grammar_js_1 = require("../grammar/Grammar.js"); const XMLUtils_js_1 = require("../XMLUtils.js"); const ContentModel_js_1 = require("./ContentModel.js"); const EntityDecl_js_1 = require("./EntityDecl.js"); class DTDGrammar { models; entitiesMap; attributesMap; elementDeclMap; notationsMap; constructor() { this.models = new Map(); this.elementDeclMap = new Map(); this.attributesMap = new Map(); this.entitiesMap = new Map(); this.notationsMap = new Map(); this.addPredefinedEntities(); } addPredefinedEntities() { this.addEntity(new EntityDecl_js_1.EntityDecl('lt', false, '<', '', '', '')); this.addEntity(new EntityDecl_js_1.EntityDecl('gt', false, '>', '', '', '')); this.addEntity(new EntityDecl_js_1.EntityDecl('amp', false, '&', '', '', '')); this.addEntity(new EntityDecl_js_1.EntityDecl('apos', false, "'", '', '', '')); this.addEntity(new EntityDecl_js_1.EntityDecl('quot', false, '"', '', '', '')); } getContentModel(elementName) { return this.models.get(elementName); } toString() { let result = ''; this.models.forEach((value) => { result = result + value.toString() + '\n'; }); return result; } addElement(elementDecl, override = false) { const name = elementDecl.getName(); if (override || !this.elementDeclMap.has(name)) { this.elementDeclMap.set(name, elementDecl); } } addAttributes(element, attributes, override = false, preexistingKeys) { let existingAttributes = this.attributesMap.get(element); if (!existingAttributes) { existingAttributes = new Map(); this.attributesMap.set(element, existingAttributes); } if (override) { attributes.forEach((value, key) => { const existedBeforeParse = preexistingKeys ? preexistingKeys.has(key) : false; if (existedBeforeParse) { existingAttributes.set(key, value); return; } if (!existingAttributes.has(key)) { existingAttributes.set(key, value); } }); } else { attributes.forEach((value, key) => { if (!existingAttributes.has(key)) { existingAttributes.set(key, value); } }); } } resolveParameterEntities(text) { while (XMLUtils_js_1.XMLUtils.hasParameterEntity(text)) { let start = text.indexOf('%'); let end = text.indexOf(';'); let entityName = text.substring(start + '%'.length, end); let entity = this.getParameterEntity(entityName); if (entity === undefined) { throw new Error('Unknown entity: ' + entityName); } text = text.replaceAll('%' + entityName + ';', entity.getValue()); } return text; } addEntity(entityDecl, override = false) { // Parameter entities use %name key to avoid conflicts with general entities const key = entityDecl.isParameterEntity() ? `%${entityDecl.getName()}` : entityDecl.getName(); if (override || !this.entitiesMap.has(key)) { this.entitiesMap.set(key, entityDecl); } } getEntity(entityName) { return this.entitiesMap.get(entityName); } getParameterEntity(entityName) { return this.entitiesMap.get(`%${entityName}`); } addNotation(notation, override = false) { const name = notation.getName(); if (override || !this.notationsMap.has(name)) { this.notationsMap.set(name, notation); } } merge(grammar) { grammar.getEntitiesMap().forEach((value, key) => { if (!this.entitiesMap.has(key)) { this.entitiesMap.set(key, value); } }); grammar.getAttributesMap().forEach((value, key) => { if (!this.attributesMap.has(key)) { this.attributesMap.set(key, value); } }); grammar.getElementDeclMap().forEach((value, key) => { if (!this.elementDeclMap.has(key)) { this.elementDeclMap.set(key, value); } }); grammar.getNotationsMap().forEach((value, key) => { if (!this.notationsMap.has(key)) { this.notationsMap.set(key, value); } }); } getNotationsMap() { return this.notationsMap; } getElementDeclMap() { return this.elementDeclMap; } getEntitiesMap() { return this.entitiesMap; } processModels() { this.elementDeclMap.forEach((elementDecl) => { let name = elementDecl.getName(); if (XMLUtils_js_1.XMLUtils.hasParameterEntity(name)) { name = this.resolveParameterEntities(name); } let contentSpec = elementDecl.getContentSpec(); if (XMLUtils_js_1.XMLUtils.hasParameterEntity(contentSpec)) { contentSpec = this.resolveParameterEntities(contentSpec); } let model = ContentModel_js_1.ContentModel.parse(contentSpec); this.models.set(name, model); }); } getAttributesMap() { return this.attributesMap; } getElementAttributesMap(element) { return this.attributesMap.get(element); } validateElement(element, namespace, children, text) { const colonIndex = element.indexOf(':'); if (colonIndex !== -1) { // element with colon means it has a namespace prefix and is not coming from a DTD return Grammar_js_1.ValidationResult.success(); } const elementDecl = this.elementDeclMap.get(element); if (!elementDecl) { return Grammar_js_1.ValidationResult.error('Element "' + element + '" is not declared in the DTD'); } const model = this.getContentModel(element); if (!model) { return Grammar_js_1.ValidationResult.error('No content model found for element "' + element + '" in the DTD'); } if (model.getType() === 'EMPTY') { if (children.length > 0) { return Grammar_js_1.ValidationResult.error('Element "' + element + '" is declared as EMPTY but has child elements'); } if (text !== '') { return Grammar_js_1.ValidationResult.error('Element "' + element + '" is declared as EMPTY but has text content'); } } if (model.getType() === ContentModel_js_1.ContentModelType.ANY) { return Grammar_js_1.ValidationResult.success(); } if (model.getType() === ContentModel_js_1.ContentModelType.PCDATA) { // PCDATA content model allows any text content but no child elements if (children.length > 0) { return Grammar_js_1.ValidationResult.error('Element "' + element + '" is declared as #PCDATA but has child elements'); } return Grammar_js_1.ValidationResult.success(); } if (model.getType() === ContentModel_js_1.ContentModelType.MIXED || model.getType() === ContentModel_js_1.ContentModelType.CHILDREN) { // MIXED and CHILDREN content model allows PCDATA and specified child elements const isValid = model.validateChildren(children); if (!isValid) { return Grammar_js_1.ValidationResult.error('Element "' + element + '" has invalid child elements as per CHILDREN content model:' + model.toString()); } return Grammar_js_1.ValidationResult.success(); } return Grammar_js_1.ValidationResult.success(); } validateAttributes(element, attributes) { const declaredAttributes = this.getElementAttributes(element); if (declaredAttributes.size === 0) { // No attributes declared - check if the attributes come from XML namespace for (const [attrName, attrValue] of attributes) { const xmlNamespace = ['xml:lang', 'xml:space', 'xml:base', 'xml:id']; if (xmlNamespace.includes(attrName)) { continue; } else { return Grammar_js_1.ValidationResult.error('Undeclared attribute "' + attrName + '" found in element "' + element + '"'); } } return Grammar_js_1.ValidationResult.success(); } // Check each provided attribute against declarations const attributeDeclarations = this.attributesMap.get(element); for (const [attrName, attrValue] of attributes) { if (!declaredAttributes.has(attrName)) { return Grammar_js_1.ValidationResult.error('Undeclared attribute "' + attrName + '" found in element "' + element + '"'); } if (attributeDeclarations) { // Perform DTD datatype validation using AttDecl const attDecl = attributeDeclarations.get(attrName); if (attDecl) { const validationResult = this.validateAttributeValue(attrName, attrValue, attDecl, element); if (!validationResult.isValid) { return validationResult; } } } } // Check for required attributes that are missing for (const [attrName, attrInfo] of declaredAttributes) { if (attrInfo.use === 'required' && !attributes.has(attrName)) { return Grammar_js_1.ValidationResult.error('Required attribute "' + attrName + '" is missing from element "' + element + '"'); } } return Grammar_js_1.ValidationResult.success(); } validateAttributeValue(attrName, attrValue, attDecl, element) { const attrType = attDecl.getType(); // Skip ID and IDREF validation - these require document-level tracking if (attrType === 'ID' || attrType === 'IDREF' || attrType === 'IDREFS') { // ID/IDREF validation is handled by DOMBuilder or a custom content handler with document-level state tracking return Grammar_js_1.ValidationResult.success(); } // Use AttDecl's built-in validation for basic datatypes if (!attDecl.isValid(attrValue)) { return Grammar_js_1.ValidationResult.error('Invalid value ' + attrValue + ' for attribute ' + attrName + ' of type ' + attrType + ' in element ' + element); } // Additional validation for ENTITY/ENTITIES types - check if entities exist if (attrType === 'ENTITY') { if (!this.entityExists(attrValue)) { return Grammar_js_1.ValidationResult.error('Entity "' + attrValue + '" referenced in attribute "' + attrName + '" is not declared in element "' + element + '"'); } } else if (attrType === 'ENTITIES') { const entityNames = attrValue.split(/\s+/); for (const entityName of entityNames) { if (!this.entityExists(entityName)) { return Grammar_js_1.ValidationResult.error('Entity "' + entityName + '" referenced in attribute "' + attrName + '" is not declared in element "' + element + '"'); } } } // Additional validation for NOTATION types - check if notations exist if (attrType.startsWith('NOTATION')) { if (!this.notationExists(attrValue)) { return Grammar_js_1.ValidationResult.error('Notation "' + attrValue + '" referenced in attribute "' + attrName + '" is not declared in element "' + element + '"'); } } if (attrType.startsWith('(') && attrType.endsWith(')')) { const enumeration = attDecl.getEnumeration(); if (!enumeration.includes(attrValue)) { return Grammar_js_1.ValidationResult.error('Value "' + attrValue + '" for attribute "' + attrName + '" is not in the enumeration ' + attDecl.getType() + ' in element "' + element + '"'); } } // check for entities inside attribute value const entityReferences = this.extractEntityReferences(attrValue); for (const entityName of entityReferences) { if (!this.entityExists(entityName)) { return Grammar_js_1.ValidationResult.error('Entity "' + entityName + '" referenced in attribute "' + attrName + '" is not declared in element "' + element + '"'); } } return Grammar_js_1.ValidationResult.success(); } entityExists(entityName) { return this.getEntity(entityName) !== undefined; } notationExists(notationName) { return this.notationsMap.has(notationName); } extractEntityReferences(value) { const references = []; let index = 0; while (index < value.length) { const ampIndex = value.indexOf('&', index); if (ampIndex === -1) { break; } const semicolonIndex = value.indexOf(';', ampIndex + 1); if (semicolonIndex === -1) { break; } const candidate = value.substring(ampIndex + 1, semicolonIndex); if (candidate.length > 0 && XMLUtils_js_1.XMLUtils.isValidXMLName(candidate)) { references.push(candidate); } index = semicolonIndex + 1; } return references; } getElementAttributes(element) { const colonIndex = element.indexOf(':'); // element with colon means it has a namespace prefix and is not coming from a DTD if (colonIndex !== -1) { return new Map(); } const result = new Map(); const dtdAttributes = this.getElementAttributesMap(element); if (dtdAttributes) { dtdAttributes.forEach((attDecl, attName) => { const use = this.mapDTDAttributeUse(attDecl); const datatype = attDecl.getType(); const defaultValue = attDecl.getDefaultValue(); result.set(attName, new Grammar_js_1.AttributeInfo(attName, datatype, use, defaultValue)); }); } return result; } getDefaultAttributes(element) { const colonIndex = element.indexOf(':'); // element with colon means it has a namespace prefix and is not coming from a DTD if (colonIndex !== -1) { return new Map(); } const result = new Map(); const dtdAttributes = this.getElementAttributesMap(element); if (dtdAttributes) { dtdAttributes.forEach((attDecl, attName) => { const defaultValue = attDecl.getDefaultValue(); if (defaultValue && attDecl.getDefaultDecl() !== '#IMPLIED' && attDecl.getDefaultDecl() !== '#REQUIRED') { result.set(attName, defaultValue); } }); } return result; } resolveEntity(name) { const entity = this.getEntity(name); if (!entity) { return undefined; } const unresolvedError = entity.getUnresolvedError(); if (unresolvedError) { throw new Error(unresolvedError); } return entity.getValue(); } getGrammarType() { return Grammar_js_1.GrammarType.DTD; } getTargetNamespaces() { return new Set(); } getElementTextDefault(_element) { return undefined; } getNamespaceDeclarations() { return new Map(); } mapDTDAttributeUse(attDecl) { const defaultDecl = attDecl.getDefaultDecl(); switch (defaultDecl) { case '#REQUIRED': return Grammar_js_1.AttributeUse.REQUIRED; case '#IMPLIED': return Grammar_js_1.AttributeUse.IMPLIED; case '#FIXED': return Grammar_js_1.AttributeUse.FIXED; default: return Grammar_js_1.AttributeUse.OPTIONAL; } } } exports.DTDGrammar = DTDGrammar; //# sourceMappingURL=DTDGrammar.js.map