Skip to content

Commit

Permalink
ghtml
Browse files Browse the repository at this point in the history
  • Loading branch information
gurgunday committed Jan 18, 2024
1 parent e529aa2 commit 2c6eca9
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"root": true,
"env": { "node": true },
"extends": ["plugin:grules/all"]
}
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: CI

on:
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ^18
- run: npm run test
23 changes: 23 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: npm-publish

on:
release:
types: [published]

jobs:
npm-publish:
runs-on: ubuntu-latest

permissions:
contents: read
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ^20
registry-url: https://registry.npmjs.org
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
package-lock.json

# Logs
logs
*.log
Expand Down
28 changes: 28 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "ghtml",
"description": "Replace your template engine with fast JavaScript by leveraging the power of tagged templates.",
"author": "Gürgün Dayıoğlu",
"license": "MIT",
"version": "0.0.0",
"type": "module",
"main": "./src/index.js",
"exports": {
".": "./src/index.js",
"./*.js": "./src/*.js"
},
"engines": {
"node": ">=18"
},
"scripts": {
"test": "npm run lint && node --experimental-test-coverage test/index.js",
"lint": "eslint . && prettier --check .",
"lint:fix": "eslint --fix . && prettier --write ."
},
"devDependencies": {
"grules": "^0.5.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/gurgunday/ghtml.git"
}
}
56 changes: 56 additions & 0 deletions src/html.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const escapeDictionary = {
'"': """,
"'": "'",
"&": "&",
"<": "&lt;",
">": "&gt;",
};

const escapeRegExp = new RegExp(
`[${Object.keys(escapeDictionary).join("")}]`,
"gu",
);

const escapeFunction = (key) => {
return escapeDictionary[key];
};

/**
* @param {{ raw: string[] }} literals
* @param {...any} expressions
* @returns {string}
*/
const html = (literals, ...expressions) => {
const lastLiteralIndex = literals.raw.length - 1;
let accumulator = "";

if (lastLiteralIndex === -1) {
return accumulator;
}

for (let index = 0; index < lastLiteralIndex; ++index) {
let literal = literals.raw[index];
let expression =
typeof expressions[index] === "string"
? expressions[index]
: expressions[index] == null
? ""
: Array.isArray(expressions[index])
? expressions[index].join("")
: `${expressions[index]}`;

if (literal.length && literal.charCodeAt(literal.length - 1) === 33) {
literal = literal.slice(0, -1);
} else if (expression.length) {
expression = expression.replace(escapeRegExp, escapeFunction);
}

accumulator += literal + expression;
}

accumulator += literals.raw[lastLiteralIndex];

return accumulator;
};

export { html };
22 changes: 22 additions & 0 deletions src/includeFile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { readFileSync } from "node:fs";

const readFileSyncOptions = { encoding: "utf8" };

const fileCache = new Map();

/**
* @param {string} path
* @returns {string}
*/
const includeFile = (path) => {
let file = fileCache.get(path);

if (file === undefined) {
file = readFileSync(path, readFileSyncOptions);
fileCache.set(path, file);
}

return file;
};

export { includeFile };
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { html } from "./html.js";
115 changes: 115 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import test from "node:test";
import assert from "node:assert";
import { html } from "../src/index.js";

const username = "G";
const descriptionSafe = "This is a safe description.";
const descriptionUnsafe =
"<script>alert('This is an unsafe description.')</script>";
const array1 = [1, 2, 3, 4, 5];
const conditionTrue = true;
const conditionFalse = false;
const empty = "";

test("renders correctly", () => {
assert.strictEqual(html({ raw: [] }, []), "");
});

test("renders correctly", () => {
assert.strictEqual(html`${empty}`, "");
});

test("renders correctly", () => {
assert.strictEqual(html`Hey, ${username}!`, `Hey, ${username}!`);
});

test("renders safe content", () => {
assert.strictEqual(
html`<p>${descriptionSafe}</p>`,
"<p>This is a safe description.</p>",
);
});

test("escapes unsafe output", () => {
assert.strictEqual(
html`<p>${descriptionUnsafe}</p>`,
`<p>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>`,
);
});

test("escapes unsafe output", () => {
assert.strictEqual(
html`<p>${descriptionUnsafe}</p>`,
`<p>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>`,
);
});

test("renders arrays", () => {
assert.strictEqual(
html`<p>${[descriptionSafe, descriptionUnsafe]}</p>`,
"<p>This is a safe description.&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</p>",
);
});

test("bypass escaping", () => {
assert.strictEqual(
html`<p>!${[descriptionSafe, descriptionUnsafe]}</p>`,
"<p>This is a safe description.<script>alert('This is an unsafe description.')</script></p>",
);
});

test("renders wrapped html calls", () => {
assert.strictEqual(
// prettier-ignore
html`<p>!${conditionTrue ? html`<strong>${descriptionUnsafe}</strong>` : ""}</p>`,
"<p><strong>&lt;script&gt;alert(&apos;This is an unsafe description.&apos;)&lt;/script&gt;</strong></p>",
);
});

test("renders multiple html calls", () => {
assert.strictEqual(
html`
<p>
!${conditionFalse ? "" : html`<strong> ${descriptionSafe} </strong>`}
<em> ${array1} </em>
!${conditionFalse ? html`<em> ${array1} </em>` : ""}
</p>
`,
// it should be formatted
`
<p>
<strong> This is a safe description. </strong>
<em> 12345 </em>
</p>
`,
);
});

test("renders multiple html calls with different expression types", () => {
const obj = {};
obj.toString = () => {
return "Description of the object.";
};

assert.strictEqual(
html`
<p>
!${conditionTrue ? html`<strong> ${descriptionSafe} </strong>` : ""}
!${conditionFalse
? ""
: // prettier-ignore
html`<em> ${array1.map((i) => {return i + 1;})} </em>`}<br />
And also, ${false} ${null}${undefined}${obj}
</p>
`,
// it should be formatted
`
<p>
<strong> This is a safe description. </strong>
<em> 23456 </em><br />
And also, false Description of the object.
</p>
`,
);
});

0 comments on commit 2c6eca9

Please sign in to comment.