Skip to content

Commit

Permalink
Merge pull request #6 from beforesemicolon/builder
Browse files Browse the repository at this point in the history
Builder
  • Loading branch information
ECorreia45 authored Jul 5, 2021
2 parents 6cad124 + be287c4 commit a4b547d
Show file tree
Hide file tree
Showing 85 changed files with 1,638 additions and 439 deletions.
1 change: 1 addition & 0 deletions .github/workflows/mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ jobs:
# Runs a single command using the runners shell
- name: Test code
run: npm run tests

69 changes: 41 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,37 @@

HTML Template Language

![html+](https://img.shields.io/badge/beforesemicolon-html%2B-blue)
![Build](https://github.com/beforesemicolon/html-plus//actions/workflows/main.yml/badge.svg)
![license](https://img.shields.io/github/license/beforesemicolon/html-plus)
![npm](https://img.shields.io/npm/v/@beforesemicolon/html-plus)
![Build](https://github.com/beforesemicolon/html-plus/actions/workflows/mac.yml/badge.svg)
![Build](https://github.com/beforesemicolon/html-plus/actions/workflows/linux.yml/badge.svg)

## Simply HTML and much more
```html
<variable name="page" value="$data.pages.home"></variable>

<include partial="layout" data="page">
<!-- reference SASS, LESS, and STYLUS files directly -->
<link rel="stylesheet" href="./home.scss" inject-id="style">
<link rel="stylesheet" href="./../node_modules/material/styles/theme.css" inject-id="style">

<include partial="header"></include>

<section role="banner" class="wrapper">
<h2>{$data.site.description}</h2>
<p>{page.banner.description}</p>
<div class="doc-links">
<a #repeat="page.banner.links"
#attr="class, cta, $item.path === '/learn'"
href="{$item.path}" class="link-button">{$item.label}</a>
</div>
</section>

<!-- reference typescript files directly -->
<link rel="stylesheet" href="./home.ts" inject-id="script">

</include>
```

## Install
Install the engine inside your project directory.
Expand All @@ -27,12 +54,10 @@ const app = express();

// initialize the engine by passing the express app
// and the absolute path to the HTML pages directory
// everything else is taken care of for you
// from routing to processing linked files on your pages
engine(app, path.resolve(__dirname, './pages'));

app.get('/', function (req, res) {
res.render('index'); // render the page file name
})

const server = http.createServer(app);

server.listen(3000, () => {
Expand Down Expand Up @@ -61,20 +86,18 @@ The way you organize your page structure will be used to create your website rou
## Template Tags & Attributes
HTML+ comes with couple of built-in tags that are meant to aid you with your pages. These are:

* **[include]()**: lets you include reusable partial html parts
* **[inject]()**: lets you inject html into partial files. Works like html slot
* **[variable]()**: lets you create scope data inside your template
* **[fragment]()**: lets you exclude the wrapping tag from rendering as a place to add logic
* **[include](https://html-plus.beforesemicolon.com/documentation/api-reference/include-tag)**: lets you include reusable partial html parts
* **[inject](https://html-plus.beforesemicolon.com/documentation/api-reference/inject-tag)**: lets you inject html into partial files. Works like html slot
* **[variable](https://html-plus.beforesemicolon.com/documentation/api-reference/variable-tag)**: lets you create scope data inside your template
* **[fragment](https://html-plus.beforesemicolon.com/documentation/api-reference/fragment-tag)**: lets you exclude the wrapping tag from rendering as a place to add logic

There are also some built-in attributes that let you control your tags even further. These are:

* **[if]()**: lets you conditionally render a tag
* **[repeat]()**: allows you to specify how the tag repeats bases on data you provide
* **[fragment]()**: has the same purpose as the tag version of it
* **[inject]()**: lets you flags the html you need to inject. works like the slot attribute
* **[compiler]()**: lets you specify the compiler for style and script tags like typescript and SASS.
* **[if](https://html-plus.beforesemicolon.com/documentation/api-reference/if-attribute)**: lets you conditionally render a tag
* **[repeat](https://html-plus.beforesemicolon.com/documentation/api-reference/repeat-attribute)**: allows you to specify how the tag repeats bases on data you provide
* **[fragment](https://html-plus.beforesemicolon.com/documentation/api-reference/fragment-attribute)**: has the same purpose as the tag version of it

These list have the potential to grow but you can also [create your own tags and attributes]()
These list have the potential to grow but you can also [create your own tags and attributes](https://html-plus.beforesemicolon.com/documentation/advanced-templating)
that fits your project. You can come up with your own rules and behavior for the template and this
is what makes HTML+ more appealing. It allows you to extend the template easily

Expand All @@ -93,21 +116,11 @@ All data is scoped and immutable. This is done using curly braces. Take the foll
You can reference the `post.json` file inside your template like so.

```html
<div #repeat="posts as post">
<div #repeat="$data.posts as post">
<h2>{post.title}</h2>
<p>{post.description}</p>
</div>
```

For special attributes you don't need the curly braces to bind data, but everywhere else you need to wrap
your data reference inside curly braces. [Check full DOC to learn more]().


## Contributing to this Project
Anyone can help this project grow by using and reporting issues to be addressed.

You can also fork the project and jump into code addressing reported issues, improving code and tests altogether. By doing so, you must follow the following rules:
* Fixing an issue in code must be followed by test updates or new tests that test your solution;
* Improving code quality is always welcomed and when necessary, comment accordingly;
* Any breaking change or new feature must be first reported as an issue with "new feature" or "proposal" tags and can be added to the milestone and project plan to be addressed;
* Replacing current packages used or creating custom code to address things is super encouraged and preferred where it makes sense.
your data reference inside curly braces. [Check full DOC to learn more](https://html-plus.beforesemicolon.com/documentation/).
50 changes: 42 additions & 8 deletions core/File.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
const path = require('path');
const {required} = require("./utils/required");
const {readFileContent} = require("./utils/readFileContent");
const {defineGetter} = require("./utils/define-getter");

class File {
resources = [];
resourceBase = '';
#content = '';
#loaded = false;
#srcDirectoryPath = '';
#ext = '';
#file = '';
#name = '';
#filePath = '';
#fileAbsolutePath = '';
#fileDirectoryPath = '';

constructor(itemPath = required('itemPath'), src = '') {
const file = path.basename(itemPath);
Expand All @@ -16,13 +22,41 @@ class File {
src = src || (itemPath.replace(file, ''));
itemPath = path.resolve(src, itemPath);

defineGetter(this, 'srcDirectoryPath', src);
defineGetter(this, 'ext', ext);
defineGetter(this, 'file', file);
defineGetter(this, 'name', file.replace(ext, ''));
defineGetter(this, 'filePath', itemPath.replace(src, ''));
defineGetter(this, 'fileAbsolutePath', itemPath);
defineGetter(this, 'fileDirectoryPath', itemPath.replace(file, ''));
this.#srcDirectoryPath = src;
this.#ext = ext;
this.#file = file;
this.#name = file.replace(ext, '');
this.#filePath = itemPath.replace(src, '');
this.#fileAbsolutePath = itemPath;
this.#fileDirectoryPath = itemPath.replace(file, '');
}

get srcDirectoryPath() {
return this.#srcDirectoryPath;
}

get ext() {
return this.#ext;
}

get file() {
return this.#file;
}

get name() {
return this.#name;
}

get filePath() {
return this.#filePath;
}

get fileAbsolutePath() {
return this.#fileAbsolutePath;
}

get fileDirectoryPath() {
return this.#fileDirectoryPath;
}

get content() {
Expand Down
2 changes: 1 addition & 1 deletion core/PartialFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class PartialFile extends File {

render(contextData = {}) {
const parsedHTML = parse(replaceSpecialCharactersInHTML(this.content), {
comment: true
comment: this.options.env === 'development'
});
parsedHTML.context = contextData;
const partialNode = new HTMLNode(parsedHTML, this.options);
Expand Down
164 changes: 164 additions & 0 deletions core/builder/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
const {File} = require('./../File');
const fs = require('fs');
const {mkdir, rmdir, copyFile, writeFile} = require('fs/promises');
const path = require('path');
const chalk = require("chalk");
const {processPageResource} = require("./utils/process-page-resource");
const {processPage} = require("./utils/process-page");
const {collectFilePaths} = require("./utils/collect-file-paths");
const {getDirectoryFilesDetail} = require("../utils/getDirectoryFilesDetail");

const defaultOptions = {
// absolute path to the directory containing the page, style, script and asset files
srcDir: '',
// absolute path to the directory where all assets and transformed page and files
destDir: '',
// an object containing static data for all pages
staticData: null,
// a callback function called with page absolute path and the corresponding path and must return a context data for the page
contextDataProvider: null,
// an array of custom tags to be passed to the pages
customTags: [],
// an array of custom attributes to be passed to the pages
customAttributes: [],
// an array of templates ({path: string, dataList: object[], dataProvider: fn, count: number}) to build based on their context data list
// the equivalent on dynamic routes that use same template
templates: []
};
let resources = {};
let partials = [];
let pages = [];

async function build(options = defaultOptions) {
options = {...defaultOptions, ...options, env: 'production'};

if (!options.srcDir) {
throw new Error('The build option "srcDir" is required to find all assets, partials and resources linked to the template.')
}

if (!fs.existsSync(options.srcDir)) {
throw new Error('Source directory not found: ' + options.srcDir)
}

resources = {};
partials = [];
pages = [];
console.time(chalk.cyan('\ntotal duration'));
console.log(chalk.blue('\nReading source directory'));
console.time(chalk.blue('reading duration'));
return getDirectoryFilesDetail(options.srcDir, collectFilePaths(options.srcDir, {partials, pages, resources}))
.then(async () => {
console.timeEnd(chalk.blue('reading duration'));
// clear previous destination directory
if (fs.existsSync(options.destDir)) {
await rmdir(options.destDir, {recursive: true});
}

// create destination directory with all essential subdirectories
await mkdir(options.destDir);
await mkdir(path.join(options.destDir, 'stylesheets'));
await mkdir(path.join(options.destDir, 'scripts'));
await mkdir(path.join(options.destDir, 'assets'));

const pageResources = {};

console.log(chalk.cyan('\nBuilding static pages...'));
console.time(chalk.cyan('build duration'));
await Promise.all(
pages.map(async page => {
const pageRoutePath = page
.replace(options.srcDir, '')
.replace(/\/index\.html$/, '')
.replace(/\.html$/, '') || '/';

const logMsg = chalk.green(`${pageRoutePath} `).padEnd(75, '-');
console.time(logMsg);

const contextData = typeof options.contextDataProvider === 'function'
? options.contextDataProvider({file: page, path: pageRoutePath})
: {}

await handleProcessedPageResult(
processPage(page, path.basename(page), resources, {...options, contextData, partials}),
pageResources,
options
);

console.timeEnd(logMsg);
})
)

console.timeEnd(chalk.cyan('build duration'));

if (options.templates.length) {
console.log(chalk.cyan('\nBuilding dynamic pages...'));
console.time(chalk.cyan('build duration'));
for (let template of options.templates) {
for (let [filePath, contextData] of template.dataList) {
const logMsg = chalk.green(`${filePath} `).padEnd(75, '-');
console.time(logMsg);
let fileName = path.basename(filePath);

if (!fileName.endsWith('.html')) {
fileName += '.html';
filePath += '.html';
}

await handleProcessedPageResult(
processPage(template.path, fileName, resources, {...options, contextData, partials}, filePath),
pageResources,
options,
filePath
);

console.timeEnd(logMsg);
}
}
console.timeEnd(chalk.cyan('build duration'));
}

console.log(chalk.greenBright('\nProcessing pages connected resources...'));
console.time(chalk.greenBright('processing duration'));
await Promise.all(
Object.values(pageResources).map(resource => {
console.log(resource.srcPath.replace(process.cwd(), ''));
return processPageResource(resource, options.destDir, resources)
})
)
console.timeEnd(chalk.greenBright('processing duration'));

console.timeEnd(chalk.cyan('\ntotal duration'));
})
.catch(async e => {
await rmdir(options.destDir, {recursive: true});
throw e;
})
}

async function handleProcessedPageResult({content, linkedSources, file}, pageResources, options, filePath) {
linkedSources.forEach(rsc => {
pageResources[rsc.srcPath] = rsc;
});

let pageDestPath = '';
let pageDir = ''

if (filePath) {
pageDestPath = path.join(options.destDir, filePath);
pageDir = path.join(
options.destDir,
filePath.replace(path.basename(filePath), '')
);
} else {
pageDestPath = path.join(options.destDir, file.filePath);
pageDir = path.join(
options.destDir,
file.fileDirectoryPath.replace(options.srcDir, '')
)
}

await mkdir(pageDir, {recursive: true});
await writeFile(pageDestPath, content);
}

module.exports.build = build;
Loading

0 comments on commit a4b547d

Please sign in to comment.