Coder Social home page Coder Social logo

combyne's Introduction

Combyne

Stable: 2.0.0

No dependencies. Can be loaded as a browser global, AMD module, and Node module. Works with Browserify. Can be installed via NPM or Bower.

Combyne works great with:

Install.

Node:

npm install combyne

Bower:

bower install combyne

Getting started.

Node.

Require in your source:

var combyne = require("combyne");

AMD.

// Configure the path if necessary.
require({
  paths: {
    combyne: "path/to/combyne"
  }
});

// Use in a module.
define(["combyne"], function(combyne) {});

There is also an AMD plugin for easier consumption and building:

https://github.com/tbranyen/combyne-amd-loader

Browserify.

combynify is a browserify transform plugin to pre-compile combyne templates.

In your code:

var template = require("./template.combyne");
var data = { ... }

template.render(data)

Install combynify and browserify it:

npm install --save-dev combynify
browserify -t combynify main.js > bundle.js

Once the template is precompiled, there is no dependency on the combyne engine.

Browser global.

Include the latest stable in your markup:

<script src="path/to/dist/combyne.js"></script>

Compatibility.

Combyne is written in ES5 and contains polyfills to provide support back to IE 7. These polyfills are omitted in the dist/combyne.js file, but exist in the dist/combyne.legacy.js file. Use this if you are developing/testing with older IE.

Basic usage.

var tmpl = combyne("hello {{msg}}!");
tmpl.render({ msg: "world" });

// => hello world!

Features.

Combyne works by parsing your template into an AST. This provides mechanisms for intelligent compilation and optimization. The template is converted to JavaScript and invoked upon calling render with data.

Security.

By default all templates are encoded to avoid possible issues arising from XSS attacks. This is specifically applied to properties and you can avoid this by using the raw property style: {{{ value }}}. This is very similar to Mustache.

While using this template engine in the browser, it is important to note that you should not trust unknown values to render unencoded. The recommendation is to forget it exists while writing templates in the browser, unless you know what you're doing and have a valid use case.

View this XSS (Cross Site Scripting) Prevention Cheat Sheet for more information.

Comments.

Comments are useful for ignoring anything between the open and close. They can be nested.

var tmpl = combyne("test {%-- not parsed --%}");
tmpl.render();

// => test

Custom delimiters.

If you are not happy with the default Mustache-like syntax, you can trivially change the delimiters to suit your needs. You may only change the delimiters at a global level, because templates are compiled immediately after invoking the combyne function.

// This sets the delimiters, and applies to all templates.
combyne.settings.delimiters = {
  START_PROP: "[[",
  END_PROP: "]]"
};

var tmpl = combyne("[[msg]]", { msg: "hello world" });

tmpl.render();
// => hello world

Defaults:

START_RAW:  "{{{"
END_RAW:    "}}}"
START_PROP: "{{"
END_PROP:   "}}"
START_EXPR: "{%"
END_EXPR:   "%}"
COMMENT:    "--"
FILTER:     "|"

Template variables.

var template = "{{foo}}";
var context = { foo: "hello" };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "hello"

Variables can be literal values, functions, or even objects.

Passing arguments to a function.

var template = "{{toUpper 'hi'}}";
var context = { toUpper: function(val) { return val.toUpperCase(); } };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "HI"

Using filters on variables.

var template = "{{foo|reverse}}";
var context = { foo: "hello" };

var tmpl = combyne(template);

tmpl.registerFilter("reverse", function(val) {
  return val.split("").reverse().join("");
});

var output = tmpl.render(context);
/// output == "olleh"

Passing arguments to filters.

You may find that the property value is not enough information for the filter function, in which case you can send additional arguments.

var tmpl = combyne("{{ code|highlight 'javascript' }}");

tmpl.registerFilter("highlight", function(code, language) {
  // Magic highlight function that takes code and language.
  return highlight(code, language);
});

Chaining filters on variables.

var template = "{{foo|reverse|toUpper}}";
var context = { foo: "hello" };

var tmpl = combyne(template);

tmpl.registerFilter("reverse", function(val) {
  return val.split("").reverse().join("");
});

tmpl.registerFilter("toUpper", function(val) {
  return val.toUpperCase();
});

var output = tmpl.render(context);
/// output == "OLLEH"

