Giter Site home page Giter Site logo

abelljs / abell-renderer Goto Github PK

View Code? Open in Web Editor NEW
42.0 4.0 9.0 673 KB

A template engine that lets you write variables, loops, and conditions in HTML using JavaScript Syntax.

Home Page: http://npmjs.com/package/abell-renderer

License: MIT License

JavaScript 100.00%
template-engine abell abell-renderer

abell-renderer's Introduction

Abell renderer cover

NOT READY FOR PRODUCTION USE

GitHub package.json version    Twitter profile badge of @abellland



Warning

This package is deprecated and most of its code is moved inside the core package - abelljs/abell.

A template parser that lets you use JavaScript syntax to render loops, conditions, do maths, and require JSONs from HTML. Low level library used in abelljs/abell

🚀 Installation & Usage

Executing directly with npx

npx abell-renderer build --input src/index.abell --output dist/index.html

or

npm install -g abell-renderer
abell-renderer build --input src/index.abell --output dist/index.html

Check out Abell Template Guide for how to write .abell files.

📘 Abell Template Guide

.abell files are nothing but .html files which can contain JavaScript inside double curly brackets {{ and }}.

Note that abell-renderer renders abell files in NodeJS context which means you cannot access DOM inside brackets.

Simplest example of .abell file can look like:

{{ const siteTitle = "Abell Demo" }}
<html>
  <head>
    <title>{{ siteTitle }}</title>
  </head>
  <body>
    {{
      const a = 3;
      const b = 5;
    }}
    <h1>{{ siteTitle.toUpperCase() }}</h1>
    <div>Addition of {{ a }} and {{ b }} is {{ a + b }}</div>
  </body>
</html>

All the JavaScript inside curly brakets will be rendered on virtual instance on NodeJS and you will get the output as completely renderer .html file:

<html>
  <head>
    <title>Abell Demo</title>
  </head>
  <body>
    <h1>ABELL DEMO</h1>
    <div>Addition of 3 and 5 is 8</div>
  </body>
</html>

➿ Loops in Abell

You can use JavaScript Array methods to loop over array. Other JavaScript Array methods like .filter, .map, .reduce can be used as well.

{{
  const users = [
    {name: 'Saurabh', age: 20},
    {name: 'John Doe', age: 78}
  ]
}}

<main>
  {{
    users.map(user => `
      <div>
        <h2>${user.name}</h2>
        <span>Age: ${user.age}</span>
      </div>
    `).join('')
  }}
</main>

/*
Ouputs:

<main>
  <div>
    <h2>Saurabh</h2>
    <span>Age: 20</span>
  </div>
  <div>
    <h2>John Doe</h2>
    <span>Age: 78</span>
  </div>
</main>

⤵️ Import JS/JSON/NPM Modules

NOTE: Starting v0.1.10 require() can only be used when allowRequire: true is passed from options or --allow-require flag is passed in CLI

With Abell you can import your Native NodeJS Modules, NPM Modules, JS Files (should export data), and JSON Files with require()

{{ const MarkdownIt = require('markdown-it') }}
<!-- NPM Module to convert markdown to HTML (npm install --save markdown-it) -->

{{ const md = new MarkdownIt(); }}
<!DOCTYPE html>
<html>
  <body>
    {{ md.render("[Open Google](https://google.com)") }}
  </body>
</html>

/*
Outputs:

<!DOCTYPE html>
<html>
  <body>
    <p><a href="https://google.com">Open Google</a></p>
  </body>
</html>
*/

Note: fs module or any module that deals with external files cannot be used. The only way to read any external file is require()

💛 JavaScript API

npm install --save-dev abell-renderer
const abellRenderer = require('abell-renderer');

const sandbox = {
  nameObjects: [{ name: 'Nice' }, { name: 'very cool' }],
  globalMeta: {
    siteName: 'Abell Renderer Demo'
  }
};

const template = `
<body>
  <h1>{{ globalMeta.siteName }}</h1>
  <div class="article-container">
    {{
      nameObjects
        .map(content => '<b>' + content.name + '</b>')
        .join('');
    }}
  </div>
</body>
`;

const htmlTemplate = abellRenderer.render(template, sandbox);

console.log(htmlTemplate);

/*
Outputs:
<body>
  <h1>Abell Renderer Demo</h1>
  <div class="article-container">
    <b>Nice</b>
    <b>very cool</b>
  </div>
</body>
*/

🤖 Server-side Rendering with Abell + Express

You can tell express to use Abell as a template engine, check out following example to know how

const express = require('express');
const app = express();

