{"id":13847507,"url":"https://github.com/wspringer/cruftless","last_synced_at":"2026-02-16T03:19:19.666Z","repository":{"id":43969851,"uuid":"162248638","full_name":"wspringer/cruftless","owner":"wspringer","description":"Get rid of the cruft of XML processing","archived":false,"fork":false,"pushed_at":"2023-12-19T06:23:06.000Z","size":542,"stargazers_count":30,"open_issues_count":21,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-07-12T09:39:19.784Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"CoffeeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wspringer.png","metadata":{"files":{"readme":"README.js.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-12-18T07:34:13.000Z","updated_at":"2025-01-25T21:23:31.000Z","dependencies_parsed_at":"2024-01-15T20:52:13.518Z","dependency_job_id":"3144c1ba-57e6-4b34-9c68-3cd8ea803b37","html_url":"https://github.com/wspringer/cruftless","commit_stats":null,"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/wspringer/cruftless","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wspringer%2Fcruftless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wspringer%2Fcruftless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wspringer%2Fcruftless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wspringer%2Fcruftless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wspringer","download_url":"https://codeload.github.com/wspringer/cruftless/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wspringer%2Fcruftless/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29499612,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-16T02:07:14.481Z","status":"online","status_checked_at":"2026-02-16T02:03:22.852Z","response_time":115,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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-08-04T18:01:34.257Z","updated_at":"2026-02-16T03:19:19.624Z","avatar_url":"https://github.com/wspringer.png","language":"CoffeeScript","funding_links":[],"categories":["CoffeeScript"],"sub_categories":[],"readme":"```javascript --hide\nrequire(\"coffeescript/register\");\nconst format = require(\"xml-formatter\");\nrunmd.onRequire = function (path) {\n  if (path === \"cruftless\") {\n    return \"./readme.cruftless.coffee\";\n  }\n};\n```\n\n# README\n\nAn XML builder / parser that tries to ease the common cases, allowing you to quickly build a model from your document structure and get a builder / parser for free.\n\n[![CircleCI](https://dl.circleci.com/status-badge/img/gh/wspringer/cruftless/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/wspringer/cruftless/tree/master)\n\n[![Greenkeeper badge](https://badges.greenkeeper.io/wspringer/cruftless.svg)](https://greenkeeper.io/)\n\n## Yet another XML binding framework?\n\nI hate to say this, but: 'yes'. Or, perhaps: 'no'. Because Cruftless is not really an XML binding framework as you know it. It's almost more like Handlebars. But where Handlebars allows you to only _generate_ documents, Cruftless also allows you to _extract_ data from documents.\n\n## Building XML documents\n\nCruftless builds a simplified metamodel of your XML document, and it's not based on a DOM API. So, if this is the XML document:\n\n```xml\n\u003cperson\u003e\n  \u003cname\u003eJohn Doe\u003c/name\u003e\n  \u003cage\u003e16\u003c/age\u003e\n\u003c/person\u003e\n```\n\nThen, using the builder API, Cruftless allows you to _build_ a model of your document like this:\n\n```javascript --run simple\nconst { element, attr, text } = require(\"cruftless\")();\n\nlet el = element(\"person\").content(\n  element(\"name\").content(text().value(\"John Doe\")),\n  element(\"age\").content(text().value(16))\n);\n```\n\n… and then to turn it back into XML, you'd use the `toXML()` operation:\n\n```javascript --run simple\nel.toXML(); // RESULT\n```\n\n… or the `toDOM()` operation instead to return a DOM representation of the document:\n\n```javascript --run simple\nel.toDOM();\n```\n\n## Binding\n\nNow, this itself doesn't seem all that useful. Where it gets useful is when you start adding references to your document model:\n\n```javascript --run simple\nel = element(\"person\").content(\n  element(\"name\").content(text().bind(\"name\")),\n  element(\"age\").content(text().bind(\"age\"))\n);\n```\n\nNow, if you want to generate different versions of your XML document for different persons, you can simply pass in an object with `name` and `age` properties:\n\n```javascript --run simple\nlet xml = el.toXML({ name: \"John Doe\", age: \"16\" }); // RESULT\n```\n\nBut the beauty is, it also works the other way around. If you have your model with binding expressions, then you're able to _extract_ data from XML like this:\n\n```javascript --run simple\nel.fromXML(xml); // RESULT\n```\n\n## Less tedious, please\n\nI hope you can see how this is useful. However, I also hope you can see that this is perhaps not ideal. I mean, it's nice that you're able to build a model of XML, but in many cases, you already have the snippets of XML that you need to populate with data. So, the question is if there is an easier way to achieve the same, if you already have snippets of XML. Perhaps not surprisingly, there is:\n\n```javascript --run simple\nlet template = `\u003cperson\u003e\n  \u003cname\u003e{{name}}\u003c/name\u003e\n  \u003cage\u003e{{age}}\u003c/age\u003e\n\u003c/person\u003e`;\n\nlet { parse } = require(\"cruftless\")();\n\nel = parse(template);\nconsole.log(el.toXML({ name: \"Jane Doe\", age: \"18\" }));\n```\n\n## Additional metadata\n\nThe example above is rather simple. However, Cruftless allows you also deal with\nmore complex cases. And not only that, it also allows you to set additional\nmetadata on binding expressions, using the pipe symbol. In the template below,\nwe're binding `\u003cperson/\u003e` elements inside a `\u003cpersons/\u003e` element to a property\n`persons`, and we're inserting every occurence of it into the `persons` array.\nThe processing instruction annotation might be feel a little awkward at first. There are\nother ways to define the binding, including one that requires using attributes\nof particular namespace. Check the test files for examples.\n\n```javascript --run simple\ntemplate = parse(`\u003cpersons\u003e\n  \u003cperson\u003e\u003c?bind persons|array?\u003e\n    \u003cname\u003e{{name|required}}\u003c/name\u003e\n    \u003cage\u003e{{age|integer|required}}\u003c/age\u003e\n  \u003c/person\u003e\n\u003c/persons\u003e`);\n\n// Note that because of the 'integer' modifier, integer values are\n// now automatically getting transfered from and to strings.\n\nconsole.log(\n  template.toXML({\n    persons: [\n      { name: \"John Doe\", age: 16 },\n      { name: \"Jane Doe\", age: 18 },\n    ],\n  })\n);\n```\n\nYou can add your own value types to convert from and to the string literals\nincluded in the XML representation.\n\n```javascript --run simple-2\nconst { element, attr, text, parse } = require(\"cruftless\")({\n  types: {\n    zeroOrOne: {\n      type: \"boolean\",\n      from: (str) =\u003e str == \"1\",\n      to: (value) =\u003e (value ? \"1\" : \"0\"),\n    },\n  },\n});\n\ntemplate = parse(`\u003cfoo\u003e{{value|zeroOrOne}})\u003c/foo\u003e`);\nconsole.log(template.toXML({ value: true }));\nconsole.log(template.toXML({ value: false }));\n```\n\nThe same works with attributes as well:\n\n```javascript --run simple-2\ntemplate = parse(`\u003cfoo bar=\"{{value|zeroOrOne}}\"/\u003e`);\nconsole.log(template.toXML({ value: true }));\nconsole.log(template.toXML({ value: false }));\n```\n\nSometimes, it's still useful to be able to access the raw field values, ignoring\nthe type annotations.\n\nTo get the actual data:\n\n```javascript --run simple-2\n// The second argument defaults to false, so might as well leave it out\nconsole.log(template.fromXML(\"\u003cfoo bar='1'/\u003e\", false));\n```\n\nTo get the raw data:\n\n```javascript --run simple-2\nconsole.log(template.fromXML(\"\u003cfoo bar='1'/\u003e\", true));\n```\n\n## Alternative notation\n\nThe `\u003c!--persons|array--\u003e` way of annotating an element is not the only way you are able to add metadata. Another way to add metadata to elements is by using one of the reserved attributes prefixed with `c-`.\n\n```javascript --run simple-2\ntemplate = parse(`\u003cpersons\u003e\n  \u003cperson c-bind=\"persons|array\"\u003e\n    \u003cname\u003e{{name|required}}\u003c/name\u003e\n    \u003cage\u003e{{age|integer|required}}\u003c/age\u003e\n  \u003c/person\u003e\n\u003c/persons\u003e`);\n\nconsole.log(\n  template.toXML({\n    persons: [\n      { name: \"John Doe\", age: 16 },\n      { name: \"Jane Doe\", age: 18 },\n    ],\n  })\n);\n```\n\nIf you hate the magic `c-` prefixed attributes, then you can also a slightly\nless readable but admittedly more correct XML namespace:\n\n```javascript --run simple-2\ntemplate = parse(`\u003cpersons\u003e\n  \u003cperson xmlns:c=\"https://github.com/wspringer/cruftless\" c:bind=\"persons|array\"\u003e\n    \u003cname\u003e{{name|required}}\u003c/name\u003e\n    \u003cage\u003e{{age|integer|required}}\u003c/age\u003e\n  \u003c/person\u003e\n\u003c/persons\u003e`);\n\nconsole.log(\n  template.toXML({\n    persons: [\n      { name: \"John Doe\", age: 16 },\n      { name: \"Jane Doe\", age: 18 },\n    ],\n  })\n);\n```\n\n## Conditionals\n\nThere may be times when you want to exclude entire sections of an XML structure\nif a particular condition is met. Cruftless has some basic support for that,\nalbeit limited. You can set conditions on elements, using the `c-if` attribute.\nIn that case, the element will only be included in case the expression of the\n`c-if` attribute is evaluating to something else than `undefined` or `null`.\n\n```javascript --run simple-2\ntemplate = parse(`\u003cfoo\u003e\u003cbar c-if=\"a\"\u003etext\u003c/bar\u003e\u003c/foo\u003e`);\n\ntemplate.toXML({}); // RESULT\ntemplate.toXML({ a: null }); // RESULT\ntemplate.toXML({ a: void 0 }); // RESULT\ntemplate.toXML({ a: 3 }); // RESULT\n```\n\nIf your template contains variable references, and the data structure you are\npassing in does not contain these references, then — instead of generating the\nvalue `undefined`, Cruftless will drop the entire element. In fact, if a deeply\nnested element contains references to variable, and that variable is not\ndefined, then it will not only drop _that_ element, but all elements that\nincluded that element referring to a non-existing variable.\n\n```javascript --run simple-2\ntemplate = parse(`\u003clevel1\u003e\n  \u003clevel2 b=\"{{b}}\"\u003e\n    \u003clevel3\u003e{{a}}\u003c/level3\u003e\n  \u003c/level2\u003e\n\u003c/level1\u003e`);\n\nconsole.log(template.toXML({ b: 2 }));\n```\n\n```javascript --run simple-2\nconsole.log(template.toXML({ b: 2, a: 3 }));\n```\n\n```javascript --run simple-2\nconsole.log(template.toXML({ a: 3 }));\n```\n\n## CDATA\n\nYour XML documents might contain CDATA sections. Cruftless will treat those like\nordinary text nodes. That is, if you have an element that has a text node bound\nto a variable, then it will resolve those values regardless of the fact if the\nincoming XML document has a text node or a CDATA node.\n\n```javascript --run simple-2\ntemplate = parse(`\u003cperson\u003e{{name}}\u003c/person\u003e`);\nconsole.log(template.fromXML(`\u003cperson\u003eAlice\u003c/person\u003e`));\n```\n\n```javascript --run simple-2\nconsole.log(template.fromXML(`\u003cperson\u003e\u003c![CDATA[Alice]]\u003e\u003c/person\u003e`));\n```\n\nHowever, if you would _produce_ XML, then — by default — it will always produce\na text node:\n\n```javascript --run simple-2\nconsole.log(template.toXML({ name: \"Alice\" }));\n```\n\nThat is, unless you specifiy a `cdata` option in your binding:\n\n```javascript --run simple-2\ntemplate = parse(`\u003cperson\u003e{{name|cdata}}\u003c/person\u003e`);\nconsole.log(template.toXML({ name: \"Alice\" }));\n```\n\n## JSON-ish Schema (incomplete, subject to change)\n\nSince Cruftless has all of the metadata of your XML document and how it binds to\nyour data structures at its disposal, it also allows you to generate a 'schema'\nof the data structure it expects.\n\n```javascript --run simple-2\nlet schema = template.descriptor();\nconsole.log(JSON.stringify(schema, null, 2));\n```\n\nThe schema will include additional metadata you attached to expressions:\n\n```javascript --run simple-2\ntemplate = parse(`\u003cperson\u003e\n  \u003cname\u003e{{name|sample:Wilfred}}\u003c/name\u003e\n  \u003cage\u003e{{age|integer|sample:45}}\u003c/age\u003e\n\u003c/person\u003e`);\n\nschema = template.descriptor();\nconsole.log(JSON.stringify(schema, null, 2));\n```\n\n## RelaxNG Schema\n\nSince Cruftless captures the structure of the XML document, it's also able to\ngenerate an XML Schema representation of the document structure. Only, it's not\nrelying on XML Schema. It's using RelaxNG instead. If you never heard of\nRelaxNG before: think of it as a more readable better version of XML Schema,\nwithout the craziness.\n\nSo based on the template above, this would give you the RelaxNG schema:\n\n```javascript --run simple-2\nconst { relaxng } = require(\"cruftless\")();\n\nconsole.log(relaxng(template));\n```\n\n## Support for xsi:type\n\nXML Schema introduced a kind of polymorphism that many schema designers are a\nbit too eager to embrace. Supporting that in Cruftless is not trivial, so\nalthough some level of support exists, be advised it's very limited. Also, be\naware that whatever support for RelaxNG we have, it completely falls apart when\nusing `xsi:type`.\n\nThis is how you use it: suppose that you have a set of students and teachers,\nbut for students you need a different content model than for teachers. Then you\ncould model that using `xsi:type`.\n\n```javascript --run simple-2\ntemplate = parse(\n  `\n\u003cpeople xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\u003e\n  \u003cperson xsi:type=\"Student\" c-bind=\"people|array\" nickName=\"{{name}}\" grade=\"{{grade}}\" /\u003e\n  \u003cperson xsi:type=\"Teacher\" c-bind=\"people|array\" name=\"{{name}}\" subject=\"{{subject}}\" /\u003e\n\u003c/people\u003e\n`.trim()\n);\n\nconsole.log(\n  template.fromXML(\n    `\n\u003cpeople xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\u003e\n  \u003cperson xsi:type=\"Student\" nickName=\"Jesse\" grade=\"D\" /\u003e\n  \u003cperson xsi:type=\"Student\" nickName=\"Badger\" grade=\"C\" /\u003e\n  \u003cperson xsi:type=\"Teacher\" name=\"Walther\" subject=\"chemistry\" /\u003e\n\u003c/people\u003e\n`.trim()\n  )\n);\n```\n\nIn the above case, the `xsi:type` attribute determines the content model of the\ndata getting parsed. It will pass the value to the `kind` property of the data\nextracted. Reversely, it will use the `kind` property to determine how to render\nthe data as XML.\n\nNow, `xsi:type` values are assumed to `QName`s. That means that there might be a\nprefix in the name that resolves to a namespace. The documents that you _parse_\nmight use different namespace prefixes than the binding template. In order to\navoid issues with that, Cruftless accepts a `prefixes` option specifically for\nnormalization of the prefixes. So, if your template refers to a type with an\n`ns1:` prefix, and the document you are passing is using the `ns0:` prefix, then\nyou can make sure the types in the documents you are parsing are rewritten to\n`ns1`, by the namespace of `ns1` in the configuration options of your Cruftless\ninstance. (See \u003chttps://github.com/wspringer/cruftless/blob/60-xsitype-should-always-be-interpreted-as-a-qname/test/model/xsi-type-test.coffee#L84\u003e.)\n\n**NOTE:** There are [various\nissues](https://github.com/wspringer/cruftless/issues/50) with the way RelaxNG\nschemas are generated. Consider this to be work in progress.\n\n## Nodeset Capture\n\nThere are situations where it makes very little sense to have one template\ndictating the structure of the entire document. Typically, in those cases, you\nwant to slowly peel the entire structure about, starting with the outer envelope\n/ container, and then slowly work your way in.\n\nIn order to support that, Cruftless offers a solution to capture parts of the\nDOM tree as is and store it in a variable to be processed further downstream.\nThe syntax is not all that different than the bind syntax and might be\nharmonized at some point.\n\nThis is how you use it:\n\n```javascript --run simple-2\ntemplate = parse(`\u003cfoo\u003e\u003c?capture nodes?\u003e\u003c/foo\u003e`);\nconst { nodes } = template.fromXML(`\u003cfoo\u003e\u003cbar/\u003e\u003cbar/\u003e\u003c/foo\u003e`);\nconsole.log(nodes.length);\nconsole.log(nodes[0].tagName);\n```\n\n## Rudimentary xinclude support\n\nWith cruftless, it often makes sense to break a larger template apart into\nsmaller ones that can be referenced in various locations. To that end, we're\nrelying on basic xinclude implementation.\n\n```javascript --run simple-2\nresolve = (href) =\u003e {\n  return [\"\u003cbla/\u003e\", resolve];\n};\ntemplate = parse(\n  `\u003cfoo xmlns:xi=\"http://www.w3.org/2001/XInclude\"\u003e\u003cxi:include href=\"bla.xml\"/\u003e\u003c/foo\u003e`,\n  resolve\n);\nconsole.log(template.toXML({}));\n```\n\nNote that the resolve function is expected to resolve the href within a context\nand then return both the XML _and_ a new resolve function that is capable fo\nresolving hrefs from within the context of the resolved file. In this case,\nwe're not really doing that. In fact, this resolver will **always** return the\nsame snippet of XML, but it doesn't require a lot of imagination to figure out\nhow to turn this resolver into something sensible.\n\nIf you are not passing the resolve function, then it will simply leave the\nxinclude unharmed.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwspringer%2Fcruftless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwspringer%2Fcruftless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwspringer%2Fcruftless/lists"}