{"_id":"@anuraagvaidya/trool","_rev":"3-83e07c30e141c1d22bd98b13ec16ae7b","name":"@anuraagvaidya/trool","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.8":{"name":"@anuraagvaidya/trool","version":"1.0.8","description":"a rule engine for NodeJS written in TypeScript","main":"./lib/index.js","typings":"./lib/index.d.ts","directories":{"lib":"lib"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"repository":{"type":"git","url":"git+ssh://git@github.com/anuraagvaidya/Trool.git"},"keywords":["rule","engine","drool","typescript","nodejs","trool","csv","spreadsheet","node","ts","rule-engine"],"author":{"name":"seanpmaxwell"},"license":"MIT","bugs":{"url":"https://github.com/anuraagvaidya/Trool/issues"},"homepage":"https://github.com/anuraagvaidya/Trool#readme","dependencies":{"colors":"^1.3.3","csvtojson":"^2.0.8"},"devDependencies":{"@types/colors":"^1.2.1","@types/node":"^11.10.4","tslint":"^5.13.1","tslint-lines-between-class-members":"^1.3.1","typescript":"^3.3.3333"},"_id":"@anuraagvaidya/trool@1.0.8","_nodeVersion":"14.4.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-4vy3sE2xh+XAWFSEbUeJqFuEttopd/4nC64l/Rl0zPwj9Lz9OieR2KIQthvNBFfUc0PL/PWERVFpzxgoEkRw7w==","shasum":"ec6b3307bb942c2af9023384474dde23ab6c1502","tarball":"https://registry.npmjs.org/@anuraagvaidya/trool/-/trool-1.0.8.tgz","fileCount":17,"unpackedSize":162577,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfTSAJCRA9TVsSAnZWagAAdmQP+gJRyxWCVYLs9EngrSPT\nsVEk3JA/kJvXFqGVgBbIJAKpeK6nHq18BtK+ZusoVURLJFZZhV1Cll8jRQNg\na0eJr0mNFfK0CfNmt2n8wRnzei+ouBGPjWa/zZj3d33nLs4gCf6ZhUZCNvJS\neeFQU8sGY/lZWdaphvXWta9rYicVxJLLqwFIDZV1AMgy2rBmoSeeYzpmfUga\nTznULRfU/wuD7cWciflGOYgf74yilIJOWT4qDKkUeG76OaUUj4Ib2wc38Rdu\nKeusPD8ptdjo3bfybK1gHbA/XDzr1R8m9l2CNRFJm2ZFyQB/Jz1NaE44yUNN\nk0UXnEQcdxBhydqW7poJKbql/QCtWLYmw5xWOYkaU4D5smY2KSTIHZmNAl+Q\n1ubXgZl+We/s/+CPHLWMcEoRHwKddpP7FH9uuHCsqidkWhNfPMnC/trEbCqj\nhsMHs7OOS1K7nfFsmRxpwZon3CCe9nz9QiDn2OnjTEST6KXr6aOT+8sWvtcY\nFXZJ6DdNCF87qVvOnsLGMTwgQLRgt+koeGU7HlWJh8z7LtO+ghyP3IYfNlxZ\niNeW/kVujSuRKXDMydKmHBEs6CwTvjw5L9uP4fMsMRhEx5ntH1NCFzJ+nRGq\nMo5Lc440Cc7ZX4mCa4xkrH8LjPpcKXVS3MsF6NyIOchB8ttf6IKo429GE2w0\nFSP2\r\n=YL12\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCICMSmkeU/27V/LE7xjpr5SXulx9lV25RkFvnKaQdPBj6AiEA2U5L7IjmsGw+2rAEvKfgxT+iplh54VZzd2VB/FB+2js="}]},"maintainers":[{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"}],"_npmUser":{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/trool_1.0.8_1598889993336_0.7746284353861355"},"_hasShrinkwrap":false},"1.0.9":{"name":"@anuraagvaidya/trool","version":"1.0.9","description":"a rule engine for NodeJS written in TypeScript","main":"./lib/index.js","typings":"./lib/index.d.ts","directories":{"lib":"lib"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"repository":{"type":"git","url":"git+ssh://git@github.com/anuraagvaidya/Trool.git"},"keywords":["rule","engine","drool","typescript","nodejs","trool","csv","spreadsheet","node","ts","rule-engine"],"author":{"name":"seanpmaxwell"},"license":"MIT","bugs":{"url":"https://github.com/anuraagvaidya/Trool/issues"},"homepage":"https://github.com/anuraagvaidya/Trool#readme","dependencies":{"colors":"^1.3.3","csvtojson":"^2.0.8"},"devDependencies":{"@types/colors":"^1.2.1","@types/node":"^11.10.4","tslint":"^5.13.1","tslint-lines-between-class-members":"^1.3.1","typescript":"^3.3.3333"},"_id":"@anuraagvaidya/trool@1.0.9","_nodeVersion":"14.4.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-qhB+6EidxNQsDwG11Ov0QddxUHkpVM9KGvqUkmFkNrGVPnhRFywc4Jcw8ooSjVqV2A5gmOG8pvaR6VyE373kSw==","shasum":"ac31abecbd1475bf6d4400e15462cbce75c9568b","tarball":"https://registry.npmjs.org/@anuraagvaidya/trool/-/trool-1.0.9.tgz","fileCount":17,"unpackedSize":162791,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfTyo9CRA9TVsSAnZWagAAs84P/iBE/3KnZwSMTAOTKwZk\nI/6jwP3W8vfnd0fGy+avsG2nbGO/LiLVJWlxBQ0rBJH1tnTrYwnQDBR0XNgb\nQR9EYzmSqKCDUmxNs3RC7MC6hYgpQJ9Q14gZYnBmhvOpUTZNOCZ+doLAvTxz\nbpZq+HnaxoSqIi+vTRh5oPxg0xYvvFTW1cD/8VlnOgsA2+ECqSSWttxchMtB\np696GRt/7FjUwlpjQL1zhhzd/5pMi8msCs25rJ2b2H02oipMLf6UPPaJg2j3\n2O2MFdd8E0AHZi4yCeJZIyPtNwZKM+XjRHGovFwSlXiPi6/ygrBWlHKxTvM3\nlu2aitzq7kAFizzgubLIJzq3iV2JJusEHrdvDx7dR1rrCvDijicEFxoaWM5A\nmus0GcWUweeKCqXQjH9HRuKzUWvlfaMcAXhw8AtFxpaiIhe0QWvzDybewABd\nLle3MjTsZI+JozkCFZJLpsE3c9uxMiudFpAP3iu7O2/5wz26sY8BZ4h7CpFD\n9TE8Q49qqNbr3+5pFgHaVyWPONAReUmkgm2inNWC0j/iNHQo+cAZkx0Js7Lt\n5q9oVG0Ve0ft5y4lZd2C1TGS9yccAwyW/+UWtxrLWVGCxrXa1e4P+A8WhEjF\ntzUOSqj1rNmud2E17FqXzllJSvmHndW/ZCUsipHGW1veueP0gYbe5XHG/0m7\nLwkI\r\n=3g5B\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEUCIAabsi5KiRXgPHo0hZCIfuDZsS9r8IilvmKavuU3xKIMAiEA9zTWrqVDgs5HX00TiPWDMV6zmDMCIHU7c4auOxaYnrg="}]},"maintainers":[{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"}],"_npmUser":{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/trool_1.0.9_1599023676628_0.8963717842241605"},"_hasShrinkwrap":false},"1.1.0":{"name":"@anuraagvaidya/trool","version":"1.1.0","description":"a rule engine for NodeJS written in TypeScript","main":"./lib/index.js","typings":"./lib/index.d.ts","directories":{"lib":"lib"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"repository":{"type":"git","url":"git+ssh://git@github.com/anuraagvaidya/Trool.git"},"keywords":["rule","engine","drool","typescript","nodejs","trool","csv","spreadsheet","node","ts","rule-engine"],"author":{"name":"seanpmaxwell"},"license":"MIT","bugs":{"url":"https://github.com/anuraagvaidya/Trool/issues"},"homepage":"https://github.com/anuraagvaidya/Trool#readme","dependencies":{"colors":"^1.3.3","csvtojson":"^2.0.8"},"devDependencies":{"@types/colors":"^1.2.1","@types/node":"^11.10.4","tslint":"^5.13.1","tslint-lines-between-class-members":"^1.3.1","typescript":"^3.3.3333"},"_id":"@anuraagvaidya/trool@1.1.0","_nodeVersion":"14.4.0","_npmVersion":"6.14.5","dist":{"integrity":"sha512-l6uypt6nAuIbVzPuJYzQSDLOqE+tgY4PKjlbGGphPsjdqKoubXvNW9rm+4uF/nI++tZj3kBqlZYS4S3/fa8Vgg==","shasum":"cf31c75c07bb98d394e03d8fd58bcb19e285db03","tarball":"https://registry.npmjs.org/@anuraagvaidya/trool/-/trool-1.1.0.tgz","fileCount":17,"unpackedSize":162791,"npm-signature":"-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJfftyxCRA9TVsSAnZWagAA5LQQAJWDfZdqGjtxa7iwurcX\nTPwr0ogsnWFOoN9RGdW3Hy70lowR5Jk7JastAL54bpKOuAPb+9jmTJnmdtyC\ne/+2cyawxzNgL+tm6CVyB4w13AoqXevn2eJyU34X3EPLicSirEVDak2q+7Zg\n2gqvJJfEMPjQ9f2e9occhJ+xNup5EU40b1lw/79QJxRltR9FIJuW8x2cgvrW\nBb6i5ZbWyR1j8gT9xascG0i0pEZpXm92nlGOLrWN33dg9zCKI1HiE7bscq+B\nVNZDgAvg67GTl7Q+gFPxfxr/NQtPm1KoneGUIx2+YsW5GpqRl0pmIoykcVI4\nsvpjuEPrM6YRFrtOvBdZdLMRQdUicFWfCjXy+Pe16puf6kMsuSMB9p6Q3a8k\nLGFxA224qFDIbQTu1BKttG7caOi55NyXKT+fuFfF5EkazAV+LGPeaaJG0aVp\numCl8XpNH6/jb07ZGJJMJcrHb59fZxoEXU+ssonxf+jfJa9n8KE1mwROktS1\nzlb8PoNE2PeHUAYsY3ukuFGO23E3ar4ADqDbWrRd+1QBzUKpLzgArlGXgb1u\nj6I4WXnyniwLZc0lpzcn2S7d5sYePzKF3CC7BGZ3uKV5nRy2NsDiClU7KNSS\nBFP59FNoONtYXjdVN+jn4vAd+ESNleJaLR6Mm3l3HQexOYMmV88Dta2wrnHM\n0KfO\r\n=ObSd\r\n-----END PGP SIGNATURE-----\r\n","signatures":[{"keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA","sig":"MEQCIB/IdvSDprx16EgnLsTSHf03qqD1Js1VISMoFN2Di3e8AiBCrXuuZ/zbERLw5NeYnqK9SEZVu3RXzMibZgIMQ/gP4w=="}]},"maintainers":[{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"}],"_npmUser":{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages","tmp":"tmp/trool_1.1.0_1602149552912_0.43962831289803517"},"_hasShrinkwrap":false}},"time":{"created":"2020-08-31T16:06:33.293Z","1.0.8":"2020-08-31T16:06:33.472Z","modified":"2022-04-04T14:25:50.427Z","1.0.9":"2020-09-02T05:14:36.757Z","1.1.0":"2020-10-08T09:32:33.074Z"},"maintainers":[{"name":"anuraagvaidya","email":"me@anuraagvaidya.com"}],"description":"a rule engine for NodeJS written in TypeScript","homepage":"https://github.com/anuraagvaidya/Trool#readme","keywords":["rule","engine","drool","typescript","nodejs","trool","csv","spreadsheet","node","ts","rule-engine"],"repository":{"type":"git","url":"git+ssh://git@github.com/anuraagvaidya/Trool.git"},"author":{"name":"seanpmaxwell"},"bugs":{"url":"https://github.com/anuraagvaidya/Trool/issues"},"license":"MIT","readme":"# TROOL - a spreadsheet rule engine for NodeJS/TypeScript\r\n<h3>Get rules out of the code so non-engineers can make updates over time!</h3>\r\n<br>\r\n\r\n\r\n## Features\r\n- Manage rules in a business spreadsheet format.\r\n- Heavily inspired by Java's KnowledgeBase Library.\r\n- Allows use of `Import` objects so values can be reused. These can be passed dynamically through\r\nthe code or hardcoded in the spreadsheet.\r\n- GitHub Repo contains fully-functional sample project so you can start practicing right away.\r\n- Fully type-safe :)\r\n\r\n\r\n## Requirements\r\n- The spreadsheet must be exported as a _.csv_ before usage. \r\n- Can be used directly with JavaScript (ES5 and ES6) but documentation is in TypeScript.\r\n\r\n\r\n## Screenshot\r\n<img alt='fullOverview' src='https://github.com/seanpmaxwell/trool/raw/master/fullOverview.png' border='0'>\r\n\r\n\r\n## Quick Start\r\n- install `$ npm install --save trool`\r\n\r\n- Open Excel, LibreOffice Calc, or some other spreadsheet tool of your choice.\r\n\r\n- A **Fact** is an instance-object or array of instance-objects, which you want to update based on\r\nconditions that may change over time. Create at least one decision-table on the spreadsheet so you\r\ncan update a fact.\r\n\r\n- You must follow the format closely for setting up a decision-table. Trool may throw errors if\r\nyou do not set things up correctly. The guide contains all the details for setting up a decision-table.\r\nYou can look at the screen-shot above if you want a quick glimpse on what decision-tables look like. \r\n\r\n- Export your spreadsheet as a CSV file. The rules for formatting the csv are the same as they are\r\nfor the `csvtojson` library. That's what Trool uses internally to convert the csv to a JSON object.\r\n\r\n- Create a new NodeJS program (preferably with TypeScript) and import the `trool` library at the top.\r\nInstantiate a new `trool` object and pass `true` to the constructor if you want to show logs while\r\nthe library updates your facts.\r\n\r\n```typescript\r\nimport Trool from 'trool';\r\n\r\nclass PriceCalculator {\r\n\r\n    private trool: Trool;\r\n\r\n    constructor() {\r\n        this.trool = new Trool(true);\r\n    }\r\n```\r\n\r\n- The trool library only provides 1 public method `applyRules(...)` which returns a promise containing\r\nthe updated facts. So create an `async/await` method to fire off `applyRules()` and wrap it in a `try/catch`\r\nblock.\r\n\r\n- `applyRules(...)` needs the path to the csv file, the facts to be updated, and optionally any imports.\r\n\r\n- The facts and the imports must be wrapped in holder objects, with the key being the name of the \r\nfact/import to use in the spreadsheet, and the value being the actual fact or import. The `imports`\r\nparam is optional because you only want to use ones specified in the spreadsheet or have no need for any. \r\n\r\n```typescript\r\npublic async calcTotalPrice(): Promise<void> {\r\n    \r\n    const factsHolder = {\r\n        Visitors: [new Visitor(), new Visitor()],\r\n        Tickets: new Ticket()\r\n    }\r\n\r\n    const importsHolder = { \r\n        VisitorTypes: {\r\n            ADULT: 'Adult',\r\n            CHILD: 'Child'\r\n        }\r\n    };\r\n\r\n    try {\r\n        const csvFilePath = path.join(__dirname, 'Name_of_Spreadsheet.csv');\r\n        const updatedFacts = await this.trool.applyRules(csvFilePath, factsHolder, importsHolder);\r\n\r\n    } catch (err) {\r\n        console.error(err.message);\r\n    }\r\n}\r\n```\r\n\r\n- The `updatedFacts` variable in the previous snippet will contain all the same key/value pairs and\r\narrays in the same order as the `factsHolder` that was passed in.\r\n\r\n<br>\r\n\r\n\r\n## Guide\r\n\r\n**Important! When you setup your decision-tables and imports there are some rules to follow the in order \r\nfor your tables/imports to be properly loaded into memory. Strict formatting is enforced for readability \r\npurposes**.\r\n\r\n**Decision-Tables:**\r\n\r\n- All decision-tables must start with a cell containing the text `Table: \"Fact Name\"`. A table without a \r\nfact name or with a fact name that does not exist on the facts-holder will throw an error. If you create\r\n2 tables that have the same fact-name, the second table will overwrite all the changes from the first.\r\n\r\n- A table will end when it reaches an empty row, the end of the file, or the start of a new table\r\nor import. For readability, you should terminate all tables with an empty row.\r\n\r\n- The first 2 rows on a decision-table are for specifying the conditions and the actions. If all conditions\r\nare true, then the actions will execute. After the start cell (the cell with `Table: \"Fact Name\"`) you\r\nmust specify at least 1 condition and 1 action.\r\n\r\n- Specifying Condition and Action columns must be done by putting `'Condition'` or `'Action'`, at the \r\ntop of each column. These are case sensitive so make sure to capitalize the values. All conditions\r\nmust come before all actions and you cannot have anything other than `'Condition'` or `'Action'` at\r\nthe top of your table columns. \r\n\r\n- The condition must be a statement which evaluates to `true` or `false`. The left side of the statement \r\nmust be a method or getter on the fact's instance-object and the right side must be `$param`. The operator \r\nmust be an existing JavaScript comparator such as `==` or `<=`. The values in the rows below will replace \r\n`$param`. For example, suppose I want to get the age of a visitor for an app which calculates ticket prices. I\r\nwould need to create a TypeScript getter (`get age(): number {}`) or a method like `getAge() {}` to \r\nfetch the visitor's age and compare it to the parameter value.\r\n\r\n- Actions are methods on a fact which will execute if all the conditions evaluate to true. Unlike conditions,\r\nyou can have multiple params passed in. The action must be a method or a TypeScript setter function on\r\nthe fact or else Trool will throw an error. The number of params in the action columns' cells below\r\nmust match the number or `$param` strings or else Trool will throw an error. \r\n\r\n- All rows on a decision-table, except for the first 2, are referred to as _rules_. A rule works by evaluating \r\na list of conditions against cell values which, if they all evaluate to true, will execute the specified \r\nactions. A rule must start with a rule name and can be anything but cannot be blank.\r\n\r\n- For each cell on the rule, if it is a condition column, the cell value will replace the `$param` value\r\nand evaluate the cell as `true` or `false`. An empty cell will automatically be evaluated as `true`. If\r\nany cell evaluates to `false`, that rule will fail and the decision-table will go on the next rule.\r\n\r\n- While on a rule's action column, each param specified must be separated by a comma. If no params\r\nare specified (the cell is blank), the rule will skip that action column. \r\n\r\n- Whew that was a lot! Now that we've gone over the rules for creating tables, let's look at an\r\nexample in detail. In the following snippet, we see an example on a decision-table and the fact `Tickets`.\r\n\r\n<img alt='sampleTable' src='https://github.com/seanpmaxwell/trool/raw/master/sampleTable.png' border='0'>\r\n\r\n- To update this fact you needs to make sure the `Tickets` property exists on the facts-holder when it\r\ngets passed to `applyRules()`. If `Tickets` is an array, the decision-table will get applied to each\r\nTicket instance in the array. \r\n\r\n- The table has one condition and one action. There's also 2 rules: `Set Price - Regular` and `Set Price - Season`. \r\nLook at the operations for the condition and action. On the left side of each operation \r\nwe can see the properties `option` and `price`. This means that each Ticket instance object passed in must have \r\ngetters/setters for the `option` and `price` properties or else an error will be thrown.\r\n\r\n- The first rule `Set Price - Regular` will take the value for `option` and check and see if its value\r\nis equal to the string `\"Regular\"`. If so, it will apply the action column to the fact. The setter for\r\n`price` will be called and the value `70` will be passed in. The exact same sequence of events will take\r\nplace for the next rule `Set Price - Season`. In other words, if the Ticket option is `\"Season\"`, the price\r\nwill be `600`, if the option is `\"Regular\"`, the price will be `70`.\r\n\r\n- And that's how Trool works! If you need to change the price for a Regular or Seasonal ticket over\r\ntime without bugging your engineers, just have someone else make updates to the spreadsheet :)\r\n<br>\r\n\r\n\r\n**Imports:**\r\n\r\n- For large complicated spreadsheets you might want to reuse certain values. Suppose for Visitors who\r\nmight be buying these tickets the maximum age for a child is `18`. One might need to reuse this value\r\nfor multiple rules/tables and if it's updated in once place, it needs to be updated everywhere. For \r\nexample, the maximum age for a child might change from `18` to `15` or something like that. This is where\r\nimports come in handy. An import basically sets up a simple JSON object that you can access in your tables.\r\nImports can be created in the spreadsheet or passed through `applyRules()`. \r\n\r\n- Trool iterates the entire spreadsheet and first looks for all the imports, then it goes back through\r\nand initializes all the decision-tables. So the ordering of your tables/imports does not matter.\r\nFor cleanliness I recommend keeping them separated.\r\n\r\n- All imports must begin with the cell `Import: \"Import Name\"`. If you pass an import in the imports holder \r\n(via `applyRules()`) that has a key matching an import name in the spreadsheet, you will get the warning:\r\n`!!WARNING!! The spreadsheet is using an import name already passed via the imports object. The spreadsheet \r\nwill overwrite the import: \"Import Name\"`.\r\n\r\n- The quick-start had an example of passing imports through `applyRules()`. Let's look at an example of\r\nan import hardcoded in the spreadsheet.\r\n<img alt='importExample' src='https://github.com/seanpmaxwell/trool/raw/master/importExample.png' border='0'>\r\n\r\n- With this import, each table will have access to an object named `TicketTypes` and all of its properties.\r\nIf you were to place `TicketTypes.SEASON` in a cell for the operation `option == $param`, \r\nthe Ticket object would call the `option` getter and pass `\"SEASON\"` as the value.\r\n\r\n- When using imports through `applyRules()`, you don't have to necessary use an object as a property\r\nand could have it as a primitive. VisitorTypes itself could be a string or number. I don't\r\nrecommend using imports this way though; it could be confusing in a collaborative environment.\r\n\r\n- One more thing, you cannot use nested properties on imports: i.e. `Import.key.key` This is intentional,\r\nit would lead to a very message spreadsheet. \r\n<br>\r\n\r\n\r\n**Special Notes:**\r\n\r\n- In Trool spreadsheets, `==` under the hood is actually using `===`. \r\n\r\n- The values you can pass through cells are strings, numbers, true, false, and null. Don't use objects\r\nor undefined. Via imports, you could actually use an object as a `$param` value, but don't do it. This\r\ncould be confusing for non-engineers. Stick with primitives. Create extra getters and setters when dealing\r\nwith multiple values. \r\n\r\n- Import property name rules are the same as for JavaScript keys. That means alphanumeric, underscores,\r\nand dashes. Anything other than characters will throw an error.\r\n","readmeFilename":"README.md"}