UNPKG

sequaljs

Version:

JavaScript/TypeScript library for parsing and manipulating ProForma peptide sequence notation

1,270 lines (1,269 loc) 63.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ModificationMap = exports.GlobalModification = exports.Modification = exports.ModificationValue = exports.PipeValue = void 0; const resources_1 = require("./resources"); const base_block_1 = require("./base_block"); /** * Represents a value in a ProForma modification string. */ class PipeValue { /** * Initializes a PipeValue object. * * @param value - The value of the pipe value. * @param valueType - The type of the pipe value. * @param originalValue - The original value of the pipe value. */ constructor(value, valueType, originalValue = null) { this.crosslinkId = null; this.isBranch = false; this.isBranchRef = false; this.isCrosslinkRef = false; this.ambiguityGroup = null; this.isAmbiguityRef = false; this.localizationScore = null; this.source = null; this.mass = null; this.observedMass = null; this.isValidGlycan = false; this.isValidFormula = false; this.assignedTypes = []; this.charge = null; // e.g., "z+2", "z-1" this.chargeValue = null; // e.g., 2, -1 this.value = value; this._type = valueType; this.originalValue = originalValue; this._extractProperties(); } /** * Extracts properties from the value. * @private */ _extractProperties() { if (this.type === PipeValue.CROSSLINK && this.value.includes("#")) { const parts = this.value.split("#", 2); if (parts[1] === "BRANCH") { this.isBranch = true; } else { this.crosslinkId = parts[1]; } } else if (this.type === PipeValue.AMBIGUITY && this.value.includes("#")) { const parts = this.value.split("#", 2); this.ambiguityGroup = parts[1]; if (this.ambiguityGroup.includes("(") && this.ambiguityGroup.includes(")")) { const match = this.ambiguityGroup.match(/\(([\d.]+)\)/); if (match) { this.localizationScore = parseFloat(match[1]); } } } // ProForma 2.1: Handle charged formulas (Section 11.1) if (this.type === PipeValue.FORMULA) { const chargeMatch = this.value.match(/:z([+-]\d+)$/); if (chargeMatch) { this.charge = 'z' + chargeMatch[1]; this.chargeValue = parseInt(chargeMatch[1]); // Remove charge notation from value this.value = this.value.replace(/:z[+-]\d+$/, ''); } } } /** * Returns a string representation of the pipe value. * @returns The string representation. */ toString() { return this.value; } /** * Gets the type of the pipe value. */ get type() { return this._type; } /** * Sets the type of the pipe value. * @param value - The new type. */ set type(value) { this._type = value; if (this.assignedTypes.length > 0) { this.assignedTypes[0] = value; } else { this.assignType(value); } } /** * Assigns a type to the pipe value. * @param value - The type to assign. */ assignType(value) { if (!this.assignedTypes.includes(value)) { this.assignedTypes.push(value); } } } exports.PipeValue = PipeValue; PipeValue.SYNONYM = "synonym"; PipeValue.INFO_TAG = "info_tag"; PipeValue.MASS = "mass"; PipeValue.OBSERVED_MASS = "observed_mass"; PipeValue.CROSSLINK = "crosslink"; PipeValue.BRANCH = "branch"; PipeValue.AMBIGUITY = "ambiguity"; PipeValue.GLYCAN = "glycan"; PipeValue.GAP = "gap"; PipeValue.FORMULA = "formula"; /** * Represents the value of a modification. */ class ModificationValue { /** * Initializes a ModificationValue object. * * @param value - The value of the modification. * @param mass - The mass of the modification. */ constructor(value, mass = null) { this._primaryValue = ""; this._source = null; this._pipeValues = []; this._mass = mass; this._parseValue(value); } /** * Validates a glycan string. * * @param glycan - The glycan string to validate. * @returns True if the glycan is valid, false otherwise. */ static validateGlycan(glycan) { return ModificationValue._validateGlycan(glycan); } /** * Validates a formula string. * * @param formula - The formula string to validate. * @returns True if the formula is valid, false otherwise. */ static validateFormula(formula) { return ModificationValue._validateFormula(formula); } /** * Gets the localization score of the modification. */ get localizationScore() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.localizationScore)[0] || null; } /** * Parses the value of the modification. * @param value - The value to parse. * @private */ _parseValue(value) { if (value.includes("|")) { const components = value.split("|"); this._processPrimaryValue(components[0]); for (let i = 1; i < components.length; i++) { this._processPipeComponent(components[i]); } } else { this._processPrimaryValue(value); } } /** * Processes the primary value of the modification. * @param value - The primary value to process. * @private */ _processPrimaryValue(value) { if (value === "#BRANCH") { this._primaryValue = ""; const pipeVal = new PipeValue(value, PipeValue.BRANCH, value); pipeVal.isBranchRef = true; pipeVal.isBranch = true; this._pipeValues.push(pipeVal); return; } else if (value.startsWith("#")) { this._primaryValue = ""; const pipeValType = value.substring(1).startsWith("XL") ? PipeValue.CROSSLINK : PipeValue.AMBIGUITY; const pipeVal = new PipeValue(value, pipeValType, value); pipeVal.isCrosslinkRef = pipeValType === PipeValue.CROSSLINK; pipeVal.isAmbiguityRef = pipeValType === PipeValue.AMBIGUITY; pipeVal.crosslinkId = pipeVal.isCrosslinkRef ? value.substring(1) : null; pipeVal.ambiguityGroup = pipeVal.isAmbiguityRef ? value.substring(1) : null; if (pipeVal.ambiguityGroup) { const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); return; } // Handle source prefix if (value.includes(":")) { // ProForma 2.1: Split only on first colon to preserve charge notation (Section 11.1) const firstColonIndex = value.indexOf(":"); const parts = [value.substring(0, firstColonIndex), value.substring(firstColonIndex + 1)]; if (ModificationValue.KNOWN_SOURCES.has(parts[0])) { this._source = parts[0]; this._primaryValue = parts[1]; let isValidGlycan = false; let isValidFormula = false; // ProForma 2.1: Extract charge notation from formula before validation (Section 11.1) let chargeNotation = null; if (this._source.toUpperCase() === "FORMULA") { const chargeMatch = this._primaryValue.match(/:z([+-]\d+)$/); if (chargeMatch) { chargeNotation = 'z' + chargeMatch[1]; this._primaryValue = this._primaryValue.replace(/:z[+-]\d+$/, ''); } isValidFormula = ModificationValue._validateFormula(this._primaryValue); } else if (this._source.toUpperCase() === "GLYCAN") { isValidGlycan = ModificationValue._validateGlycan(this._primaryValue); } if (this._primaryValue.includes("#")) { const pvParts = this._primaryValue.split("#", 2); this._primaryValue = pvParts[0]; let pipeVal; if (["XL", "XLMOD", "XL-MOD", "X"].includes(this._source)) { pipeVal = new PipeValue(`${this._primaryValue}`, PipeValue.CROSSLINK, value); pipeVal.source = this._source; pipeVal.crosslinkId = pvParts[1]; } else if (pvParts[1] === "BRANCH") { pipeVal = new PipeValue(pvParts[0], PipeValue.BRANCH); pipeVal.source = this._source; pipeVal.isBranch = true; } else { pipeVal = new PipeValue(`${this._primaryValue}`, PipeValue.AMBIGUITY, value); if (isValidGlycan) { pipeVal.isValidGlycan = isValidGlycan; pipeVal.assignedTypes.push("glycan"); } else if (isValidFormula) { pipeVal.isValidFormula = isValidFormula; pipeVal.assignedTypes.push("formula"); } if (this._source.toUpperCase() === "GNO" || this._source.toUpperCase() === "G") { pipeVal.isValidGlycan = true; pipeVal.assignedTypes.push("glycan"); } pipeVal.source = this._source; pipeVal.ambiguityGroup = pvParts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); } else { let pipeVal; if (this._source.toUpperCase() === "INFO") { pipeVal = new PipeValue(parts[1], PipeValue.INFO_TAG, value); } else if (this._source.toUpperCase() === "OBS") { pipeVal = new PipeValue(parts[1], PipeValue.OBSERVED_MASS, value); pipeVal.observedMass = parseFloat(parts[1]); } else if (this._source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(parts[1], PipeValue.GLYCAN, value); pipeVal.isValidGlycan = isValidGlycan; } else if (this._source.toUpperCase() === "GNO" || this._source.toUpperCase() === "G") { pipeVal = new PipeValue(parts[1], PipeValue.GAP, value); pipeVal.isValidGlycan = true; } else if (this._source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(this._primaryValue, PipeValue.FORMULA, value); pipeVal.isValidFormula = isValidFormula; // ProForma 2.1: Set charge notation if present (Section 11.1) if (chargeNotation) { pipeVal.charge = chargeNotation; pipeVal.chargeValue = parseInt(chargeNotation.substring(1)); } } else { pipeVal = new PipeValue(parts[1], PipeValue.SYNONYM, value); } pipeVal.source = this._source; this._pipeValues.push(pipeVal); } } else if (parts[0].toUpperCase() === "MASS") { this._primaryValue = value; try { this._mass = parseFloat(parts[1]); const pipeVal = new PipeValue(parts[1], PipeValue.MASS, value); pipeVal.mass = this._mass; if (parts[1].includes("#")) { const pvParts = this._primaryValue.split("#", 2); this._primaryValue = pvParts[0]; pipeVal.value = pvParts[0]; if (pvParts[1] === "BRANCH") { pipeVal.isBranch = true; pipeVal.type = PipeValue.BRANCH; } else if (pvParts[1].startsWith("XL")) { pipeVal.crosslinkId = pvParts[1]; pipeVal.type = PipeValue.CROSSLINK; } else { pipeVal.ambiguityGroup = pvParts[1]; pipeVal.type = PipeValue.AMBIGUITY; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } pipeVal.assignedTypes.push(PipeValue.MASS); } this._pipeValues.push(pipeVal); } catch (e) { // Parse error handling } } else { this._primaryValue = value; const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } else { if (value.includes("#")) { const parts = value.split("#", 2); this._primaryValue = parts[0]; let pipeVal; if (parts[1] === "BRANCH") { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.BRANCH, value); pipeVal.isBranch = true; } else if (parts[1].startsWith("XL")) { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.CROSSLINK, value); pipeVal.crosslinkId = parts[1]; } else { pipeVal = new PipeValue(`${parts[0]}`, PipeValue.AMBIGUITY, value); pipeVal.ambiguityGroup = parts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } if (parts[0].startsWith("+") || parts[0].startsWith("-")) { try { this._mass = parseFloat(parts[0]); pipeVal.mass = this._mass; pipeVal.assignedTypes.push(PipeValue.MASS); } catch (e) { // Parse error handling } } else { pipeVal.assignedTypes.push(PipeValue.SYNONYM); } this._pipeValues.push(pipeVal); } else { this._primaryValue = value; if ((this._primaryValue.startsWith("+") || this._primaryValue.startsWith("-")) && /\d/.test(this._primaryValue)) { try { this._mass = parseFloat(this._primaryValue); const pipeVal = new PipeValue(this._primaryValue, PipeValue.MASS, value); pipeVal.mass = this._mass; this._pipeValues.push(pipeVal); } catch (e) { const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } else { const pipeVal = new PipeValue(value, PipeValue.SYNONYM, value); this._pipeValues.push(pipeVal); } } } } /** * Validates a glycan string. * @param glycan - The glycan string to validate. * @returns True if the glycan is valid, false otherwise. * @private */ static _validateGlycan(glycan) { const glycanClean = glycan.replace(/\s/g, ""); const sortedMonos = [...resources_1.monosaccharides].sort((a, b) => b.length - a.length); const escapedMonos = sortedMonos.map(m => m.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')); const monoPatternString = "(" + escapedMonos.join("|") + ")((\\(([1-9]\\d*)\\))|[1-9]\\d*)?"; const monoPattern = new RegExp(monoPatternString); let i = 0; while (i < glycanClean.length) { if (glycanClean[i] === '{') { const closeBrace = glycanClean.indexOf('}', i); if (closeBrace === -1) { return false; } const formulaPart = glycanClean.substring(i + 1, closeBrace); if (!this._validateCustomMonosaccharide(formulaPart)) { return false; } i = closeBrace + 1; const isAtEnd = i === glycanClean.length; let countStr = ''; if (i < glycanClean.length && glycanClean[i] === '(') { const closeParen = glycanClean.indexOf(')', i); if (closeParen === -1) { return false; } countStr = glycanClean.substring(i + 1, closeParen); if (!/^[1-9]\d*$/.test(countStr)) { return false; } i = closeParen + 1; } else if (i < glycanClean.length && /\d/.test(glycanClean[i])) { const startIdx = i; while (i < glycanClean.length && /\d/.test(glycanClean[i])) { i++; } countStr = glycanClean.substring(startIdx, i); if (!/^[1-9]\d*$/.test(countStr)) { return false; } } else if (!isAtEnd) { return false; } continue; } const match = glycanClean.substring(i).match(monoPattern); if (!match) { return false; } const monoLength = match[0].length; const hasCount = match[2] !== undefined && match[2] !== ''; i += monoLength; const isAtEnd = i === glycanClean.length; if (!hasCount && !isAtEnd) { return false; } } return i === glycanClean.length; } /** * Validates a custom monosaccharide formula. * @param formula - The formula to validate. * @returns True if the formula is valid, false otherwise. * @private */ static _validateCustomMonosaccharide(formula) { let cleanFormula = formula; if (cleanFormula.includes(':z')) { const chargeIndex = cleanFormula.lastIndexOf(':z'); const chargeNotation = cleanFormula.substring(chargeIndex); if (!/^:z[+-]\d+$/.test(chargeNotation)) { return false; } cleanFormula = cleanFormula.substring(0, chargeIndex); } return this._validateFormula(cleanFormula); } /** * Validates a formula string. * @param formula - The formula to validate. * @returns True if the formula is valid, false otherwise. * @private */ static _validateFormula(formula) { if (!formula.trim()) { return false; } // Check for balanced brackets if ((formula.match(/\[/g) || []).length !== (formula.match(/\]/g) || []).length) { return false; } const formulaNoSpaces = formula.replace(/\s/g, ""); let i = 0; while (i < formulaNoSpaces.length) { if (formulaNoSpaces[i] === "[") { const endBracket = formulaNoSpaces.indexOf("]", i); if (endBracket === -1) { return false; } const isotopePart = formulaNoSpaces.substring(i + 1, endBracket); if (!isotopePart.match(/\d+[A-Z][a-z]?(-?\d+)?/)) { return false; } i = endBracket + 1; if (i < formulaNoSpaces.length && (formulaNoSpaces[i] === '-' || /\d/.test(formulaNoSpaces[i]))) { let start = i; if (formulaNoSpaces[i] === '-') { i++; } while (i < formulaNoSpaces.length && /\d/.test(formulaNoSpaces[i])) { i++; } if (parseInt(formulaNoSpaces.substring(start, i)) === 0) { return false; } } } else if (/[A-Z]/.test(formulaNoSpaces[i])) { if (i + 1 < formulaNoSpaces.length && /[a-z]/.test(formulaNoSpaces[i + 1])) { i += 2; } else { i++; } if (i < formulaNoSpaces.length && (formulaNoSpaces[i] === '-' || /\d/.test(formulaNoSpaces[i]))) { let start = i; if (formulaNoSpaces[i] === '-') { i++; } while (i < formulaNoSpaces.length && /\d/.test(formulaNoSpaces[i])) { i++; } if (parseInt(formulaNoSpaces.substring(start, i)) === 0) { return false; } } } else { // Unexpected character return false; } } return true; } /** * Processes a pipe component of the modification. * @param component - The pipe component to process. * @private */ _processPipeComponent(component) { if (component === "#BRANCH") { const pipeVal = new PipeValue(component, PipeValue.BRANCH, component); pipeVal.isBranchRef = true; pipeVal.isBranch = true; this._pipeValues.push(pipeVal); return; } else if (component.startsWith("#")) { const pipeValType = component.substring(1).startsWith("XL") ? PipeValue.CROSSLINK : PipeValue.AMBIGUITY; const pipeVal = new PipeValue(component, pipeValType, component); pipeVal.isCrosslinkRef = pipeValType === PipeValue.CROSSLINK; pipeVal.isAmbiguityRef = pipeValType === PipeValue.AMBIGUITY; pipeVal.crosslinkId = pipeVal.isCrosslinkRef ? component.substring(1) : null; pipeVal.ambiguityGroup = pipeVal.isAmbiguityRef ? component.substring(1) : null; if (pipeVal.ambiguityGroup) { const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } this._pipeValues.push(pipeVal); return; } // Handle source prefix if (component.includes(":")) { const parts = component.split(":", 2); if (ModificationValue.KNOWN_SOURCES.has(parts[0])) { const source = parts[0]; const value = parts[1]; // Handle crosslinks or ambiguity in value if (value.includes("#")) { const pvParts = value.split("#", 2); const componentValue = pvParts[0]; let isValidGlycan = false; let isValidFormula = false; if (source.toUpperCase() === "FORMULA") { isValidFormula = ModificationValue._validateFormula(componentValue); } else if (source.toUpperCase() === "GLYCAN") { isValidGlycan = ModificationValue._validateGlycan(componentValue); } let pipeVal; if (source === "XL" || source === "XLMOD" || source === "XL-MOD" || source === "X") { pipeVal = new PipeValue(componentValue, PipeValue.CROSSLINK); pipeVal.crosslinkId = pvParts[1]; } else if (pvParts[1] === "BRANCH") { pipeVal = new PipeValue(componentValue, PipeValue.BRANCH); pipeVal.isBranch = true; } else if (source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(componentValue, PipeValue.GLYCAN); pipeVal.isValidGlycan = isValidGlycan; } else if (source.toUpperCase() === "GNO" || source.toUpperCase() === "G") { pipeVal = new PipeValue(componentValue, PipeValue.GAP); pipeVal.isValidGlycan = true; } else if (source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(componentValue, PipeValue.FORMULA); pipeVal.isValidFormula = isValidFormula; } else { pipeVal = new PipeValue(componentValue, PipeValue.AMBIGUITY); pipeVal.ambiguityGroup = pvParts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } pipeVal.source = source; this._pipeValues.push(pipeVal); } else { let pipeVal; if (source.toUpperCase() === "INFO") { pipeVal = new PipeValue(value, PipeValue.INFO_TAG, component); } else if (source.toUpperCase() === "OBS") { pipeVal = new PipeValue(value, PipeValue.OBSERVED_MASS, component); pipeVal.observedMass = parseFloat(value); } else if (source.toUpperCase() === "GLYCAN") { pipeVal = new PipeValue(value, PipeValue.GLYCAN, component); pipeVal.isValidGlycan = ModificationValue._validateGlycan(value); } else if (source.toUpperCase() === "GNO" || source.toUpperCase() === "G") { pipeVal = new PipeValue(value, PipeValue.GAP, component); pipeVal.isValidGlycan = true; } else if (source.toUpperCase() === "FORMULA") { pipeVal = new PipeValue(value, PipeValue.FORMULA, component); pipeVal.isValidFormula = ModificationValue._validateFormula(value); } else { pipeVal = new PipeValue(value, PipeValue.SYNONYM, component); } pipeVal.source = source; this._pipeValues.push(pipeVal); } } else if (parts[0].toUpperCase() === "MASS") { try { const mass = parseFloat(parts[1]); const pipeVal = new PipeValue(parts[1], PipeValue.MASS, component); pipeVal.mass = mass; if (parts[1].includes("#")) { const hashParts = parts[1].split("#", 2); if (hashParts[1] === "BRANCH") { pipeVal.isBranch = true; pipeVal.type = PipeValue.BRANCH; } else if (hashParts[1].startsWith("XL")) { pipeVal.crosslinkId = hashParts[1]; pipeVal.type = PipeValue.CROSSLINK; } else { pipeVal.ambiguityGroup = hashParts[1]; pipeVal.type = PipeValue.AMBIGUITY; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } } this._pipeValues.push(pipeVal); } catch (e) { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { // Handle crosslink ID or ambiguity for values without source prefix if (component.includes("#")) { const parts = component.split("#", 2); const value = parts[0]; let pipeVal; if (parts[1] === "BRANCH") { pipeVal = new PipeValue(value, PipeValue.BRANCH, component); pipeVal.isBranch = true; } else if (parts[1].startsWith("XL")) { pipeVal = new PipeValue(value, PipeValue.CROSSLINK, component); pipeVal.crosslinkId = parts[1]; } else { pipeVal = new PipeValue(value, PipeValue.AMBIGUITY, component); pipeVal.ambiguityGroup = parts[1]; const scoreMatch = /\(([\d.]+)\)/.exec(pipeVal.ambiguityGroup); if (scoreMatch) { try { pipeVal.localizationScore = parseFloat(scoreMatch[1]); pipeVal.ambiguityGroup = pipeVal.ambiguityGroup.replace(scoreMatch[0], ""); } catch (e) { // Parse error handling } } } if ((value.startsWith("+") || value.startsWith("-")) && /\d/.test(value)) { try { const mass = parseFloat(value); pipeVal.mass = mass; pipeVal.assignedTypes.push(PipeValue.MASS); } catch (e) { // Parse error handling } } else { pipeVal.assignedTypes.push(PipeValue.SYNONYM); } this._pipeValues.push(pipeVal); } else { // Handle mass shifts if ((component.startsWith("+") || component.startsWith("-")) && /\d/.test(component)) { try { const mass = parseFloat(component); const pipeVal = new PipeValue(component, PipeValue.MASS, component); pipeVal.mass = mass; this._pipeValues.push(pipeVal); } catch (e) { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } else { this._pipeValues.push(new PipeValue(component, PipeValue.SYNONYM, component)); } } } } /** * Gets the source of the modification. */ get source() { return this._source; } /** * Gets the primary value of the modification. */ get primaryValue() { return this._primaryValue; } /** * Gets the mass of the modification. */ get mass() { return this._mass; } /** * Gets the pipe values of the modification. */ get pipeValues() { return this._pipeValues; } /** * Gets the info tags of the modification. */ get infoTags() { return this._pipeValues .filter((pv) => pv.type === PipeValue.INFO_TAG) .map((pv) => pv.value); } /** * Gets the synonyms of the modification. */ get synonyms() { return this._pipeValues .filter((pv) => pv.type === PipeValue.SYNONYM) .map((pv) => pv.value); } /** * Gets the observed mass of the modification. */ get observedMass() { return this._pipeValues .filter((pv) => pv.type === PipeValue.OBSERVED_MASS) .map((pv) => pv.observedMass)[0] || null; } /** * Gets the ambiguity group of the modification. */ get ambiguityGroup() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.ambiguityGroup)[0] || null; } /** * Checks if the modification is an ambiguity reference. */ get isAmbiguityRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.AMBIGUITY) .map((pv) => pv.isAmbiguityRef)[0] || false; } /** * Checks if the modification is a crosslink reference. */ get isCrosslinkRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.CROSSLINK) .map((pv) => pv.isCrosslinkRef)[0] || false; } /** * Checks if the modification is a branch reference. */ get isBranchRef() { return this._pipeValues .filter((pv) => pv.type === PipeValue.BRANCH) .map((pv) => pv.isBranchRef)[0] || false; } /** * Checks if the modification is a branch. */ get isBranch() { return this._pipeValues .filter((pv) => pv.type === PipeValue.BRANCH) .map((pv) => pv.isBranch)[0] || false; } /** * Gets the crosslink ID of the modification. */ get crossLinkId() { return this._pipeValues .filter((pv) => pv.type === PipeValue.CROSSLINK) .map((pv) => pv.crosslinkId)[0] || null; } } exports.ModificationValue = ModificationValue; ModificationValue.KNOWN_SOURCES = new Set([ "Unimod", "U", "PSI-MOD", "M", "RESID", "R", "XL-MOD", "X", "XLMOD", "GNO", "G", "MOD", "Obs", "Formula", "FORMULA", "GLYCAN", "Glycan", "Info", "INFO", "OBS", "XL" ]); /** * Represents a modification to a sequence. */ class Modification extends base_block_1.BaseBlock { /** * Initializes a Modification object. * * @param value - The value of the modification. * @param position - The position of the modification in the sequence. * @param regexPattern - The regex pattern for the modification. * @param fullName - The full name of the modification. * @param modType - The type of the modification. * @param labile - Whether the modification is labile. * @param labilNumber - The labile number of the modification. * @param mass - The mass of the modification. * @param allFilled - Whether all positions of the modification are filled. * @param crosslinkId - The crosslink ID of the modification. * @param isCrosslinkRef - Whether the modification is a crosslink reference. * @param isBranchRef - Whether the modification is a branch reference. * @param isBranch - Whether the modification is a branch. * @param ambiguityGroup - The ambiguity group of the modification. * @param isAmbiguityRef - Whether the modification is an ambiguity reference. * @param inRange - Whether the modification is in a range. * @param rangeStart - The start of the range. * @param rangeEnd - The end of the range. * @param localizationScore - The localization score of the modification. * @param modValue - The modification value. * @param isIonType - Whether the modification is an ion type. * @param positionConstraint - The position constraint of the modification. * @param limitPerPosition - The limit per position of the modification. * @param colocalizeKnown - Whether to colocalize known modifications. * @param colocalizeUnknown - Whether to colocalize unknown modifications. */ constructor(value, position, regexPattern, fullName, modType = "static", labile = false, labilNumber = 0, mass = 0.0, allFilled = false, crosslinkId, isCrosslinkRef = false, isBranchRef = false, isBranch = false, ambiguityGroup, isAmbiguityRef = false, inRange = false, rangeStart, rangeEnd, localizationScore, modValue, isIonType = false, positionConstraint, limitPerPosition, colocalizeKnown = false, colocalizeUnknown = false) { // Initialize parameters for superclass let processedValue = value; if (value.startsWith("#") && isCrosslinkRef) { crosslinkId = value.substring(1); processedValue = "#" + crosslinkId; } // Call BaseBlock constructor super(processedValue, position, true, mass); // Initialize Modification specific properties this._source = null; this._originalValue = value; this._crosslinkId = crosslinkId || null; this._isCrosslinkRef = isCrosslinkRef; this._isBranchRef = isBranchRef; this._isBranch = isBranch; this._isAmbiguityRef = isAmbiguityRef; this._ambiguityGroup = ambiguityGroup || null; this.inRange = inRange; this.rangeStart = rangeStart || null; this.rangeEnd = rangeEnd || null; this.localizationScore = localizationScore || null; this._modValue = modValue || new ModificationValue(value, mass); const validModTypes = new Set([ "static", "variable", "terminal", "ambiguous", "crosslink", "branch", "gap", "labile", "unknown_position", "global" ]); if ((crosslinkId || isCrosslinkRef) && modType !== "crosslink") { modType = "crosslink"; } if (!validModTypes.has(modType)) { throw new Error(`mod_type must be one of: ${Array.from(validModTypes).join(', ')}`); } this._regex = regexPattern ? new RegExp(regexPattern) : null; this._modType = modType; this._labile = labile; this._labileNumber = labilNumber; this._fullName = fullName || null; this._allFilled = allFilled; // ProForma 2.1: Initialize ion type flag (Section 11.6) this._isIonType = isIonType; // ProForma 2.1: Initialize placement controls (Section 11.2) this._positionConstraint = positionConstraint || null; this._limitPerPosition = limitPerPosition || null; this._colocalizeKnown = colocalizeKnown; this._colocalizeUnknown = colocalizeUnknown; if (modType === "labile") { this._labile = true; } if (this.inRange) { this._modType = "ambiguous"; } } /** * Gets the value of the modification. */ get value() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.primaryValue) || super.value; } /** * Sets the value of the modification. * @param val - The new value. */ set value(val) { super.value = val; } /** * Gets the mass of the modification. */ get mass() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.mass) || super.mass || 0.0; } /** * Sets the mass of the modification. * @param val - The new mass. */ set mass(val) { super.mass = val; } /** * Gets the observed mass of the modification. */ get observedMass() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.observedMass) || null; } /** * Gets the ambiguity group of the modification. */ get ambiguityGroup() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.ambiguityGroup) || this._ambiguityGroup; } /** * Checks if the modification is an ambiguity reference. */ get isAmbiguityRef() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.isAmbiguityRef) || this._isAmbiguityRef; } /** * Gets the synonyms of the modification. */ get synonyms() { return this._modValue.synonyms; } /** * Gets the modification value. */ get modValue() { return this._modValue; } /** * Sets the modification value. * @param val - The new modification value. */ set modValue(val) { this._modValue = val; } /** * Gets the info tags of the modification. */ get infoTags() { return this._modValue.infoTags; } /** * Gets the crosslink ID of the modification. */ get crosslinkId() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.crossLinkId) || this._crosslinkId; } /** * Checks if the modification is a crosslink reference. */ get isCrosslinkRef() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.isCrosslinkRef) || this._isCrosslinkRef; } /** * Gets the source of the modification. */ get source() { var _a; return ((_a = this._modValue) === null || _a === void 0 ? void 0 : _a.source) || this._source; } /** * Gets the original value of the modification. */ get originalValue() { return this._originalValue; } /** * Gets the regex pattern of the modification. */ get regex() { return this._regex; } /** * Gets the type of the modification. */ get modType() { return this._modType; } /** * Checks if the modification is labile. */ get labile() { return this._labile; } /** * Gets the labile number of the modification. */ get labileNumber() { return this._labileNumber; } /** * Gets the full name of the modification. */ get fullName() { return this._fullName; } /** * Checks if all positions of the modification are filled. */ get allFilled() { return this._allFilled; } /** * Checks if the modification is an ion type. */ get isIonType() { return this._isIonType; } /** * Gets the position constraint of the modification. */ getPositionConstraint() { return this._positionConstraint; } /** * Gets the limit per position of the modification. */ getLimitPerPosition() { return this._limitPerPosition; } /** * Checks if to colocalize known modifications. */ getColocalizeKnown() { return this._colocalizeKnown; } /** * Checks if to colocalize unknown modifications. */ getColocalizeUnknown() { return this._colocalizeUnknown; } /** * Finds the positions of the modification in a sequence. * * @param seq - The sequence to search in. * @yields The start and end positions of the modification. */ *findPositions(seq) { if (!this._regex) { throw new Error(`No regex pattern defined for modification '${this.value}'`); } let match; let regex = new RegExp(this._regex); if (!regex.global) { regex = new RegExp(this._regex, 'g'); } while ((match = regex.exec(seq)) !== null) { const groups = match.length > 1 ? match.slice(1) : []; if (groups.length > 0) { for (let groupIdx = 0; groupIdx < groups.length; groupIdx++) { if (groups[groupIdx]) { const start = match.index + match[0].indexOf(match[groupIdx + 1]); yield [start, start + match[groupIdx + 1].length]; } } } else { yield [match.index, match.index + match[0].length]; } } } /** * Converts the modification to a dictionary representation. * @returns A dictionary containing the modification's attributes. */ toDict() { var _a, _b; const baseDict = super.toDict(); return Object.assign(Object.assign({}, baseDict), { source: this._source, original_value: this._originalValue, regex_pattern: (_b = (_a = this._regex) === null || _a === void 0 ? void 0 : _a.source) !== null && _b !== void 0 ? _b : null, full_name: this._fullName, mod_type: this._modType, labile: this._labile, labile_number: this._labileNumber, all_filled: this._allFilled, crosslink_id: this._crosslinkId, is_crosslink_ref: this._isCrosslinkRef }); } /** * Checks if two modifications are equal. * @param other - The other modification to compare with. * @returns True if the modifications are equal, false otherwise. */ equals(other) { if (!super.equals(other)) { return false; } if (!(other instanceof Modification)) { return false; } return (this._modType === other.modType && this._labile === other.labile && this._labileNumber === other.labileNumber); } /** * Generates a hash for the modification. * @returns The hash code. */ hashCode() { var _a; const baseHash = super.hashCode(); return baseHash ^ ((((_a = this._modType) === null || _a === void 0 ? void 0 : _a.length) || 0) * 17) ^ (this._labile ? 1 : 0) ^ this._labileNumber; } /** * Returns a string representation of the modification. * @returns The string representation. */ toString() { if (this._isCrosslinkRef && this._crosslinkId) { return `#${this._crosslinkId}`; } if (this._isBranchRef) { return "#BRANCH"; } let result = this._modValue.toString(); if (this._crosslinkId && !this._isCrosslinkRef) { result += `#${this._crosslinkId}`; } if (this._isBranch && !this._isBranchRef) { result += "#BRANCH"; } if (this._labile) { result += `${this._labileNumber}`; } return result; } /** * Validates a glycan string.