Conditionals.

Instead of being logic-less, combyne doesn't make any assumptions and allows you to do things like if/elsif/else with simple conditionals, such as if something == somethingElse or if not something. All data types will be coerced to Strings except for Numbers.

var template = "{%if not foo%}why not?{%endif%}";
var context = { foo: false };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "why not?"

or a more complicated example...

var template = "{%if foo == 'hello'%}Hi!{%else%}bye...{%endif%}";
var context = { foo: "hello" };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "Hi!"

elsif is also supported:

var template = "{%if foo == ''%}goodbye!{%elsif foo == 'hello'%}hello!{%endif%}";
var context = { foo: "hello" };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "hello!"

You can also pass conditionals through filters to do more complex logic:

var tmpl = combyne("{%if hello|upper|reverse == 'OLLEH'%}hello{%endif%}");

tmpl.registerFilter('upper', function(value) {
  return value.toUpperCase();
});

tmpl.registerFilter("reverse", function(value) {
  return value.split("").reverse().join("");
});

var output = tmpl.render({ hello: 'hello'});
/// output == "hello"

It also works with properties that need to be not encoded

var tmpl = combyne("{%if {{{hello}}} == '<>'%}hello{%endif%}");

var output = tmpl.render({ hello: '<>'});
/// output == "hello";

Iterating arrays.

Also works on array-like objects: arguments and NodeList.

var template = "{%each foo%}{{.}} {%endeach%}";
var context = { foo: [1,2,3,4] };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "1 2 3 4 "

You can also pass the value into a filter before iterating over it

var template = "{%each foo|upper%}{{.}} {%endeach%}";
var context = { foo: ["a", "b", "c"] };

template.registerFilter("upper", function(array) {
  return array.map(function (entry) {
    return entry.toUpperCase();
  });
});

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "A B C"

You can even use filters on the root object by either specifying '.' or leaving it blank

var template = "{%each .|upper%}{{.}} {%endeach%}";
var context = ["a", "b", "c"];

template.registerFilter("upper", function(array) {
  return array.map(function (entry) {
    return entry.toUpperCase();
  });
});

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "A B C"

Iterating an array of objects shorthand.

If you pass an array of objects to Combyne, you may iterate it via a shorthand:

var template = "{%each%}{{foo}} {%endeach%}";
var context = [{ foo: 1 }, { foo: 2 }, { foo: 3 }, { foo: 4 }];

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "1 2 3 4 "

Change the iterated identifer within loops.

var template = "{%each arr as val%}{{val}}{%endeach%}";
var context = { arr: [1,2,3] };

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output = "123"

Iterating objects.

var template = "{%each fruits as val key%}the {{key}} is {{val}}{%endeach%}";
var context = {
  fruits: {
    apple: "green"
  }
};

var tmpl = combyne(template);

var output = tmpl.render(context);
/// output == "the apple is green"

Partials.

var template = "{{foo}} {%partial bar%}";
var context = { foo: "hello" };

var tmpl = combyne(template);

tmpl.registerPartial("bar", combyne("{{name}}", {
  name: "john"
}));

var output = tmpl.render(context);
/// output == "hello john"

Pass template data to partial.

If you need to pass the template's data to the partial, simply use the magic operator ..

var template = "{{foo}} {%partial bar .%}";
var context = { foo: "hello", name: "carl" };

var tmpl = combyne(template);

tmpl.registerPartial("bar", combyne("{{name}}"));

var output = tmpl.render(context);
/// output == "hello carl"

If you need to manipulate the data passed to any partial, you must create a function on the parent template's data that returns an object or array that will be used by the nested partial.

You can even pass arguments along to that function to use.

An example follows:

var template = "{%partial bar showName 'carl'%}";
var context = {
  showName: function(name) {
    return { displayName: name };
  }
};

var tmpl = combyne(template);

tmpl.registerPartial("bar", combyne("hello {{displayName}}"));

var output = tmpl.render(context);
/// output == "hello carl"

If you wish to filter the data passed to the partial you can supply a filter.

var people = { carl: { knownAs: 'Carl, the Duke' } };
var template = "{%partial bar people|find 'carl'%}";
var context = {
  find: function(name) {
    return people[name];
  }
};

var tmpl = combyne(template);

