Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 112 additions & 34 deletions webapp/cypress/component/ChemicalFormulaTest.cy.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,116 @@
import ChemFormulaInput from "@/components/ChemFormulaInput.vue";
import ChemicalFormula from "@/components/ChemicalFormula.vue";

describe("ChemFormulaInput", () => {
beforeEach(() => {
cy.mount(ChemFormulaInput);
cy.get("span").click({ force: true });
describe("ChemicalFormula", () => {
it("renders single element formula correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Na" } });
cy.get("span").should("contain", "Na");
});

it("renders single element formula correctly", () => {
cy.get("input").type("Na");
cy.get("input").should("have.value", "Na");
});

// it("renders single element with subscript correctly", () => {
// cy.get("input").type("Na3");
// cy.get("input").should("have.value", "Na<sub>3</sub>P");
// });

// it("renders formula with parentheses correctly", () => {
// cy.get("input").type("Na3P");
// cy.get("input").should("have.value", "Na<sub>3</sub>P");
// });

// it("renders formula with multiple elements in parentheses correctly", () => {
// cy.get("input").type("(NaLi)3P");
// cy.get("input").should("have.value", "(NaLi)<sub>3</sub>P");
// });

// it("renders formula with multiple elements and subscripts correctly", () => {
// cy.get("input").type("Na3P4");
// cy.get("input").should("have.value", "Na<sub>3</sub>P<sub>4</sub>");
// });

// it("handles invalid input gracefully", () => {
// cy.get("input").type("Invalid@Formula");
// cy.get("input").should("have.value", "InFo");
// });
it("renders single element with subscript correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Na3P" } });
cy.get("span").should("contain.html", "Na<sub>3</sub>P");
});

it("renders formula with multiple elements and subscripts correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Na3P4" } });
cy.get("span").should("contain.html", "Na<sub>3</sub>P<sub>4</sub>");
});

it("renders formula with parentheses correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Ca(OH)2" } });
cy.get("span").should("contain.html", "Ca(OH)<sub>2</sub>");
});

it("renders formula with multiple elements in parentheses correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "(NaLi)3P" } });
cy.get("span").should("contain.html", "(NaLi)<sub>3</sub>P");
});

it("renders hydrate formula with interpunct correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Cu2SO4.H2O" } });
cy.get("span").should("contain.html", "Cu<sub>2</sub>SO<sub>4</sub> · H<sub>2</sub>O");
});

it("renders formula with variables correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Na3+xP" } });
cy.get("span").should("contain.html", "Na<sub>3+x</sub>P");
});

it("renders charged ions correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Na+" } });
cy.get("span").should("contain.html", "Na<sup>+</sup>");
});

it("renders negatively charged ions correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Cl-" } });
cy.get("span").should("contain.html", "Cl<sup>-</sup>");
});

it("renders formula with multiple charges correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Ca2+" } });
cy.get("span").should("contain.html", "Ca<sup>2+</sup>");
});

it("handles empirical shorthand units correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "[pyr]" } });
cy.get("span").should("contain.html", "[pyr]");
});

it("handles complex formula with empirical units", () => {
cy.mount(ChemicalFormula, { props: { formula: "Li[pyr]3" } });
cy.get("span").should("contain.html", "Li[pyr]<sub>3</sub>");
});

it("handles invalid input gracefully", () => {
cy.mount(ChemicalFormula, { props: { formula: "Invalid@Formula" } });
cy.get("span").should("contain.html", "Invalid@Formula");
});

it("handles decimal subscripts correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "LiNi0.8Co0.1Mn0.1O2" } });
cy.get("span").should(
"contain.html",
"LiNi<sub>0.8</sub>Co<sub>0.1</sub>Mn<sub>0.1</sub>O<sub>2</sub>",
);
});

it("handles Greek letter alpha correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Li-α-NMC" } });
cy.get("span").should("contain", "Li-α-NMC");
});

it("handles Greek letter beta in formula correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "β-Li3N" } });
cy.get("span").should("contain.html", "β-Li<sub>3</sub>N");
});

it("handles Greek letter gamma phase correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "γ-Fe2O3" } });
cy.get("span").should("contain.html", "γ-Fe<sub>2</sub>O<sub>3</sub>");
});

it("handles multiple Greek letters correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "α-β-γ-Al2O3" } });
cy.get("span").should("contain.html", "α-β-γ-Al<sub>2</sub>O<sub>3</sub>");
});

it("handles delta phase with subscripts", () => {
cy.mount(ChemicalFormula, { props: { formula: "δ-MnO2" } });
cy.get("span").should("contain.html", "δ-MnO<sub>2</sub>");
});

it("handles prime notation correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Li′" } });
cy.get("span").should("contain", "Li′");
});

it("handles middle dot correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Cu2SO4∙H2O" } });
cy.get("span").should("contain.html", "Cu<sub>2</sub>SO<sub>4</sub> · H<sub>2</sub>O");
});

it("handles uppercase Greek letters correctly", () => {
cy.mount(ChemicalFormula, { props: { formula: "Δ-MnO2" } });
cy.get("span").should("contain.html", "Δ-MnO<sub>2</sub>");
});
});
2 changes: 1 addition & 1 deletion webapp/cypress/e2e/editPage.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ describe("Edit Page", () => {
cy.findByText("editable_sample");
cy.findByText("This is a sample name");
cy.findByText("1990-01-07");
cy.findByText("NaCoO2"); // sorta check the formula
cy.get("body").should("contain.html", "NaCoO<sub>2</sub>");
});