app.engine('abell', require('abell-renderer').engine({ allowRequire: true }));
app.set('views', './views'); // specify the views directory
app.set('view engine', 'abell'); // register the template engine

app.get('/', function (req, res) {
  res.render('index', { foo: 'I am coming from server.js' });
});

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

Then you can have your index.abell file in views/ directory.

Check out saurabhdaware/abell-ssr-express for full example.

📚 Reference

abellRenderer.render(template, sandbox, options)

template: Abell template in String sandbox: Object over which the scripts execute, Can define variables and inject them into script. options.basePath: basePath which is prefixed on require() paths in abellTemplate. options.allowRequire: Passing true allows using require() in templates. Default is false.

🤗 Contributing

Check out CONTRIBUTING.md for Local Setup Guide, and Contribution Guidelines.

🕑 Changelogs

CHANGELOG.md


Buy me a Coffee Button   Buy me a Coffee Button

For status updates you can follow me on Twitter @saurabhcodes

abell-renderer's People

Contributors

beingbiplov avatar dependabot[bot] avatar judicaelandria avatar pantharshit00 avatar prafulla-codes avatar saurabhdaware avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

abell-renderer's Issues

Deny using require by default

We can have allowRequire option which is set to false by default and user can pass true if they want to use require()

console.log in .abell template

It can help in debugging if we could log a variable from inside the template.

{{ const a = 5 }}
<body>
<div>
    {{ console.log(a) }}
</div>
</body>

Allow require to only work when the path is ahead of basePath

abell-renderer was built with abell in mind so security was never really an issue since abell is a static site generator. But moving forward, we also want to support places where require can be unsafe to use (e.g server-side-rendering). One thing we can do is to throw an error if user tries to require and read files from the system.

so if basePath is /home/saurabh/myproject and user tries to do require('../../systemFile.js') we can throw an error saying access denied.

Fix attribute parser to support edge-cases

Current parseAttribute function in src/component-parser.js file fails for cases like onload="this.rel= 'stylesheet'; this.onload=null"

In current method, we split string on spaces and consider it as separate attribute value combination and then we separate on = to find key and value.

Add support for await

We should be able to render something like {{ await myAsyncWork() }}. Currently, it throws an error saying await should be inside async. Even if we add async the value it will return will be a promise only but to print the content in HTML file we need that value to be the actual value instead of promise.

A sequential compilation of JavaScript

Currently, every block of javascript in the template, expects a value to be returned and every block is executed separately so something defined in the first block cannot be used in the next block.

Imagine if we could do this:

{{ const { globalMeta } = require('abell.config.js') }}
<html>
    {{ let a = 3 }}
   <head><title> {{ globalMeta.siteName }}</title></head>
   <body>
       {{ a }} <!-- outputs: 3 -->
    </body>
</html>

currently, it is not possible because the execute() method expects something that returns a value (a function, .map method, etc)

I'm not really sure how can we implement this, especially the require file part on the top.

Also, I would like to get some suggestions on if we should actually try to do this or let things be the way they are.

Scoped CSS does not work when element is added from JavaScript DOM

This code below will not change the color of inner-element to red.

<AbellComponent>
<template>
  <div id="hello"></div>
</template>
<style>
  .inner-element {
    background-color: #f30;
  }
</style>
<script>
  document.querySelector('#hello').innerHTML = `
    <div class="inner-element">I am added from JS</div>
  `
</script>
</AbellComponent>

Currently, we add data-abell-<hash> attribute to all the elements inside template tag. The elements added from DOM do not get these attributes and hence ignored by styles.

Not really sure how we can solve this issue just leaving it here for future reference.

Ignore adding scope hash when styles are not scoped

<AbellComponent>
<template>
  <!-- This code does not need to be scoped -->
  <div></div>
</template>
</AbellComponent>
<AbellComponent>
<template>
  <!-- This code does not need to be scoped -->
  <div></div>
</template>
<style global>
  /* global styles */
</style> 
</AbellComponent>

Problem:
In both of the above cases, CSS is not needed to be scoped. Above components will add abell-data-<hash> attribute to all html elements inside template.

And thus, it outputs:

<div data-abell-kGoBAj></div>

data-abell-<hash> is required to scope CSS. But since in above examples, styles are not scoped, we can ignore adding hash to HTML elements.

Expected:
Component with this code is not required to be scoped so does not need those abell-data attributes. So they are expected to output.

<div></div>

This is the function that adds those hashes to HTML output https://github.com/abelljs/abell-renderer/blob/main/src/parsers/component-parser.js#L158

