diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 9e4f0cd0..6ca60d86 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
- node: [10.x, 12.x, 14.x]
+ node: [14.x]
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml
index 6c6f33a0..9fe9a819 100644
--- a/.github/workflows/mac.yml
+++ b/.github/workflows/mac.yml
@@ -22,7 +22,7 @@ jobs:
strategy:
matrix:
- node: [10.x, 12.x, 14.x]
+ node: [14.x]
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
diff --git a/core/engine/index.js b/core/engine/index.js
index 56018ecf..6dbbffa0 100644
--- a/core/engine/index.js
+++ b/core/engine/index.js
@@ -7,32 +7,73 @@ const {transform} = require('../transform');
const {getDirectoryFilesDetail} = require('../utils/getDirectoryFilesDetail');
const {File} = require('../File');
const validUrl = require('valid-url');
+const {isObject, isArray, isFunction} = require("util");
+const {mergeObjects} = require("../utils/merge-objects");
const defaultOptions = {
staticData: {},
customTags: [],
customAttributes: [],
env: 'development',
+ sass: {
+ indentWidth: 2,
+ precision: 5,
+ indentType: 'space',
+ linefeed: 'lf',
+ sourceComments: false,
+ includePaths: [],
+ functions: {},
+ },
+ less: {
+ strictUnits: false,
+ insecure: false,
+ paths: [],
+ math: 1,
+ urlArgs: '',
+ modifyVars: null,
+ lint: false,
+ },
+ stylus: {
+ paths: [],
+ },
+ postCSS: {
+ plugins: []
+ },
onPageRequest() {
}
}
-const engine = (app, pagesDirectoryPath, opt = defaultOptions) => {
+const engine = (app, pagesDirectoryPath, opt = {}) => {
if (!app) {
throw new Error('engine first argument must be provided and be a valid express app.')
}
- opt = {...defaultOptions, ...opt}
+ let hbConfig = {};
- if (typeof opt.staticData !== 'object') {
- throw new Error('HTML+ static data must be an javascript object')
+ try {
+ hbConfig = require(path.join(process.cwd(), 'hp.config.js'));
+ } catch (e) {
+ if (e.code !== 'MODULE_NOT_FOUND') {
+ e.message = `hp.config.js file loading failed: ${e.message}`
+ throw new Error(e)
+ }
+ }
+
+ opt = {...defaultOptions, ...mergeObjects(hbConfig, opt)};
+
+ if (!isObject(opt.staticData)) {
+ throw new Error('HTML+ static data option must be a javascript object')
+ }
+
+ if (!isArray(opt.customTags)) {
+ throw new Error('HTML+ custom tags option must be an array of valid tags.')
}
- if (!Array.isArray(opt.customTags)) {
- throw new Error('HTML+ custom tags must be an array of valid tags.')
+ if (!isArray(opt.customAttributes)) {
+ throw new Error('HTML+ custom attributes option must be an array of valid attributes.')
}
- if (typeof opt.onPageRequest !== 'function') {
+ if (!isFunction(opt.onPageRequest)) {
throw new Error('"onPageRequest" option must be a function')
}
@@ -54,7 +95,7 @@ const engine = (app, pagesDirectoryPath, opt = defaultOptions) => {
filePath = filePath.replace(pagesDirectoryPath, '');
const template = `${filePath.replace('.html', '')}`.slice(1);
let tempPath = '';
-
+
if (filePath.endsWith('index.html')) {
tempPath = filePath.replace('/index.html', '');
pagesRoutes[tempPath || '/'] = template;
@@ -73,11 +114,11 @@ const engine = (app, pagesDirectoryPath, opt = defaultOptions) => {
.then(() => {
app.engine('html', (filePath, {settings, _locals, cache, ...context}, callback) => {
const fileName = path.basename(filePath);
-
+
if (fileName.startsWith('_')) {
callback(new Error(`Cannot render partial(${fileName}) file as page. Partial files can only be included.`));
}
-
+
fs.readFile(filePath, (err, content) => {
if (err) return callback(err);
const file = new File(filePath, settings.views);
@@ -92,25 +133,25 @@ const engine = (app, pagesDirectoryPath, opt = defaultOptions) => {
partialFiles: partials,
onBeforeRender: (node, nodeFile) => {
let attrName = '';
-
+
if (node.tagName === 'link') {
attrName = 'href';
} else if (node.tagName === 'script') {
attrName = 'src';
}
-
+
const srcPath = node.attributes[attrName];
-
+
if (srcPath && !validUrl.isUri(srcPath)) {
const resourceFullPath = path.resolve(nodeFile.fileDirectoryPath, srcPath);
-
+
if (resourceFullPath.startsWith(pagesDirectoryPath)) {
node.setAttribute(attrName, resourceFullPath.replace(pagesDirectoryPath, ''))
}
}
}
})
-
+
callback(null, result);
} catch (e) {
console.error(e.message);
@@ -121,17 +162,17 @@ const engine = (app, pagesDirectoryPath, opt = defaultOptions) => {
}
})
});
-
+
app.set('views', pagesDirectoryPath);
app.set('view engine', 'html');
-
+
app.use(pageAndResourcesMiddleware(
pagesRoutes,
pagesDirectoryPath,
opt
));
app.use(express.static(pagesDirectoryPath))
-
+
console.log('HTML+ engine is ready');
})
};
diff --git a/core/engine/page-and-resources-middleware.js b/core/engine/page-and-resources-middleware.js
index 9b7615e7..4d8629db 100644
--- a/core/engine/page-and-resources-middleware.js
+++ b/core/engine/page-and-resources-middleware.js
@@ -17,7 +17,7 @@ const sourcesExtensions = new Set([
]);
const cache = {};
-function pageAndResourcesMiddleware(pagesRoutes, pagesDirectoryPath, {env, onPageRequest}) {
+function pageAndResourcesMiddleware(pagesRoutes, pagesDirectoryPath, {env, onPageRequest, sass, less}) {
return async (req, res, next) => {
if (req.method === 'GET') {
const ext = path.extname(req.path);
@@ -45,11 +45,11 @@ function pageAndResourcesMiddleware(pagesRoutes, pagesDirectoryPath, {env, onPag
switch (ext) {
case '.scss':
case '.sass':
- content = await transformResource.sass({file});
+ content = await transformResource.sass({file, ...sass});
content = (await transformResource.css(content, {file, env})).content;
break;
case '.less':
- content = await transformResource.less({file});
+ content = await transformResource.less({file, ...less});
content = (await transformResource.css(content, {file, env})).content;
break;
case '.styl':
diff --git a/core/transformers/less.transformer.js b/core/transformers/less.transformer.js
index d63a8334..a0e95c9f 100644
--- a/core/transformers/less.transformer.js
+++ b/core/transformers/less.transformer.js
@@ -5,7 +5,14 @@ const render = promisify(less.render);
const defaultOptions = {
env: 'development',
- file: null
+ file: null,
+ strictUnits: false,
+ insecure: false,
+ paths: [],
+ math: 1,
+ urlArgs: '',
+ modifyVars: null,
+ lint: false,
}
async function lessTransformer(content, opt = defaultOptions) {
@@ -22,10 +29,9 @@ async function lessTransformer(content, opt = defaultOptions) {
content = content ?? '';
const options = {
- ...defaultOptions,
- env: opt.env,
+ ...opt,
filename: opt.file?.fileAbsolutePath,
- // ...(opt.env === 'development' && {sourceMap: {}})
+ ...(opt.env === 'production' && {sourceMap: {}})
}
return render(content, options).then(res => {
diff --git a/core/transformers/sass.transformer.js b/core/transformers/sass.transformer.js
index 8a317ccc..b3a470d8 100644
--- a/core/transformers/sass.transformer.js
+++ b/core/transformers/sass.transformer.js
@@ -6,6 +6,13 @@ const render = promisify(nodeSass.render);
const defaultOptions = {
env: 'development',
file: null,
+ indentWidth: 2,
+ precision: 5,
+ indentType: 'space',
+ linefeed: 'lf',
+ sourceComments: false,
+ functions: {},
+ includePaths: [],
}
async function sassTransformer(content, opt = defaultOptions) {
diff --git a/core/transformers/sass.transformer.spec.js b/core/transformers/sass.transformer.spec.js
index cb39bb1c..3166f522 100644
--- a/core/transformers/sass.transformer.spec.js
+++ b/core/transformers/sass.transformer.spec.js
@@ -87,4 +87,61 @@ describe('sassTransformer', () => {
return expect(sassTransformer({})).rejects.toThrowError('If no string content is provided, the "file" option must be provided.')
});
+ describe('should work with other options', () => {
+ it('indentWidth', () => {
+ return sassTransformer(`
+ body {
+ background: #edd;
+ }
+ `, {
+ indentWidth: 8
+ }).then(res => {
+ expect(res).toEqual('body {\n' +
+ ' background: #edd; }\n');
+ })
+ });
+
+ it('precision', () => {
+ return sassTransformer(`
+ $w: (10 / 3);
+ body {
+ width: #{$w}px;
+ }
+ `, {
+ precision: 5
+ }).then(res => {
+ expect(res).toEqual('body {\n' +
+ ' width: 3.33333px; }\n');
+ })
+ });
+
+ it('indentType', () => {
+ return sassTransformer(`
+ body {
+ width: 300px;
+ }
+ `, {
+ indentWidth: 1,
+ indentType: 'tab'
+ }).then(res => {
+ expect(res).toEqual('body {\n' +
+ '\twidth: 300px; }\n');
+ })
+ });
+
+ it('sourceComments', () => {
+ return sassTransformer(`
+ body {
+ width: 300px;
+ }
+ `, {
+ sourceComments: true
+ }).then(res => {
+ expect(res).toEqual('/* line 2, stdin */\n' +
+ 'body {\n' +
+ ' width: 300px; }\n');
+ })
+ });
+ });
+
});
\ No newline at end of file
diff --git a/core/transformers/stylus.transformer.js b/core/transformers/stylus.transformer.js
index 3a4ce983..16eb4bf9 100644
--- a/core/transformers/stylus.transformer.js
+++ b/core/transformers/stylus.transformer.js
@@ -1,18 +1,16 @@
const stylus = require('stylus');
-const {promisify} = require('util');
-
-const render = promisify(stylus.render);
const defaultOptions = {
env: 'development',
file: null,
+ paths: []
}
async function stylusTransformer(content = null, opt = defaultOptions) {
if (content && typeof content === 'object') {
opt = content;
content = null;
-
+
if (!opt.file) {
throw new Error('If no string content is provided, the "file" option must be provided.')
}
@@ -21,13 +19,23 @@ async function stylusTransformer(content = null, opt = defaultOptions) {
opt = {...defaultOptions, ...opt};
content = content ?? '';
- return await render(content, {
- filename: opt.file?.fileAbsolutePath,
- // ...(opt.env === 'development' && {sourceMap: 'inline'}),
- })
- .then(css => {
- return css;
+ return await (new Promise((res, rej) => {
+
+ const styl = stylus(content, {
+ ...opt,
+ filename: opt.file?.fileAbsolutePath,
+ ...(opt.env === 'production' && {sourceMap: 'inline'}),
});
+
+
+ styl.render((err, css) => {
+ if (err) {
+ return rej(err);
+ }
+
+ res(css)
+ })
+ }))
}
module.exports.stylusTransformer = stylusTransformer;
diff --git a/core/utils/merge-objects.js b/core/utils/merge-objects.js
new file mode 100644
index 00000000..5fcd3255
--- /dev/null
+++ b/core/utils/merge-objects.js
@@ -0,0 +1,17 @@
+const {isObject, isArray} = require("util");
+
+function mergeObjects(a, b) {
+ if(!isObject(a) || !isObject(b)) return b ?? {};
+
+ const obj = isArray(a) ? [...a] : a;
+
+ for(const key in b) {
+ if(b.hasOwnProperty(key)) {
+ obj[key] = mergeObjects(obj[key], b[key]);
+ }
+ }
+
+ return obj;
+}
+
+module.exports.mergeObjects = mergeObjects;
diff --git a/core/utils/merge-objects.spec.js b/core/utils/merge-objects.spec.js
new file mode 100644
index 00000000..b514fd29
--- /dev/null
+++ b/core/utils/merge-objects.spec.js
@@ -0,0 +1,32 @@
+const {mergeObjects} = require('./merge-objects');
+
+describe('mergeObjects', () => {
+ it('should return second object if one is null or not an object', () => {
+ expect(mergeObjects(null, {b: 10})).toEqual({b: 10})
+ expect(mergeObjects(12, {b: 10})).toEqual({b: 10})
+ expect(mergeObjects('12', {b: 10})).toEqual({b: 10})
+ expect(mergeObjects(() => {}, {b: 10})).toEqual({b: 10})
+
+ expect(mergeObjects({a: 10}, null)).toEqual({})
+ });
+
+ it('should merge arrays', () => {
+ expect(mergeObjects([1, 2, 4], [1, 9, 0])).toEqual([1, 9, 0])
+ expect(mergeObjects([6, 2, 4], [9, 0])).toEqual([9, 0, 4])
+ expect(mergeObjects([6, [2, 4]], [9, [0, 9]])).toEqual([9, [0, 9]])
+ expect(mergeObjects([6, [2, [3, 1]]], [9, [0, 9]])).toEqual( [9, [0, 9]])
+ expect(mergeObjects([6, [2, 6]], [9, [0, [3, 1]]])).toEqual([9, [0, [3, 1]]])
+ expect(mergeObjects([6, [2, 6]], [9])).toEqual([9, [2, 6]])
+ expect(mergeObjects([6, [2, 6]], [9, [1]])).toEqual([9, [1, 6]])
+ });
+
+ it('should merge object literals', () => {
+ expect(mergeObjects({a: 12}, {b: 34})).toEqual({a: 12, b: 34})
+ expect(mergeObjects({a: 12}, {a: 34})).toEqual({a: 34})
+ expect(mergeObjects({a: [12, 67, 10]}, {a: [34, 90]})).toEqual({a: [34, 90, 10]})
+ expect(mergeObjects({a: 10}, {a: [34, 90]})).toEqual({a: [34, 90]})
+ expect(mergeObjects({a: [34, 90]}, {a: 10})).toEqual({a: 10})
+ expect(mergeObjects({a: {b: 100}}, {a: {c: 200, b: 13}})).toEqual({a: {b: 13, c: 200}})
+ expect(mergeObjects({a: {c: 200, b: 13}}, {a: {b: 100}})).toEqual({a: {b: 100, c: 200}})
+ });
+});
\ No newline at end of file
diff --git a/hp.config.js b/hp.config.js
new file mode 100644
index 00000000..4d76ce1c
--- /dev/null
+++ b/hp.config.js
@@ -0,0 +1,27 @@
+const homePage = require('./website/data/home.page');
+const documentationPage = require('./website/data/documentation.page');
+const learnPage = require('./website/data/learn.page');
+const site = require('./website/data/site.json');
+const packageJSON = require('./package.json');
+const {CodeSnippet} = require('./website/tags/code-snippet');
+
+const env = process.env.NODE_ENV || 'development';
+
+module.exports = {
+ env,
+ staticData: {
+ pages: {
+ documentation: documentationPage,
+ home: homePage,
+ learn: learnPage,
+ },
+ site: {
+ ...site,
+ version: packageJSON.version,
+ license: packageJSON.license
+ }
+ },
+ customTags: [
+ CodeSnippet,
+ ]
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index fec6d946..4c88f751 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@beforesemicolon/html-plus",
- "version": "0.4.6",
+ "version": "0.5.0",
"description": "HTML Template Engine/Language",
"main": "dist/index.js",
"files": [
diff --git a/tsconfig.json b/tsconfig.json
index 0d015b55..09529c52 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -73,6 +73,7 @@
"exclude": [
"./website",
"./docs",
+ "./hp.config.js",
"**/*.spec.js"
]
}
diff --git a/website/pages/_partials/_layout.html b/website/pages/_partials/_layout.html
index 59f434ab..0a0bf113 100644
--- a/website/pages/_partials/_layout.html
+++ b/website/pages/_partials/_layout.html
@@ -51,5 +51,13 @@
+
+