it("adds a chemical formula to component1", () => {
Expand Down
87 changes: 72 additions & 15 deletions webapp/src/components/ChemicalFormula.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
<template>
<span>
{{ chemFormulaFormat }}
<!--
<span v-for="(match, index) in chemFormulaFormat" :key="index">
{{ match[1] }}<sub v-if="match[2]">{{ match[2] }}</sub>
</span>
-->
</span>
<span v-html="chemFormulaFormat"></span>
</template>

<script>
Expand All @@ -19,13 +12,77 @@ export default {
},
computed: {
chemFormulaFormat() {
return this.formula;
//if (!this.formula) {
// return " ";
//}
//const re = /([A-Z][a-z]?)(\d+\.?\d*)?/g;
//var all_matches = [...this.formula.matchAll(re)];
//return all_matches;
// Need to capture several groups, if the overall format doesn't apply, then
// there should be no additional formatting whatsoever
//
// Some rules:
//
// * numbers between element symbols need to be subscripted, including "." and variables like "x"
// - e.g., Na3P => Na<sub>3</sub>P, Na3+xP => Na<sub>3+x</sub>P
// * charges need to be handled separately and superscripted
// - e.g., Na+Cl- => Na<sup>+</sup>Cl<sup>-</sup>
// * empirical labels for formula units like [pyr] must be left alone
// * dots, when not used within numbers, must be treated as an interpunct "dot product" style dot
// - e.g., Cu2SO4.H2O => Cu<sub>2</sub>SO<sub>4</sub> · H<sub>2</sub>O
if (!this.formula) {
return this.formula;
}

// From an LLM, needs checking...
const elementSymbols =
"H|He|Li|Be|B|C|N|O|F|Ne|Na|Mg|Al|Si|P|S|Cl|Ar|K|Ca|Sc|Ti|V|Cr|Mn|Fe|Co|Ni|Cu|Zn|Ga|Ge|As|Se|Br|Kr|Rb|Sr|Y|Zr|Nb|Mo|Tc|Ru|Rh|Pd|Ag|Cd|In|Sn|Sb|Te|I|Xe|Cs|Ba|La|Ce|Pr|Nd|Pm|Sm|Eu|Gd|Tb|Dy|Ho|Er|Tm|Yb|Lu|Hf|Ta|W|Re|Os|Ir|Pt|Au|Hg|Tl|Pb|Bi|Po|At|Rn|Fr|Ra|Ac|Th|Pa|U|Np|Pu|Am|Cm|Bk|Cf|Es|Fm|Md|No|Lr|Rf|Db|Sg|Bh|Hs|Mt|Ds|Rg|Cn|Nh|Fl|Mc|Lv|Ts|Og";

const greekLetters =
"α|β|γ|δ|ε|ζ|η|θ|ι|κ|λ|μ|ν|ξ|ο|π|ρ|σ|τ|υ|φ|χ|ψ|ω|Α|Β|Γ|Δ|Ε|Ζ|Η|Θ|Ι|Κ|Λ|Μ|Ν|Ξ|Ο|Π|Ρ|Σ|Τ|Υ|Φ|Χ|Ψ|Ω";
const specialChars = "′|″|‴|⁰|¹|²|³|⁴|⁵|⁶|⁷|⁸|⁹|₀|₁|₂|₃|₄|₅|₆|₇|₈|₉|∙|•|×|·|∗|∞";

// Create a regex that matches either element symbols or sequences of digits/periods
const validFormulaRegex = new RegExp(
`^[A-Za-z0-9.+x()\\[\\]\\s${greekLetters.replace(/\|/g, "")}${specialChars.replace(
/\|/g,
"",
)}-]+$`,
"u",
);

const isValidFormula = validFormulaRegex.test(this.formula);

if (!isValidFormula) {
return this.formula;
}

let formatted = this.formula;

formatted = formatted.replace(/\.(?=\s*[A-Z([∙•])/g, " · ");

formatted = formatted.replace(/[∙•]/g, " · ");

formatted = formatted.replace(/\[([^\]]+)\](\d+)/g, (match, content, number) => {
return `<span data-bracket>${content}</span><span data-number>${number}</span>`;
});

formatted = formatted.replace(/([A-Z][a-z]?)(\d+)([+-])(?=\s|$|[A-Z])/g, "$1<sup>$2$3</sup>");

formatted = formatted.replace(/([A-Z][a-z]?)([+-])(?=\s|$|[A-Z])/g, "$1<sup>$2</sup>");

formatted = formatted.replace(
new RegExp(`(${elementSymbols})(\\d+\\.?\\d*[+xyzn-]*)`, "g"),
(match, element, number) => {
if (number && !number.match(/^[+-]$/)) {
return `${element}<sub>${number}</sub>`;
}
return match;
},
);

formatted = formatted.replace(/\)(\d+\.?\d*)/g, ")<sub>$1</sub>");

formatted = formatted.replace(
/<span data-bracket>([^<]+)<\/span><span data-number>(\d+)<\/span>/g,
"[$1]<sub>$2</sub>",
);

return formatted;
},
},
};
Expand Down