We have to ignore calling this function if-

  • <style> tag exists and has global attribute
  • <style> tag does not exist

This is a good-first-issue so feel free to ask any additional questions in comments below

Better way to differentiate between Assignment and a returned value in JS parsing.

Currently, we do super hacky things to find whether it is an assignment or a value.
https://github.com/abelljs/abell-renderer/blob/main/src/execute.js#L30-L73

If user writes {{ const a = 3 }}, we want to add variable a to sandbox and print nothing. But, if user does {{ a }}, then we want to print the value to the html.

This sounds nice and smooth for example above, but imagine this case

{{ 
  (() => {
    const a = 3;
    return a;
  })();
}}

We want this to be classified as a value rather than assignment since it outputs 3 in HTML.

There are few ways we can approach this
1. Write more specific RegExp

  • Easier to code
  • Hard to maintain

2. Tokenize everything inside JavaScript

  • Hard to code
  • Easier than RegExp method to maintain.

Farewell to Acorn.

One of the most uncomfortable things right now for me is the existence of acorn in the renderer.

Literally, the only place where acorn is used to is to generate something what we call a statementTypeMap https://github.com/abelljs/abell-renderer/blob/main/src/compiler.js#L38

Statement Type Map is an array of the types of statements we have in the code. This is eventually used to determine if the last statement is an assignment or expression.

E.g.

const a = 3;
a + b;
b = 9;

Will output-

['VariableDeclaration', 'ExpressionStatement', 'AssignmentExpression']

Why?

JavaScript returns the value when you assign. So if you run a = 3 in console, it will return 3, And so does only writing 3, And so does writing 2 + 1.

And in vm module, we only get what the code outputs.

But hey, how do we know if it is 3 coming from assignment or 3 coming from expression? We use the statement type maps!

So when it is an assignment, we don't return a value and thus printing a blank string.

Why acorn is a problem?

Using a whole frikin JavaScript parser just to tell if the code has assignment in the end is an overkill!

Solution

Probably write a custom tokenizer so instead of doing the whole parsing, we can just tokenize and figure out the statement type map from tokens.

Performance Improvements-

Whole parsing takes around 27ms for 3 lines of js code. Whereas tokenizing can take 10ms. This difference will be much larger for larger scripts.

scopedSelector value changes when the code execution is delayed

Describe the bug

Consider an output like this-

scopedSelector = (queryString) => document.querySelector(queryString + '[data-abell-ctznot]');scopedSelectorAll = (queryString) => document.querySelectorAll(queryString + '[data-abell-ctznot]');
scopedSelector('.nav-overlay').addEventListener('click', () => {
  console.log(scopedSelector);
  scopedSelector('.nav').classList.remove('open');
})
scopedSelector = (queryString) => document.querySelector(queryString + '[data-abell-hOOSwD]');scopedSelectorAll = (queryString) => document.querySelectorAll(queryString + '[data-abell-hOOSwD]');

Here the scopedSelector value is changed later so in the onclick event, the new scopedSelector value is chosen.

To Reproduce

  1. In one component, write delayed execution code. (Click event) that has scopedSelector inside it.
  2. In other components, use scopedSelector (this will overwrite the value of the first scopedSelector)

Expected behavior

Screenshots

Specs:

  • Abell Version: *
  • Node Version: *
  • Operating System: *

Additional context

{{ 0 }}, {{ false }} doesn't work

Printing falsy values is not possible currently.
Not printing undefined is intended (functions like console.log returns undefined) but everything else should be printed.

Reproducing Issue
In .abell file, write {{ 0 }} or {{ false }}. It is expected to print 0 and false but currently it prints blank string.

bug: bundled css output is two columns shifted

Describe the bug
image

The bundled css output is two columns shifted

To Reproduce

Build the example blog and see dist/bundled-css/main.abell.css

Expected behavior

Output should not be shifted. Ideally I would like the output to be minified by default but that is a separate issue that I will open.

Specs:

  • Abell Version: 0.4.0
  • Node Version: 12.18.2
  • Operating System: MacOS 10.15.6

Scoped Scripts

Is your feature request related to a problem? Please describe.

We have added scoped styles in abelljs/abell#59. We also need a way to make sure the elements that user selects in script tag are scoped so that the code is more maintainable.

Describe the solution you'd like

This snippet should only colors divtags that are inside this component.

<AbellComponent>
  <template>
    <div>Select only this div</div>
  </template>

  <script>
    scopedSelector('div').style.color = '#f30';
  </script>
</AbellComponent>

We have to inject scopedSelector and scopedSelectorAll code inside the javascript of the component.

Approach

