Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

mf-parser

Package Overview
Dependencies
Maintainers
0
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

mf-parser - npm Package Compare versions

Comparing version 3.1.1 to 3.2.0

lib/ensureCase.d.ts

1682

lib/index.js

@@ -1,1659 +0,27 @@

'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var chemicalElements = require('chemical-elements');
var chemicalGroups = require('chemical-groups');
var atomSorter = require('atom-sorter');
/**
* Define static variable corresponding to the various Kinds of a molecular formula part.
*/
const Kind = {
BEGIN: 'begin',
ATOM: 'atom',
MULTIPLIER_RANGE: 'multiplierRange',
ISOTOPE: 'isotope',
ISOTOPE_RATIO: 'isotopeRatio',
CHARGE: 'charge',
SALT: 'salt',
OPENING_PARENTHESIS: 'openingParenthesis',
CLOSING_PARENTHESIS: 'closingParenthesis',
PRE_MULTIPLIER: 'preMultiplier',
MULTIPLIER: 'multiplier',
TEXT: 'text',
ANCHOR: 'anchor',
COMMENT: 'comment',
};
/**
* Parse a string to extract the charge
* The charge may be in the form --, +++, +3, -2, 4+, 2-
* @param {*} charge
*/
function parseCharge(charge) {
charge = charge.replace(/[()]/g, '');
let chargeNumber = 0;
if (charge.match(/^[+-]+$/)) {
for (let i = 0; i < charge.length; i++) {
if (charge.charAt(i) === '+') chargeNumber++;
else chargeNumber--;
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
} else if (charge.match(/^[0-9]+[+-]$/)) {
chargeNumber = Number(
charge.charAt(charge.length - 1) + charge.substring(0, charge.length - 1),
);
} else {
chargeNumber = Number(charge);
}
return chargeNumber;
}
/**
* Parse a mf to an array of kind / value
* @param {String} mf
*/
function parse(mf) {
return new MFParser().parse(mf);
}
class MFParser {
parse(mf = '') {
this.mf = mf;
this.i = 0;
this.result = [];
let lastKind = Kind.BEGIN;
while (this.i < mf.length) {
if (
this.result.length > 0 &&
this.result[this.result.length - 1].kind !== Kind.TEXT
) {
lastKind = this.result[this.result.length - 1].kind;
}
let char = mf.charAt(this.i);
let ascii = mf.charCodeAt(this.i);
let nextAscii = 0;
if (this.i + 1 < mf.length) nextAscii = mf.charCodeAt(this.i + 1);
if (
(ascii > 47 && ascii < 58) ||
(char === '-' && nextAscii > 47 && nextAscii < 58)
) {
// a number
let value = this.getNumber(ascii);
if (
lastKind === Kind.SALT ||
lastKind === Kind.BEGIN ||
lastKind === Kind.OPENING_PARENTHESIS
) {
if (value.to) {
throw new MFError(
this.mf,
this.i,
'Premultiplier may not contain a -',
);
}
this.result.push({ kind: Kind.PRE_MULTIPLIER, value: value.from });
} else if (lastKind === Kind.ANCHOR) {
if (value.to) {
throw new MFError(this.mf, this.i, 'Anchor ID may not contain -');
}
this.result[this.result.length - 1].value = value.from;
} else if (value.to) {
this.result.push({
kind: Kind.MULTIPLIER_RANGE,
value: {
from: Math.min(value.from, value.to),
to: Math.max(value.from, value.to),
},
});
} else {
this.result.push({ kind: Kind.MULTIPLIER, value: value.from });
}
continue;
} else if (char === '.') {
// a point
this.result.push({ kind: Kind.SALT, value: char });
// it is not in a number otherwise it would have been taken before
// it must be in a salt
} else if (char === '#') {
// an anchor
this.result.push({ kind: Kind.ANCHOR, value: 0 });
// it is not in a number otherwise it would have been taken before
// it must be in a salt
} else if (ascii > 64 && ascii < 91) {
// an uppercase = new atom
let value = this.getAtom(ascii);
this.result.push({ kind: Kind.ATOM, value });
continue;
} else if (ascii > 96 && ascii < 123) {
// a lowercase
throw new MFError(
this.mf,
this.i,
'found a lowercase not following an uppercase',
);
} else if (char === '(') {
let charge = this.getParenthesisCharge(ascii);
if (charge) {
this.result.push({ kind: Kind.CHARGE, value: charge });
} else {
this.result.push({ kind: Kind.OPENING_PARENTHESIS, value: '(' });
}
} else if (char === ')') {
this.result.push({ kind: Kind.CLOSING_PARENTHESIS, value: ')' });
} else if (char === '[') {
// defines an isotope
let isotope = this.getIsotope(ascii);
this.result.push({ kind: Kind.ISOTOPE, value: isotope });
} else if (char === ']') {
throw new MFError(
this.mf,
this.i,
'should never meet an closing bracket not in isotopes',
);
} else if (char === '{') {
// can define an exotic isotopic ratio or mixtures of groups
let isotopeRatio = this.getCurlyBracketIsotopeRatio(ascii);
if (lastKind === Kind.ATOM) {
let lastResult = this.result[this.result.length - 1];
lastResult.kind = Kind.ISOTOPE_RATIO;
lastResult.value = {
atom: lastResult.value,
ratio: isotopeRatio,
};
} else {
throw new MFError(
this.mf,
this.i,
'isotopic composition has to follow an atom',
);
}
} else if (char === '}') {
throw new MFError(
this.mf,
this.i,
'found a unexpected closing curly bracket',
);
} else if (char === '+') {
// charge not in parenthesis
let charge = this.getNonParenthesisCharge(ascii);
this.result.push({ kind: Kind.CHARGE, value: charge });
} else if (char === '-') {
// charge not in parenthesis
let charge = this.getNonParenthesisCharge(ascii);
this.result.push({ kind: Kind.CHARGE, value: charge });
} else if (char === '$') {
// it is a comment after
this.result.push({
kind: Kind.COMMENT,
value: this.mf.substring(this.i + 1),
});
break;
} else {
this.result.push({ kind: Kind.TEXT, value: char });
}
this.i++;
}
this.checkParenthesis();
return this.result;
}
checkParenthesis() {
let counter = 0;
for (let line of this.result) {
if (line.kind === Kind.OPENING_PARENTHESIS) counter++;
if (line.kind === Kind.CLOSING_PARENTHESIS) counter--;
}
if (counter !== 0) {
throw new MFError(
this.mf,
this.i,
'number of opening and closing parenthesis not equal',
);
}
}
getNumber(ascii) {
let number = '';
let previous;
do {
previous = ascii;
number += String.fromCharCode(ascii);
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (
(ascii > 47 && ascii < 58) ||
ascii === 46 ||
ascii === 45 ||
ascii === 47
); // number . - /
// we need to deal with the case there is a from / to
if (previous === 46) this.i--;
let indexOfDash = number.indexOf('-', 1);
if (indexOfDash > -1) {
return {
from: parseNumberWithDivision(number.substr(0, indexOfDash)),
to: parseNumberWithDivision(number.substr(indexOfDash + 1)),
};
}
return { from: parseNumberWithDivision(number) };
}
getAtom(ascii) {
let atom = '';
do {
atom += String.fromCharCode(ascii);
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (ascii > 96 && ascii < 123);
return atom;
}
getIsotope(ascii) {
// [13C]
let substring = '';
do {
substring += String.fromCharCode(ascii);
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (ascii !== 93 && this.i <= this.mf.length);
let atom = substring.replace(/[^a-zA-Z]/g, '');
let isotope = Number(substring.replace(/[^0-9]/g, ''));
return { atom, isotope };
}
getCurlyBracketIsotopeRatio(ascii) {
let substring = '';
let first = true;
do {
if (!first) {
substring += String.fromCharCode(ascii);
} else {
first = false;
}
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (ascii !== 125 && this.i <= this.mf.length); // closing curly bracket
if (substring.match(/^[0-9,]+$/)) {
return substring.split(',').map((a) => Number(a));
}
throw new MFError(
this.mf,
this.i,
'Curly brackets should contain only number and comma',
);
}
getParenthesisCharge(ascii) {
let substring = '';
let begin = this.i;
do {
substring += String.fromCharCode(ascii);
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (ascii !== 41 && this.i <= this.mf.length); // closing parenthesis
if (substring.match(/^\([0-9+-]+$/)) {
return parseCharge(substring.substring(1));
} else {
this.i = begin;
return undefined;
}
}
getNonParenthesisCharge(ascii) {
let substring = '';
do {
substring += String.fromCharCode(ascii);
this.i++;
ascii = this.mf.charCodeAt(this.i);
} while (ascii === 43 || ascii === 45 || (ascii > 47 && ascii < 58));
this.i--;
return parseCharge(substring);
}
}
class MFError extends SyntaxError {
constructor(mf, i, message) {
let text = `${message}\n\n${mf}\n${' '.repeat(i)}^`;
super(text);
}
}
function parseNumberWithDivision(string) {
if (string.includes('/')) {
let parts = string.split('/');
if (parts.length !== 2) {
throw new TypeError('Can not parse MF with number like: ', string);
}
return Number(parts[0]) / Number(parts[1]);
} else {
return Number(string);
}
}
const superscript = {
0: '⁰',
1: '¹',
2: '²',
3: '³',
4: '⁴',
5: '⁵',
6: '⁶',
7: '⁷',
8: '⁸',
9: '⁹',
'+': '⁺',
'-': '⁻',
'(': '⁽',
')': '⁾',
'{': '⁽',
'}': '⁾',
'.': '˙',
',': '˙',
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
const subscript = {
0: '₀',
1: '₁',
2: '₂',
3: '₃',
4: '₄',
5: '₅',
6: '₆',
7: '₇',
8: '₈',
9: '₉',
'(': '₍',
')': '₎',
'{': '₍',
'}': '₎',
'.': ' ',
',': ' ',
};
/**
* Defines static variables corresponding to the various formatting possibilities
*/
const Format = {
SUBSCRIPT: 'subscript',
SUPERSCRIPT: 'superscript',
SUPERIMPOSE: 'superimpose',
TEXT: 'text',
};
function formatCharge(charge) {
if (charge === 1) return '+';
if (charge > 1) return `+${charge}`;
if (charge < 0) return String(charge);
return '';
}
/**
* Converts an array of mf elements to an array of formatting information
* @param {object[]} lines of the parse method
*/
function toDisplay(lines) {
let results = [];
let result = {};
for (let line of lines) {
switch (line.kind) {
case Kind.MULTIPLIER:
if (line.value !== 1) {
result = {
kind: Format.SUBSCRIPT,
value: String(line.value),
};
results.push(result);
}
break;
case Kind.MULTIPLIER_RANGE:
result = {
kind: Format.SUBSCRIPT,
value: `${String(line.value.from)}-${line.value.to}`,
};
results.push(result);
break;
case Kind.CHARGE:
if (result.kind === Format.SUBSCRIPT) {
result.kind = Format.SUPERIMPOSE;
result.over = formatCharge(line.value);
result.under = result.value;
result.value = undefined;
} else {
result = {
kind: Format.SUPERSCRIPT,
value: formatCharge(line.value),
};
results.push(result);
}
break;
case Kind.ISOTOPE:
result = {
kind: Format.SUPERSCRIPT,
value: line.value.isotope,
};
results.push(result);
result = {
kind: Format.TEXT,
value: line.value.atom,
};
results.push(result);
break;
case Kind.ISOTOPE_RATIO:
if (result.kind === Format.TEXT) {
result.value += line.value.atom;
} else {
result = {
kind: Format.TEXT,
value: line.value.atom,
};
results.push(result);
}
result = {
kind: Format.SUPERSCRIPT,
value: `{${line.value.ratio.join(',')}}`,
};
results.push(result);
break;
case Kind.SALT:
if (result.kind === Format.TEXT) {
result.value += ' • ';
} else {
result = {
kind: Format.TEXT,
value: ' • ',
};
results.push(result);
}
break;
default:
if (result.kind === Format.TEXT) {
result.value += line.value;
} else {
result = {
kind: Format.TEXT,
value: line.value,
};
results.push(result);
}
}
}
return results;
}
function isMF(mf) {
let tmpMF = mf.replace(/[^a-zA-Z]/g, '');
let parts = tmpMF.replace(/([A-Za-z])(?=[A-Z])/g, '$1 ').split(' ');
for (let i = 0; i < parts.length; i++) {
if (!chemicalElements.elementsObject[parts[i]] && !chemicalGroups.groupsObject[parts[i]]) {
return false;
}
}
return true;
}
const Style = {
SUPERIMPOSE:
'flex-direction: column;display: inline-flex;justify-content: center;text-align: left;vertical-align: middle;',
SUPERIMPOSE_SUP_SUB: 'line-height: 1; font-size: 70%',
};
function toHtml(lines) {
let html = [];
for (let line of lines) {
switch (line.kind) {
case Format.SUBSCRIPT:
html.push(`<sub>${line.value}</sub>`);
break;
case Format.SUPERSCRIPT:
html.push(`<sup>${line.value}</sup>`);
break;
case Format.SUPERIMPOSE:
html.push(`<span style="${Style.SUPERIMPOSE}">`);
html.push(
`<sup style="${Style.SUPERIMPOSE_SUP_SUB}">${line.over}</sup>`,
);
html.push(
`<sub style="${Style.SUPERIMPOSE_SUP_SUB}">${line.under}</sub>`,
);
html.push('</span>');
break;
default:
html.push(line.value);
}
}
return html.join('');
}
const elements = Object.keys(chemicalElements.elementsObject).sort(
(a, b) => b.length - a.length,
);
/**
* Ensure that the mf has been entered with capital letters and not only lowercase
* If there is only lowercase we try to capitalize the mf
* @param {string} mf
*/
function ensureCase(mf) {
for (let i = 0; i < mf.length; i++) {
if (mf.charCodeAt(i) > 64 && mf.charCodeAt(i) < 91) {
return mf;
}
}
let parts = mf.replace(/([a-z]*)([^a-z]*)/g, '$1 $2 ').split(/ +/);
for (let i = 0; i < parts.length; i++) {
if (parts[i].match(/^[a-z]$/)) {
parts[i] = parts[i].toUpperCase();
} else if (parts[i].match(/^[a-z]+$/)) {
let newPart = '';
for (let j = 0; j < parts[i].length; j++) {
let two = parts[i].substr(j, 2);
let one = parts[i].charAt(j).toUpperCase();
if (
['c', 'h', 'o', 'n'].includes(two.charAt(0)) &&
['h', 'o', 'n'].includes(two.charAt(1))
) {
newPart += two.toUpperCase();
j++;
} else {
two = two.charAt(0).toUpperCase() + two.charAt(1);
if (elements.includes(two)) {
newPart += two;
j++;
} else if (elements.includes(one)) {
newPart += one;
} else {
return mf;
}
}
}
parts[i] = newPart;
}
}
return parts.join('');
}
function flatten(parsed, options = {}) {
const { groupIdentical = false, limit = 100000 } = options;
if (parsed.length === 0) return [''];
let parts = [];
let parenthesisLevel = 0;
let currentPart;
let comments = [];
for (const entry of parsed) {
if (
(entry.kind === 'atom' ||
entry.kind === 'isotope' ||
entry.kind === 'openingParenthesis' ||
!currentPart) &&
parenthesisLevel === 0
) {
currentPart = {
mf: '',
min: 1,
max: 1,
};
parts.push(currentPart);
}
switch (entry.kind) {
case 'atom':
currentPart.mf += entry.value;
break;
case 'isotope':
currentPart.mf += `[${entry.value.isotope}${entry.value.atom}]`;
break;
case 'multiplier':
currentPart.mf += entry.value;
break;
case 'multiplierRange':
if (parenthesisLevel !== 0) {
throw new Error(
'Range definition inside parenthesis is not allowed.',
);
}
currentPart.min = entry.value.from;
currentPart.max = entry.value.to;
break;
case 'openingParenthesis':
parenthesisLevel++;
currentPart.mf += entry.value;
break;
case 'charge':
if (entry.value === 1) {
currentPart.mf += '+';
} else if (entry.value > 1) {
currentPart.mf += `(+${entry.value})`;
} else if (entry.value < 0) {
currentPart.mf += `(${entry.value})`;
}
break;
case 'closingParenthesis':
parenthesisLevel--;
currentPart.mf += entry.value;
break;
case 'comment':
comments.push(entry.value);
break;
case 'text':
break;
default:
throw new Error(
`Could not flatten the parsed MF. Unknown kind: ${entry.kind}`,
);
}
}
if (groupIdentical) {
parts = optimizeRanges(parts);
}
const mfs = createMFs(parts, comments.join(' '), limit);
return mfs;
}
/**
* If we have many times the same mf we can combine them
* This should only be applied if there are acutally some ranges
*/
function optimizeRanges(parts) {
let newParts = [];
let mfsObject = {};
let hasRange = false;
for (const mf of parts) {
if (mf.min !== mf.max) {
hasRange = true;
break;
}
}
if (!hasRange) return parts;
for (const mf of parts) {
if (!mfsObject[mf.mf]) {
mfsObject[mf.mf] = {
mf: mf.mf,
min: mf.min,
max: mf.max,
};
newParts.push(mfsObject[mf.mf]);
} else {
mfsObject[mf.mf].min = mfsObject[mf.mf].min + mf.min;
mfsObject[mf.mf].max = mfsObject[mf.mf].max + mf.max;
}
}
return newParts;
}
function createMFs(parts, comment, limit) {
const currents = new Array(parts.length);
for (let i = 0; i < currents.length; i++) {
currents[i] = parts[i].min;
}
const mfs = [];
let position = 0;
while (position < currents.length) {
if (currents[position] < parts[position].max) {
mfs.push(getMF(parts, currents, comment));
currents[position]++;
for (let i = 0; i < position; i++) {
currents[i] = parts[i].min;
}
position = 0;
} else {
position++;
}
if (mfs.length > limit) {
throw Error(`MF.flatten generates too many fragments (over ${limit})`);
}
}
mfs.push(getMF(parts, currents, comment));
return mfs;
}
function getMF(parts, currents, comment) {
let mf = '';
for (let i = 0; i < parts.length; i++) {
if (currents[i] === 0) {
continue;
}
mf += parts[i].mf;
if (currents[i] !== 1) {
mf += currents[i];
}
}
if (comment) mf += `$${comment}`;
return mf;
}
function getIsotopeRatioInfo(value) {
let result = { mass: 0, monoisotopicMass: 0 };
let element = chemicalElements.elementsAndStableIsotopesObject[value.atom];
if (!element) throw new Error(`Element not found: ${value.atom}`);
let isotopesArray = element.isotopes;
let ratios = normalize$1(value.ratio);
let max = Math.max(...ratios);
if (ratios.length > isotopesArray.length) {
throw new Error(
`the number of specified ratios is bigger that the number of stable isotopes: ${value.atom}`,
);
}
for (let i = 0; i < ratios.length; i++) {
result.mass += ratios[i] * isotopesArray[i].mass;
if (max === ratios[i] && result.monoisotopicMass === 0) {
result.monoisotopicMass = isotopesArray[i].mass;
}
}
return result;
}
function normalize$1(array) {
let sum = array.reduce((prev, current) => prev + current, 0);
return array.map((a) => a / sum);
}
/**
*
* @param {*} parts
* @param {*} [options={}]
*/
function getEA(parts) {
let results = {};
for (let part of parts) {
for (let line of part) {
switch (line.kind) {
case Kind.ISOTOPE: {
let isotope = chemicalElements.isotopesObject[line.value.isotope + line.value.atom];
if (!isotope) {
throw new Error(
`Unknown isotope: ${line.value.isotope}${line.value.atom}`,
);
}
addMass(results, line.value.atom, isotope.mass * line.multiplier);
break;
}
case Kind.ISOTOPE_RATIO: {
let isotopeRatioInfo = getIsotopeRatioInfo(line.value);
addMass(
results,
line.value.atom,
isotopeRatioInfo.mass * line.multiplier,
);
break;
}
case Kind.ATOM: {
let element = chemicalElements.elementsObject[line.value];
if (!element) {
element = chemicalGroups.groupsObject[line.value];
if (!element) throw Error(`Unknown element: ${line.value}`);
// need to explode group ????
}
addMass(results, line.value, element.mass * line.multiplier);
break;
}
case Kind.CHARGE:
break;
default:
throw new Error('partToMF unhandled Kind: ', line.kind);
}
}
}
let eas = [];
let sum = 0;
for (let key in results) {
sum += results[key];
eas.push({
element: key,
mass: results[key],
});
}
eas.forEach((ea) => {
ea.ratio = ea.mass / sum;
});
return eas;
}
function addMass(results, atom, mass) {
if (!results[atom]) results[atom] = 0;
results[atom] += mass;
}
/**
*
* @param {*} parts
* @param {*} [options={}]
*/
function getElements(parts) {
const elements = [];
for (const part of parts) {
for (const line of part) {
let number = line.multiplier;
switch (line.kind) {
case Kind.ATOM: {
let symbol = line.value;
let element = chemicalElements.elementsObject[symbol];
if (!element) {
throw new Error(`element unknown: ${symbol} - ${line}`);
}
addElement(elements, { symbol, number });
break;
}
case Kind.ISOTOPE: {
let element = chemicalElements.elementsAndIsotopesObject[line.value.atom];
if (!element) {
throw new Error(`element unknown: ${part.value.atom} - ${line}`);
}
let isotope = element.isotopes.filter(
(a) => a.nominal === line.value.isotope,
)[0];
if (!isotope) {
throw new Error(`isotope unknown: ${line.value.isotope} - ${line}`);
}
addElement(elements, {
symbol: line.value.atom,
number,
isotope: line.value.isotope,
});
break;
}
default:
throw new Error(`unknown type: ${line.kind}`);
}
}
}
return elements;
}
function addElement(elements, newElement) {
for (let element of elements) {
if (
element.symbol === newElement.symbol &&
element.isotope === newElement.isotope
) {
element.number += newElement.number;
return;
}
}
elements.push(newElement);
}
/**
* Convert a MF part to an array of atoms
* This procedure will suppress the isotopes !
* This is mainly used to make queries
*/
function partToAtoms(part) {
let atoms = {};
for (let line of part) {
switch (line.kind) {
case Kind.ISOTOPE:
if (!atoms[line.value.atom]) atoms[line.value.atom] = 0;
atoms[line.value.atom] += line.multiplier;
break;
case Kind.ISOTOPE_RATIO:
if (!atoms[line.value.atom]) atoms[line.value.atom] = 0;
atoms[line.value.atom] += line.multiplier;
break;
case Kind.ATOM:
if (!atoms[line.value]) atoms[line.value] = 0;
atoms[line.value] += line.multiplier;
break;
case Kind.CHARGE:
break;
case Kind.ANCHOR:
break;
default:
throw new Error('partToMF unhandled Kind: ', line.kind);
}
}
return atoms;
}
function partToMF(part, options = {}) {
let mf = [];
for (let line of part) {
switch (line.kind) {
case Kind.ISOTOPE:
if (line.multiplier !== 0) {
mf.push(
`[${line.value.isotope}${line.value.atom}]${
line.multiplier !== 1 ? line.multiplier : ''
}`,
);
}
break;
case Kind.ISOTOPE_RATIO:
if (line.multiplier !== 0) {
mf.push(
`${line.value.atom}{${line.value.ratio.join(',')}}${
line.multiplier !== 1 ? line.multiplier : ''
}`,
);
}
break;
case Kind.ATOM:
if (line.multiplier !== 0) {
mf.push(line.value + (line.multiplier !== 1 ? line.multiplier : ''));
}
break;
case Kind.CHARGE:
if (line.value === 0 || options.neutral) break;
mf.push(`(${line.value > 0 ? `+${line.value}` : line.value})`);
break;
}
}
return mf.join('');
}
/**
*
* @param {*} parts
* @param {*} [options={}]
*/
function getInfo(parts, options = {}) {
let {
customUnsaturations = {},
emFieldName = 'monoisotopicMass',
msemFieldName = 'observedMonoisotopicMass',
} = options;
if (parts.length === 0) return {};
if (parts.length === 1) {
return getProcessedPart$1(parts[0], {
customUnsaturations,
emFieldName,
msemFieldName,
});
}
let result = { parts: [] };
for (let part of parts) {
result.parts.push(
getProcessedPart$1(part, {
customUnsaturations,
emFieldName,
msemFieldName,
}),
);
}
result[emFieldName] = 0;
result.mass = 0;
result.charge = 0;
result.unsaturation = 0;
result.atoms = {};
result.mf = result.parts.map((a) => a.mf).join('.');
for (const part of result.parts) {
result.mass += part.mass;
result[emFieldName] += part[emFieldName];
result.charge += part.charge;
result.unsaturation += part.unsaturation;
for (const atom in part.atoms) {
if (!result.atoms[atom]) {
result.atoms[atom] = 0;
}
result.atoms[atom] += part.atoms[atom];
}
}
return result;
}
function getProcessedPart$1(part, options) {
let { customUnsaturations, emFieldName, msemFieldName } = options;
let currentPart = {
mass: 0,
charge: 0,
mf: '',
atoms: partToAtoms(part),
};
currentPart[emFieldName] = 0;
let unsaturation = 0;
let validUnsaturation = true;
currentPart.mf = partToMF(part);
for (let line of part) {
let currentElement = '';
switch (line.kind) {
case Kind.ATOM: {
currentElement = line.value;
let element = chemicalElements.elementsAndIsotopesObject[line.value];
// todo should we have a kind GROUP ?
if (!element) {
element = chemicalGroups.groupsObject[line.value];
if (!element) throw Error(`Unknown element: ${line.value}`);
if (!customUnsaturations[line.value]) {
customUnsaturations[line.value] = element.unsaturation;
}
}
if (!element) throw new Error(`Unknown element: ${line.value}`);
currentPart[emFieldName] += element.monoisotopicMass * line.multiplier;
currentPart.mass += element.mass * line.multiplier;
break;
}
case Kind.ISOTOPE: {
currentElement = line.value.atom;
let isotope = chemicalElements.isotopesObject[line.value.isotope + line.value.atom];
if (!isotope) {
throw new Error(
`Unknown isotope: ${line.value.isotope}${line.value.atom}`,
);
}
currentPart[emFieldName] += isotope.mass * line.multiplier;
currentPart.mass += isotope.mass * line.multiplier;
break;
}
case Kind.ISOTOPE_RATIO: {
currentElement = line.value.atom;
let isotopeRatioInfo = getIsotopeRatioInfo(line.value);
currentPart[emFieldName] +=
isotopeRatioInfo[emFieldName] * line.multiplier;
currentPart.mass += isotopeRatioInfo.mass * line.multiplier;
break;
}
case Kind.CHARGE:
currentPart.charge = line.value;
if (validUnsaturation) {
unsaturation -= line.value;
}
break;
default:
throw new Error('Unimplemented Kind in getInfo', line.kind);
}
if (currentElement) {
if (customUnsaturations[currentElement] !== undefined) {
unsaturation += customUnsaturations[currentElement] * line.multiplier;
} else if (chemicalElements.unsaturationsObject[currentElement] !== undefined) {
unsaturation += chemicalElements.unsaturationsObject[currentElement] * line.multiplier;
} else {
validUnsaturation = false;
}
}
}
// need to calculate the observedMonoisotopicMass
if (currentPart.charge) {
currentPart[msemFieldName] =
(currentPart[emFieldName] - currentPart.charge * chemicalElements.ELECTRON_MASS) /
Math.abs(currentPart.charge);
}
if (validUnsaturation) {
currentPart.unsaturation = unsaturation / 2 + 1;
}
return currentPart;
}
/**
*
* @param {*} parts
* @param {*} options
*/
function getIsotopesInfo(parts) {
if (parts.length === 0) return [];
if (parts.length > 1) {
throw new Error('getIsotopesInfo can not be applied on multipart MF');
}
return getProcessedPart(parts[0]);
}
function getProcessedPart(part) {
let result = {
charge: 0,
isotopes: [],
};
for (let line of part) {
switch (line.kind) {
case Kind.ISOTOPE: {
let isotope = chemicalElements.isotopesObject[line.value.isotope + line.value.atom];
if (!isotope) {
throw Error('unknown isotope:', line.value.atom, line.value.isotope);
}
result.isotopes.push({
atom: `[${line.value.isotope}${line.value.atom}]`,
number: line.multiplier,
distribution: [{ x: isotope.mass, y: 1 }],
});
break;
}
case Kind.ISOTOPE_RATIO:
{
let element = chemicalElements.elementsAndStableIsotopesObject[line.value.atom];
if (!element) throw new Error('unknown element:', line.value);
let distribution = getDistribution(
element.isotopes,
line.value.ratio,
);
result.isotopes.push({
atom: `${line.value.atom}{${line.value.ratio.join(',')}}`,
number: line.multiplier,
distribution,
});
}
break;
case Kind.ATOM: {
let element = chemicalElements.elementsAndStableIsotopesObject[line.value];
if (!element) throw new Error('unknown element:', line.value);
result.isotopes.push({
atom: line.value,
number: line.multiplier,
distribution: element.isotopes.map((e) => ({
x: e.mass,
y: e.abundance,
})),
});
break;
}
case Kind.CHARGE:
result.charge += line.value;
break;
default:
throw new Error('partToMF unhandled Kind: ', line.kind);
}
}
return result;
}
function getDistribution(isotopesArray, ratio) {
let ratios = normalize(ratio);
let result = [];
if (ratios.length > isotopesArray.length) {
throw new Error(
`the number of specified ratios is bigger that the number of stable isotopes: ${chemicalElements.isotopesObject}`,
);
}
for (let i = 0; i < ratios.length; i++) {
result.push({
x: isotopesArray[i].mass,
y: ratios[i],
});
}
return result;
}
function normalize(array) {
let sum = array.reduce((prev, current) => prev + current, 0);
return array.map((a) => a / sum);
}
/**
* Converts an array of mf elements to an array of formatting information
* @param {Array<Object>} result of the parse method
*/
function partsToDisplay(parts) {
let lines = [];
for (let part of parts) {
if (lines.length > 0) lines.push({ kind: Kind.SALT, value: '•' });
for (let partLine of part) {
lines.push(partLine);
if (partLine.multiplier) {
lines.push({
kind: Kind.MULTIPLIER,
value: partLine.multiplier,
});
}
}
}
return toDisplay(lines);
}
function partsToMF(parts, options) {
let mf = [];
for (let part of parts) {
mf.push(partToMF(part, options));
}
return mf.join(' . ');
}
/**
*
* @param {*} lines
* @param {object} [options={}]
* @param {boolean} [options.expand=true] - Should we expand the groupsObject
*/
function toParts(lines, options = {}) {
const { expand: shouldExpandgroupsObject = true } = options;
let parts = [];
let currentPart = createNewPart();
let previousKind = Kind.BEGIN;
parts.push(currentPart);
for (let line of lines) {
switch (line.kind) {
case Kind.ATOM:
case Kind.ISOTOPE_RATIO:
case Kind.ISOTOPE:
case Kind.CHARGE:
currentPart.lines.push({ ...line, multiplier: 1 });
break;
case Kind.OPENING_PARENTHESIS:
openingParenthesis(currentPart);
break;
case Kind.CLOSING_PARENTHESIS:
closingParenthesis(currentPart);
break;
case Kind.PRE_MULTIPLIER:
preMultiplier(currentPart, line);
break;
case Kind.MULTIPLIER:
postMultiplier(currentPart, line.value, previousKind);
break;
case Kind.SALT:
globalPartMultiplier(currentPart);
currentPart = createNewPart();
parts.push(currentPart);
break;
case Kind.ANCHOR: // we ignore anchors to create the parts and canonized MF
break;
case Kind.COMMENT: // we ignore comments to create the parts and canonized MF
break;
case Kind.TEXT:
break;
default:
throw new Error(`Can not process mf having: ${line.kind}`);
}
previousKind = line.kind;
}
globalPartMultiplier(currentPart);
if (shouldExpandgroupsObject) expandgroupsObject(parts);
return combineAtomsIsotopesCharges(parts);
}
function createNewPart() {
let currentMultiplier = { value: 1, fromIndex: 0 };
return { lines: [], multipliers: [currentMultiplier], currentMultiplier };
}
function openingParenthesis(currentPart) {
currentPart.currentMultiplier = {
value: 1,
fromIndex: currentPart.lines.length,
};
currentPart.multipliers.push(currentPart.currentMultiplier);
}
function closingParenthesis(currentPart) {
currentPart.currentMultiplier = currentPart.multipliers.pop();
if (currentPart.currentMultiplier !== 1) {
for (
let i = currentPart.currentMultiplier.fromIndex;
i < currentPart.lines.length;
i++
) {
currentPart.lines[i].multiplier *= currentPart.currentMultiplier.value;
}
}
}
function preMultiplier(currentPart, line) {
currentPart.currentMultiplier.value *= line.value;
}
function globalPartMultiplier(currentPart) {
for (
let i = currentPart.multipliers[0].fromIndex;
i < currentPart.lines.length;
i++
) {
currentPart.lines[i].multiplier *= currentPart.multipliers[0].value;
}
}
function postMultiplier(currentPart, value, previousKind) {
if (previousKind === Kind.CLOSING_PARENTHESIS) {
// need to apply to everything till the previous parenthesis
for (
let i = currentPart.currentMultiplier.fromIndex;
i < currentPart.lines.length;
i++
) {
currentPart.lines[i].multiplier *= value;
}
} else {
// just applies to the previous element
currentPart.lines[currentPart.lines.length - 1].multiplier *= value;
}
}
function expandgroupsObject(parts) {
for (let part of parts) {
let expanded = false;
for (let i = 0; i < part.lines.length; i++) {
let line = part.lines[i];
if (line.kind === Kind.ATOM) {
let group = chemicalGroups.groupsObject[line.value];
if (group) {
expanded = true;
for (let element of group.elements) {
if (element.isotope) {
part.lines.push({
kind: 'isotope',
value: { atom: element.symbol, isotope: element.isotope },
multiplier: line.multiplier * element.number,
});
} else {
part.lines.push({
kind: 'atom',
value: element.symbol,
multiplier: line.multiplier * element.number,
});
}
}
part.lines[i] = undefined;
}
}
}
if (expanded) part.lines = part.lines.filter((a) => a);
}
}
function combineAtomsIsotopesCharges(parts) {
let results = [];
for (let part of parts) {
let result = [];
results.push(result);
calculateAndSortKeys(part);
let currentKey = '';
for (let key of part.keys) {
if (key.key === Kind.CHARGE) {
if (currentKey !== key.key) {
result.push({
kind: Kind.CHARGE,
value: key.value.value * key.value.multiplier,
});
} else {
result[result.length - 1].value +=
key.value.value * key.value.multiplier;
}
} else if (currentKey !== key.key) {
result.push(key.value);
} else {
result[result.length - 1].multiplier += key.value.multiplier;
}
currentKey = key.key;
}
result.sort((a, b) => {
if (a.kind === Kind.CHARGE) return 1;
if (b.kind === Kind.CHARGE) return -1;
let atomA = a.kind === Kind.ATOM ? a.value : a.value.atom;
let atomB = b.kind === Kind.ATOM ? b.value : b.value.atom;
if (atomA !== atomB) return atomSorter.atomSorter(atomA, atomB);
// same atome but some isotopes ...
if (a.kind === Kind.ATOM) return -1;
if (b.kind === Kind.ATOM) return 1;
if (a.kind === Kind.ISOTOPE) return -1;
if (b.kind === Kind.ISOTOPE) return 1;
if (a.kind === Kind.ISOTOPE_RATIO) return -1;
if (b.kind === Kind.ISOTOPE_RATIO) return 1;
return 0;
});
}
return results;
}
function calculateAndSortKeys(part) {
part.keys = [];
for (let line of part.lines) {
part.keys.push({ key: getKey(line), value: line });
}
part.keys.sort((a, b) => stringComparator(a.key, b.key));
}
function getKey(line) {
let key = [line.kind];
switch (line.kind) {
case Kind.CHARGE:
break;
default:
if (typeof line.value === 'string') {
key.push(line.value);
} else {
for (let prop of Object.keys(line.value).sort()) {
key.push(line.value[prop]);
}
}
}
return key.join('-');
}
function stringComparator(a, b) {
if (a < b) return -1;
if (a > b) return 1;
return 0;
}
function toText(lines) {
let text = [];
for (let line of lines) {
switch (line.kind) {
case Format.SUBSCRIPT:
{
const value = String(line.value);
for (let i = 0; i < value.length; i++) {
const char = value[i];
if (subscript[char]) {
text.push(subscript[char]);
} else {
throw new Error(`Subscript problem with: ${char}`);
}
}
}
break;
case Format.SUPERSCRIPT: {
const value = String(line.value);
for (let i = 0; i < value.length; i++) {
const char = value[i];
if (superscript[char]) {
text.push(superscript[char]);
} else {
throw new Error(`Superscript problem with: ${char}`);
}
}
break;
}
case Format.SUPERIMPOSE: {
const under = String(line.under);
for (let i = 0; i < under.length; i++) {
const char = under[i];
if (subscript[char]) {
text.push(subscript[char]);
} else {
throw new Error(`Subscript problem with: ${char}`);
}
}
const over = String(line.over);
for (let i = 0; i < over.length; i++) {
const char = over[i];
if (superscript[char]) {
text.push(superscript[char]);
} else {
throw new Error(`Superscript problem with: ${char}`);
}
}
break;
}
default:
text.push(line.value);
}
}
return text.join('');
}
/**
* Class allowing to deal with molecular formula and derived information
*/
class MF {
constructor(mf, options = {}) {
if (options.ensureCase) {
mf = ensureCase(mf);
}
this.parsed = parse(mf);
this.cache = {};
}
toDisplay() {
if (!this.cache.displayed) this.cache.displayed = toDisplay(this.parsed);
return this.cache.displayed;
}
toHtml() {
if (!this.cache.html) {
this.toDisplay();
this.cache.html = toHtml(this.cache.displayed);
}
return this.cache.html;
}
toText() {
if (!this.cache.text) {
this.toDisplay();
this.cache.text = toText(this.cache.displayed);
}
return this.cache.text;
}
toCanonicText() {
if (!this.cache.canonicText) {
this.cache.canonicText = new MF(this.toMF()).toText(this.cache.displayed);
}
return this.cache.canonicText;
}
toParts(options) {
if (!this.cache.parts) {
this.cache.parts = toParts(this.parsed, options);
}
return this.cache.parts;
}
/**
* Returns an object with the global MF, global charge, monoisotopic mass and mass
* as well as the same information for all the parts
* @param {object} [options={}] options
* @param {object} [options.customUnsaturations={}] custom unsaturations
* @param {string} [options.emFieldName='monoisotopicMass'] name of the monoisotopic mass field
* @param {string} [options.msemFieldName='observedMonoisotopicMass'] name of the observed monoisotopic mass field
*/
getInfo(options = {}) {
if (!this.cache.info) {
this.toParts();
this.cache.info = getInfo(this.cache.parts, options);
}
return this.cache.info;
}
/**
* Returns an object with the elemental analysis
*/
getEA(options = {}) {
if (!this.cache.ea) {
this.toParts();
this.cache.ea = getEA(this.cache.parts);
}
return this.cache.ea;
}
/**
* Get the different elements for each part
* @returns an array
*/
getElements() {
if (!this.cache.elements) {
this.toParts();
this.cache.elements = getElements(this.cache.parts);
}
return this.cache.elements;
}
/**
* Returns an array with each atom and isotopic composition
*/
getIsotopesInfo(options = {}) {
if (!this.cache.isotopesInfo) {
this.toParts();
this.cache.isotopesInfo = getIsotopesInfo(this.cache.parts);
}
return this.cache.isotopesInfo;
}
/**
* Get a canonized MF
*/
toMF() {
if (!this.cache.mf) {
this.toParts();
this.cache.mf = partsToMF(this.cache.parts);
}
return this.cache.mf;
}
/**
* Get a canonized MF
*/
toNeutralMF() {
if (!this.cache.neutralMF) {
this.toParts();
this.cache.neutralMF = partsToMF(this.cache.parts, { neutral: true });
}
return this.cache.neutralMF;
}
canonize() {
this.toParts();
this.cache.displayed = partsToDisplay(this.cache.parts);
this.cache.html = undefined;
}
flatten(options) {
return flatten(this.parsed, options);
}
}
/**
* Parse a molecular formula and converts it to an HTML code
* @param {String} mf String containing the molecular formula
*/
function parseToHtml(mf) {
let parsed = parse(mf);
let display = toDisplay(parsed);
return toHtml(display);
}
exports.Format = Format;
exports.Kind = Kind;
exports.MF = MF;
exports.Style = Style;
exports.ensureCase = ensureCase;
exports.isMF = isMF;
exports.parse = parse;
exports.parseToHtml = parseToHtml;
exports.subscript = subscript;
exports.superscript = superscript;
exports.toDisplay = toDisplay;
exports.toHtml = toHtml;
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./parse.js"), exports);
__exportStar(require("./util/subSuperscript.js"), exports);
__exportStar(require("./util/toDisplay.js"), exports);
__exportStar(require("./util/isMF.js"), exports);
__exportStar(require("./util/toHtml.js"), exports);
__exportStar(require("./Kind.js"), exports);
__exportStar(require("./Format.js"), exports);
__exportStar(require("./Style.js"), exports);
__exportStar(require("./ensureCase.js"), exports);
__exportStar(require("./MF.js"), exports);
__exportStar(require("./parseToHtml.js"), exports);
{
"name": "mf-parser",
"version": "3.1.1",
"version": "3.2.0",
"description": "Parse a molecular formula",

@@ -23,6 +23,6 @@ "main": "lib/index.js",

"dependencies": {
"atom-sorter": "^2.0.1",
"chemical-elements": "^2.0.4",
"chemical-groups": "^2.1.1",
"mf-utilities": "^3.1.1"
"atom-sorter": "^2.1.0",
"chemical-elements": "^2.1.0",
"chemical-groups": "^2.2.0",
"mf-utilities": "^3.2.0"
},

@@ -32,3 +32,3 @@ "devDependencies": {

},
"gitHead": "b9f99ec6f05ce6b0034d35f4ae0452ae653f90c2"
"gitHead": "28dae91d3b42556a23097ee08acfe4061f276ed0"
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc