Skip to content
Merged
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@
"@types/nunjucks": "^3.2.6",
"@types/pug": "^2.0.10",
"@types/underscore": "^1.13.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/coverage-v8": "^4.0.3",
"docula": "^0.30.0",
"rimraf": "^6.0.1",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
"vitest": "^3.2.4",
"vitest": "^4.0.3",
"webpack": "^5.101.3"
},
"files": [
Expand Down
7 changes: 5 additions & 2 deletions src/ecto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,10 @@ export class Ecto extends Hookified {
await this.writeFile(filePathOutput, result);

return result;
/* c8 ignore next 4 */
} catch (error) {
/* v8 ignore next -- @preserve */
this.emit(EctoEvents.error, error);
/* v8 ignore next -- @preserve */
return "";
}
}
Expand Down Expand Up @@ -726,6 +727,7 @@ export class Ecto extends Hookified {
return "handlebars";
}
// Check for basic mustache/handlebars syntax
/* v8 ignore next -- @preserve */
if (source.includes("}}") && !source.includes("{%")) {
return "handlebars";
}
Expand Down Expand Up @@ -818,9 +820,9 @@ export class Ecto extends Hookified {
}

// Check for markdown links and images
/* v8 ignore next -- @preserve */
if (
source.includes("](") &&
/* c8 ignore next */
(source.includes("[") || source.includes("!["))
) {
markdownIndicators++;
Expand All @@ -834,6 +836,7 @@ export class Ecto extends Hookified {
// Determine if it's Markdown
if (markdownIndicators > 0) {
// Make sure it's not mixed with template syntax
/* v8 ignore next -- @preserve */
if (
!source.includes("<%") &&
!source.includes("{{") &&
Expand Down
8 changes: 8 additions & 0 deletions test/base-engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,11 @@ it("BaseEngine - deleteExtension should be 1 with case", () => {
be.deleteExtension("Md ");
expect(be.getExtensions().length).toBe(1);
});

it("BaseEngine - deleteExtension with non-existent extension should remain same length", () => {
const be = new BaseEngine();
be.setExtensions(["md", "markdown"]);
expect(be.getExtensions().length).toBe(2);
be.deleteExtension("html");
expect(be.getExtensions().length).toBe(2);
});
132 changes: 132 additions & 0 deletions test/ecto-detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,4 +501,136 @@ html(lang="en")
expect(ecto.detectEngine(template)).toBe("pug");
});
});

