From 6f24ee10a6e44dc5ac3f6f19508a98b14392d63d Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Thu, 29 May 2025 13:35:21 +0300 Subject: [PATCH] Reliability --- README.md | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++ inkject.js | 226 ++++++++++++++++++++++++++++++++++---------- package.json | 2 +- 3 files changed, 438 insertions(+), 49 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e84ae09 --- /dev/null +++ b/README.md @@ -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! + +--- diff --git a/inkject.js b/inkject.js index 01f1f92..d6dcc5a 100644 --- a/inkject.js +++ b/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; \ No newline at end of file diff --git a/package.json b/package.json index a281512..197a481 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inkject", - "version": "1.0.0", + "version": "1.1.1", "description": "", "main": "inkject.js", "scripts": {