tmpl.registerPartial("bar", combyne("hello {{knownAs}}"));

var output = tmpl.render(context);
/// output == "hello Carl, the Duke"

Template inheritance.

When using a framework that handles rendering for you and you wish to inject your template into a different template (maybe a layout) in a given region you can express this through template inheritance expressions.

Illustrated below is a typical use case for this feature:

var template = "{%extend layout as content%}<h1>{{header}}</h1>{%endextend%}";
var context = { header: "Home page" };

var page = combyne(template);

// Register the layout template into the page template.
page.registerPartial("layout", combyne("<body>{%partial content%}</body>"));

var output = page.render(context);
/// output == "<body><h1>Home page</h1></body>"

The context object you pass at the page.render line will be propagated to the partial template. This means that you can optionally pass a nested object structure like:

var context = {
  header: "My site",

  page: {
    header: "Home page"
  }
};

// Pass the page object to the page template, restricting what it has access
// to.
var layout = "<title>{{header}}</title><body>{%partial content page%}</body>";

// Register it in the partial.
page.registerPartial("layout", combyne(layout));

var output = page.render(context);
/// output == "<title>My site</title><body><h1>Home page</h1></body>"

Unit tests.

There are many ways to run the unit tests as this library can operate in various environments.

Browser

Open test/index.html in your web browser.

Node

Run the tests inside the Node runtime and within PhantomJS:

grunt test

This will run the tests against the AMD source, the built modern dist/combyne.js, and the built legacy dist/combyne.legacy.js files.

Continuous testing

To keep the PhantomJS tests running continuously, run:

grunt karma:watch

The tests will automatically run whenever files change.

Code coverage

If you run the tests through Karma, a test/coverage directory will be created containing folders that correspond with the environment where the tests were run.

If you are running the defaults you should see something that looks like:

.
└── coverage
    ├── Chrome 33.0.1750 (Linux)
    └── PhantomJS 1.9.7 (Linux)

Inside PhantomJS contains the HTML output that can be opened in a browser to inspect the source coverage from running the tests.

combyne's People

Contributors

aag avatar bmoore avatar chesles avatar johnhaley81 avatar kadamwhite avatar tbranyen avatar trismcc 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  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  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

combyne's Issues

Internal Grammar has some flaws

There are some flaws with in the internal grammar, such that it is required to have START_IF represented as if_ instead of if. This is also true for:

  • ELSIF
  • NOT
  • AS

Corrections to the stack/tree builder are required to alleviate this. It is problematic, because currently the following template:

{%if%}...{%endif%}

Gets the incorrect error: Error: Invalid expression type: OTHER. The expected error is: Error: Missing conditions to if statement.

Achieve 100% test coverage

The provisional roadmap for 1.0 (#73) includes 100% code coverage. This issue exists to make sure that, prior to tagging that release, we do in fact have said coverage.

Allow templates to loop over "this"

var template = '{%each this%} {{name}} {%endeach%}';
var context = [{name:"Fred"}];
combyne(template, context).render();

would output "Fred"

combyne (-amd-loader?) not working properly after rjs build

Hi,

I'm facing a weird issue. I use requirejs' optimizer to generate a production-ready build, and using combyne-loader (as "tpl" in my requirejs paths), it fails at properly loading the templates.

Runs perfectly fine with the separated scripts ("tpl!some/template" returns a Combyne object), but once I generate the build, it rather returns an object, with registerFilter and registerPartial properties instead of the expected compiler, data, etc. I guess this is the inlined version of the template, but well, it breaks the rendering :)

Order of key val in each… as…

Hi,

From what I've experienced, the README may have it wrong. It says:

var template = "{%each fruits as val key%}the {{key}} is {{val}}{%endeach%}";
// …

but I had to reverse val and key (as key val) to get it working as expected. Which is fine (reads better IMO), but might not be what you want and, somehow, could be less "standard" (with regards to what common libraries such as Underscore, LoDash… expect with their iterators).

Object properties

Hi (again),

More or less on the same topic as #61, I wonder whether it would be nice to add support for hash properties. A use-case to illustrate this:

# data
fullname: @user.fullname() # for instance ^^

# template
<span class="greeting">{{ i18n "ui.msg.welcome" {name: fullname} }}</span>

where the "ui.msg.welcome" I18N key would typically be defined as:

# en.coffee
ui:
  msg:
    welcome: "Welcome %{name}"

# fr.coffee
ui:
  msg:
    welcome: "Bienvenue %{name}"

That is, given a fullname property available in the rendering context (data), make it possible to pass it along through an object.

The current parser can't handle that just yet, but thanks to #62, one can workaround this by wrapping the object:

# data
fullname: {fullname: @user.fullname()}

# template
<span class="greeting">{{ i18n "ui.msg.welcome" fullname }}</span>

Between those two options, I don't know which one reads better but, altough I have the feeling that the second option is "fine", the wrapping is cumbersome and somehow less standard than the first option where the object is written in the template.

In the simple cases where there is only one variable to pass around, this wrapping can be automated, but if several variables must be merged in the same object (for instance, {name: fullname, gender: gender, vip: isVIP} to interpolate a complex sentence), then automation is not an option anymore. It is still possible to manually wrap the object beforehand, but being able to write the object within the template may feel more straightforward.

So it basically boils down to template management preferences 🔪

What do you think?

Render parent template from a partial, via injecting a named partial.

Conversation around being able to reference a parent partial came up. This level of inversion is very handy for Express development. Consider the following outside of Express:

var layout = combyne("<body>{%partial content%}</body>");

layout.registerPartial("content", {
  render: function() { console.log("hello world"); }
});

It is quite easy to see how composition can work going down from the layout referencing the partial. What isn't clear is how to go in the opposite direction:

var layout = combyne("<body>{%partial content%}</body>");
var content = combyne("hello world");

content.registerPartial("layout", layout);

There's no way here for the child to know how to inject itself into the layout. This is frustrating for pages in Express that would like to be contextual to the given route:

app.get("/about", function(req, res) {
  // Wants to be injected into the main page layout.
  res.render("about");
});

One way of handling this is an inject/yield style expression being added:

{%render layout as content%}
hello world
{%endrender%}

This would allow referencing a previously defined partial and injecting into it's own defined partial region.

An alternative syntax is also being considered, named yield:

{%yield into "layout" as "content"%}
hello world
{%endyield%}

Possibly render?

{%render layout as content%}
hello world
{%endrender%}

Refactor.

Once all tests are complete and 100% code coverage is achieved, refactor.

Pass argument[s] to filter

Ability to pass data to filters:

tmpl.filters.add('tease', function(val, n) {
    var a = str.split(/\s+/);
    return a.slice(0, n).join(" ") + (a.length > n ? "..." : "");
});

and then use like:

{{ article|tease:30 }}

Call functions *with arguments* from within a template

@carldanley any thoughts on the following idea:

template:

{{ obj.toUpper 'test' }}

Would then call the respective function with the passed argument:

template.render({
  toUpper: function(str) {
    return str.toUpperCase();
  }
});

I'm thinking this could be useful for Backbone models:

{{ model.get 'someAttr' }}

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory

Hello,

I'm using ~0.8.1, and following the same process of nodegit project, when I generate source code from template, i got below error message

Please advise, thanks!

