{"id":23719512,"url":"https://github.com/appfigures/leaf.js","last_synced_at":"2026-02-12T02:30:15.035Z","repository":{"id":13106574,"uuid":"15788136","full_name":"appfigures/leaf.js","owner":"appfigures","description":"A Tiny, Imperative XML Transformation Library for Node.js","archived":false,"fork":false,"pushed_at":"2021-10-25T17:57:34.000Z","size":133,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-12-30T21:52:17.273Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/appfigures.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-01-10T04:21:36.000Z","updated_at":"2021-10-25T17:57:37.000Z","dependencies_parsed_at":"2022-08-23T17:40:18.644Z","dependency_job_id":null,"html_url":"https://github.com/appfigures/leaf.js","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfigures%2Fleaf.js","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfigures%2Fleaf.js/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfigures%2Fleaf.js/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/appfigures%2Fleaf.js/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/appfigures","download_url":"https://codeload.github.com/appfigures/leaf.js/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239808684,"owners_count":19700490,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-12-30T21:52:19.541Z","updated_at":"2026-02-12T02:30:14.990Z","avatar_url":"https://github.com/appfigures.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"leaf (beta)\n=======\n\nA tiny XML transformation library with recursive evaluation for Node. Useful for generating email html, among other things. Loosely inspired by the Angular.js interface.\n\n## Command line interface\n\n\tleaf input [options]\n\nInput can either be a file path or a markup string.\n\n## Example\n\nLet's turn this markup (xml or html):\n\n\t\u003cperson data-title=\"Mr.\"\u003ePotter\u003c/person\u003e\n\nInto this one:\n\n\t\u003ch1 class=\"person-header\"\u003eHi Mr.Potter\u003c/h1\u003e\n\n### Code\n\n*index.html*\n\t\n\t\u003cperson data-title=\"Mr.\"\u003ePotter\u003c/person\u003e\n\n*directive.html*\n\n\t\u003ch1 class=\"person-header\"\u003eHi \u003c%= title %\u003e\u003ccontent /\u003e\u003c/h1\u003e\n\n*Run this code*\n\n\tvar leaf = require('leaf');\n\n\tfunction module (session) {\n\t\tsession.directive('person', {\n\t\t\ttemplate: 'directive.html'\n\t\t});\n\t}\n\t\n\tconsole.log(leaf.parse('index.html', module));\n\n## Features\n\n- Recursive template expansion\n- Smart tag merging\n- Define custom element logic imperatively\n- Completely syncronous parsing\n- Extendible through modules\n- Includes basic DOM manipulation with jQuery-like API (with cheerio.js)\n- Output as XML, HTML or plain text (TBD)\n- Use as node module or command line utility\n\n## Getting started\n\n\tnpm install leaf-af\n\nRun this javascript:\n\n\tvar string = require('leaf').parse('\u003cdiv /\u003e', function (session) {\n\t\tsession.directive('div', '\u003cp\u003eIt worked!\u003c/p\u003e');\n\t});\n\tconsole.log(string);\n\n## How it works\n\nLeaf reads in xml, transforms all of its nodes based on defined directives, applies optional user-defined transformations on them, then returns a stringified version of the resulting dom. It's recursive, meaning it'll keep transforming nodes until none of them match any of its directives.\n\n## The public API\n\n- `leaf.parse(input, options)` The main method of the library.\n- `leaf.cheerio` A reference to the local cheerio library.\n- `leaf.modules` All global modules\n- `leaf.templates` A collection of utility methods for loading and compiling templates (uses underscore be default).\n- `leaf.utils` A collection of utility methods. Leaf uses these and thought you might find them helpful too.\n\t- Of note, `leaf.utils._` points to leaf's copy of lodash.\n- `leaf.Cache` A constructor function for the cache class. Can be used to create a custom cache and persist it across different calls to `parse()`.\n- `leaf.errors` A collection of error objects that this library throws.\n- `leaf.ext` External \n\n## Parsing xml\n\nParsing is a synchronous operation. It's done through the static function `leaf.parse`.\n\n### leaf.parse(input, options)\n\n- `input` Either an xml string, a file path, or a dom element (via cheerio).\n\t- Leaf uses `leaf.utils.isHtmlString` to figure out if the input is a path or markup.\n\t- The input xml must be valid (as defined by cheerio) and have a single root node.\n- `options` Either an object or a transform function. The important params are listed below (all optional). For the full docs see [libs/parse.js](libs/parse.js)\n\t- `modules: Object` The underlying implementations for modules that the input may require. Has the format (`{ 'moduleName': moduleFn, ...}`).\n\t- `transform: Function (session) -\u003e void` Allows you to mess with the session object (to add directives, change globals, etc.) before the rendering starts.\n\t- `cache: Cache` Leaf tries to cache as much as possible during a parse. Pass your own `leaf.Cache` instance if you'd like to persist the cache across multiple calls to `parse()`. It provides a great speed improvement when parsing similar input multiple times. If you're interested in what exactly gets cache, pass in your own object and call `.toString() / .size()` on it after a parse operation.\n\t- `outputFormat: String` Either `'xml'` or `'html'` (default: `'xml'`).\n\nThis method has two helper versions. Use these if leaf can't properly figure out what type of input you're passing it.\n\n- `parse.file(filePath, options)`\n- `parse.string(markupString, options)`\n\n## Creating directives\n\nFor every call to `leaf.parse` a new `ParseSession` object is created. Directives are added to the this object. The `session` is passed into a module function prior to parsing. At that point directives can be added to the session. Example:\n\n\tleaf.parse('...', function (session) {\n\t\tsession.directive(name, options);\n\t});\n\n### session.directive(name, options | template)\n\n#### name\n\nA camel-cased name. When using the directive the name can be specified as either `-` cased or `_` cased. For example if you name your directive `'myDirective'` it can either be used like this:\n\n\t\u003cmy-directive\u003e\n\nOr\n\n\t\u003cmy_directive\u003e\n\n#### options\n\n- `template: String` An html string or file path. Will be compiled by lodash.template by default. Default template compiler can be set via `leaf.ext.templateCompiler`.\n- `context: Object`\tThe default context to evaluate the template with. Can be `{...}` or a function `(globals) =\u003e context`. The resulting object gets extended with the dom element's attributes.\n- `source: String` The base path to evaluate template assets with. If `template` is a url and this value isn't defined, it serves as the source.\n- `mergeOptions: Object`\nDefine how this directive should be merged with the dom element it replaces. These options are passed to `leaf.utils.mergeElements()`.\n- `matches: Function(element) -\u003e boolean`\nAn optional function to test if an element matches this directive. If false is returned, the directive is ignored by the parser for that element. Note that overriding this function prevents the default behavior from happening. To bring that back, call `return this.matchesName(element)` in your implementation.\n- `prepare: Function(context, originalElement) -\u003e object`\nAn optional method that can be used to modify the context based on the original element that matched this directive. At this point the element parameters have already been merged into the default context to form 'context'.\n- `logic: Function(el, context) -\u003e void`\nAn optional method that can be used to modify the new element after it's been created, but before its children have been parsed.\n\n### Directive attribute passing\n\nLet's say we make a directive called `myDirective` with a template `\u003cdiv /\u003e` and then in our template write:\n\n\t\u003cmy-directive some-attr=\"100\" data-other-attr=\"true\" /\u003e\n\nWhen our directive gets called, it's context will look like this:\n\n\t{\n\t\tattr: 100,\n\t\totherAttr: true\n\t}\n\nand the resulting output will look like this:\n\n\t\u003cdiv some-attr=\"100\"/\u003e\n\nNotice that:\n\n- All original dom attributes that don't start with `data-` will show up in the transformed element.\n- All the original dom attributes get parsed and merged to the directive's `context` object, whether they start with `data-` or not.\n\t- Since all dom attributes are actually strings, leaf does its best to figure out the actual type. It can figure out `Number`, `Boolean`, and `String` values.\n\n## Modules\n\nA module defines a group of transformations and directives. To include a module, a template must provide a comment at the top of the form:\n\n\t\u003c!-- modules: moduleA, moduleB, etc --\u003e\n\n### Creating modules\n\nLet's say we have a file `file.html` that we'd like to transform:\n\n\t\u003c!-- modules: myModule --\u003e\n\t\u003ccontainer\u003e\n\t\t\u003ccustom-tag data-x=\"hi\" /\u003e\n\t\t\u003ctransform-me\u003eHello\u003c/transform-me\u003e\n\t\u003c/container\u003e\n\nWe can see that it requires a module named `myModule`. We have two ways to\nprovide it.\n\n#### The quick way\n\n\tvar modules = {\n\t\t'myModule': function (leaf) {\n\t\t\treturn function (session) {\n\t\t\t\t// Mutate the session\n\t\t\t\t// by adding directives, globals, etc.\n\t\t\t}\n\t\t},\n\t\t'myOtherModule': ...\n\t}\n\t\n\tleaf.parse('file.html', { modules: modules });\n\n#### The resuable way\n\nWe put a `leaf-modules.js` file somewhere up the folder hierarchy of `file.html`:\n\n\tmodule.exports = {\n\t\t'myModule': function (leaf) { ... },\n\t\t'myOtherModule': require('./other-module.js')\n\t};\n\t\nThen in our code we can just write:\n\n\tleaf.parse('file.html')\n\n# Globals\n\nThe `session.globals` object lets directives share values, and for modules to communicate.\n\t\n\tfunction module (session) {\n\t\t// The globals object is shared by everyone\n\t\t// so we namespace our variables to be good\n\t\t// citizens.\n\t\tsession.globals.myModule = {\n\t\t\tcontainerWidth = 100\n\t\t};\n\t\n\t\tsession.directive('container', {\n\t\t\ttemplate: '\u003ctable width=\"{{width}}\"/\u003e',\n\t\t\tcontext: function (globals) {\n\t\t\t\treturn {\n\t\t\t\t\twidth: globals.myModule.containerWidth\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\tparse('\u003ccontainer /\u003e', module) // =\u003e \u003ctable width=\"100\"/\u003e\n\tparse('\u003ccontainer width=\"300\"\u003e', module) // =\u003e \u003ctable width=\"300\"/\u003e\n\n## Known Issues\n\n- There are currently some design issues that should be tightened up\n\t- The structure of a global module (in `leaf.modules`) is `function (session) {}` while the structure of a module that gets loaded from an external file is `function (leaf) { return function (session) {}; }`.\n\t- When reading a leaf markup file\n\t\t- It's hard to tell which attributes are part of the directive, which are of one of the super directives, and which are for the underlying dom element.\n\t\t- It's also hard to know when to use a `data-` attribute (doesn't get propagated to super) and when not to.\n\t- The style is stored across css, js, and html files. It makes it hard to track down what's going on and where to make changes.\n\t- (FIXED - Now modules can require and inject other modules) A module cannot require that another module be loaded. On the other hand, a module may attempt to use utils from another (eg. `session.module('someModule').util()`).\n\t\t- If modules were allowed to include other modules there could be unwanted side effects when the included module adds an unwanted transformation to the dom.\n- cheerio's dom manipulation functions will throw errors when trying to manipulate the children of text/comment/etc elements. This may affect new directives being written.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappfigures%2Fleaf.js","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fappfigures%2Fleaf.js","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fappfigures%2Fleaf.js/lists"}