Skip to content

Commit 77cf56e

Browse files
authored
fix: mjml format issue (#1658)
* fix: mjml format issue * chore: add changeset
1 parent d744592 commit 77cf56e

File tree

3 files changed

+156
-4
lines changed

3 files changed

+156
-4
lines changed

.changeset/swift-times-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"lingo.dev": patch
3+
---
4+
5+
fix mjml format issue

packages/cli/src/cli/loaders/mjml.spec.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,4 +697,140 @@ describe("mjml loader", () => {
697697
expect(output).toMatch(/Comience con (<\/span>)?<strong>/);
698698
expect(output).not.toContain("Comience con<strong>");
699699
});
700+
701+
test("should handle empty input string in pull", async () => {
702+
const loader = createMjmlLoader();
703+
loader.setDefaultLocale("en");
704+
705+
const result = await loader.pull("en", "");
706+
707+
expect(result).toEqual({});
708+
});
709+
710+
test("should handle whitespace-only input in pull", async () => {
711+
const loader = createMjmlLoader();
712+
loader.setDefaultLocale("en");
713+
714+
const result = await loader.pull("en", " \n\t ");
715+
716+
expect(result).toEqual({});
717+
});
718+
719+
test("should handle empty input string in push", async () => {
720+
const loader = createMjmlLoader();
721+
loader.setDefaultLocale("en");
722+
723+
// Need to pull first to initialize state
724+
await loader.pull("en", "");
725+
const output = await loader.push("es", {}, "");
726+
727+
expect(output).toBe("");
728+
});
729+
730+
test("should handle whitespace-only input in push", async () => {
731+
const loader = createMjmlLoader();
732+
loader.setDefaultLocale("en");
733+
734+
// Need to pull first to initialize state
735+
await loader.pull("en", " \n\t ");
736+
const output = await loader.push("es", {}, " \n\t ");
737+
738+
expect(output).toBe(" \n\t ");
739+
});
740+
741+
test("should not add XML declaration to output", async () => {
742+
const loader = createMjmlLoader();
743+
loader.setDefaultLocale("en");
744+
745+
const inputWithoutDeclaration = `<mjml>
746+
<mj-body>
747+
<mj-section>
748+
<mj-column>
749+
<mj-text>Hello World</mj-text>
750+
</mj-column>
751+
</mj-section>
752+
</mj-body>
753+
</mjml>`;
754+
755+
await loader.pull("en", inputWithoutDeclaration);
756+
757+
const translations = {
758+
"mjml/mj-body/0/mj-section/0/mj-column/0/mj-text/0": "Hola Mundo",
759+
};
760+
761+
const output = await loader.push("es", translations, inputWithoutDeclaration);
762+
763+
// Should NOT start with XML declaration
764+
expect(output).not.toMatch(/^<\?xml/);
765+
// Should start with <mjml>
766+
expect(output.trim()).toMatch(/^<mjml>/);
767+
expect(output).toContain("Hola Mundo");
768+
});
769+
770+
test("should handle input with XML declaration and not duplicate it", async () => {
771+
const loader = createMjmlLoader();
772+
loader.setDefaultLocale("en");
773+
774+
const inputWithDeclaration = `<?xml version="1.0" encoding="UTF-8"?>
775+
<mjml>
776+
<mj-body>
777+
<mj-section>
778+
<mj-column>
779+
<mj-text>Hello World</mj-text>
780+
</mj-column>
781+
</mj-section>
782+
</mj-body>
783+
</mjml>`;
784+
785+
await loader.pull("en", inputWithDeclaration);
786+
787+
const translations = {
788+
"mjml/mj-body/0/mj-section/0/mj-column/0/mj-text/0": "Hola Mundo",
789+
};
790+
791+
const output = await loader.push("es", translations, inputWithDeclaration);
792+
793+
// Should not duplicate XML declaration
794+
const declarationMatches = output.match(/<\?xml/g);
795+
expect(declarationMatches).toBeNull(); // No XML declaration in output
796+
expect(output).toContain("Hola Mundo");
797+
});
798+
799+
test("should match structure of source file without XML declaration", async () => {
800+
const loader = createMjmlLoader();
801+
loader.setDefaultLocale("en");
802+
803+
// Source file format (en-US)
804+
const sourceInput = `<mjml>
805+
<mj-body>
806+
<mj-section padding="26px 0px 86px 45px">
807+
<mj-column>
808+
<mj-image
809+
src="cid:logo.png"
810+
alt="GitProtect logo"
811+
width="140px"
812+
height="35px"
813+
padding="0"
814+
align="left"
815+
/>
816+
</mj-column>
817+
</mj-section>
818+
</mj-body>
819+
</mjml>`;
820+
821+
await loader.pull("en", sourceInput);
822+
823+
const translations = {
824+
"mjml/mj-body/0/mj-section/0/mj-column/0/mj-image/0#alt": "Logo de GitProtect",
825+
};
826+
827+
const output = await loader.push("es", translations, sourceInput);
828+
829+
// Generated file should match source structure
830+
expect(output.trim()).toMatch(/^<mjml>/);
831+
expect(output).not.toMatch(/^<\?xml/);
832+
expect(output).toContain("Logo de GitProtect");
833+
expect(output).toContain("<mjml>");
834+
expect(output).toContain("</mjml>");
835+
});
700836
});

packages/cli/src/cli/loaders/mjml.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ export default function createMjmlLoader(): ILoader<
3434
async pull(locale, input) {
3535
const result: Record<string, any> = {};
3636

37+
// Handle empty input
38+
if (!input || input.trim() === "") {
39+
return result;
40+
}
41+
3742
try {
3843
const parsed = await parseStringPromise(input, {
3944
explicitArray: true,
@@ -88,8 +93,13 @@ export default function createMjmlLoader(): ILoader<
8893
},
8994

9095
async push(locale, data, originalInput) {
96+
// Handle empty input
97+
if (!originalInput || originalInput.trim() === "") {
98+
return originalInput || "";
99+
}
100+
91101
try {
92-
const parsed = await parseStringPromise(originalInput || "", {
102+
const parsed = await parseStringPromise(originalInput, {
93103
explicitArray: true,
94104
explicitChildren: true,
95105
preserveChildrenOrder: true,
@@ -275,13 +285,14 @@ function convertDomToXmlNode(domNode: any): any {
275285
}
276286

277287
function serializeMjml(parsed: any): string {
278-
const xmlDec = '<?xml version="1.0" encoding="UTF-8"?>\n';
279-
280288
const rootKey = Object.keys(parsed).find(key => !key.startsWith("_") && !key.startsWith("$"));
281289
const rootNode = rootKey ? parsed[rootKey] : parsed;
282290

283291
const body = serializeElement(rootNode);
284-
return xmlDec + body;
292+
293+
// Don't add XML declaration - xml2js already preserves it in the parsed object
294+
// or it will be added by the consumer if needed
295+
return body;
285296
}
286297

287298
function serializeElement(node: any, indent: string = ""): string {

0 commit comments

Comments
 (0)