sequaljs
Version:
JavaScript/TypeScript library for parsing and manipulating ProForma peptide sequence notation
1,270 lines (1,269 loc) • 63.6 kB
JavaScript
"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.