describe("Edge Cases and Additional Coverage", () => {
it("should fallback to default when has <% but incomplete EJS syntax", () => {
// This has <% but not <%=, <%-, or %> - tests the false branch of line 602
// This is an edge case - invalid/incomplete EJS syntax
const template = "Some text with <% only";
// Should fall through to default since it doesn't match complete EJS patterns
expect(ecto.detectEngine(template)).toBe("ejs"); // Falls through to default
});

it("should detect Liquid with filters and {% assign %}", () => {
const template = "{{ name | capitalize }} {% assign foo = 'bar' %}";
expect(ecto.detectEngine(template)).toBe("liquid");
});

it("should detect Liquid with filters and {% capture %}", () => {
const template =
"{{ name | upcase }} {% capture greeting %}Hello{% endcapture %}";
expect(ecto.detectEngine(template)).toBe("liquid");
});

it("should detect Liquid with filters and {% unless %}", () => {
const template =
"{{ price | money }} {% unless sold %}Available{% endunless %}";
expect(ecto.detectEngine(template)).toBe("liquid");
});

it("should detect Handlebars with pipe but not Liquid keywords", () => {
// Has | and }} but not Liquid-specific keywords
const template = "{{ items | length }}";
expect(ecto.detectEngine(template)).toBe("handlebars");
});

it("should detect Handlebars when has }} but not {%", () => {
const template = "Hello {{ name }} and {{ city }}";
expect(ecto.detectEngine(template)).toBe("handlebars");
});

it("should detect Markdown with ordered lists", () => {
const template = `
1. First item
2. Second item
3. Third item
`;
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should detect Markdown with numbered lists (double digits)", () => {
const template = `
10. Tenth item
11. Eleventh item
`;
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should detect Markdown with links", () => {
const template = "[Click here](https://example.com)";
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should detect Markdown with images", () => {
const template = "![Alt text](image.png)";
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should detect Markdown with headers without template syntax", () => {
const template = `
# Heading 1
## Heading 2

Some content here.
`;
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should detect Markdown with blockquotes without template syntax", () => {
const template = `
> This is a quote
> Another line of quote

Regular text.
`;
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should handle edge case with number but not valid ordered list", () => {
// Has a number at start but doesn't form a valid list (no dot and space)
const template = "123 is a number\n456test";
// This should not match markdown ordered list pattern
expect(ecto.detectEngine(template)).toBe("ejs"); // defaults to ejs
});

it("should detect Markdown with unordered lists and avoid template syntax check", () => {
const template = `
# Title

- Item one
- Item two

Some text
`;
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should not detect Handlebars when has }} AND {%", () => {
// Line 730: has }} but also has {% so condition is false
// This tests the AND condition - it has }} but also {% so it doesn't match handlebars
const template = "{{ name }} {% if test %}";
// Should detect as Nunjucks or Liquid (not Handlebars) since it has {%
expect(ecto.detectEngine(template)).not.toBe("handlebars");
});

it("should handle markdown indicators without ]( link syntax", () => {
// Line 823: has markdown but not the ]( pattern
const template = "# Header\n\nSome text without links";
expect(ecto.detectEngine(template)).toBe("markdown");
});

it("should not detect as markdown when has markdown indicators AND template syntax", () => {
// Line 838: has markdown indicators but also template syntax
const template = "# Header\n\n<%= name %>";
// Should detect as EJS, not markdown
expect(ecto.detectEngine(template)).toBe("ejs");
});

it("should not detect as markdown when has markdown with handlebars syntax", () => {
// Line 838: has markdown indicators but also template syntax
const template = "# Header\n\n{{ name }}";
// Should detect as Handlebars, not markdown
expect(ecto.detectEngine(template)).toBe("handlebars");
});
});
});
78 changes: 78 additions & 0 deletions test/ecto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,16 @@ it("Find Template without Extension Sync", () => {
expect(filePath).toBe(`${templatePath}/bar.njk`);
});

it("Find Template without Extension Sync - not found", () => {
const ecto = new Ecto();
const templatePath = `${testRootDirectory}/find-templates`;
const filePath = ecto.findTemplateWithoutExtensionSync(
templatePath,
"nonexistent",
);
expect(filePath).toBe("");
});

