odac 1.4.6 → 1.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/ai/skills/backend/views.md +6 -1
- package/docs/backend/00-getting-started/01-quick-start.md +77 -0
- package/docs/backend/07-views/03-template-syntax.md +5 -0
- package/docs/backend/07-views/04-request-data.md +13 -0
- package/docs/index.json +10 -0
- package/package.json +1 -1
- package/src/View.js +33 -18
- package/test/View/addNavigateAttribute.test.js +53 -0
- package/test/View/print.test.js +45 -1
- package/test/View/tags.test.js +132 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
### ⚙️ Engine Tuning
|
|
2
2
|
|
|
3
|
+
- **test:** remove unused fsPromises and standardize async I/O in View tests
|
|
4
|
+
|
|
5
|
+
### 📚 Documentation
|
|
6
|
+
|
|
7
|
+
- add quick start guide and register in documentation index
|
|
8
|
+
|
|
9
|
+
### 🛠️ Fixes & Improvements
|
|
10
|
+
|
|
11
|
+
- add raw attribute support to `<odac get>` tag for unescaped HTML output
|
|
12
|
+
- improve title extraction logic and optimize data-odac-navigate attribute injection in View rendering
|
|
13
|
+
- prevent data-odac-navigate injection into closing HTML tags
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
Powered by [⚡ ODAC](https://odac.run)
|
|
20
|
+
|
|
21
|
+
### ⚙️ Engine Tuning
|
|
22
|
+
|
|
3
23
|
- replace global Odac reference with private instance and update dependency overrides for security hardening
|
|
4
24
|
- **test/view:** remove unused CACHE_DIR variable in parseOdacTag tests
|
|
5
25
|
|
|
@@ -18,7 +18,8 @@ Views in ODAC are logic-light but powerful. They support automatic XSS protectio
|
|
|
18
18
|
3. **Data Binding — Two Equivalent Syntaxes**:
|
|
19
19
|
- `<odac var="key" />`: Tag-based output (HTML-escaped, XSS-safe).
|
|
20
20
|
- `{{ key }}`: Inline/interpolation output (HTML-escaped, XSS-safe). Identical behavior to `<odac var>`.
|
|
21
|
-
- `<odac
|
|
21
|
+
- `<odac get="key" />`: Query Parameter output (HTML-escaped, XSS-safe).
|
|
22
|
+
- `<odac var="key" raw />`, `<odac get="key" raw />` or `{!! key !!}`: Raw output (use with extreme caution).
|
|
22
23
|
4. **Choosing the Right Syntax**:
|
|
23
24
|
- **Inside HTML attributes** (`src`, `alt`, `href`, `class`, `value`, etc.) → Always prefer `{{ }}`.
|
|
24
25
|
- **Inline within text or mixed HTML** → Prefer `{{ }}` for short interpolations.
|
|
@@ -112,6 +113,10 @@ module.exports = function (Odac) {
|
|
|
112
113
|
<!-- Inline text interpolation -->
|
|
113
114
|
<p>Welcome, {{ user.name }}. You have {{ notifications }} new messages.</p>
|
|
114
115
|
|
|
116
|
+
<!-- Query parameter from URL (/page?q=search) -->
|
|
117
|
+
<p>Search Results for: <odac get="q" /></p>
|
|
118
|
+
<div class="raw-content"><odac get="htmlContent" raw /></div>
|
|
119
|
+
|
|
115
120
|
<!-- Conditional -->
|
|
116
121
|
<odac:if condition="stats.users > 100">
|
|
117
122
|
<span class="badge">Popular!</span>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
## 🚀 Quick Start
|
|
2
|
+
|
|
3
|
+
ODAC is built for speed and zero-configuration. You can bootstrap a production-ready, high-performance web application in seconds.
|
|
4
|
+
|
|
5
|
+
### 1. Requirements
|
|
6
|
+
|
|
7
|
+
- **Node.js:** 18.0.0 or higher.
|
|
8
|
+
- **npm:** 8.0.0 or higher.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
### 2. Initialize Your Project
|
|
13
|
+
|
|
14
|
+
The standard way to start an ODAC project is using the interactive **init** command via `npx`. Run this in your terminal:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx odac init my-app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**What this does:**
|
|
21
|
+
- Creates a new folder named `my-app`.
|
|
22
|
+
- Copies the ODAC enterprise skeleton (controllers, routes, views, etc.).
|
|
23
|
+
- Initializes `package.json` with the latest ODAC framework dependency.
|
|
24
|
+
- Runs `npm install` automatically.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### 3. Launch Development Mode
|
|
29
|
+
|
|
30
|
+
Navigate into your project directory and start the smart development server:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
cd my-app
|
|
34
|
+
npm run dev
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Your app is now live at `http://localhost:1071` (default port).
|
|
38
|
+
|
|
39
|
+
**Features enabled in Dev Mode:**
|
|
40
|
+
- **Hot-reloading:** The server and cluster workers restart instantly on backend changes.
|
|
41
|
+
- **Zero-Config Tailwind CSS v4:** Automatically watches and compiles your styles.
|
|
42
|
+
- **Detailed Stack Traces:** Helps you debug errors quickly in the browser and console.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### 4. Setup AI Agent Skills
|
|
47
|
+
|
|
48
|
+
ODAC is designed to be **AI-First**. It provides pre-built "skills" (knowledge files) that teach your AI coding assistant (like Antigravity, Claude, Cursor, or Windsurf) exactly how to write ODAC-compatible code.
|
|
49
|
+
|
|
50
|
+
Run this command inside your project root:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx odac skills
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Follow the interactive prompt to sync the documentation and patterns directly into your IDE's agent configuration folder.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### 5. Essential Commands
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
| :--- | :--- |
|
|
64
|
+
| `npm run dev` | Start development server with hot-reload & styles. |
|
|
65
|
+
| `npm run build` | Compile and minify styles for production. |
|
|
66
|
+
| `npm start` | Run the application in high-performance production mode. |
|
|
67
|
+
| `npx odac migrate` | Run pending database migrations. |
|
|
68
|
+
| `npx odac skills` | Sync/Update AI Agent knowledge files. |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### 6. Next Steps
|
|
73
|
+
|
|
74
|
+
- **Define Routes:** Open `route/web.js` to see how URLs are mapped to views.
|
|
75
|
+
- **Project Structure:** Learn how to organize your [file/folder layout](../02-structure/01-typical-project-layout.md).
|
|
76
|
+
- **Build Views:** Check the `view/` directory for your HTML templates.
|
|
77
|
+
- **Manage Database:** Update `odac.json` to connect to PostgreSQL, MySQL, or SQLite.
|
|
@@ -50,6 +50,10 @@ Access URL query parameters directly:
|
|
|
50
50
|
<!-- URL: /search?q=laptop -->
|
|
51
51
|
<odac get="q" />
|
|
52
52
|
<!-- Output: laptop -->
|
|
53
|
+
|
|
54
|
+
<!-- Get raw query parameter (unescaped) -->
|
|
55
|
+
<odac get="htmlContent" raw />
|
|
56
|
+
<!-- Output: <b>Trusted HTML</b> -->
|
|
53
57
|
```
|
|
54
58
|
|
|
55
59
|
**Note:** `<odac get>` is for URL parameters. For controller data, use `<odac var>`.
|
|
@@ -185,6 +189,7 @@ Full access to the Odac object in templates:
|
|
|
185
189
|
| Raw HTML (inline) | `{!! x !!}` | [Variables](./03-variables.md) |
|
|
186
190
|
| String | `<odac>text</odac>` | [Variables](./03-variables.md) |
|
|
187
191
|
| Query Parameter | `<odac get="key" />` | [Request Data](./04-request-data.md) |
|
|
192
|
+
| Query Parameter Raw | `<odac get="key" raw />` | [Request Data](./04-request-data.md) |
|
|
188
193
|
| Translation | `<odac translate>key</odac>` | [Translations](./07-translations.md) |
|
|
189
194
|
| Translation Raw | `<odac translate raw>key</odac>` | [Translations](./07-translations.md) |
|
|
190
195
|
| If | `<odac:if condition="x">` | [Conditionals](./05-conditionals.md) |
|
|
@@ -33,6 +33,19 @@ If a parameter doesn't exist, it safely returns an empty string:
|
|
|
33
33
|
|
|
34
34
|
This prevents errors when parameters are optional.
|
|
35
35
|
|
|
36
|
+
### Raw HTML Output
|
|
37
|
+
|
|
38
|
+
By default, `<odac get>` automatically escapes HTML special characters to prevent XSS attacks. If you need to output raw HTML from a query parameter (only from trusted sources), use the `raw` attribute:
|
|
39
|
+
|
|
40
|
+
```html
|
|
41
|
+
<!-- URL: /page?content=%3Cb%3EHello%3C/b%3E -->
|
|
42
|
+
|
|
43
|
+
<odac get="content" raw />
|
|
44
|
+
<!-- Output: <b>Hello</b> -->
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Security Warning:** Never use `raw` with query parameters if they can contain user-generated content. Query parameters are easily manipulated by users. Only use `raw` if you are certain the value is safe (e.g., predefined tokens).
|
|
48
|
+
|
|
36
49
|
### Difference: get vs var
|
|
37
50
|
|
|
38
51
|
**`<odac get>` - Query Parameters (from URL):**
|
package/docs/index.json
CHANGED
package/package.json
CHANGED
package/src/View.js
CHANGED
|
@@ -177,7 +177,7 @@ class View {
|
|
|
177
177
|
// Extract title if present inside the part
|
|
178
178
|
const titleMatch = html.match(TITLE_REGEX)
|
|
179
179
|
if (titleMatch && titleMatch[1]) {
|
|
180
|
-
title = titleMatch[1]
|
|
180
|
+
title = titleMatch[1].trim()
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
}
|
|
@@ -187,7 +187,8 @@ class View {
|
|
|
187
187
|
if (!title) {
|
|
188
188
|
const priorityParts = ['head', 'header', 'meta']
|
|
189
189
|
for (const key of priorityParts) {
|
|
190
|
-
if
|
|
190
|
+
// Only extract if it wasn't already processed in the main loop above
|
|
191
|
+
if (this.#part[key] && output[key] === undefined) {
|
|
191
192
|
let viewPath = this.#part[key]
|
|
192
193
|
if (viewPath.includes('.')) viewPath = viewPath.replace(/\./g, '/')
|
|
193
194
|
if (await this.#exists(`./view/${key}/${viewPath}.html`)) {
|
|
@@ -195,7 +196,7 @@ class View {
|
|
|
195
196
|
const partHtml = await this.#render(`./view/${key}/${viewPath}.html`)
|
|
196
197
|
const titleMatch = partHtml.match(TITLE_REGEX)
|
|
197
198
|
if (titleMatch && titleMatch[1]) {
|
|
198
|
-
title = titleMatch[1]
|
|
199
|
+
title = titleMatch[1].trim()
|
|
199
200
|
break
|
|
200
201
|
}
|
|
201
202
|
} catch (e) {
|
|
@@ -314,7 +315,8 @@ class View {
|
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
if (attrs.get) {
|
|
317
|
-
|
|
318
|
+
const escaped = attrs.get.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
319
|
+
return attrs.raw ? `{!! get('${escaped}') || '' !!}` : `{{ get('${escaped}') || '' }}`
|
|
318
320
|
} else if (attrs.var) {
|
|
319
321
|
if (attrs.raw) {
|
|
320
322
|
return `{!! ${attrs.var} !!}`
|
|
@@ -343,7 +345,8 @@ class View {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
if (attrs.get) {
|
|
346
|
-
|
|
348
|
+
const escaped = attrs.get.replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
349
|
+
return attrs.raw ? `{!! get('${escaped}') || '' !!}` : `{{ get('${escaped}') || '' }}`
|
|
347
350
|
} else if (attrs.var) {
|
|
348
351
|
if (attrs.raw) {
|
|
349
352
|
return `{!! ${attrs.var} !!}`
|
|
@@ -566,20 +569,32 @@ class View {
|
|
|
566
569
|
}
|
|
567
570
|
|
|
568
571
|
#addNavigateAttribute(skeleton) {
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
572
|
+
// Single-pass regex to inject data-odac-navigate into parent tags or auto-wrap unwrapped placeholders.
|
|
573
|
+
// Group 1: Opening tag (<tag>)
|
|
574
|
+
// Group 2: Whitespace between tag and placeholder
|
|
575
|
+
// Group 3: Placeholder ({{ PART_NAME }})
|
|
576
|
+
// Group 4: Part name inside wrapped placeholder
|
|
577
|
+
// Group 5: Standalone/Unwrapped placeholder
|
|
578
|
+
// Group 6: Part name inside unwrapped placeholder
|
|
579
|
+
skeleton = skeleton.replace(
|
|
580
|
+
/(<(?!\/)[^>]+>)(\s*)(\{\{\s*([A-Z_]+)\s*\}\})|(\{\{\s*([A-Z_]+)\s*\}\})/g,
|
|
581
|
+
(match, openTag, spaces, wrappedPlaceholder, wrappedPartName, unwrappedPlaceholder, unwrappedPartName) => {
|
|
582
|
+
if (openTag) {
|
|
583
|
+
// Case 1: Placeholder immediately follows an opening tag. Inject attribute into the tag.
|
|
584
|
+
const attrName = wrappedPartName.toLowerCase()
|
|
585
|
+
const tagWithAttr = openTag.includes('data-odac-navigate') ? openTag : openTag.slice(0, -1) + ` data-odac-navigate="${attrName}">`
|
|
586
|
+
return `${tagWithAttr}${spaces}${wrappedPlaceholder}`
|
|
587
|
+
}
|
|
576
588
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
589
|
+
if (unwrappedPlaceholder) {
|
|
590
|
+
// Case 2: Placeholder is unwrapped (e.g. sibling to a closing tag). Wrap in display:contents.
|
|
591
|
+
const attrName = unwrappedPartName.toLowerCase()
|
|
592
|
+
return `<div style="display:contents" data-odac-navigate="${attrName}">${unwrappedPlaceholder}</div>`
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return match
|
|
596
|
+
}
|
|
597
|
+
)
|
|
583
598
|
|
|
584
599
|
const skeletonName = this.#part.skeleton || 'main'
|
|
585
600
|
const pageName = this.#odac.Request.page || ''
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const fs = require('fs').promises
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const View = require('../../src/View')
|
|
4
|
+
|
|
5
|
+
describe('View.#addNavigateAttribute()', () => {
|
|
6
|
+
let view
|
|
7
|
+
let mockOdac
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
global.__dir = path.resolve(__dirname, '../../')
|
|
11
|
+
mockOdac = {
|
|
12
|
+
Config: {view: {earlyHints: {enabled: false}}},
|
|
13
|
+
View: {},
|
|
14
|
+
Request: {
|
|
15
|
+
req: {url: '/test'},
|
|
16
|
+
res: {finished: false},
|
|
17
|
+
isAjaxLoad: false,
|
|
18
|
+
header: jest.fn(),
|
|
19
|
+
end: jest.fn(),
|
|
20
|
+
get: jest.fn(),
|
|
21
|
+
hasEarlyHints: jest.fn().mockReturnValue(false)
|
|
22
|
+
},
|
|
23
|
+
Lang: {get: jest.fn()}
|
|
24
|
+
}
|
|
25
|
+
view = new View(mockOdac)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.restoreAllMocks()
|
|
30
|
+
delete global.__dir
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not wrap placeholder in display:contents if it is the first child of an element', async () => {
|
|
34
|
+
const skeletonDir = path.join(global.__dir, 'skeleton')
|
|
35
|
+
const contentDir = path.join(global.__dir, 'view/content/pages')
|
|
36
|
+
await fs.mkdir(skeletonDir, {recursive: true})
|
|
37
|
+
await fs.mkdir(contentDir, {recursive: true})
|
|
38
|
+
|
|
39
|
+
await fs.writeFile(path.join(skeletonDir, 'main.html'), '<main id="app-main">\n {{ CONTENT }}\n</main>')
|
|
40
|
+
await fs.writeFile(path.join(contentDir, 'test.html'), '<h1>Content</h1>')
|
|
41
|
+
|
|
42
|
+
view.skeleton('main').set({content: 'pages/test'})
|
|
43
|
+
|
|
44
|
+
await view.print()
|
|
45
|
+
|
|
46
|
+
expect(mockOdac.Request.end).toHaveBeenCalled()
|
|
47
|
+
const outputHtml = mockOdac.Request.end.mock.calls[0][0]
|
|
48
|
+
|
|
49
|
+
expect(outputHtml).toContain('<main id="app-main" data-odac-navigate="content">')
|
|
50
|
+
expect(outputHtml).toContain('<h1>Content</h1>')
|
|
51
|
+
expect(outputHtml).not.toContain('<div style="display:contents"')
|
|
52
|
+
})
|
|
53
|
+
})
|
package/test/View/print.test.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const fs = require('fs').promises
|
|
2
|
+
const path = require('path')
|
|
1
3
|
const View = require('../../src/View')
|
|
2
4
|
|
|
3
5
|
describe('View.print()', () => {
|
|
@@ -5,15 +7,57 @@ describe('View.print()', () => {
|
|
|
5
7
|
let mockOdac
|
|
6
8
|
|
|
7
9
|
beforeEach(() => {
|
|
10
|
+
global.__dir = path.resolve(__dirname, '../../')
|
|
8
11
|
mockOdac = {
|
|
9
12
|
Config: {view: {earlyHints: {enabled: false}}},
|
|
10
13
|
View: {},
|
|
11
|
-
Request: {
|
|
14
|
+
Request: {
|
|
15
|
+
req: {url: '/test'},
|
|
16
|
+
res: {finished: false},
|
|
17
|
+
isAjaxLoad: false,
|
|
18
|
+
header: jest.fn(),
|
|
19
|
+
end: jest.fn(),
|
|
20
|
+
get: jest.fn(),
|
|
21
|
+
hasEarlyHints: jest.fn().mockReturnValue(false)
|
|
22
|
+
},
|
|
23
|
+
Lang: {get: jest.fn()}
|
|
12
24
|
}
|
|
13
25
|
view = new View(mockOdac)
|
|
14
26
|
})
|
|
15
27
|
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
jest.restoreAllMocks()
|
|
30
|
+
delete global.__dir
|
|
31
|
+
})
|
|
32
|
+
|
|
16
33
|
it('should be a function', () => {
|
|
17
34
|
expect(typeof view.print).toBe('function')
|
|
18
35
|
})
|
|
36
|
+
|
|
37
|
+
describe('AJAX Rendering Edge Cases', () => {
|
|
38
|
+
it("should extract title from priority parts even if their view path hasn't changed", async () => {
|
|
39
|
+
const headDir = path.join(global.__dir, 'view/head/inc')
|
|
40
|
+
const contentDir = path.join(global.__dir, 'view/content/pages')
|
|
41
|
+
await fs.mkdir(headDir, {recursive: true})
|
|
42
|
+
await fs.mkdir(contentDir, {recursive: true})
|
|
43
|
+
|
|
44
|
+
await fs.writeFile(path.join(headDir, 'head.html'), '<title>Dynamic Title</title>')
|
|
45
|
+
await fs.writeFile(path.join(contentDir, 'test.html'), '<h1>Test</h1>')
|
|
46
|
+
|
|
47
|
+
mockOdac.Request.isAjaxLoad = true
|
|
48
|
+
mockOdac.Request.ajaxLoad = ['head', 'content']
|
|
49
|
+
mockOdac.Request.clientParts = {head: 'inc/head'}
|
|
50
|
+
mockOdac.Request.page = 'test_page'
|
|
51
|
+
|
|
52
|
+
view.set({head: 'inc/head', content: 'pages/test'})
|
|
53
|
+
|
|
54
|
+
await view.print()
|
|
55
|
+
|
|
56
|
+
expect(mockOdac.Request.end).toHaveBeenCalled()
|
|
57
|
+
|
|
58
|
+
const payload = mockOdac.Request.end.mock.calls[0][0]
|
|
59
|
+
expect(payload.output.head).toBeUndefined()
|
|
60
|
+
expect(payload.title).toBe('Dynamic Title')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
19
63
|
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const fsPromises = fs.promises
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const View = require('../../src/View')
|
|
5
|
+
|
|
6
|
+
const FIXTURE_DIR = path.resolve(__dirname, '_fixtures_get')
|
|
7
|
+
|
|
8
|
+
describe('View odac tags (var, get, translate) raw attribute', () => {
|
|
9
|
+
let originalDir
|
|
10
|
+
let originalCwd
|
|
11
|
+
|
|
12
|
+
beforeAll(async () => {
|
|
13
|
+
await fsPromises.mkdir(path.join(FIXTURE_DIR, 'skeleton'), {recursive: true})
|
|
14
|
+
await fsPromises.mkdir(path.join(FIXTURE_DIR, 'view', 'content'), {recursive: true})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterAll(async () => {
|
|
18
|
+
await fsPromises.rm(FIXTURE_DIR, {recursive: true, force: true})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
originalDir = global.__dir
|
|
23
|
+
originalCwd = process.cwd()
|
|
24
|
+
global.__dir = FIXTURE_DIR
|
|
25
|
+
process.chdir(FIXTURE_DIR)
|
|
26
|
+
|
|
27
|
+
if (global.Odac?.View?.cache) {
|
|
28
|
+
global.Odac.View.cache = {}
|
|
29
|
+
}
|
|
30
|
+
if (global.Odac?.View?.skeletons) {
|
|
31
|
+
global.Odac.View.skeletons = {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const key of Object.keys(require.cache)) {
|
|
35
|
+
if (key.includes('.cache')) {
|
|
36
|
+
delete require.cache[key]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
global.__dir = originalDir
|
|
43
|
+
process.chdir(originalCwd)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
let testCounter = 0
|
|
47
|
+
|
|
48
|
+
async function renderTemplate(templateContent, getValues = {}) {
|
|
49
|
+
const uniqueId = `test_get_${++testCounter}`
|
|
50
|
+
const viewFile = path.join(FIXTURE_DIR, 'view', 'content', `${uniqueId}.html`)
|
|
51
|
+
await fsPromises.writeFile(viewFile, templateContent, 'utf8')
|
|
52
|
+
|
|
53
|
+
const skeletonFile = path.join(FIXTURE_DIR, 'skeleton', 'main.html')
|
|
54
|
+
await fsPromises.writeFile(skeletonFile, '{{ CONTENT }}', 'utf8')
|
|
55
|
+
|
|
56
|
+
let capturedOutput = ''
|
|
57
|
+
const mockOdac = {
|
|
58
|
+
Config: {debug: true},
|
|
59
|
+
Var: value => {
|
|
60
|
+
const str = value === null || value === undefined ? '' : String(value)
|
|
61
|
+
return {
|
|
62
|
+
html: () => str.replace(/[&<>"']/g, m => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''})[m])
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
Request: {
|
|
66
|
+
get: key => getValues[key] || '',
|
|
67
|
+
req: {url: '/test'},
|
|
68
|
+
res: {finished: false, headersSent: false},
|
|
69
|
+
isAjaxLoad: false,
|
|
70
|
+
ajaxLoad: [],
|
|
71
|
+
variables: {},
|
|
72
|
+
sharedData: {},
|
|
73
|
+
page: '',
|
|
74
|
+
header: () => {},
|
|
75
|
+
end: output => {
|
|
76
|
+
capturedOutput = output
|
|
77
|
+
},
|
|
78
|
+
hasEarlyHints: () => false,
|
|
79
|
+
setEarlyHints: () => {}
|
|
80
|
+
},
|
|
81
|
+
Lang: {
|
|
82
|
+
get: (...args) => args[0]
|
|
83
|
+
},
|
|
84
|
+
View: {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const view = new View(mockOdac)
|
|
88
|
+
view.skeleton('main')
|
|
89
|
+
view.set('content', uniqueId)
|
|
90
|
+
await view.print()
|
|
91
|
+
|
|
92
|
+
return capturedOutput
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
it('should escape <odac get> by default', async () => {
|
|
96
|
+
const result = await renderTemplate('<odac get="html" />', {html: '<b>bold</b>'})
|
|
97
|
+
expect(result).toContain('<b>bold</b>')
|
|
98
|
+
expect(result).not.toContain('<b>')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('should not escape <odac get> when raw attribute is present (self-closing)', async () => {
|
|
102
|
+
const result = await renderTemplate('<odac get="html" raw />', {html: '<b>bold</b>'})
|
|
103
|
+
expect(result).toContain('<b>bold</b>')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should not escape <odac get> when raw attribute is present (block-level)', async () => {
|
|
107
|
+
const result = await renderTemplate('<odac get="html" raw></odac>', {html: '<b>bold</b>'})
|
|
108
|
+
expect(result).toContain('<b>bold</b>')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should handle missing parameters gracefully with raw', async () => {
|
|
112
|
+
const result = await renderTemplate('<odac get="missing" raw />', {})
|
|
113
|
+
expect(result).toContain('data-odac-navigate="content"')
|
|
114
|
+
expect(result).toContain('id="odac-data"')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should not escape <odac translate> when raw attribute is present', async () => {
|
|
118
|
+
const result = await renderTemplate('<odac translate raw><b>bold</b></odac>')
|
|
119
|
+
expect(result).toContain('<b>bold</b>')
|
|
120
|
+
expect(result).not.toContain('<b>')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should escape <odac var> by default', async () => {
|
|
124
|
+
const result = await renderTemplate('<script:odac>var htmlVar = "<em>test</em>";</script:odac><odac var="htmlVar" />')
|
|
125
|
+
expect(result).toContain('<em>test</em>')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should not escape <odac var> when raw attribute is present', async () => {
|
|
129
|
+
const result = await renderTemplate('<script:odac>var htmlVar = "<em>test</em>";</script:odac><odac var="htmlVar" raw />')
|
|
130
|
+
expect(result).toContain('<em>test</em>')
|
|
131
|
+
})
|
|
132
|
+
})
|