If you look at the output of the Abell website, you will find that the components have data-abell-<hash> in their attributes in build.

Example:
<header data-abell-eKvkRo><nav class="top-nav shadow-clean" data-abell-eKvkRo>

For example imagine the hash being data-abell-xyz, We can inject function scopedSelector with following defination:

const scopedSelector = (queryString) => document.querySelector(queryString + '[data-abell-xyz]');

Similar thing can be done with scopedSelectorAll.

Recursively build abell files

Currently, abell-renderer CLI only supports building one file,

abell-renderer --input src/index.abell --output dist/index.html

It would be very helpful if we can build every .abell file when folder is provided

abell-renderer --input src --output dist

Note: Since abell-renderer is low-level API we will have to do this without adding any additional dependencies unless there's no other way to do it.

Files to change:
src/bin.js includes all the code that is executed when CLI is called

Abell AST explorations

( I am going to do a thought dump here, I can also see us writing a tech blog about this. Basically I will start working after I other abell team members approves )

Right now we basically do string modifications to an abell file. We extract out the contents of {{ }} which are essentially regions of javascript inside of your html, which may or may no return a value. That is determined by the execution of that javascript in a V8 isolate provided by Node(https://nodejs.org/api/vm.html).

This has worked pretty fine for now. But this approach makes it very hard to add features to abell especially features which require us to add additional metadata to the abell tree. Doing string replacements on a whole html tree is very error prone and it will be a unmaintainable mess for us. This also debars us from adding error messages to abell when a abell component is invalid. Right now it will just generate rubbish html.

For example, right now no one will stop you from doing this

<AbellComponent>
{{ const something = "this"}}
<template>
    <h1>{{ something }} }}</h1>
</template>
</AbellComponent>

Guess the output of the above template 😛. IMO it should be an error instead with a proper stack trace.

Now the solution here is what almost every compiler or template engine does. We will need to convert abell source code into a data structure which has all of the necessary information about the source code. This process is generally known as parsing. The DS should have enough information for us to implement a “printer” for that data structure which will convert it back to a source code.

Then what we can do is transform the data structure into our need. That process is far easier than relying on string transforms. At the end we can just print the result of transform. If we went with approach that I am going to suggest below, we should to perform the script execution before the printing step.

This data structure is typically called an Abstract Syntax Tree which represents the code in form of a tree data structure and it omits details like punctuations to make it “abstract”. For example, we don’t need to keep the < token in order to know it is a tag. That detail is abstracted out of parse tree. We will need to design an AST for abell which will enable to us all of the above mentioned things.

Now we come to the real question, what it should look like, should we just do the current replacements and then just parse the html and css syntax using an existing parser? Is this even worth the effort? Will this make abell “strict”? Argh?!

Approach 1

Here is the first thought that came to my mind. Use htmlparser2 + stylis for the CSS. We do the replacements, then these tools can parse the data we pipe into them and do the transformations. This is a very good naive approach at first. Much less work and we gain.

Pros

  • Fast to implement as most of the work is already done by these libs
  • No need to maintain a parser

Cons

  • htmlparser2 doesn’t come with a printer. It is strict parsing library, so we would have need to figure out how to results from it.
  • No way to embed abell tags {{ }} in the source tree. They are just replacements in the pre parsing step.
  • It adds dependency on two libraries

Approach 2

Now the next approach that comes in the mind is going all custom. Doing all the parsing the from scratch. Implementing all lexing, parsing and printing steps from scratch. This will give us full control.

Pros

  • It will give us full control
  • Abell renderer will remain zero dep

Cons

  • We will need to maintain it (bittersweet)
  • It will need a lot of work and we will need to make it fast
  • Doing CSS parsing would be actually moderately challenging as there are a lot of selectors
  • It will increase the time we will need to implement

Approach 3

You know it’s coming. It’s always the hybrid approach. The reasoning in this approach is that since most of the abell syntax is baked into the html part, we can write a parser from scratch for that to have full control there. We can have stylis to parse the CSS for us. It will also give us additional features which stylis provides.

Props

  • It will still give us control over the abell part. We will be able to add more syntax features in the future
  • It will cut scope for the work need to done for the CSS part.
  • CSS will be parsed tree which we will embed inside of parsed tag from our abell. We can apply our transformations there.
  • It will still give us stylis features like & reference

I will suggest we go with approach 3. That sounds most promising to me. If you all agree I can start working on it and I will writing a brief design proposal of the AST here.

Also, all this means almost a rewrite of abell-renderer 😅.

With this, abell will be like a superset of html with some additional features.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.