it("Find Template without Extension on duplicate Sync", async () => {
const ecto = new Ecto();
const templatePath = `${testRootDirectory}/find-templates`;
Expand All @@ -448,3 +458,71 @@ it("Render with Configuration via Nunjucks", async () => {

expect(source).toContain("Hello <script>alert('XSS')</script>");
});

it("ensureFilePath - creates directory when it doesn't exist", async () => {
const ecto = new Ecto();
const testPath = `${testOutputDirectory}/new-dir/test.txt`;

// Clean up if it exists
if (fs.existsSync(`${testOutputDirectory}/new-dir`)) {
fs.rmSync(`${testOutputDirectory}/new-dir`, { recursive: true });
}

await ecto.ensureFilePath(testPath);
expect(fs.existsSync(`${testOutputDirectory}/new-dir`)).toBe(true);

// Clean up
fs.rmSync(`${testOutputDirectory}/new-dir`, { recursive: true });
});

it("ensureFilePath - does nothing when directory already exists", async () => {
const ecto = new Ecto();
const testPath = `${testOutputDirectory}/existing-dir/test.txt`;

// Ensure directory exists first
if (!fs.existsSync(`${testOutputDirectory}/existing-dir`)) {
fs.mkdirSync(`${testOutputDirectory}/existing-dir`, { recursive: true });
}

// This should not throw and should handle existing directory
await ecto.ensureFilePath(testPath);
expect(fs.existsSync(`${testOutputDirectory}/existing-dir`)).toBe(true);

// Clean up
fs.rmSync(`${testOutputDirectory}/existing-dir`, { recursive: true });
});

it("ensureFilePathSync - creates directory when it doesn't exist", () => {
const ecto = new Ecto();
const testPath = `${testOutputDirectory}/new-dir-sync/test.txt`;

// Clean up if it exists
if (fs.existsSync(`${testOutputDirectory}/new-dir-sync`)) {
fs.rmSync(`${testOutputDirectory}/new-dir-sync`, { recursive: true });
}

ecto.ensureFilePathSync(testPath);
expect(fs.existsSync(`${testOutputDirectory}/new-dir-sync`)).toBe(true);

// Clean up
fs.rmSync(`${testOutputDirectory}/new-dir-sync`, { recursive: true });
});

it("ensureFilePathSync - does nothing when directory already exists", () => {
const ecto = new Ecto();
const testPath = `${testOutputDirectory}/existing-dir-sync/test.txt`;

// Ensure directory exists first
if (!fs.existsSync(`${testOutputDirectory}/existing-dir-sync`)) {
fs.mkdirSync(`${testOutputDirectory}/existing-dir-sync`, {
recursive: true,
});
}

// This should not throw and should handle existing directory
ecto.ensureFilePathSync(testPath);
expect(fs.existsSync(`${testOutputDirectory}/existing-dir-sync`)).toBe(true);

// Clean up
fs.rmSync(`${testOutputDirectory}/existing-dir-sync`, { recursive: true });
});
16 changes: 16 additions & 0 deletions test/engine-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ it("EngineMap - set with multiple extensions", () => {
expect(mappings.get("ejs")?.length).toBe(3);
});

it("EngineMap - set with duplicate extensions should deduplicate", () => {
const mappings = new EngineMap();
mappings.set("ejs", ["ejs", "md", "ejs", "njk", "md"]);
expect(mappings.get("ejs")?.length).toBe(3);
expect(mappings.get("ejs")?.toString()).toBe("ejs,md,njk");
});

it("EngineMap - set with no extensions should be undefined", () => {
const mappings = new EngineMap();
mappings.set("ejs", []);
Expand Down Expand Up @@ -48,6 +55,15 @@ it("EngineMap - deleteExtension with extensions", () => {
expect(mappings.get("ejs")?.toString()).toBe("ejs,md");
});

it("EngineMap - deleteExtension from non-existent engine should not error", () => {
const mappings = new EngineMap();
mappings.set("ejs", ["ejs", "md"]);
// Try to delete extension from an engine that doesn't exist
mappings.deleteExtension("handlebars", "hbs");
// Original engine should still be intact
expect(mappings.get("ejs")?.toString()).toBe("ejs,md");
});

it("EngineMap - getName with extensions", () => {
const mappings = new EngineMap();
mappings.set("ejs", ["ejs", "md", "njk"]);
Expand Down
20 changes: 20 additions & 0 deletions test/engines/liquid.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,23 @@ it("Liquid - Rendering Partials", async () => {
);
expect(output).toContain("John Doe");
});

it("Liquid - Rendering multiple times with same engine instance (async)", async () => {
const engine = new Liquid();
const firstRender = await engine.render(exampleSource1, exampleData1);
expect(firstRender).toBe("John");

// Second render should reuse the existing engine
const secondRender = await engine.render(exampleSource1, exampleData1);
expect(secondRender).toBe("John");
});

it("Liquid - Rendering multiple times with same engine instance (sync)", () => {
const engine = new Liquid();
const firstRender = engine.renderSync(exampleSource1, exampleData1);
expect(firstRender).toBe("John");

// Second render should reuse the existing engine
const secondRender = engine.renderSync(exampleSource1, exampleData1);
expect(secondRender).toBe("John");
});
Loading