typesxml
Version:
Open source XML library written in TypeScript
295 lines • 12.1 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.AttListDecl = void 0;
const Constants_js_1 = require("../Constants.js");
const XMLUtils_js_1 = require("../XMLUtils.js");
const AttDecl_js_1 = require("./AttDecl.js");
class AttListDecl {
name;
attributes;
static attTypes = ['CDATA', 'ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS'];
constructor(name, attributesText) {
this.name = name;
this.attributes = new Map();
this.parseAttributes(attributesText);
}
getName() {
return this.name;
}
getAttributes() {
return this.attributes;
}
parseAttributes(text) {
const parts = this.split(text);
const state = { index: 0 };
let scanIndex = 0;
while (state.index < parts.length) {
const name = parts[state.index++];
if (!name) {
continue;
}
if (!XMLUtils_js_1.XMLUtils.isValidXMLName(name)) {
throw new Error('Invalid attribute name in ATTLIST declaration: ' + '\'' + name + '\'');
}
const nameMatch = this.locateToken(text, name, scanIndex);
scanIndex = nameMatch.position + nameMatch.length;
if (state.index >= parts.length) {
throw new Error('Missing attribute type for attribute ' + '\'' + name + '\'');
}
const attType = this.readAttributeType(parts, state);
if (!this.isValidAttributeType(attType)) {
throw new Error('Invalid attribute type in ATTLIST declaration: ' + '\'' + attType + '\'');
}
const typeMatch = this.locateToken(text, attType, scanIndex);
scanIndex = typeMatch.position + typeMatch.length;
let defaultDecl = '';
let defaultValue = '';
if (state.index < parts.length) {
const nextPart = parts[state.index];
if (nextPart === '#REQUIRED' || nextPart === '#IMPLIED') {
const keywordMatch = this.locateToken(text, nextPart, scanIndex);
this.ensureSeparated(text, scanIndex, keywordMatch.position, attType, nextPart);
scanIndex = keywordMatch.position + keywordMatch.length;
defaultDecl = nextPart;
state.index++;
}
else if (nextPart === '#FIXED') {
const fixedMatch = this.locateToken(text, nextPart, scanIndex);
this.ensureSeparated(text, scanIndex, fixedMatch.position, attType, nextPart);
scanIndex = fixedMatch.position + fixedMatch.length;
defaultDecl = nextPart;
state.index++;
if (state.index >= parts.length) {
throw new Error('Invalid attribute declaration: missing value for #FIXED attribute ' + '\'' + name + '\'');
}
const valueToken = parts[state.index++];
const valueMatch = this.locateToken(text, valueToken, scanIndex);
this.ensureSeparated(text, scanIndex, valueMatch.position, nextPart, valueToken);
defaultValue = this.isQuotedValue(valueToken) ? this.trimQuotes(valueToken) : valueToken;
scanIndex = valueMatch.position + valueMatch.length;
}
else if (nextPart && this.isQuotedValue(nextPart)) {
const valueMatch = this.locateToken(text, nextPart, scanIndex);
this.ensureSeparated(text, scanIndex, valueMatch.position, attType, nextPart);
defaultDecl = nextPart;
defaultValue = this.trimQuotes(nextPart);
scanIndex = valueMatch.position + valueMatch.length;
state.index++;
}
else if (nextPart && !XMLUtils_js_1.XMLUtils.isValidXMLName(nextPart)) {
throw new Error('Invalid attribute declaration: unexpected token ' + '\'' + nextPart + '\'' + ' after attribute type ' + '\'' + attType + '\'');
}
}
const att = new AttDecl_js_1.AttDecl(name, attType, defaultDecl, defaultValue);
this.attributes.set(name, att);
}
}
split(text) {
let result = [];
let word = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < text.length; i++) {
let c = text.charAt(i);
if ((c === '"' || c === "'") && !inQuotes) {
if (word.length > 0) {
result.push(word);
word = '';
}
inQuotes = true;
quoteChar = c;
word += c;
}
else if (inQuotes && c === quoteChar) {
inQuotes = false;
quoteChar = '';
word += c;
if (word.length > 0) {
result.push(word);
word = '';
}
}
else if ((c === ' ' || c === '\n' || c === '\r' || c === '\t') && !inQuotes) {
// Whitespace outside quotes - split here
if (word.length > 0) {
result.push(word);
word = '';
}
}
else {
// Regular character (including parentheses)
word += c;
}
}
if (word.length > 0) {
result.push(word);
}
return result;
}
readAttributeType(parts, state) {
let token = parts[state.index++];
if (token === 'NOTATION') {
if (state.index >= parts.length) {
throw new Error('Expected NOTATION enumeration in ATTLIST declaration');
}
let enumeration = parts[state.index++];
enumeration = this.readParenthesized(enumeration, parts, state);
return 'NOTATION ' + enumeration;
}
if (token.includes('(')) {
return this.readParenthesized(token, parts, state);
}
return token;
}
readParenthesized(initial, parts, state) {
let result = initial;
let balance = this.countParenthesis(initial);
while (balance > 0) {
if (state.index >= parts.length) {
throw new Error('Unterminated parenthesized list in ATTLIST declaration');
}
const next = parts[state.index++];
result += ' ' + next;
balance += this.countParenthesis(next);
}
return this.normalizeEnumeration(result);
}
countParenthesis(value) {
let balance = 0;
for (const char of value) {
if (char === '(') {
balance++;
}
else if (char === ')') {
balance--;
}
}
return balance;
}
normalizeEnumeration(value) {
let normalized = value.replaceAll(/\s+/g, ' ');
normalized = normalized.replaceAll(' | ', '|').replaceAll(' |', '|').replaceAll('| ', '|');
normalized = normalized.replaceAll('( ', '(');
normalized = normalized.replaceAll(' )', ')');
return normalized.trim();
}
isQuotedValue(value) {
return (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"));
}
trimQuotes(value) {
return this.isQuotedValue(value) ? value.substring(1, value.length - 1) : value;
}
isValidAttributeType(attType) {
if (AttListDecl.attTypes.includes(attType)) {
return true;
}
if (attType.startsWith('NOTATION ')) {
const notation = attType.substring('NOTATION '.length).trim();
return notation.startsWith('(') && notation.endsWith(')');
}
if (attType.startsWith('(') && attType.endsWith(')')) {
return true;
}
return false;
}
locateToken(text, token, startIndex) {
const direct = text.indexOf(token, startIndex);
if (direct !== -1) {
return { position: direct, length: token.length };
}
if (token.startsWith('(') && token.endsWith(')')) {
const normalizedToken = this.normalizeEnumeration(token);
let searchIndex = startIndex;
while (searchIndex < text.length) {
const openIndex = text.indexOf('(', searchIndex);
if (openIndex === -1) {
break;
}
let depth = 0;
let cursor = openIndex;
while (cursor < text.length) {
const char = text.charAt(cursor);
if (char === '(') {
depth++;
}
else if (char === ')') {
depth--;
if (depth === 0) {
const segment = text.substring(openIndex, cursor + 1);
if (this.normalizeEnumeration(segment) === normalizedToken) {
return { position: openIndex, length: segment.length };
}
cursor++;
break;
}
}
cursor++;
}
if (depth > 0) {
break;
}
searchIndex = cursor;
}
}
throw new Error('Invalid attribute declaration: unable to locate token ' + '\'' + token + '\'');
}
ensureSeparated(text, startIndex, tokenPosition, previousToken, currentToken) {
if (tokenPosition < startIndex) {
throw new Error('Invalid attribute declaration: unexpected ordering between ' + '\'' + previousToken + '\'' + ' and ' + '\'' + currentToken + '\'');
}
const between = text.substring(startIndex, tokenPosition);
if (!this.containsWhitespace(between)) {
throw new Error('Invalid attribute declaration: missing whitespace between ' + '\'' + previousToken + '\'' + ' and ' + '\'' + currentToken + '\'');
}
}
containsWhitespace(segment) {
for (const char of segment) {
if (XMLUtils_js_1.XMLUtils.isXmlSpace(char)) {
return true;
}
}
return false;
}
getNodeType() {
return Constants_js_1.Constants.ATTRIBUTE_LIST_DECL_NODE;
}
toString() {
let result = '<!ATTLIST ' + this.name + '\n';
this.attributes.forEach((a) => {
result += ' ' + a.toString() + '\n';
});
return result + '>';
}
equals(node) {
if (node instanceof AttListDecl) {
let nodeAtts = node.getAttributes();
if (this.name !== node.getName() || this.attributes.size !== nodeAtts.size) {
return false;
}
for (let [key, value] of this.attributes) {
let att = nodeAtts.get(key);
if (att === undefined) {
return false;
}
if (!value.equals(att)) {
return false;
}
}
return true;
}
return false;
}
}
exports.AttListDecl = AttListDecl;
//# sourceMappingURL=AttListDecl.js.map