<--- Last few GCs --->

   39211 ms: Scavenge 700.3 (737.9) -> 700.3 (737.9) MB, 12.0 / 0 ms (+ 16.0 ms
in 1 steps since last GC) [allocation failure] [incremental marking delaying mar
k-sweep].
   40209 ms: Mark-sweep 700.3 (737.9) -> 696.2 (737.9) MB, 985.6 / 0 ms (+ 16.0
ms in 2 steps since start of marking, biggest step 16.0 ms) [last resort gc].
   41195 ms: Mark-sweep 696.2 (737.9) -> 696.1 (737.9) MB, 989.1 / 0 ms [last re
sort gc].


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 17B6CC95 <JS Object>
    1: constructProperty [\node_modules\combyne\dist\c
ombyne.js:~634] [pc=083DF926] (this=10B1D319 <a Tree with map 21B4FD51>,encoded=
17B08115 <true>)
    2: constructConditional [\node_modules\combyne\dis
t\combyne.js:905] [pc=0839A922] (this=10B1D319 <a Tree with map 21B4FD51>,root=1
0B1D33D <an Object with map 21B4FEB1>,kind=09249239 <String[8]: ST...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - process out of memory

Use variable as argument of a function

Hi,

I wonder whether it is possible to use a variable as an argument of a function (Property).

My use-case is pluralization of i18n strings. Starting with the following:

i18nFn =
   i18n: (key, options) =>
      # options could either be an object, or a integer in which case it's a count.
      # Here, I'm interested by the use-case with a count argument.
      @langManager.engine.t(key, options)

then passing i18nFn to a rendering context allows me to write things like

{{i18n 'some.translation.key'}}

But, when it comes to pluralization, I would need to be able to write things like

{{i18n 'some.translation.key' count}}

where count is part of the rendering context (data) and should evaluate to an integer, say 3. Unfortunately, this raises a ReferenceError (count is not defined).

How could one work around this?

The compiled template would currently look like:

[…] encode(data['i18n']('some.translation.key',count)) […]

which is short a data[] accessor/context on the count variable to make it right! Would you think poking for the data object as a possible fallback value for otherwise undefined argument would do the trick? (count || data["count"]) that is, or maybe using .call to enforce the scope to data? It would not support any variable to fit in, just those in the scope of the current context, which seems to be ok.

Thank you.

TODO

  • Filters support.
  • Partials support.
  • Loop compilation.
  • Else/Elsif compilation.

Thinking about a 1.0 release

Ideally for the 1.0 release, we'd have at the minimum:

  • 100% test coverage
  • All adapters/plugins updated and well tested
  • Known bugs fixed
  • Better inheritance, most likely introducing the block concept
  • SourceMaps support

Allow nested objects for variables.

I noticed that I cannot access variables like one.two.three for some reason. I was trying to iterate over something like page.scripts.header and it didn't work.

More flexible template inheritance

Currently one kind of inheritance exits:

var template = "{%extend layout as content%}<h1>Header</h1>{%endextend%}";
var page = combyne(template);

// Register the layout template into the page template.
page.registerPartial("layout", combyne("<body>{%partial content%}</body>"));

page.render(context); // "<body><h1>Header</h1></body>"

(Note: if two {%extend ... %} statements exist in a file, Combyne throws an error.)

Other templating systems, notably Nunjucks and Handlebars, support the concept of blocks, which can be seen as multiple-inheritance. A Nunjucks example would have a layout file like this:

{% block header %}This is the default content{% endblock %}
<section class="left">{% block left %}{% endblock %}</section>
<section class="right">{% block right %}This is more content{% endblock %}</section>

so that rendering a template of this sort

{% extends "parent.html" %}
{% block left %}This is the left side!{% endblock %}
{% block right %}This is the right side!{% endblock %}

would generate

This is the default content
<section class="left">This is the left side!</section>
<section class="right">This is the right side!</section>

where each block statement would extend the relevant partial within the extended layout. (Nunjucks docs on block inheritance)

This issue exists to discuss whether this feature should be added, and how it should be implemented. Support for blocks is provisionally on the list for a 1.0 milestone: #73


Hypothetical syntax for block support

One option would be to just support multiple extends in one file: Render this template

{%extend layout1 as partial1%}<h1>Header</h1>{%endextend%}
{%extend layout1 as partial2%}<h2>Subheader</h2>{%endextend%}

with layout "layout1"

<div class="one">{%partial partial1%}</div>
<div class="two">{%partial partial2%}</div>

yields

<div class="one"><h1>Header</h1></div>
<div class="two"><h2>Subheader</h2></div>

Extending from multiple layouts

If different templates were extended, one intuitive option would be to have their output concatenated:

{%extend layout1 as partial1%}<h1>Header</h1>{%endextend%}
{%extend layout2 as partial1%}<h2>Subheader</h2>{%endextend%}

layout1:

<div class="one">{%partial partial1%}</div>

layout2:

<div class="two">{%partial partial1%}</div>

yields

<div class="one"><h1>Header</h1></div>
<div class="two"><h2>Subheader</h2></div>

Make a public-facing website

@tbranyen and I discussed that it would be good to have a non-Github-readme public face for Combyne. After a quick brainstorm of what a Combyne website should be, I would argue it should:

  1. Use github pages
  2. Contain a human-oriented introduction
  3. Contain the information from the README
  4. Contain a small "cookbook" of common examples
  5. Look pretty marvelous

1-3 are pre-reqs for an initial launch. I will spearhead this

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.