typesxml
Version:
Open source XML library written in TypeScript
393 lines • 17 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.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