Reliability
This commit is contained in:
259
README.md
Normal file
259
README.md
Normal file
@@ -0,0 +1,259 @@
|
||||
# Inkject
|
||||
|
||||
A lightweight, powerful JavaScript template engine with support for conditionals, variables, and nested data structures.
|
||||
|
||||
## Features
|
||||
|
||||
- **Zero dependencies** - Pure JavaScript implementation
|
||||
- **Variable interpolation** with nested object support
|
||||
- **Conditional rendering** with if/elseif/else logic
|
||||
- **Inline conditionals** for compact templates
|
||||
- **Nested conditionals** for complex logic
|
||||
- **Customizable delimiters** - Use any delimiter you want
|
||||
- **Clean syntax** - Easy to read and write templates
|
||||
|
||||
## Installation
|
||||
|
||||
```javascript
|
||||
// Include the inkject class in your project
|
||||
const inkject = require('inkject');
|
||||
const renderer = new inkject();
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```javascript
|
||||
const template = "Hello &name&! You have &messages& new messages.";
|
||||
const data = {
|
||||
name: "Alice",
|
||||
messages: 5
|
||||
};
|
||||
|
||||
const result = renderer.render(template, data);
|
||||
console.log(result); // "Hello Alice! You have 5 new messages."
|
||||
```
|
||||
|
||||
## Syntax Reference
|
||||
|
||||
### Variable Interpolation
|
||||
|
||||
Access data using dot notation for nested objects:
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
Welcome &user.name&!
|
||||
Email: &user.profile.email&
|
||||
Score: &stats.points&
|
||||
`;
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "John",
|
||||
profile: { email: "john@example.com" }
|
||||
},
|
||||
stats: { points: 1250 }
|
||||
};
|
||||
```
|
||||
|
||||
### Custom Delimiters
|
||||
|
||||
Change delimiters by passing a third parameter:
|
||||
|
||||
```javascript
|
||||
// Using $$ as delimiters
|
||||
renderer.render("Hello $$name$$", { name: "World" }, "$$$$");
|
||||
// NOTE: It splits the delimiter string in two, so $$$$ means $$ for start and $$ for end
|
||||
|
||||
// Using different open/close delimiters
|
||||
renderer.render("Hello {{name}}", { name: "World" }, "{{}}");
|
||||
|
||||
// Using triple delimiters
|
||||
renderer.render("Hello $$$name$$$", { name: "World" }, "$$$$$$");
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
||||
#### Basic If/Else
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
&#if user.isActive&
|
||||
Welcome back, &user.name&!
|
||||
&#else&
|
||||
Please activate your account.
|
||||
&/if&
|
||||
`;
|
||||
```
|
||||
|
||||
#### If/Elseif/Else Chains
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
&#if user.role === "admin"&
|
||||
🔑 Administrator Access
|
||||
&#elseif user.role === "manager"&
|
||||
👔 Manager Access
|
||||
&#else&
|
||||
👤 Standard User
|
||||
&/if&
|
||||
`;
|
||||
```
|
||||
|
||||
#### Inline Conditionals
|
||||
|
||||
Perfect for conditional text within sentences:
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
You have &#if notifications > 0&¬ifications& new&#else&no&/if& notifications.
|
||||
Status: &#if user.isActive&✅ Active&#else&❌ Inactive&/if&`;
|
||||
```
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
Support for all common comparison operators:
|
||||
|
||||
- `===` - Strict equality
|
||||
- `!==` - Strict inequality
|
||||
- `==` - Loose equality
|
||||
- `!=` - Loose inequality
|
||||
- `>` - Greater than
|
||||
- `<` - Less than
|
||||
- `>=` - Greater than or equal
|
||||
- `<=` - Less than or equal
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
&#if age >= 18&
|
||||
You are an adult.
|
||||
&#elseif age >= 13&
|
||||
You are a teenager.
|
||||
&#else&
|
||||
You are a child.
|
||||
&/if&`;
|
||||
```
|
||||
|
||||
### Negation
|
||||
|
||||
Use `!` to negate conditions:
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
&#if !user.isSubscribed&
|
||||
Subscribe now for premium features!
|
||||
&/if&
|
||||
`;
|
||||
```
|
||||
|
||||
### Nested Conditionals
|
||||
|
||||
Create complex logic with nested conditions:
|
||||
|
||||
```javascript
|
||||
const template = `
|
||||
&#if user.isActive&
|
||||
Welcome, &user.name&!
|
||||
&#if user.role === "admin"&
|
||||
&#if company.employees > 100&
|
||||
Managing large organization (&company.employees& employees)
|
||||
&#else&
|
||||
Managing small team (&company.employees& employees)
|
||||
&/if&
|
||||
&/if&
|
||||
&/if&
|
||||
`;
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### User Dashboard
|
||||
|
||||
```javascript
|
||||
const dashboardTemplate = `
|
||||
=== USER DASHBOARD ===
|
||||
|
||||
Welcome, &user.name&!
|
||||
|
||||
&#if user.role === "admin"&
|
||||
🔑 ADMIN ACCESS GRANTED
|
||||
You have full system privileges.
|
||||
|
||||
&#if company.employees > 100&
|
||||
📊 Large Company Management:
|
||||
- Company: &&company.name&&
|
||||
- Total Employees: &&company.employees&
|
||||
&#else&
|
||||
📊 Small Company Management:
|
||||
- Company: &&company.name&&
|
||||
- Team Size: &company.employees&
|
||||
&/if&
|
||||
|
||||
&#elseif user.role === "manager"&
|
||||
👔 MANAGER ACCESS
|
||||
You can view team data and reports.
|
||||
&#else&
|
||||
👤 USER ACCESS
|
||||
Limited access to personal data only.
|
||||
&/if&
|
||||
|
||||
Account Status: &#if user.isActive&✅ Active&#else&❌ Inactive&/if&
|
||||
`;
|
||||
|
||||
const data = {
|
||||
user: {
|
||||
name: "Alice Johnson",
|
||||
role: "admin",
|
||||
isActive: true
|
||||
},
|
||||
company: {
|
||||
name: "TechCorp",
|
||||
employees: 150
|
||||
}
|
||||
};
|
||||
|
||||
console.log(renderer.render(dashboardTemplate, data));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `render(template, data, delimiter)`
|
||||
|
||||
Renders a template with the provided data.
|
||||
|
||||
**Parameters:**
|
||||
- `template` (string): The template string to render
|
||||
- `data` (object): The data object containing variables
|
||||
- `delimiter` (string, optional): Custom delimiter (default: '&&')
|
||||
|
||||
**Returns:** Rendered string
|
||||
|
||||
### Delimiter Rules
|
||||
|
||||
- **Even length**: Split in half for open/close (e.g., `{{}}` -> `{{` and `}}`)
|
||||
- **Odd length**: Same delimiter for open/close (e.g., `$$$` -> `$$$` and `$$$`)
|
||||
|
||||
## Error Handling
|
||||
|
||||
Inkject handles errors gracefully:
|
||||
|
||||
- **Missing variables**: Returns empty string
|
||||
- **Undefined nested properties**: Returns empty string
|
||||
- **Invalid conditions**: Evaluates to false
|
||||
- **Malformed templates**: Processes what it can, leaves invalid syntax unchanged
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Reuse renderer instances** - Create one `inkject` instance and reuse it
|
||||
2. **Simple conditions** - Complex JavaScript expressions aren't supported
|
||||
3. **Avoid deep nesting** - Keep conditional nesting reasonable for readability
|
||||
4. **Cache templates** - Store frequently used templates in variables
|
||||
|
||||
## Browser Compatibility
|
||||
|
||||
Works in all modern browsers and Node.js environments. No transpilation required.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - feel free to use in your projects!
|
||||
|
||||
---
|
||||
226
inkject.js
226
inkject.js
@@ -1,14 +1,9 @@
|
||||
class inkject {
|
||||
render(template, data = {}, delimiter = '&&') {
|
||||
const [openDelim, closeDelim] = this.parseDelimiter(delimiter);
|
||||
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const openPattern = escapeRegex(openDelim);
|
||||
const closePattern = escapeRegex(closeDelim);
|
||||
|
||||
let result = template;
|
||||
result = this.processConditionals(result, data, openPattern, closePattern);
|
||||
result = this.processVariables(result, data, openPattern, closePattern);
|
||||
|
||||
result = this.processConditionals(result, data, openDelim, closeDelim);
|
||||
result = this.processVariables(result, data, openDelim, closeDelim);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -22,65 +17,194 @@ class inkject {
|
||||
}
|
||||
}
|
||||
|
||||
processConditionals(template, data, openPattern, closePattern) {
|
||||
const conditionalRegex = new RegExp(
|
||||
`${openPattern}#if\\s+([^${closePattern.charAt(0)}]+?)${closePattern}([\\s\\S]*?)(?:${openPattern}#elseif\\s+([^${closePattern.charAt(0)}]+?)${closePattern}([\\s\\S]*?))*(?:${openPattern}#else${closePattern}([\\s\\S]*?))?${openPattern}/if${closePattern}`,
|
||||
'g'
|
||||
);
|
||||
processConditionals(template, data, openDelim, closeDelim) {
|
||||
let result = template;
|
||||
let changed = true;
|
||||
|
||||
return template.replace(conditionalRegex, (fullMatch) => {
|
||||
return this.processComplexConditional(fullMatch, data, openPattern, closePattern);
|
||||
});
|
||||
while (changed) {
|
||||
changed = false;
|
||||
const match = this.findInnermostConditional(result, openDelim, closeDelim);
|
||||
if (match) {
|
||||
const replacement = this.evaluateConditionalBlock(match.content, data, openDelim, closeDelim);
|
||||
const beforeMatch = result.slice(0, match.start);
|
||||
const afterMatch = result.slice(match.end);
|
||||
|
||||
const isInline = this.isInlineConditional(beforeMatch, afterMatch, match.content);
|
||||
|
||||
if (isInline) {
|
||||
result = beforeMatch + replacement + afterMatch;
|
||||
} else {
|
||||
let cleanedReplacement = replacement.trim();
|
||||
let cleanedBefore = beforeMatch;
|
||||
let cleanedAfter = afterMatch;
|
||||
|
||||
if (cleanedReplacement === '') {
|
||||
if (cleanedBefore.endsWith('\n')) {
|
||||
cleanedBefore = cleanedBefore.slice(0, -1);
|
||||
}
|
||||
if (cleanedAfter.startsWith('\n')) {
|
||||
cleanedAfter = cleanedAfter.slice(1);
|
||||
}
|
||||
result = cleanedBefore + cleanedAfter;
|
||||
} else {
|
||||
const needsNewlineBefore = cleanedBefore.length > 0 && !cleanedBefore.endsWith('\n') && cleanedReplacement.length > 0;
|
||||
const needsNewlineAfter = cleanedAfter.length > 0 && !cleanedAfter.startsWith('\n') && cleanedReplacement.length > 0;
|
||||
|
||||
result = cleanedBefore +
|
||||
(needsNewlineBefore ? '\n' : '') +
|
||||
cleanedReplacement +
|
||||
(needsNewlineAfter ? '\n' : '') +
|
||||
cleanedAfter;
|
||||
}
|
||||
}
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
processComplexConditional(match, data, openPattern, closePattern) {
|
||||
const blocks = [];
|
||||
const ifMatch = new RegExp(`${openPattern}#if\\s+([^${closePattern.charAt(0)}]+?)${closePattern}`).exec(match);
|
||||
if (ifMatch) {
|
||||
blocks.push({ type: 'if', condition: ifMatch[1].trim() });
|
||||
isInlineConditional(beforeMatch, afterMatch, conditionalContent) {
|
||||
const beforeLastNewline = beforeMatch.lastIndexOf('\n');
|
||||
const afterFirstNewline = afterMatch.indexOf('\n');
|
||||
|
||||
const beforeText = beforeLastNewline === -1 ? beforeMatch : beforeMatch.slice(beforeLastNewline + 1);
|
||||
const afterText = afterFirstNewline === -1 ? afterMatch : afterMatch.slice(0, afterFirstNewline);
|
||||
|
||||
const hasContentBefore = beforeText.trim().length > 0;
|
||||
const hasContentAfter = afterText.trim().length > 0;
|
||||
|
||||
const conditionalHasNewlines = conditionalContent.includes('\n');
|
||||
|
||||
return (hasContentBefore || hasContentAfter) && !conditionalHasNewlines;
|
||||
}
|
||||
|
||||
findInnermostConditional(template, openDelim, closeDelim) {
|
||||
const ifPattern = openDelim + '#if ';
|
||||
const endifPattern = openDelim + '/if' + closeDelim;
|
||||
|
||||
let deepestIf = null;
|
||||
let maxDepth = 0;
|
||||
|
||||
for (let i = 0; i < template.length; i++) {
|
||||
if (template.substr(i, ifPattern.length) === ifPattern) {
|
||||
let depth = 0;
|
||||
let currentPos = i;
|
||||
let foundMatching = false;
|
||||
|
||||
while (currentPos < template.length) {
|
||||
if (template.substr(currentPos, ifPattern.length) === ifPattern) {
|
||||
depth++;
|
||||
currentPos += ifPattern.length;
|
||||
} else if (template.substr(currentPos, endifPattern.length) === endifPattern) {
|
||||
depth--;
|
||||
if (depth === 0) {
|
||||
if (!foundMatching || depth >= maxDepth) {
|
||||
let hasNestedIf = false;
|
||||
const blockContent = template.slice(i + ifPattern.length, currentPos);
|
||||
if (blockContent.indexOf(ifPattern) !== -1) {
|
||||
hasNestedIf = true;
|
||||
}
|
||||
|
||||
if (!hasNestedIf) {
|
||||
deepestIf = {
|
||||
start: i,
|
||||
end: currentPos + endifPattern.length,
|
||||
content: template.slice(i, currentPos + endifPattern.length)
|
||||
};
|
||||
maxDepth = depth;
|
||||
}
|
||||
}
|
||||
foundMatching = true;
|
||||
break;
|
||||
}
|
||||
currentPos += endifPattern.length;
|
||||
} else {
|
||||
currentPos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elseifRegex = new RegExp(`${openPattern}#elseif\\s+([^${closePattern.charAt(0)}]+?)${closePattern}`, 'g');
|
||||
let elseifMatch;
|
||||
while ((elseifMatch = elseifRegex.exec(match)) !== null) {
|
||||
blocks.push({ type: 'elseif', condition: elseifMatch[1].trim() });
|
||||
}
|
||||
return deepestIf;
|
||||
}
|
||||
|
||||
if (match.includes(`${this.unescapeRegex(openPattern)}#else${this.unescapeRegex(closePattern)}`)) {
|
||||
blocks.push({ type: 'else' });
|
||||
}
|
||||
evaluateConditionalBlock(block, data, openDelim, closeDelim) {
|
||||
const sections = this.parseConditionalSections(block, openDelim, closeDelim);
|
||||
|
||||
const content = this.extractConditionalContent(match, openPattern, closePattern, blocks);
|
||||
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
const block = blocks[i];
|
||||
if (block.type === 'else' || this.evaluateCondition(block.condition, data)) {
|
||||
return content[i] || '';
|
||||
for (const section of sections) {
|
||||
if (section.type === 'else' || this.evaluateCondition(section.condition, data)) {
|
||||
return section.content;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
extractConditionalContent(match, openPattern, closePattern, blocks) {
|
||||
const unescapedOpen = this.unescapeRegex(openPattern);
|
||||
const unescapedClose = this.unescapeRegex(closePattern);
|
||||
parseConditionalSections(block, openDelim, closeDelim) {
|
||||
const sections = [];
|
||||
|
||||
const parts = match.split(new RegExp(`${openPattern}(?:#(?:if|elseif|else)|/if)(?:\\s+[^${closePattern.charAt(0)}]*?)?${closePattern}`));
|
||||
return parts.slice(1, -1);
|
||||
const ifRegex = new RegExp(`^${this.escapeRegex(openDelim)}#if\\s+([^${this.escapeRegex(closeDelim)}]+)${this.escapeRegex(closeDelim)}`);
|
||||
const elseifRegex = new RegExp(`${this.escapeRegex(openDelim)}#elseif\\s+([^${this.escapeRegex(closeDelim)}]+)${this.escapeRegex(closeDelim)}`, 'g');
|
||||
const elseRegex = new RegExp(`${this.escapeRegex(openDelim)}#else${this.escapeRegex(closeDelim)}`);
|
||||
const endifRegex = new RegExp(`${this.escapeRegex(openDelim)}/if${this.escapeRegex(closeDelim)}$`);
|
||||
|
||||
const ifMatch = block.match(ifRegex);
|
||||
if (!ifMatch) return sections;
|
||||
|
||||
let content = block.replace(ifRegex, '').replace(endifRegex, '');
|
||||
let pos = 0;
|
||||
let currentCondition = ifMatch[1].trim();
|
||||
let currentType = 'if';
|
||||
|
||||
const elseifMatches = [];
|
||||
let elseifMatch;
|
||||
while ((elseifMatch = elseifRegex.exec(content)) !== null) {
|
||||
elseifMatches.push({
|
||||
index: elseifMatch.index,
|
||||
condition: elseifMatch[1].trim(),
|
||||
fullMatch: elseifMatch[0]
|
||||
});
|
||||
}
|
||||
|
||||
const elseMatch = content.match(elseRegex);
|
||||
const elsePos = elseMatch ? content.search(elseRegex) : -1;
|
||||
|
||||
const allBreakpoints = [
|
||||
...elseifMatches.map(m => ({ pos: m.index, type: 'elseif', condition: m.condition, length: m.fullMatch.length })),
|
||||
...(elsePos !== -1 ? [{ pos: elsePos, type: 'else', condition: null, length: elseMatch[0].length }] : [])
|
||||
].sort((a, b) => a.pos - b.pos);
|
||||
|
||||
for (let i = 0; i <= allBreakpoints.length; i++) {
|
||||
const start = i === 0 ? 0 : allBreakpoints[i - 1].pos + allBreakpoints[i - 1].length;
|
||||
const end = i === allBreakpoints.length ? content.length : allBreakpoints[i].pos;
|
||||
|
||||
const sectionContent = content.slice(start, end);
|
||||
|
||||
sections.push({
|
||||
type: currentType,
|
||||
condition: currentCondition,
|
||||
content: sectionContent
|
||||
});
|
||||
|
||||
if (i < allBreakpoints.length) {
|
||||
currentType = allBreakpoints[i].type;
|
||||
currentCondition = allBreakpoints[i].condition;
|
||||
}
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
||||
unescapeRegex(str) {
|
||||
return str.replace(/\\(.)/g, '$1');
|
||||
}
|
||||
|
||||
processVariables(template, data, openPattern, closePattern) {
|
||||
const varRegex = new RegExp(`${openPattern}([^#/][^${closePattern.charAt(0)}]*?)${closePattern}`, 'g');
|
||||
processVariables(template, data, openDelim, closeDelim) {
|
||||
const openPattern = this.escapeRegex(openDelim);
|
||||
const closePattern = this.escapeRegex(closeDelim);
|
||||
const varRegex = new RegExp(`${openPattern}([^#/][^${closePattern.slice(-1)}]*)${closePattern}`, 'g');
|
||||
|
||||
return template.replace(varRegex, (match, variable) => {
|
||||
const value = this.getValue(variable.trim(), data);
|
||||
return value !== undefined ? String(value) : '';
|
||||
});
|
||||
}).replace(/\n\s*\n\s*\n/g, '\n\n');
|
||||
}
|
||||
|
||||
evaluateCondition(condition, data) {
|
||||
@@ -94,8 +218,10 @@ class inkject {
|
||||
|
||||
const operators = ['===', '!==', '==', '!=', '>=', '<=', '>', '<'];
|
||||
for (const op of operators) {
|
||||
if (condition.includes(op)) {
|
||||
const [left, right] = condition.split(op).map(s => s.trim());
|
||||
const opIndex = condition.indexOf(op);
|
||||
if (opIndex !== -1) {
|
||||
const left = condition.slice(0, opIndex).trim();
|
||||
const right = condition.slice(opIndex + op.length).trim();
|
||||
const leftValue = this.getValue(left, data);
|
||||
const rightValue = this.parseValue(right);
|
||||
|
||||
@@ -149,6 +275,10 @@ class inkject {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
escapeRegex(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = inkject;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inkject",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"description": "",
|
||||
"main": "inkject.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in New Issue
Block a user