{"id":27105151,"url":"https://github.com/xrm-oss/xrm-templating-language","last_synced_at":"2025-12-28T19:31:47.625Z","repository":{"id":40957087,"uuid":"123020868","full_name":"XRM-OSS/Xrm-Templating-Language","owner":"XRM-OSS","description":"A domain specific language for Dynamics CRM allowing for easy text template processing","archived":false,"fork":false,"pushed_at":"2024-07-26T20:18:10.000Z","size":3090,"stargazers_count":24,"open_issues_count":25,"forks_count":8,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-14T00:47:17.299Z","etag":null,"topics":["hacktoberfest"],"latest_commit_sha":null,"homepage":"https://xrmadventuretime.com/tag/xtl/","language":"C#","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/XRM-OSS.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-02-26T19:40:04.000Z","updated_at":"2025-01-07T13:53:31.000Z","dependencies_parsed_at":"2024-06-17T19:32:38.750Z","dependency_job_id":"b8dd7750-9d84-4edd-a548-f3dbfc06db7c","html_url":"https://github.com/XRM-OSS/Xrm-Templating-Language","commit_stats":null,"previous_names":["digitalflow/xrm-templating-language"],"tags_count":52,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/XRM-OSS%2FXrm-Templating-Language","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/XRM-OSS%2FXrm-Templating-Language/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/XRM-OSS%2FXrm-Templating-Language/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/XRM-OSS%2FXrm-Templating-Language/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/XRM-OSS","download_url":"https://codeload.github.com/XRM-OSS/Xrm-Templating-Language/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247534604,"owners_count":20954565,"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":["hacktoberfest"],"created_at":"2025-04-06T18:36:56.119Z","updated_at":"2025-12-28T19:31:47.617Z","avatar_url":"https://github.com/XRM-OSS.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# XTL - Xrm Templating Language\n[![Build status](https://ci.appveyor.com/api/projects/status/skqv53ykh62587qp?svg=true)](https://ci.appveyor.com/project/DigitalFlow/xrm-templating-language) [![NuGet Badge](https://buildstats.info/nuget/Xrm.Oss.TemplatingLanguage.Sources)](https://www.nuget.org/packages/Xrm.Oss.TemplatingLanguage.Sources)\n\n|Line Coverage|Branch Coverage|\n|-----|-----------------|\n|[![Line coverage](https://cdn.rawgit.com/digitalflow/xrm-templating-language/master/reports/badge_linecoverage.svg)](https://cdn.rawgit.com/digitalflow/xrm-templating-language/master/reports/index.htm)|[![Branch coverage](https://cdn.rawgit.com/digitalflow/xrm-templating-language/master/reports/badge_branchcoverage.svg)](https://cdn.rawgit.com/digitalflow/xrm-templating-language/master/reports/index.htm)|\n\nA domain specific language for Dynamics CRM allowing for easy text template processing\n\n- [XTL - Xrm Templating Language](#xtl---xrm-templating-language)\n  - [Purpose](#purpose)\n  - [Where to get it](#where-to-get-it)\n  - [Requirements](#requirements)\n  - [Examples](#examples)\n  - [How To Register](#how-to-register)\n    - [Using XTL Editor](#using-xtl-editor)\n    - [Manual Way](#manual-way)\n  - [Benefits](#benefits)\n  - [General Information](#general-information)\n  - [Types](#types)\n  - [Functions](#functions)\n    - [If](#if)\n    - [Or](#or)\n    - [And](#and)\n    - [Not](#not)\n    - [IsNull](#isnull)\n    - [Coalesce](#coalesce)\n    - [Case](#case)\n    - [IsEqual](#isequal)\n    - [IsLess](#isless)\n    - [IsLessEqual](#islessequal)\n    - [IsGreater](#isgreater)\n    - [IsGreaterEqual](#isgreaterequal)\n    - [Value](#value)\n    - [RecordUrl](#recordurl)\n    - [Fetch](#fetch)\n    - [First](#first)\n    - [Last](#last)\n    - [Union](#union)\n    - [Map](#map)\n    - [Filter](#filter)\n    - [Sort](#sort)\n    - [RecordTable](#recordtable)\n    - [PrimaryRecord](#primaryrecord)\n    - [RecordId](#recordid)\n    - [RecordLogicalName](#recordlogicalname)\n    - [Concat](#concat)\n    - [IndexOf](#indexof)\n    - [Substring](#substring)\n    - [Replace](#replace)\n    - [Array](#array)\n    - [Length](#length)\n    - [Join](#join)\n    - [NewLine](#newline)\n    - [DateTimeNow](#datetimenow)\n    - [DateTimeUtcNow](#datetimeutcnow)\n    - [DateToString](#datetostring)\n    - [ConvertDateTime](#convertdatetime)\n    - [Format](#format)\n    - [RetrieveAudit](#retrieveaudit)\n    - [Snippet](#snippet)\n      - [Example - Refer to Snippet using unique name](#example---refer-to-snippet-using-unique-name)\n      - [Example - Refer to snippet with dynamic name](#example---refer-to-snippet-with-dynamic-name)\n      - [Example - Refer to snippet with simple filter](#example---refer-to-snippet-with-simple-filter)\n      - [Example - Refer to snippet with dynamic filter](#example---refer-to-snippet-with-dynamic-filter)\n  - [Sample](#sample)\n  - [Templating on not yet existing records](#templating-on-not-yet-existing-records)\n  - [Template Editor](#template-editor)\n  - [Syntax Definition](#syntax-definition)\n  - [License](#license)\n  - [Credits](#credits)\n\n## Purpose\nXTL is a domain specific language created for easing text processing inside Dynamics CRM.\nIt is an interpreted programming language, with easy syntax for allowing everyone to use it.\n\nThe parsing and interpreting is done using a custom recursive descent parser implemented in C#.\nIt is embedded inside a plugin and does not need any external references, so that execution works in CRM online and on-premises environments.\n\n## Where to get it\nYou can always download the latest release from the [releases page](https://github.com/DigitalFlow/Xrm-Templating-Language/releases).\nBeware that the solution only supports CRM \u003e= v8.0, as the XTL Editor makes use of the WebAPI. If you want to use it in CRM \u003c= v7.0, you are able to use the Plugin Assemblies, but will have to configure everything manually.\n\nAs alternative:\nBuild it yourself by running `build.cmd`, or simply download from [AppVeyor](https://ci.appveyor.com/project/DigitalFlow/xrm-templating-language/build/artifacts).\n\n## Requirements\nXTL itself does not use any specific CRM features and is compatible with Dynamics CRM 2011 and higher.\nCurrently the Plugin is built against Dynamics 365 SDK however. Future releases may target specific CRM versions.\nThe template editor is only available in CRM 2016 and later, as it uses the Web Api.\n\nThe solutions which can be downloaded from the releases support the following CRM versions:\n- XTL v3.0.3 to 3.8.1: CRM v8 and later\n- XTL v3.8.2 upwards: CRM v9 (since we use the rich text pcf control for snippet expressions if \"Is HTML\" is true)\n\n## Examples\nExamples of how to use XTL can be found in our [Wiki](https://github.com/DigitalFlow/Xrm-Templating-Language/wiki).\n\n## How To Register\n### Using XTL Editor\nInside the solution you imported, you'll find that there is a configuration page.\nYou can use this configuration page for testing templates as well as managing existing ones.\nSo creating of new template handlers inside your organization can be done completely using the editor.\nIt creates SDK message processing steps in the background, applying your custom settings combined with the needed default configs.\n\nImpression:\n![xtleditor](https://user-images.githubusercontent.com/4287938/38904112-b887b490-42a8-11e8-8b0b-1ccce115728e.png)\n\n### Manual Way\nRegister the assemblies using the Plugin Registration Tool.\nYou can then create steps in the pre operation stage. If you're on update message, be sure to register a preimg with all attributes needed for generating your texts.\n\nYou'll need an unsecure json configuration per step.\n\nProperties in there:\n- target: The target field for the generated text\n- templateField: The source field for the template (for \"per record\" templates, for example if replacing place holders inside emails). Since release v3.0.1 you can use XTL expressions in here as well, for getting your per record templates from a parent record for example. You could for example set `Value(\"parentcustomerid.oss_templatefield\")` for getting your template from the oss_templatefield of a contact's account.\n- template: A constant template that will be used for all records (for example when formatting addresses)\n- executionCriteria: An XTL expression that should return true if the template should be applied, or false otherwise. If not set, default is to apply the logic. Comparison Operators such as IsEqual automatically return booleans.\n\nTarget always has to be set, in addition to either template or templateField.\n\nSample (For formatting emails):\n```JSON\n{\n    \"target\": \"description\",\n    \"templateField\": \"description\",\n    \"executionCriteria\": \"IsEqual(Value(\\\"directioncode\\\"), true)\"\n}\n```\n\n## Benefits\nWhen dealing with the default e-mail editor of Dynamics CRM, the borders of what's possible are reached fast.\nXTL aims to integrate flawlessly into Dynamics CRM, to enhance the text processing capabilities. It is not limited to any specific CRM entity.\nUsing XTL provides the following benefits:\n\n- Using of the primary entity's related entity values, no matter how \"far\" they are away (i.e. regardingobject.parentaccountid.ownerid.fullname for using the full name of the owner of the company that the contact receiving an email belongs to).\n- Using of child entity values, where the primary record does not hold the lookup (i.e. all tasks associated to an account)\n- Conditional Branching (If-Then-Else constructs)\n- Generating Record URLs for all records reachable using above expressions\n- Easy to learn syntax (I admit that there are currently quite a few brackets needed for complex expressions)\n\n## General Information\n- Make sure that your functions only return strings (i.e. string constants) or values generated by Value() at the top level.\n- XTL placeholders are identified using ${{placeholder}}  patterns currently. Please take care that your placeholder does not contain any }} patterns, as this would break the placeholder.\n- Be careful when pasting text into records from this page or other formatted content. The text will contain format instructions, which break the placeholders. It is advised to copy the examples in here to NotePad++ or similar before inserting in e-mail HTML editor.\n\n## Types\nNative Types:\n- String Constants (Alpha numeric text inside double quotes or single quotes)\n- Integers (Digit expression)\n- Doubles (Digits are separated by '.', a 'd' is appended to separate from decimals. E.g: 1.4d)\n- Decimals (Digits are separated by '.', a 'm' is appended to separate from doubles. E.g: 1.4m)\n- Booleans (true or false)\n- Functions (Identifiers without quotes followed by parenthesis with parameters)\n- Dictionaries (JS style objects, such as { propertyName: value }. Value can be a constant or another expression.\n- Arrow functions (As function handlers for usage in some places. JS Style arrow functions such as (name) =\u003e Substring(name, 0, 1) for getting the first char of the value. Parameter names can be chosen by yourself, but they may not be named after an existing XTL function or a reserved word such as true, false, null) *Available starting with v3.8.0*\n\nIn addition, null is also a reserved keyword.\n\n## Functions\n### If\nTakes 3 Parameters: A condition to check, a true action and a false action.\nIf the condition resolves to true, the true action is executed, otherwise the false action.\n\nExample:\n``` JavaScript\nIf( IsNull ( Value(\"subject\") ), \"No subject passed\", Value(\"subject\") )\n```\n\n### Or\nTakes 2 ... n parameters and checks if any of them resolves to true.\nIf yes, then true is returned, otherwise false\n\nExample:\n``` JavaScript\nOr ( IsNull( Value (\"parentaccountid\") ), IsNull( Value (\"parentcontactid\") ) )  \n```\n\n### And\nTakes 2 ... n parameters and checks if all of them resolve to true.\nIf yes, then true is returned, otherwise false\n\nExample:\n``` JavaScript\nAnd ( IsNull( Value (\"parentaccountid\") ), IsNull( Value (\"parentcontactid\") ) )  \n```\n\n### Not\nTakes a parameter that resolves to a boolean and reverts its value.\n\nExample:\n``` JavaScript\nNot ( IsNull( Value (\"parentaccountid\") ) )  \n```\n\n### IsNull\nChecks if a parameter is null. If yes, true is returned, otherwise false.\n\nExample:\n``` JavaScript\nIsNull( Value (\"parentaccountid\") )  \n```\n\n### Coalesce\n**Available since: v3.9.6**\n\nUses the first non-null value of all parameters passed.\n\nExample:\n``` JavaScript\nCoalesce ( Value ( \"parentaccountid\" ), Value ( \"parentcontactid\" ), \"None\" )\n```\n\nIf parentaccountid is not null, it will be used.\nOtherwise, if parentcontactid is not null, it will be used.\nIf none of these two are not null, the static \"None\" will be used.\n\n### Case\n**Available since: v3.9.6**\n\nChecks a list of conditions followed by their values and uses the first true condition's value as result.\nIf none matches, the last value is used as default result.\n\nBecause of this, the list of parameters has to be odd ( 2 * n + 1, where n is the number of conditions, where each condition has one check followed by one result plus the last value as default).\n\nExample:\n``` JavaScript\nCase ( IsLess ( Value (\"creditlimit\"), 1000 ), \"Low\", IsLess ( Value (\"creditlimit\"), 50000 ), \"Medium\", IsLess ( Value (\"credilimit\"), 100000 ), \"High\", \"None\" )\n```\n\nIn above example, if credit limit is below 1000, it will return \"Low\".\nOtherwise, if it is less than 50.000, it will return \"Medium\".\nOtherwise, if it is less than 100.000, it will return \"High\".\n\nIf none matches, for example if creditlimit is null, it will return \"None\".\n\n### IsEqual\nChecks if two parameters are equal. If yes, true is returned, otherwise false.\nThe parameters are required to be of the same type.\nOptionSetValues are compared with their respective integer values.\n\nExample:\n``` JavaScript\nIsEqual ( Value ( \"gendercode\" ), 1 )\n```\n\n### IsLess\nChecks if first parameter is less than the second. If yes, true is returned, otherwise false.\n\nExample:\n``` JavaScript\nIsLess ( 1, 2 )\n```\n\n### IsLessEqual\nChecks if first parameter is less or equal than the second. If yes, true is returned, otherwise false.\n\nExample:\n``` JavaScript\nIsLessEqual ( 1, 2 )\n```\n\n### IsGreater\nChecks if first parameter is greater than the second. If yes, true is returned, otherwise false.\n\nExample:\n``` JavaScript\nIsGreater ( 1, 2 )\n```\n\n### IsGreaterEqual\nChecks if first parameter is greater or equal than the second. If yes, true is returned, otherwise false.\n\nExample:\n``` JavaScript\nIsGreaterEqual ( 1, 2 )\n```\n\n### Value\nReturns the object value of the given property. If it is used as top level function, the matching string representation is returned.\nThe text function which was present in early releases was replaced by this, as the Value function hosts both the string representation and the actual value now.\nYou can jump to entities that are connected by a lookup by concatenating field names with a '.' as separator.\nDefault base entity for all operations is the primary entity. You can override this behaviour by passing an entity object as config parameter named `explicitTarget`, like so: `Value(\"regardingobjectid.firstname\", { explicitTarget: PrimaryRecord() })`.\n\nWhen working with option sets, you can instruct XTL to use a specific language's label by passing adding a key `optionSetLcid` to your configuration object and setting the desired lcid (1033 = english, 1031 = german, etc...) as value, as follows: ` Value(\"someOptionSet\", { optionSetLcid: 1033 })`. It is recommended to pass a fixed lcid matching the language you're currently creating your template for.\nWhen not passing the optionSetLcid setting, the option set integer value will be returned. If passing the optionSetLcid and no label is found for the specified language, the user language label is used. So if you just want to use your current user's label, you can pass an invalid value such as -1. \n\nExample:\n``` JavaScript\nValue (\"regardingobjectid.firstname\")\n```\n\n### RecordUrl\nReturns the record urls of all entities or entity references that are passed as paramters.\nFor this to work, you have to set a secure json configuration with property organizationUrl set to your organization's url. When using the XTL editor, this will be done automatically.\nBy default, the URL will have the ref as link text as well. You can pass a configuration object with key `linkText` for defining a custom text to show link: `RecordUrl(PrimaryRecord(), { linkText: Value(\"name\") })` (of course a normal string can be passed to linkText as well, the function call should just show the possibilities).\nSimilar to the linkText, you can also pass an appId (since v3.2.0) as configuration, which allows you to open the record directly in UCI for example (The configuration object would look something like `{ appId: \"deadbeef-b85c-4fda-80da-d2f63baf9d7a\" }`.\n\n\n__Important Remark: When using RecordUrl, the organization url is automatically saved to the step secure config. When importing the step into another organization, you will initially have to open the editor in the new organnization, select each new step and save it once, so that the secure configs will be created. This is only necessary once, on the first import of a new step to an organization.__\n\n\nExample:\n``` JavaScript\nRecordUrl ( Value ( \"regardingobjectid\") )\n```\n\n### OrganizationUrl\n**Available since: v3.6.2**\n\nReturns the URL of your organization with an additional suffix and link text.\nWhen passing the configuration property \"asHtml\" as \"false\", it will only print the URL, otherwise it will be an HTML \u003ca\u003e tag.\n\n__Important Remark: When using it, the organization url is automatically saved to the step secure config. When importing the step into another organization, you will initially have to open the editor in the new organnization, select each new step and save it once, so that the secure configs will be created. This is only necessary once, on the first import of a new step to an organization.__\n\n\nExample:\n``` JavaScript\nOrganizationUrl ( { urlSuffix: \"/WebResources/oss_/somepage.html\", asHtml: true, linkText: \"Click me\" } )\n\nOrganizationUrl ( { urlSuffix: \"/WebResources/oss_/somepage.html\" } )\n```\n\nIf the organization URL was xosstest.crm4.dynamics.com, the output for the first example would be `\u003ca href=\"https://xosstest.crm4.dynamics.com/WebResources/oss_/somepage.html\"\u003eClick me\u003c/a\u003e`.\nFor the second example the output would simply be `https://xosstest.crm4.dynamics.com/WebResources/oss_/somepage.html`.\n\n### Fetch\nReturns a list of records, which were retrieved using supplied query.\nFirst parameter is the fetch xml. You can optionally pass a list of further parameters, such as entity references, entities or option set values as references.\nInside the fetch you can then reference them by {0}, {1}, ... and so on.\nThe primary entity is always {0}, so additional references start at {1}.\n \nExample:\n``` JavaScript\nFetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Array( Value ( \"regardingobjectid\" ) ) )\n```\n\nIf there is the possibility of one of the references for the fetch being null, you need to handle those cases.\nPassing a null value inside a fetch equal constraint for example, such as ```operator=\"eq\" value=\"null\"``` will lead to CRM exceptions.\nYou could now create your FetchXML expression using the Concat function or you'll have to wrap the fetch inside an if-clause that only executes the fetch, if your reference is not null. Otherwise the function will fail and replace the whole placeholder with an empty string.\n\nExample for Concat:\n``` JavaScript\nFetch ( Concat(\"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\", If( Not ( IsNull ( Value (\"regardingobjectid\") ) ), \"\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\", \"\u003ccondition attribute='regardingobjectid' operator='eq-null' /\u003e\"), \"\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\"), Array ( Value (\"regardingobjectid\") ))\n```\n\nExample for conditional fetch:\n``` JavaScript\n${{If(IsNull(Value(\"regardingobjectid\" ) ), \"No tasks\", RecordTable ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ), \"task\", true, \"subject\", \"description\"))}}\n```\n\nExample(Please note that in recent releases (\u003e= v1.0.31) the Text function is removed. You can replace it by the value function):\n![email_table](https://user-images.githubusercontent.com/4287938/36945291-e5173fa0-1fab-11e8-86e6-3007eac254c9.gif)\n\n\n### First\nReceives a list and returns the first object found in it.\n\nExample:\n``` JavaScript\nFirst ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ) )\n```\n\nExample of using it for getting the full name of the first to recipient on an email:\n``` JavaScript\nValue ( \"partyid.fullname\", { explicitTarget: First( Value(\"to\") ) } )\n``` \n\n### Last\nReceives a list and returns the last object found in it.\n\nExample:\n``` JavaScript\nLast ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ) )\n```\n\n### Union\nMerge multiple arrays into one.\n\nExample:\n``` JavaScript\nUnion ( [\"This\", \"will\"], [\"Be\"], [\"One\", \"Array\"] )\n```\n\nwill result in one array `[\"This\", \"will\", \"Be\", \"One\", \"Array\"]`.\n\n### Map\n**Available since: v3.8.0**\nMaps values inside an array to a new array while mutating every value with the provided function.\n\nExample:\n``` JavaScript\nJoin(\" \", Map([\"Lord\", \"of\", \"the\", \"Rings\"], (s) =\u003e Substring(s, 0, 1)))\n```\n\nwill result in `\"L o t R\"`.\n\nYour input does not need to be an array of strings, you can even loop over records fetched using Fetch:\n\n``` JavaScript\nJoin(\", \", Map(Fetch(\"\u003cfetch no-lock='true'\u003e\u003centity name='contact'\u003e\u003cattribute name='ownerid' /\u003e\u003cattribute name='createdon' /\u003e\u003c/entity\u003e\u003c/fetch\u003e\"), (record) =\u003e DateToString(ConvertDateTime(Value(\"createdon\", { explicitTarget: record }), { userId: Value(\"ownerid\", { explicitTarget: record }) }), { format: \"yyyy-MM-dd hh:mm\" })))\n```\n\nwill result in something like this: `\"2018-10-27 05:39, 2019-04-24 10:24\"`\n  \n### Filter\n**Available since: v3.9.6**\nFilters values inside an array by checking every value using the provided function.\nIf the provided function returns true, the value will be kept, otherwise it will be filtered out.\n\nExample:\n``` JavaScript\nJoin(\" \", Filter([\"Lord\", \"of\", \"the\", \"Rings\"], (e) =\u003e Not(IsEqual(IndexOf(e, \"o\"), -1))))\n```\n\nwill filter out all words that don't contain the character 'o', resulting in \"Lord of\".\n\nYour input does not need to be an array of strings, you can even loop over records fetched using Fetch:\n\n``` JavaScript\nJoin(\", \", Map(Filter(Fetch(\"\u003cfetch no-lock='true'\u003e\u003centity name='contact'\u003e\u003cattribute name='ownerid' /\u003e\u003cattribute name='createdon' /\u003e\u003c/entity\u003e\u003c/fetch\u003e\"), (recordInFilter) =\u003e Not(IsNull(Value(\"createdon\", { explicitTarget: recordInFilter })))), (record) =\u003e DateToString(ConvertDateTime(Value(\"createdon\", { explicitTarget: record }), { userId: Value(\"ownerid\", { explicitTarget: record }) }), { format: \"yyyy-MM-dd hh:mm\" })))\n```\n\nwill result in something like this: `\"2018-10-27 05:39, 2019-04-24 10:24\"`, where `createdon` is not null.\n\n### Sort\nSort array, either native values or by property. Ascending by default, descending by setting the config flag\n\nExample:\n``` JavaScript\nSort ( [\"This\", \"will\", \"Be\", \"Sorted\"] )\n```\n\nExample descending:\n``` JavaScript\nSort ( [\"This\", \"will\", \"Be\", \"Sorted\"], { descending: true } )\n```\n\nExample with property:\n``` JavaScript\nSort ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='createdon' /\u003e\u003cattribute name='subject' /\u003e\u003c/entity\u003e\u003c/fetch\u003e\"), { property: \"createdon\" })\n```\n\nwill result in one array `[\"This\", \"will\", \"Be\", \"One\", \"Array\"]`.\n\n### RecordTable\nReturns a table of records with specified columns and record url if wanted.\nFirst parameter is a list of records, for displaying inside the table. Consider retrieving them using the Fetch function.\nSecond is the name of the sub entity.\nThird is an array expression containing the columns to retrieve, which will be added in the same order.\nBy default the columns are named like the display names of their respective CRM columns. If you want to override that, append a colon and the text you want to use, such as \"subject:Overridden Subject Label\".\n\nIf you wish to append the record url as last column, pass a configuration object as last parameter as follows: `{ addRecordUrl: true }`.\nYou can also set a custom label for the url in each column by using the `linkText` property.\nThere is also the possibility to configure table, header and row styling. Pass a style property inside your configuration parameter, as follows: `{ tableStyle: \"border:1px solid green;\", headerStyle: \"border:1px solid orange;\", dataStyle: \"border:1px solid red;\"}`.\nOnly pass one configuration object, it can contain information on addRecordUrl as well as on table styling.\nYou can also define different styles for even and uneven rows by using the configuration keys `unevenDataStyle` and `evenDataStyle`.\n\nBelow you can find an example, which executes on e-mail creation and searches for tasks associated to the mails regarding contact.\nIt will then print the task subject and description, including an url, into the mail.\n\nIn release v3.0.2 you will also be able to configure each column's style separately.\nThis will allow for defining a custom width to each column.\nYou can set it by passing the columns not as array of strings, but as array of objects.\nThey may contain the keys `name` for the column name, which you previously passed directly as string, `label` as custom label to show in the header, `style` for your style information for this column and `mergeStyle` for defining, whether your style information should be appended to the line (or header) style or not (defaults to true).\nYou can also use `nameByEntity` when displaying mixed records in the table and pass a dictionary with logical name as keys and column name per entity as value such as:\n`{ nameByEntity: { contact: \"firstname\", task: \"subject\" }, label: \"Column Label\" }`.\nYou might want to override the column label as above to use one that matches all types.\n\nPassing all column names as array of strings is still possible.\n \nExample:\n``` JavaScript\nRecordTable ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ), \"task\", Array( \"subject:Overridden Subject Label\", \"description\" ), { addRecordUrl: true })\n```\n\nExample of objects as columns:\n``` JavaScript\nRecordTable ( Fetch ( \"\u003cfetch no-lock='true'\u003e\u003centity name='task'\u003e\u003cattribute name='description' /\u003e\u003cattribute name='subject' /\u003e\u003cfilter\u003e\u003ccondition attribute='regardingobjectid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ), \"task\", [ { name: \"subject\", label: \"Overridden Subject Label\", style: \"width:70%\" }, {name: \"description\"} ], { addRecordUrl: true })\n```\n\nStarting with v3.8.0, you can also pass your own render function for values. This can be used for mutating values before inserting them into the table.\nExample:\n\n```JavaScript\nRecordTable(Fetch(\"\u003cfetch no-lock='true'\u003e\u003centity name='contact'\u003e\u003cattribute name='ownerid' /\u003e\u003cattribute name='createdon' /\u003e\u003c/entity\u003e\u003c/fetch\u003e\"), \"contact\", [{ name: \"createdon\", label: \"Date\", renderFunction: (record, column) =\u003e DateToString(ConvertDateTime(Value(column, { explicitTarget: record }), { userId: Value(\"ownerid\", { explicitTarget: record }) }), { format: \"yyyy-MM-dd hh:mm\" }) }])\n```\n\nAbove example fetches all contacts with their owner and createdon date.\nBefore rendering them into a HTML table, each date is converted to the time zone of its owner.\nWhen accessing values from the row record, you need to pass the record parameter of the renderFunction as explicitTarget to the Value function.\nBy leaving the explicitTarget out, you can still access values from the current primary record (for example the email record where you are going to insert this RecordTable).\n\n### PrimaryRecord\nReturns the current primary entity as Entity object.\nNo parameters are needed.\n\nExample:\n``` JavaScript\nPrimaryRecord ()\n```\n\n### RecordId\n**Available since: v3.9.0**\n\nReturns the GUID of an Entity or Entity Reference object.\n\nExample:\n``` JavaScript\nRecordId( PrimaryRecord () )\n\nRecordId( Value(\"primarycontactid\") )\n```\n\nReturns the GUID of the primary record, or in the second example the GUID of the primarycontactid lookup.\n\n### RecordLogicalName\n**Available since: v3.9.0**\n\nReturns the logical name of an Entity or Entity Reference object.\n\nExample:\n``` JavaScript\nRecordLogicalName ( PrimaryRecord () )\n\nRecordLogicalName ( Value(\"primarycontactid\") )\n```\n\nReturns the GUID of the primary record, or in the second example the GUID of the primarycontactid lookup.\n\n### Concat\nConcatenates all parameters that were passed into one string.\n\nExample:\n``` JavaScript\nConcat(Value(\"lastname\"), \", \", Value(\"firstname\"))\n```\nAbove example could return something like 'Baggins, Frodo'.\n\n### IndexOf\n**Available since: v3.6.2**\nTakes a string input where a substring should be searched and the substring to search for as parameters. Returns the index where the substring was found inside the search text, or -1 if not found.\n\nExample:\n``` JavaScript\nIndexOf ( Value(\"fullname\"), \"Baggins\")\n```\nAbove example returns '6' when input is 'Frodo Beaggins'.\n\n### Substring\nTakes the substring of your input starting from a given index. Length of substring can be passed optionally as well.\n\nExample:\n``` JavaScript\nSubstring(Value(\"firstname\"), 1, 2 )\n```\nAbove example returns 'ro' when input is 'Frodo'.\n\n### Replace\nReplaces text in an input string using your pattern and replacement regexes.\nYou can use the whole .NET regex syntax for your pattern and replacement regex.\n\nExample:\n``` JavaScript\nReplace(Value(\"firstname\"), \"o\", \"a\" )\n```\nAbove example returns 'Frada' when input is 'Frodo'.\n\n### Array\nCreates an array from the parameters that you passed. Some functions do need arrays as input parameters, use this function for generating them.\nYou can enter static values such as string values, or even use XTL functions for passing in values.\nStatic strings are for example used for column names inside the RecordTable function, XTL functions as array values are used as fetch parameters in the Fetch function.\n\nExample:\n``` JavaScript\nArray(\"those\", \"are\", \"test\", \"parameters\")\n```\nInfo: Arrays have a default textual representation, which is all of the array value text representations delimited by \", \". Above examples textual representation would therefore be \"those, are, test, parameters\". Null values are not removed, but show up as empty string.\n\n### Length\nGets the length of an array or a string that is passed as first parameter.\n\nExample:\n``` JavaScript\nLength( [ \"A\", \"B\", \"C\" ] )\n```\n  \nor \n``` JavaScript\nLength( \"ABC\" )\n```\n  \nBoth executions will return 3.\n  \n### Join\nJoins multiple strings together using the separator that you passed. You can pass dynamic expressions whose text representations will be used. The first parameter is the separator, the second one is the array of values to concatenate.\nYou can pass a boolean as third parameter, stating whether empty parameters should be left out when concatenating (defaults to false).\n\nExample:\n``` JavaScript\nJoin(NewLine(), Array ( Value(\"address1_line1\"), Value(\"address1_line2\")), true)\n```\n\n### NewLine\nInserts a new line. \n\nExample:\n``` JavaScript\nNewLine()\n```\nInfo: This is needed when wanting to concatenate multiple texts using concat or join, since passing \"\\n\" as .NET line break string is interpreted as plain string.\n\n### DateTimeNow\nGets the current local time.\n\nExample:\n``` JavaScript\nDateTimeNow()\n```\n\n### DateTimeUtcNow\nGets the current UTC time.\n\nExample:\n``` JavaScript\nDateTimeUtcNow()\n```\n\n### DateToString\nPrints the date that is passed as first parameter. A custom format can be appended using a configuration object with format key.\n\nExample:\n``` JavaScript\nDateToString(DateTimeUtcNow(), { format: \"yyyyMMdd\" })\n```\n\nRefer to the .NET style for date formatting.\n\n### ConvertDateTime\nConverts a UTC DateTime (which is what you'll usually get from CRM) to a timezoned DateTime.\nYou can either pass a user reference as userId config property, or a fixed timezone as timeZoneId config property.\nThe timeZoneId fixed property is one of the default .NET timezone IDs (a list can be found [here](https://lonewolfonline.net/timezone-information/) for example).\nAn optional format config property can directly be set for defining the format of the timezoned DateTime.\n\nExample by userId:\n```JavaScript\nConvertDateTime(Value(\"createdon\"), { userId: Value(\"ownerid\") })\n```\n\nExample by fixed timezone:\n```JavaScript\nConvertDateTime(Value(\"createdon\"), { timeZoneId: \"Eastern Standard Time\" })\n```\nYou can use the same format strings as in the DateToString function.\n\n### Format\nFormats values using .NET string.Format. This way you can format numeric, Money and DateTime values.\n\nExample (Decimal, Double and Money will work the same):\n``` JavaScript\nFormat( Value(\"revenue\"),  { format: \"{0:0,0.0}\" } ) // Will print 123,456,789.2 for revenue equal 123456789.2\n```\n\nExample Int:\n``` JavaScript\nFormat( Value(\"index\"),  { format: \"{0:00000}\" } ) // Will print 00001 for index equal 1\n```\n\nRefer to the .NET style for date formatting.\n\n### RetrieveAudit\n**Available since: v3.8.1**\n\nCan be used for retrieving the previous value for a given field of a record. Auditing has to be enabled for the specific entity for this to work.\nReceives the record as Entity (PrimaryRecord function) or EntityReference as first parameter, field name as second.\n\nExample Entity:\n``` JavaScript\nRetrieveAudit(PrimaryRecord(), \"statuscode\")\n```\n\nExample EntityReference:\n``` JavaScript\nRetrieveAudit(Value(\"regardingobjectid\"), \"statuscode\")\n```\n\n### Snippet\n**Available since: v3.7.0**\n\u003e In XTL \u003e= v3.8.2, you can set \"Contains Plain text\" and \"Is HTML\" to true, for getting a rich text editor for expressions. This allows for advanced formatting in snippets and for uploading images as well.\n\nSnippets are an easy way for storing texts globally, so that they can be referred to from anywhere in the system.\nThey are stored in the XTL Snippet entity. You can use them as simple storage for a single (long) XTL expression, or even pass a complete text with embedded XTL expressions in the usual fashion (${{expression}}) inside it.\nWhen only saving a XTL expression, be sure to set \"Contains Plain text\" to \"No\", so that XTL will automatically wrap your expression in ${{ ... }} brackets.\nWhen saving a complete text with embedded XTL expressions, set \"Contains Plain Text\" to \"Yes\", so that XTL will do no automatic wrapping.\n\nYou can use the unique name inside the XTL snippet record for referring to it, or you can use the normal name and refer to it using its name and a custom fetchXml filter.\nThe second parameter is especially useful, as you can create custom fields on the snippet entity, for example a language field, and use the filter for fetching the correct language dynamically.\n\nFor the following examples, lets define some XTL snippets that we can imagine to be existent in our organization:\n\nSnippet 1 (used for fetching the owner's name on any entity)\n- oss_uniquename: \"OwnerName\"\n- oss_containsplaintext: false\n- oss_xtlexpression: Value(\"ownerid.name\")\n\u003e Remember: We must not wrap the expression in the usual ${{...}} brackets in this case, this will be done automatically, when \"Contains Plain Text\" is false\n\nSnippet 2 (used for generating a contact's salutation)\n- oss_uniquename: \"Salutation_EN\"\n- oss_containsplaintext: true\n- oss_xtlexpression: Dear ${{If(IsEqual(Value(\"gendercode\"), 1), \"Mr.\", \"Ms.\")}} ${{Value(\"lastname\")}}\n\nSnippet 3 (used for generating a contact's salutation)\n- oss_name: \"salutation\"\n- oss_containsplaintext: true\n- new_customlanguage: \"EN\"\n- oss_xtlexpression: Dear ${{If(IsEqual(Value(\"gendercode\"), 1), \"Mr.\", \"Ms.\")}} ${{Value(\"lastname\")}}\n\n#### Example - Refer to Snippet using unique name\nRefers to Snippet 1, which will be matched by the following expression:\n\n```JS\nSnippet(\"OwnerName\")\n```\n\n#### Example - Refer to snippet with dynamic name\nRefers to Snippet 2, which will be matched by the following expression:\n\n``` JavaScript\nSnippet(Concat(\"Salutation_\", Value(\"new_languageisocode\"))\n```\n\nWhen running this on a contact with \"new_languageisocode\" equal to \"EN\", this will resolve to snippet name \"Salutation_EN\" and snippet 2 will thus be found.\n\n#### Example - Refer to snippet with simple filter\nRefers to Snippet 3, which will be matched by the following expression:\n\n``` JavaScript\nSnippet(\"salutation\", { filter: '\u003cfilter\u003e\u003ccondition attribute=\"new_customlanguage\" operator=\"eq\" value=\"EN\" /\u003e\u003c/filter\u003e' })\n```\n\n#### Example - Refer to snippet with dynamic filter\nRefers to Snippet 3, which will be matched by the following expression:\n\n``` JavaScript\nSnippet(\"salutation\", { filter: '\u003cfilter\u003e\u003ccondition attribute=\"new_customlanguage\" operator=\"eq\" value=\"${{Value(\"new_languageisocode\")}}\" /\u003e\u003c/filter\u003e' })\n```\n\nBefore using the custom filter, all XTL tokens will be processed, so for a contact with \"new_languageisocode\" equal to \"EN\", the expression inside the filter value would be exchanged for \"EN\" and the correct snippet found and applied.\n\n## Sample\nConsider the following e-mail template content:\n```\nHello ${{Value(\"regardingobjectid.parentcustomerid.ownerid.firstname\")}},\n\na new case was associated with your account ${{Value(\"regardingobjectid.parentcustomerid.name\")}}, you can open it using the following URL:\n${{RecordUrl(Value(\"regardingobjectid\"))}} \n```\n\nWhen creating an e-mail from above template or even just an outgoing e-mail with the template content as text it will resolve to:\n```\nHello Frodo,\n\na new case was associated with your account TheShire Limited, you can open it using the following URL:\nhttps://imagine-creative-url.local\n```\n\n## Templating on not yet existing records\nSometimes you might want to preset values on records that are not yet created. For this you can call the `oss_XTLApplyTemplate` custom action and provide the current values of your current entity directly as JSON.\n\nFor example if you want to process a template on the email entity and your templates need data only from the regardingobject of your entity:\n\n```JS\nconst payload = {\n            jsonInput: JSON.stringify({\n                targetEntity: {\n                    Attributes: [\n                        {\n                            \"key\": \"regardingobjectid\",\n                            \"value\": {\n                                \"__type\": \"EntityReference:http://schemas.microsoft.com/xrm/2011/Contracts\",\n                                \"Id\": regardingObjectRef[0].id,\n                                \"KeyAttributes\": [],\n                                \"LogicalName\": regardingObjectRef[0].entityType,\n                                \"Name\": null,\n                                \"RowVersion\": null\n                            }\n                        }\n                    ]\n                },\n                template: snippet\n            })\n        };\n```\n\nYou can then send this payload when executing the custom action.\n\nA json representation of an entity that can be deserialized in Dynamics looks something like this:\n\n```JSON\n{\n    \"Attributes\": [\n        {\n            \"key\": \"createdon\",\n            \"value\": \"/Date(1615542774000)/\"\n        },\n        {\n            \"key\": \"customersatisfactioncode\",\n            \"value\": {\n                \"__type\": \"OptionSetValue:http://schemas.microsoft.com/xrm/2011/Contracts\",\n                \"Value\": 3\n            }\n        },\n        {\n            \"key\": \"oss_someflag\",\n            \"value\": false\n        },\n        {\n            \"key\": \"ticketnumber\",\n            \"value\": \"123\"\n        },\n        {\n            \"key\": \"ownerid\",\n            \"value\": {\n                \"__type\": \"EntityReference:http://schemas.microsoft.com/xrm/2011/Contracts\",\n                \"Id\": \"deadbeef-dead-dead-dead-deadbeefdead\",\n                \"KeyAttributes\": [],\n                \"LogicalName\": \"systemuser\",\n                \"Name\": null,\n                \"RowVersion\": null\n            }\n        },\n        {\n            \"key\": \"processid\",\n            \"value\": \"00000000-0000-0000-0000-000000000000\"\n        },\n        {\n            \"key\": \"incidentid\",\n            \"value\": \"b4d7965c-b7d8-4c6b-a498-905eeedf6bf1\"\n        },\n    ],\n    \"EntityState\": null,\n    \"Id\": \"deadbeef-dead-dead-dead-deadbeefdead\",\n    \"KeyAttributes\": [],\n    \"LogicalName\": \"incident\",\n    \"RelatedEntities\": [],\n    \"RowVersion\": null\n}\n```\n\n## Template Editor\nThe solution that you can find inside the releases section also contains a live editor that can be used for creating, testing and serializing configurations for XTL. In addition to that, existing XTL plugin execution steps can be loaded, tested and updated.\nYou just need to import the xtl solution, open it and select the configuration page.\nInside this page, you can select which entity you want your template to be executed on, which record it should use for previews and set execution criteria and your template. When selecting to load an existing plugin execution step, this information is parsed from the existing step.\nAfter clicking \"Preview\", the template will be processed and you'll be presented the result and the interpreter trace for debugging purposes.\n\nThe example below targets an E-mail, which has an opportunity as regarding object:\n![xtl_template_editor](https://user-images.githubusercontent.com/4287938/37870319-d9e516dc-2fca-11e8-9e78-6c52d842d535.gif)\n\nSource snippet of the template being used:\n```\nHi ${{Value(\"regardingobjectid.parentaccountid.ownerid\")}},\n\n${{Value(\"regardingobjectid.createdby\")}} created a new opportunity for your account ${{Value(\"regardingobjectid.parentaccountid.name\")}}.\n\nYou can open it using the following link: ${{RecordUrl(Value(\"regardingobjectid\"))}}.\n\nBelow products have been configured:\n${{RecordTable ( Fetch ( \"\u003cfetch no-lock='true' \u003e\u003centity name='opportunityproduct' \u003e\u003cattribute name='productdescription' /\u003e\u003cattribute name='priceperunit' /\u003e\u003cattribute name='quantity' /\u003e\u003cattribute name='extendedamount' /\u003e\u003cfilter type='and' \u003e\u003ccondition attribute='opportunityid' operator='eq' value='{1}' /\u003e\u003c/filter\u003e\u003c/entity\u003e\u003c/fetch\u003e\", Value ( \"regardingobjectid\" ) ), \"opportunityproduct\", true, \"productdescription\", \"quantity\", \"priceperunit\", \"extendedamount\")}}\n\nIn case any questions are left, please give ${{Value(\"regardingobjectid.createdby\")}} a call: ${{Value(\"regardingobjectid.createdby.homephone\")}}.\n\nThis E-Mail was automatically generated by the system.\n```\n\n## Syntax definition\nThe language syntax is defined by the following BNF (Backus-Naur form):\n\n```\n(* --------------------------- *)\n(*  Root production            *)\n(* --------------------------- *)\n\nformula\n    ::= number\n     | string\n     | boolean\n     | object\n     | list\n     | null\n     | lambda_function\n     | function_call\n     | identifier\n\nexpression\n    ::= formula\n     | identifier\n\n(* --------------------------- *)\n(*  Literals                   *)\n(* --------------------------- *)\n\nnumber\n    ::= sign? integer float_suffix?\n     | sign? integer \".\" integer float_suffix\n\nsign ::= \"-\"\n\ninteger ::= digit { digit }\n\nfloat_suffix ::= \"d\" | \"m\"\n\nstring\n    ::= double_string | single_string\n\ndouble_string ::= #'\"[^\"\\\\]*(?:\\\\.[^\"\\\\]*)*\"'\nsingle_string ::= #\"\\'[^'\\\\]*(?:\\\\.[^'\\\\]*)*\\'\"\n\nboolean ::= \"true\" | \"false\"\n\nnull ::= \"null\"\n\n(* --------------------------- *)\n(*  Identifiers                *)\n(* --------------------------- *)\n\nidentifier\n    ::= letter { letter | digit }\n\nletter ::= #\"[A-Za-z]\"\ndigit  ::= #\"[0-9]\"\n\n(* --------------------------- *)\n(*  Lists                      *)\n(* --------------------------- *)\n\nlist\n    ::= \"[\" whitespace? list_elements? whitespace? \"]\"\n\nlist_elements\n    ::= expression { whitespace? \",\" whitespace? expression }\n\n(* --------------------------- *)\n(*  Objects                    *)\n(* --------------------------- *)\n\nobject\n    ::= \"{\" whitespace? key_value_pairs? whitespace? \"}\"\n\nkey_value_pairs\n    ::= key_value_pair { whitespace? \",\" whitespace? key_value_pair }\n\nkey_value_pair\n    ::= identifier whitespace? \":\" whitespace? expression\n\n(* --------------------------- *)\n(*  Functions                  *)\n(* --------------------------- *)\n\nfunction_call\n    ::= function_name \"(\" whitespace? arguments? whitespace? \")\"\n\nfunction_name ::= identifier\n\narguments\n    ::= expression { whitespace? \",\" whitespace? expression }\n\n(* --------------------------- *)\n(*  Lambda functions           *)\n(* --------------------------- *)\n\nlambda_function\n    ::= \"(\" parameter_list \")\" whitespace? \"=\u003e\" whitespace? expression\n\nparameter_list\n    ::= parameter { whitespace? \",\" whitespace? parameter }\n\nparameter ::= identifier\n\n(* --------------------------- *)\n(*  Whitespace                 *)\n(* --------------------------- *)\n\nwhitespace ::= #\"[ \\t\\n]*\"\n```\nYou can also validate your calls on this website: https://mdkrajnak.github.io/ebnftest/ \n\n## License\nLicensed using the MIT license, enjoy!\n\n## Credits\nI learnt writing the interpreter using the [Let's Build a Compiler Tutorial](https://compilers.iecc.com/crenshaw/) by Jack Crenshaw.\nAlthough being quite old, the technics are still the same and the advice is invaluable, it's a great resource for learning.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxrm-oss%2Fxrm-templating-language","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fxrm-oss%2Fxrm-templating-language","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fxrm-oss%2Fxrm-templating-language/lists"}