Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handlebars Precompiler as Custom Handler #70

Closed
bryceschober opened this issue Apr 25, 2018 · 9 comments
Closed

Handlebars Precompiler as Custom Handler #70

bryceschober opened this issue Apr 25, 2018 · 9 comments

Comments

@bryceschober
Copy link

I'd like to implement a custom handler that both inlines and precompiles handlebars.js templates. I'm a novice when it comes to both node.js and promises. I want my handler to:

  1. Match any handlebars script tags of the text/x-handlebars-template type,
  2. Inline all HTML tags declared as inline,
  3. Pre-compile the handlebars template, and finally
  4. Emit the precompiled script contents.

My naive attempt looks like:

#!/usr/bin/env node

const { inlineSource } = require('inline-source');
const Handlebars = require('handlebars');

// Custom handler for pre-compiling handlebars.js
function process_handlebars_js( source, context ) {
    if( source.tag == 'script' && source.type == 'text/x-handlebars-template' ) {
        inlineSource(source.content, {
            compress: true,
            saveRemote: true,
        })
        .then(html => {
            source.content = Handlebars.precompile(html);
            return Promise.resolve();
        })
        .catch(err => {
            process.stderr.write(`Error in Handlebars.js processor: ${err}\n`);
            return Promise.reject();
        });
    }
    return Promise.resolve();
}

process.stdin.setEncoding('utf8');

source = '';
process.stdin.on('readable', () => {
    let chunk = process.stdin.read();
    if (chunk!==null) source += chunk;
});

process.stdin.on('end', () => {
    inlineSource(source, {
        compress: true,
        saveRemote: true,
        handlers: [ process_handlebars_js ]
    })
    .then(html => {
        process.stdout.write(html + '\n');
        process.exit(0);
    })
    .catch(err => {
        process.stderr.write(`Error: ${err}\n`);
        return process.exit(1);
    });
});

But this yields the error:

Error in Handlebars.js processor: TypeError: Cannot read property 'replace' of null

Any pointers on the right direction?

@popeindustries
Copy link
Owner

Hi @bryceschober. I'm not exactly sure about the specific error, but it looks like you're on the right track. I'd suggest the following:

  1. Save your handler in a separate file handlebarsHandler.js (for example):
const Handlebars = require('handlebars');
const HANDLEBARS_TYPE = 'text/x-handlebars-template';

module.exports = function handlbars(source, context) {
  return new Promise((resolve, reject) => {
    if (
      source.fileContent &&
      !source.content &&
      source.type == HANDLEBARS_TYPE
    ) {
      let content;

      try {
        content = Handlebars.precompile(source.fileContent);
      } catch (err) {
        return reject(err);
      }
      // Need to expose templates for later use somehow...
      source.content = `window.templates=${content}`;
    }

    resolve();
  });
};
  1. require the handler when calling inlineSource:
inlineSource(source, { 
  compress: true,
  saveRemote: true,
  handlers: [ require('./handlebarsHandler.js')]
})
  1. It's generally not necessary to read file data as a stream for this kind of build-time scripting, so you could also just pass the html file as a command line arg and load it synchronously:
const fs = require('fs');
const path = require('path');
const source = fs.readFileSync(path.resolve(process.argv[2]), 'utf8');

All together it could look like this:

#!/usr/bin/env node

const fs = require('fs');
const path = require('path');
const inlineSource = require('inline-source');

const source = fs.readFileSync(path.resolve(process.argv[2]), 'utf8');

inlineSource(source, {
  compress: true,
  saveRemote: true,
  handlers: [ require('./handlebarHandler.js')]
})
.then(html => process.stdout.write(html + '\n'))
.catch(process.stderr.write(`Error: ${err}\n`));

@popeindustries
Copy link
Owner

@bryceschober
Copy link
Author

I'll try out your suggestion... but yours misses a detail of my use case. I suspect that my problem is related to the nested execution of the inliner. I want to run the inliner on the handlebars source to inline images, etc. before I precompile it, because I have elements that I want to inline both inside of and outside of the handlebars template section. I could split the handlebars templating away from the rest, but that gets in the way of easy development & testing by a mere web developer without a build environment. My HTML setup basically looks like:

<!DOCTYPE html>
<html lang="en-US">
<head>
    <link inline rel="stylesheet" href="styles.css">
</head>
<body>
    <div id="content" class="center"></div>
    <script id="tmpl-home" type="text/x-handlebars-template">
        <img inline src="logo.jpg" width="100%">
        <p>Items: {{items.length}}
            {{#each items}}
                <br/>{{this.name}}:&nbsp;{{this.value}}
            {{/each}}
        </p>
    </script>
    <script inline src="./node_modules/handlebars/dist/handlebars.min.js"></script>
    <script type="text/javascript">
        var json_data = {
            "items": [
                { "name": "foo", "value": "foobar" },
                { "name": "baz", "value": "bazaar" }
            ]
        };
        let tmpl = Handlebars.compile(document.getElementById('tmpl-home').innerHTML);
        document.getElementById('content').innerHTML = tmpl(json_data);
    </script>
</body>
</html>

That page works fine for easy editing when you view it directly a web browser. As you can see I'm curently using full compilation on the client, and inline-source works on this input just fine. But I'd really like to inline and then precompile the handlebars block before inlining in the rest of the page, without having to fragment the page into pieces that aren't trivially viewable in a browser.

@bryceschober
Copy link
Author

Does inline-source actually make the original element's innerHTML available as source.content? It seems not, which would make my current attempt impossible, with already-inline handlebars content.

@popeindustries
Copy link
Owner

I see. This may work. If you pass source.fileContent(the content of your loaded Handlebars file) to the second inlineSource call, then pass the returned html to the precomplier, you should end up with what you’re after.

@bryceschober
Copy link
Author

bryceschober commented Apr 28, 2018 via email

@popeindustries
Copy link
Owner

I’m not sure I follow. The html file you pipe in has a script tag that sources an external handlebars file. That file is loaded in after the path is parsed by inline-source, and the contents are exposed assource.fileContent. That file content is to be transformed (again by inline-source, but in principle it could be anything), and the result is saved to source.content so it can replace the script tag and be written into the final html file.

(If you are unsure what values are available, you can always console.log(source))

@bryceschober
Copy link
Author

You misunderstand me. The example I provided in the previous comment has the handlebars template between the script tags in-line in the main html source. The elements that I want to be inlined are within that handlebars template between the script begin & end tags instead of in an external handlebars template file. It looks like your inliner doesn't pre-load any content between begin / end tags in this case...

@popeindustries
Copy link
Owner

Unfortunately, the only use case is inlining external sources, and it generally won’t even parse tags without inline attributes or missing src.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants