diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/README.md b/README.md index aced19c..1b2279f 100644 --- a/README.md +++ b/README.md @@ -63,15 +63,32 @@ src src Somewhere in gulpfile.js: ```javascript -const gulp = require('gulp'); -const plugins = require('gulp-load-plugins')(); -const jsonLoaderFactory = require('./lib/gulp-json-loader'); -const jsonLoader = jsonLoaderFactory({ - // sourcePath: __dirname, +// It is optional now, but you able to tune it as you wish. +// You can pass the settings by an object, or you can pass it using package.json +const jsonLoaderSettings = { + // Chose where the source files are located. + // Use sourcePath or the pare of pathHtml and pathData + + // sourcePath: 'src', pathHtml: 'src/html', pathData: 'src/data', + + // The namespace where the Data is located. + // To get some loaded data from the JSON in a PUG context use syntax: + // $.href or $.imports.menu + dataEntry: '$', + + // It needs for the Date object to show a local date + locales: 'en-GB', + + // Will report about the loaded JSON files report: true, -}); +}; + +const gulp = require('gulp'); +const plugins = require('gulp-load-plugins')(); +const jsonLoaderFactory = require('./lib/gulp-json-loader'); +const jsonLoader = jsonLoaderFactory(jsonLoaderSettings); function html() { return gulp.src('src/html/**/*.pug') @@ -108,17 +125,20 @@ block content //- - console.log(data) div= filename - div: a(href=data.href)= data.name + div: a(href = $.href)= $.name ul.genres - each Genre in data.imports.genres + each $GenreItem in $.imports.genres li - a(href=Genre.href)= Genre.name + a(href = $GenreItem.href)= $GenreItem.name ``` Run command to build html page with data ```bash $ gulp html + +# Or +$ npx gulp html ``` ### TODO diff --git a/changes.txt b/changes.txt new file mode 100644 index 0000000..fb5f279 --- /dev/null +++ b/changes.txt @@ -0,0 +1,24 @@ +v1.1.0: +- Updated core + - Imposed restrictions on accessing external directories. + - There is no need to pass the config data to GulpJsonLoader, now it's optional + and defined from defaults. But if you want to change them, you might tune it in + both ways: using an old-style passing an object with options to GulpJsonLoader + factory or just pass the settings into a package.json. See package.json. + +- Updated dependencies + - gulp-load-plugins: ^2.0.7 + - gulp-pug: ^5.0.0 + +- Improvement of variables names + - To distinguish variables from stylistic PAG syntax it is preferable to name + variables in a different way as opposed to decoration text. That's why I name + variables with a capital letter and a dollar sign at the beginning. + See PUG files to find out more about it. + + - The entry name of the Data loaded from the JSON file is defined in a global + space as a "data" entry, now you can change this name as you like. + See package.json and PUG files to find out more about it. + +- Implemented data caching + - The data that has already been loaded will be getting from the cache diff --git a/gulpfile.js b/gulpfile.js index 55a0bdf..176ca4f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,21 +2,16 @@ const gulp = require('gulp'); const plugins = require('gulp-load-plugins')(); -const jsonLoader = require('./lib/gulp-json-loader')({ - // sourcePath: __dirname, - pathHtml: 'src/html', - pathData: 'src/data', - report: true, -}); +const jsonLoader = require('./lib/gulp-json-loader')(); function html() { - return gulp.src('src/html/**/*.pug') - .pipe(plugins.data(jsonLoader)) - .pipe(plugins.pug({ - pretty: true - })) - .pipe(gulp.dest('dist')) - ; + return gulp.src('src/html/**/*.pug') + .pipe(plugins.data(jsonLoader)) + .pipe(plugins.pug({ + pretty: true + })) + .pipe(gulp.dest('dist')) + ; } exports.html = html; diff --git a/lib/gulp-json-loader.js b/lib/gulp-json-loader.js index f2a10e8..310fa95 100644 --- a/lib/gulp-json-loader.js +++ b/lib/gulp-json-loader.js @@ -1,155 +1,226 @@ 'use strict'; -const Format = require('util').format; +const Util = require('util'); const Path = require('path'); const Fs = require('fs'); -const asyncReadFile = Fs.promises.readFile; -const BR = (process.platform === 'win32' ? '\r\n' : '\n'); -async function loadJsonData(file) { - const { report, pathHtml, pathData } = this; - const pathJson = file.path.replace(pathHtml, `${pathData}/pages`).slice(0,-3) + 'json'; - const filename = Path.basename(file.path, '.pug'); - const pocket = { filename }; - - return Fs.promises.access(pathJson, Fs.constants.R_OK) - .then(async () => { - let jsonData = {}; - - try { - jsonData = await asyncReadFile(pathJson, 'utf8'); - jsonData = JSON.parse(jsonData); - } catch(err) { - process.stderr.write(err + BR); - return pocket; - } - - report && reportAction(pathJson, filename); - - if (jsonData.hasOwnProperty('imports')) { - if (!jsonData.hasOwnProperty('data')) { - jsonData.data = {}; - } - - Object.defineProperty(jsonData.data, 'imports', { - value: await loadImports.call(this, jsonData.imports), - enumerable: true, - // configurable: true - }); - delete jsonData.imports; - } - - return Object.assign(pocket, jsonData); - }) - .catch(() => pocket) - ; -} - -async function loadImports(imports) { - const { report, pathHtml, pathData } = this; - const data = {}; - - if (Array.isArray(imports) && imports.length) { - const pathFile = imports.shift(); - const pathJson = `${pathData}/imports/${pathFile}.json`; - const filename = Path.basename(pathJson, '.json'); - - try { - - const jsonData = await asyncReadFile(pathJson, 'utf8'); - const parsedData = JSON.parse(jsonData); +const APP_PATH = process.cwd(); +const DEFAULT_SOURCE_PATH = './src'; +const BREAK_LINE = process.platform === 'win32' ? '\r\n' : '\n'; +const ERR_TOP_DIR = 'Working with top-level directories is prohibited!'; - Object.defineProperty(data, filename, { - value: parsedData, - enumerable: true - }); +const CachedData = {}; +const BrushColors = { + red: 1, + cyan: 6, + grey: 8 +}; - } catch(err) { +const getAbsolutePath = (path, subdir = '') => { + if (path.startsWith('/')) { + path = '.' + path; + } - process.stderr.write(err + BR); - - } - - report && reportAction(pathJson, filename); - - if (imports.length) { - // TODO: Find another solution with asynchronous queue. - // - // I know that recursion is a bad practice with its excessive - // consumption of resources but unfortunately, I didn't find - // any other solution by now - const jsonData = await loadImports.call(this, imports); - Object.assign(data, jsonData); - } - } - - return data; -} + path = Path.join(APP_PATH, path); -function reportAction(pathJson, filename) { - const currTime = brush('grey', new Date().toLocaleTimeString()); - const relativePath = pathJson - .replace(process.cwd(), '.') - .replace(filename, brush('cyan', filename)) - ; - process.stdout.write(Format('[%s] Loaded %s%s', currTime, relativePath, BR)); + return subdir + ? Path.join(path, subdir) + : path; } /** * brush * Adding the ANSI escape codes to a textual data * @see https://en.wikipedia.org/wiki/ANSI_escape_code - * + * * @param {string} colorName Color name * @param {string} str Text data */ -function brush(colorName, str) { - const colors = { - cyan: 6, - grey: 8 - } - const color = colors[colorName]; - return `\x1b[38;5;${color}m${str}\x1b[0m`; +const brush = (colorName, str) => { + const color = BrushColors[colorName]; + return `\x1b[38;5;${color}m${str}\x1b[0m`; } -function getAbsolutePath(sourcePath, postfix='') { - let absolutePath = sourcePath; +const reportAction = (ctx, pathJson, filename, action) => { + const currTime = new Date().toLocaleString(ctx.locales, { + timeStyle: 'medium', + }); + + const coloredCurrentTime = brush('grey', currTime); + const coloredRelativePath = pathJson + .replace(APP_PATH, '.') + .replace(filename, brush('cyan', filename)); + + if (action === 'Cached') { + action = brush('grey', action); + } + + const message = Util.format( + '[%s] %s %s%s', + coloredCurrentTime, + action, + coloredRelativePath, + BREAK_LINE + ); + + process.stdout.write(message); +} - if (absolutePath.substr(0, 2) === './') { - absolutePath = absolutePath.slice(2); +const loadImportsAsync = (ctx, imports) => { + return new Promise((resolve, reject) => { + if (!Array.isArray(imports)) { + reject(new Error('Imports should be an Array')); + } else { + const storage = {}; + loadImportsRecursively({ ctx, imports, storage, resolve, reject }); } + }); +}; - if (absolutePath[0] !== '/') { - absolutePath = process.cwd() + '/' + absolutePath; - } +const loadImportsRecursively = async (options) => { + const { ctx, imports, storage, resolve, reject, idx = 0 } = options; + const { report, pathData } = ctx; - return absolutePath + postfix; -} + if (idx < imports.length) { + let action = 'Cached'; -function manager(options) { - let opts = {}; + const jsonRelativePath = imports[idx] + '.json'; + const jsonFullPath = Path.join(pathData, 'imports', jsonRelativePath); + const jsonFilename = Path.basename(jsonFullPath, '.json'); + const cacheKey = 'imports:' + jsonFullPath.replace(APP_PATH, ''); - if (!(typeof(options) == 'object' && options !== null)) { - throw new Error('options is required'); + if (!jsonFullPath.startsWith(APP_PATH)) { + reject(new Error(ERR_TOP_DIR)); + return; } - opts.report = options.hasOwnProperty('report') ? options.report : false; + if (!CachedData.hasOwnProperty(cacheKey)) { + try { - if (options.hasOwnProperty('pathHtml') && options.hasOwnProperty('pathData')) { - opts.pathHtml = getAbsolutePath(options.pathHtml); - opts.pathData = getAbsolutePath(options.pathData); - } + const jsonData = await Fs.promises.readFile(jsonFullPath, 'utf8'); + + CachedData[cacheKey] = JSON.parse(jsonData); + action = 'Loaded'; - else if (options.hasOwnProperty('sourcePath')) { - opts.pathHtml = getAbsolutePath(options.sourcePath, '/src/html'); - opts.pathData = getAbsolutePath(options.sourcePath, '/src/data'); + } catch (error) { + + reject(error); + return; + + } } - else { - throw new Error('required "sourcePath" parameter or pare of "pathHtml" and "pathData" parameters'); + report && reportAction(ctx, jsonFullPath, jsonFilename, action); + + Object.defineProperty(storage, jsonFilename, { + value: CachedData[cacheKey], + enumerable: true + }); + + options.idx = idx + 1; + loadImportsRecursively(options); + + } else { + resolve(storage); + } +} + +async function loadJsonData(file) { + const { report, pathHtml, pathData, dataEntry } = this; + + const pathJson = file.path + .replace(pathHtml, `${pathData}/pages`) + .slice(0, -3) + 'json'; + + const cacheKey = 'data:' + pathJson.replace(APP_PATH, ''); + + const filename = Path.basename(file.path, '.pug'); + const pocket = { filename }; + + if (CachedData.hasOwnProperty(pathJson)) { + report && reportAction(this, pathJson, filename, 'Cached'); + return CachedData[cacheKey]; + } + + let jsonData = ''; + + try { + jsonData = await Fs.promises.readFile(pathJson, 'utf8'); + jsonData = JSON.parse(jsonData); + } catch (err) { + return pocket; + } + + report && reportAction(this, pathJson, filename, 'Loaded'); + + if (jsonData.hasOwnProperty('imports')) { + const { data = {}, imports = [] } = jsonData; + + try { + + const importedData = await loadImportsAsync(this, imports); + + Object.defineProperty(data, 'imports', { + value: importedData, + enumerable: true, + }); + + } catch (err) { + + const coloredErrorMesage = brush('red', err.message); + process.stderr.write(coloredErrorMesage + BREAK_LINE); + + return; + } - return loadJsonData.bind(opts); + Object.defineProperty(pocket, dataEntry, { + value: data, + enumerable: true, + }); + } + + CachedData[cacheKey] = pocket; + + return pocket; +} + +const factory = (options) => { + if (options === undefined) { + const packagePath = Path.join(APP_PATH, 'package.json'); + const Package = require(packagePath); + + options = Package.hasOwnProperty(Package.name) + ? Package[Package.name] + : {}; + } + + const { report = true, locales = 'en-EN', dataEntry = 'data' } = options; + + const sourcePath = options.hasOwnProperty('sourcePath') + ? options.sourcePath + : DEFAULT_SOURCE_PATH; + + const pathHtml = options.hasOwnProperty('pathHtml') + ? getAbsolutePath(options.pathHtml) + : getAbsolutePath(sourcePath, 'html'); + + const pathData = options.hasOwnProperty('pathData') + ? getAbsolutePath(options.pathData) + : getAbsolutePath(sourcePath, 'data'); + + if (!pathHtml.startsWith(APP_PATH) || !pathData.startsWith(APP_PATH)) { + // Please put your source files into your project directory. + throw new Error(ERR_TOP_DIR); + } + + return loadJsonData.bind({ + pathHtml, + pathData, + dataEntry, + locales, + report, + }); } -module.exports = manager; +module.exports = factory; diff --git a/package.json b/package.json index b8d71d4..68e13fe 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,23 @@ "name": "gulp-json-loader", "description": "A little tool for the gulp-data plugin. It useful for data loading in the development of HTML or the pug pages", "author": "Alexander Yukal ", - "version": "1.0.0", + "version": "1.1.0", "main": "gulpfile.js", "devDependencies": { "gulp": "^4.0.2", "gulp-data": "^1.3.1", - "gulp-load-plugins": "^2.0.3", - "gulp-pug": "^4.0.1" + "gulp-load-plugins": "^2.0.7", + "gulp-pug": "^5.0.0" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, + "gulp-json-loader": { + "sourcePath": "src", + "dataEntry": "$", + "locales": "ru-UA", + "report": true + }, "repository": { "type": "git", "url": "git+https://github.com/yukal/gulp-json-loader.git" diff --git a/src/data/imports/catalog/catalog_1.json b/src/data/imports/catalog/catalog_1.json index 859bf18..79dbdf3 100644 --- a/src/data/imports/catalog/catalog_1.json +++ b/src/data/imports/catalog/catalog_1.json @@ -1,32 +1,32 @@ [ - { - "img": "book-01.png", - "title": "Duel", - "info": "Prose, 1986 р.", - "href": "#" - }, - { - "img": "book-02.png", - "title": "Vita Nova", - "info": "Prose, 1957 р.", - "href": "#" - }, - { - "img": "book-03.jpg", - "title": "The Ukrainian Folk Dance", - "info": "Folklore, 1980 р.", - "href": "#" - }, - { - "img": "book-04.jpg", - "title": "The Moscow Games of 1980", - "info": "Politology, 1981 р.", - "href": "#" - }, - { - "img": "book-05.jpg", - "title": "Young Ukraine", - "info": "Periodicals, 1965 р.", - "href": "#" - } + { + "img": "book-01.png", + "title": "Duel", + "info": "Prose, 1986 р.", + "href": "#" + }, + { + "img": "book-02.png", + "title": "Vita Nova", + "info": "Prose, 1957 р.", + "href": "#" + }, + { + "img": "book-03.jpg", + "title": "The Ukrainian Folk Dance", + "info": "Folklore, 1980 р.", + "href": "#" + }, + { + "img": "book-04.jpg", + "title": "The Moscow Games of 1980", + "info": "Politology, 1981 р.", + "href": "#" + }, + { + "img": "book-05.jpg", + "title": "Young Ukraine", + "info": "Periodicals, 1965 р.", + "href": "#" + } ] \ No newline at end of file diff --git a/src/data/imports/catalog/catalog_2.json b/src/data/imports/catalog/catalog_2.json index edda093..71767d3 100644 --- a/src/data/imports/catalog/catalog_2.json +++ b/src/data/imports/catalog/catalog_2.json @@ -1,32 +1,32 @@ [ - { - "img": "book-06.jpg", - "title": "Theophanes daughter", - "info": "Prose, 1946 р.", - "href": "#" - }, - { - "img": "book-07.jpg", - "title": "Love story", - "info": "Prose, 1947 р.", - "href": "#" - }, - { - "img": "book-08.jpg", - "title": "Old world nobility", - "info": "History, 1999 р.", - "href": "#" - }, - { - "img": "book-09.png", - "title": "Luzes na agua", - "info": "Prose, 1997 р.", - "href": "#" - }, - { - "img": "book-10.png", - "title": "In the arms of Melpomene", - "info": "Prose, 1954 р.", - "href": "#" - } + { + "img": "book-06.jpg", + "title": "Theophanes daughter", + "info": "Prose, 1946 р.", + "href": "#" + }, + { + "img": "book-07.jpg", + "title": "Love story", + "info": "Prose, 1947 р.", + "href": "#" + }, + { + "img": "book-08.jpg", + "title": "Old world nobility", + "info": "History, 1999 р.", + "href": "#" + }, + { + "img": "book-09.png", + "title": "Luzes na agua", + "info": "Prose, 1997 р.", + "href": "#" + }, + { + "img": "book-10.png", + "title": "In the arms of Melpomene", + "info": "Prose, 1954 р.", + "href": "#" + } ] \ No newline at end of file diff --git a/src/data/imports/genres.json b/src/data/imports/genres.json index 58871bb..b0ae7c4 100644 --- a/src/data/imports/genres.json +++ b/src/data/imports/genres.json @@ -1,26 +1,26 @@ [ - { - "href": "#", - "name": "Ucrainica" - }, - { - "href": "#", - "name": "Childrens literature" - }, - { - "href": "#", - "name": "History" - }, - { - "href": "#", - "name": "Religion" - }, - { - "href": "#", - "name": "Art" - }, - { - "href": "#", - "name": "Miscellaneous" - } + { + "href": "#", + "name": "Ucrainica" + }, + { + "href": "#", + "name": "Childrens literature" + }, + { + "href": "#", + "name": "History" + }, + { + "href": "#", + "name": "Religion" + }, + { + "href": "#", + "name": "Art" + }, + { + "href": "#", + "name": "Miscellaneous" + } ] \ No newline at end of file diff --git a/src/data/imports/menu.json b/src/data/imports/menu.json index 5e2b471..5fb300f 100644 --- a/src/data/imports/menu.json +++ b/src/data/imports/menu.json @@ -1,32 +1,32 @@ [ - { - "name": "Catalog", - "href": "catalog.html", - "visible": true, - "active": true - }, - { - "name": "About Us", - "href": "about-us.html", - "visible": true, - "active": false - }, - { - "name": "Donate", - "href": "#donate", - "visible": true, - "active": false - }, - { - "name": "Contacts", - "href": "#contacts", - "visible": false, - "active": false - }, - { - "name": "RSS", - "href": "http://domain.zone/feed/", - "visible": true, - "active": false - } + { + "name": "Catalog", + "href": "catalog.html", + "visible": true, + "active": true + }, + { + "name": "About Us", + "href": "about-us.html", + "visible": true, + "active": false + }, + { + "name": "Donate", + "href": "#donate", + "visible": true, + "active": false + }, + { + "name": "Contacts", + "href": "#contacts", + "visible": false, + "active": false + }, + { + "name": "RSS", + "href": "http://domain.zone/feed/", + "visible": true, + "active": false + } ] \ No newline at end of file diff --git a/src/data/pages/about.json b/src/data/pages/about.json index 55b13af..cf35ae0 100644 --- a/src/data/pages/about.json +++ b/src/data/pages/about.json @@ -1,10 +1,10 @@ { - "data": { - "name": "About Us", - "href": "about-us.html", - "visible": true - }, - "imports": [ - "genres" - ] + "data": { + "name": "About Us", + "href": "about-us.html", + "visible": true + }, + "imports": [ + "genres" + ] } \ No newline at end of file diff --git a/src/data/pages/menu.json b/src/data/pages/menu.json index b4e708e..129d395 100644 --- a/src/data/pages/menu.json +++ b/src/data/pages/menu.json @@ -1,10 +1,10 @@ { - "data": { - "name": "Menu", - "href": "menu.html", - "visible": true - }, - "imports": [ - "menu" - ] + "data": { + "name": "Menu", + "href": "menu.html", + "visible": true + }, + "imports": [ + "menu" + ] } \ No newline at end of file diff --git a/src/data/pages/partials/catalog.json b/src/data/pages/partials/catalog.json index 81de84f..a39204d 100644 --- a/src/data/pages/partials/catalog.json +++ b/src/data/pages/partials/catalog.json @@ -1,11 +1,11 @@ { - "data": { - "name": "Catalog", - "href": "#catalog", - "visible": true - }, - "imports": [ - "catalog/catalog_1", - "catalog/catalog_2" - ] + "data": { + "name": "Catalog", + "href": "#catalog", + "visible": true + }, + "imports": [ + "catalog/catalog_1", + "catalog/catalog_2" + ] } \ No newline at end of file diff --git a/src/html/about.pug b/src/html/about.pug index 91e197c..af003d5 100644 --- a/src/html/about.pug +++ b/src/html/about.pug @@ -1,10 +1,10 @@ block content - //- - console.log(data) + //- - console.log($) - div= filename - div: a(href=data.href)= data.name + div= filename + div: a(href = $.href)= $.name - ul.genres - each g in data.imports.genres - li - a(href=g.href)= g.name + ul.genres + each $GenreItem in $.imports.genres + li + a(href = $GenreItem.href)= $GenreItem.name diff --git a/src/html/menu.pug b/src/html/menu.pug index 2d1dead..237bb06 100644 --- a/src/html/menu.pug +++ b/src/html/menu.pug @@ -1,15 +1,15 @@ block content - //- - console.log(data) + //- - console.log($) - div= filename - div: a(href=data.href)= data.name + div= filename + div: a(href = $.href)= $.name - ul.menu + ul.menu - each m in data.imports.menu - if m.active - li.nav-item.active - a.nav-link(href=m.href)= m.name - else - li.nav-item - a.nav-link(href=m.href)= m.name + each $MenuItem in $.imports.menu + if $MenuItem.active + li.nav-item.active + a.nav-link(href = $MenuItem.href)= $MenuItem.name + else + li.nav-item + a.nav-link(href = $MenuItem.href)= $MenuItem.name diff --git a/src/html/partials/catalog.pug b/src/html/partials/catalog.pug index e8b95ec..d4c82ce 100644 --- a/src/html/partials/catalog.pug +++ b/src/html/partials/catalog.pug @@ -1,25 +1,25 @@ block content - //- - console.log(data) + //- - console.log($) - div= filename - div: a(href=data.href)= data.name + div= filename + div: a(href = $.href)= $.name - .catalog-1 + .catalog-1 - each c in data.imports.catalog_1 - .catalog-item - a.catalog-img-link(href=c.href) - img(src="assets/img/"+c.img) - .catalog-info - p.catalog-text= c.info - a.catalog-title(href=c.href)= c.title + each $CatalogItem in $.imports.catalog_1 + .catalog-item + a.catalog-img-link(href = $CatalogItem.href) + img(src = "assets/img/" + $CatalogItem.img) + .catalog-info + p.catalog-text= $CatalogItem.info + a.catalog-title(href = $CatalogItem.href)= $CatalogItem.title - .catalog-2 + .catalog-2 - each c in data.imports.catalog_2 - .catalog-item - a.catalog-img-link(href=c.href) - img(src="assets/img/"+c.img) - .catalog-info - p.catalog-text= c.info - a.catalog-title(href=c.href)= c.title + each $CatalogItem in $.imports.catalog_2 + .catalog-item + a.catalog-img-link(href = $CatalogItem.href) + img(src = "assets/img/" + $CatalogItem.img) + .catalog-info + p.catalog-text= $CatalogItem.info + a.catalog-title(href = $CatalogItem.href)= $CatalogItem.title diff --git a/src/html/without_data.pug b/src/html/without_data.pug index 7cbe449..6ae5034 100644 --- a/src/html/without_data.pug +++ b/src/html/without_data.pug @@ -1,11 +1,11 @@ block content - - var local_data = 'local data' + - var local_data = 'local data' - p This content is not provided of any external data from JSON files - p instead it provides a - strong #{local_data} + p This content is not provided of any external data from JSON files + p instead it provides a + strong #{local_data} - p. - To provide an external data from JSON file you have to create - a JSON file in "src/data/pages" with same name as that file - (src/data/pages/without_data.json) + p. + To provide an external data from JSON file you have to create + a JSON file in "src/data/pages" with same name as that file + (src/data/pages/without_data.json)