Skip to content

Commit 9e22a8e

Browse files
Enhanced chemical formula formatting (#1445)
1 parent c588dcb commit 9e22a8e

File tree

2 files changed

+90
-1
lines changed

2 files changed

+90
-1
lines changed

webapp/cypress/component/ChemicalFormulaTest.cy.jsx

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,73 @@ describe("ChemicalFormula", () => {
113113
cy.mount(ChemicalFormula, { props: { formula: "Δ-MnO2" } });
114114
cy.get("span").should("contain.html", "Δ-MnO<sub>2</sub>");
115115
});
116+
117+
it("handles fractional subscripts correctly", () => {
118+
cy.mount(ChemicalFormula, { props: { formula: "LiNi1/3Mn1/3Co1/3O2" } });
119+
cy.get("span").should(
120+
"contain.html",
121+
"LiNi<sub>1/3</sub>Mn<sub>1/3</sub>Co<sub>1/3</sub>O<sub>2</sub>",
122+
);
123+
});
124+
125+
it("handles mixture notation with slashes correctly", () => {
126+
cy.mount(ChemicalFormula, { props: { formula: "NMC/C" } });
127+
cy.get("span").should("contain.html", "NMC/C");
128+
});
129+
130+
it("handles complex mixture with formula and slash", () => {
131+
cy.mount(ChemicalFormula, { props: { formula: "Li2O/graphite" } });
132+
cy.get("span").should("contain.html", "Li<sub>2</sub>O/graphite");
133+
});
134+
135+
it("preserves explicit subscript tags", () => {
136+
cy.mount(ChemicalFormula, { props: { formula: "Na<sub>x</sub>CoO2" } });
137+
cy.get("span").should("contain.html", "Na<sub>x</sub>CoO<sub>2</sub>");
138+
});
139+
140+
it("preserves explicit superscript tags", () => {
141+
cy.mount(ChemicalFormula, { props: { formula: "Na<sup>+</sup>Cl<sup>-</sup>" } });
142+
cy.get("span").should("contain.html", "Na<sup>+</sup>Cl<sup>-</sup>");
143+
});
144+
145+
it("handles mixed explicit tags and auto-formatting", () => {
146+
cy.mount(ChemicalFormula, { props: { formula: "Li<sub>1-x</sub>Ni0.8Co0.2O2" } });
147+
cy.get("span").should(
148+
"contain.html",
149+
"Li<sub>1-x</sub>Ni<sub>0.8</sub>Co<sub>0.2</sub>O<sub>2</sub>",
150+
);
151+
});
152+
153+
it("handles fractions with multiple elements correctly", () => {
154+
cy.mount(ChemicalFormula, { props: { formula: "Li1/2Mn1/2O2" } });
155+
cy.get("span").should("contain.html", "Li<sub>1/2</sub>Mn<sub>1/2</sub>O<sub>2</sub>");
156+
});
157+
158+
it("handles mixed fractions and decimals", () => {
159+
cy.mount(ChemicalFormula, { props: { formula: "LiNi1/3Co0.1Mn0.1O2" } });
160+
cy.get("span").should(
161+
"contain.html",
162+
"LiNi<sub>1/3</sub>Co<sub>0.1</sub>Mn<sub>0.1</sub>O<sub>2</sub>",
163+
);
164+
});
165+
166+
it("handles slash at start for phase notation", () => {
167+
cy.mount(ChemicalFormula, { props: { formula: "α-NMC/C/binder" } });
168+
cy.get("span").should("contain.html", "α-NMC/C/binder");
169+
});
170+
171+
it("handles complex formula with all features", () => {
172+
cy.mount(ChemicalFormula, {
173+
props: { formula: "Li<sub>1-x</sub>Ni1/3Mn1/3Co1/3O2/graphite" },
174+
});
175+
cy.get("span").should(
176+
"contain.html",
177+
"Li<sub>1-x</sub>Ni<sub>1/3</sub>Mn<sub>1/3</sub>Co<sub>1/3</sub>O<sub>2</sub>/graphite",
178+
);
179+
});
180+
181+
it("preserves multiple explicit tags in sequence", () => {
182+
cy.mount(ChemicalFormula, { props: { formula: "Ca<sup>2+</sup>(OH)<sub>2</sub>" } });
183+
cy.get("span").should("contain.html", "Ca<sup>2+</sup>(OH)<sub>2</sub>");
184+
});
116185
});

webapp/src/components/ChemicalFormula.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default {
3838
3939
// Create a regex that matches either element symbols or sequences of digits/periods
4040
const validFormulaRegex = new RegExp(
41-
`^[A-Za-z0-9.+x()\\[\\]\\s${greekLetters.replace(/\|/g, "")}${specialChars.replace(
41+
`^[A-Za-z0-9.+x()\\[\\]\\/\\s<>${greekLetters.replace(/\|/g, "")}${specialChars.replace(
4242
/\|/g,
4343
"",
4444
)}-]+$`,
@@ -53,6 +53,15 @@ export default {
5353
5454
let formatted = this.formula;
5555
56+
const tagPlaceholders = [];
57+
let tagIndex = 0;
58+
formatted = formatted.replace(/<(sub|sup)>(.*?)<\/\1>/g, (match) => {
59+
const placeholder = `__TAG_${tagIndex}__`;
60+
tagPlaceholders.push({ placeholder, content: match });
61+
tagIndex++;
62+
return placeholder;
63+
});
64+
5665
formatted = formatted.replace(/\.(?=\s*[A-Z([∙•])/g, " · ");
5766
5867
formatted = formatted.replace(/[∙•]/g, " · ");
@@ -61,6 +70,13 @@ export default {
6170
return `<span data-bracket>${content}</span><span data-number>${number}</span>`;
6271
});
6372
73+
formatted = formatted.replace(
74+
new RegExp(`(${elementSymbols})(\\d+/\\d+)`, "g"),
75+
(match, element, fraction) => {
76+
return `${element}<sub>${fraction}</sub>`;
77+
},
78+
);
79+
6480
formatted = formatted.replace(/([A-Z][a-z]?)(\d+)([+-])(?=\s|$|[A-Z])/g, "$1<sup>$2$3</sup>");
6581
6682
formatted = formatted.replace(/([A-Z][a-z]?)([+-])(?=\s|$|[A-Z])/g, "$1<sup>$2</sup>");
@@ -82,6 +98,10 @@ export default {
8298
"[$1]<sub>$2</sub>",
8399
);
84100
101+
tagPlaceholders.forEach(({ placeholder, content }) => {
102+
formatted = formatted.replace(placeholder, content);
103+
});
104+
85105
return formatted;
86106
},
87107
},

0 commit comments

Comments
 (0)