{"id":20780690,"url":"https://github.com/aroemers/crustimoney","last_synced_at":"2025-04-30T20:33:59.514Z","repository":{"id":6128824,"uuid":"7357108","full_name":"aroemers/crustimoney","owner":"aroemers","description":"A Clojure idiomatic PEG parser.","archived":false,"fork":false,"pushed_at":"2024-10-19T14:08:21.000Z","size":706,"stargazers_count":23,"open_issues_count":1,"forks_count":2,"subscribers_count":2,"default_branch":"v2","last_synced_at":"2025-04-30T20:33:07.525Z","etag":null,"topics":["clojure","parser-combinators","parser-generator","peg"],"latest_commit_sha":null,"homepage":"https://cljdoc.org/d/nl.functionalbytes/crustimoney","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aroemers.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"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,"zenodo":null}},"created_at":"2012-12-28T17:02:28.000Z","updated_at":"2025-04-21T07:08:17.000Z","dependencies_parsed_at":"2024-08-11T09:24:34.493Z","dependency_job_id":"ba9f7bea-4775-46bc-b4d1-7c5a9846a977","html_url":"https://github.com/aroemers/crustimoney","commit_stats":{"total_commits":45,"total_committers":2,"mean_commits":22.5,"dds":"0.022222222222222254","last_synced_commit":"d030377404616f2bcb092cf2d5fd33e61de2750b"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fcrustimoney","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fcrustimoney/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fcrustimoney/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aroemers%2Fcrustimoney/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aroemers","download_url":"https://codeload.github.com/aroemers/crustimoney/tar.gz/refs/heads/v2","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251777763,"owners_count":21642220,"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":["clojure","parser-combinators","parser-generator","peg"],"created_at":"2024-11-17T13:38:52.798Z","updated_at":"2025-04-30T20:33:59.490Z","avatar_url":"https://github.com/aroemers.png","language":"Clojure","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Clojars Project](https://img.shields.io/clojars/v/nl.functionalbytes/crustimoney.svg)](https://clojars.org/nl.functionalbytes/crustimoney)\n[![cljdoc badge](https://cljdoc.org/badge/nl.functionalbytes/crustimoney)](https://cljdoc.org/d/nl.functionalbytes/crustimoney/CURRENT/api/crustimoney)\n[![Clojure CI](https://github.com/aroemers/crustimoney/actions/workflows/build.yml/badge.svg)](https://github.com/aroemers/crustimoney/actions/workflows/build.yml)\n[![Changelog](https://img.shields.io/badge/change-log-informational)](https://github.com/aroemers/crustimoney/releases)\n[![Blogpost](https://img.shields.io/badge/blog-Introducing%20crustimoney-blue)](https://functionalbytes.nl/clojure/crustimoney/2024/05/24/crustimoney.html)\n\u003c!-- [![Clojars Project](https://img.shields.io/clojars/dt/nl.functionalbytes/crustimoney?color=blue)](https://clojars.org/nl.functionalbytes/crustimoney) --\u003e\n\n# 📙 crustimoney\n\nA Clojure library for PEG parsing, supporting various grammars, packrat caching and... _cuts_?\n\n![Banner](images/banner.png)\n\n## Motivation\n\nThe first version of crustimoney was my first library in Clojure, a long time ago.\nSimply put, this version is the mental exercise of making it better.\nI like to think it turned out well.\nMaybe you like it too.\n\n\u003cimg src=\"images/pooh.gif\" align=\"right\"/\u003e\n\n\u003e \"What does Crustimoney Proseedcake mean?\" said Pooh. \"For I am a Bear of Very Little\n\u003e Brain, and long words Bother me.\"\n\u003e\n\u003e \"It means the Thing to Do.\"\n\u003e\n\u003e \"As long as it means that, I don't mind,\" said Pooh humbly.\n\nLet's parse those long words from Owl.\n\n## Features\n\n- Create parsers from **combinator** functions\n- .. or from **string-based** definitions\n- .. or from **data-driven** definitions\n- Packrat **caching**, optimizing cpu usage\n- Concept of **cuts**, optimizing memory usage - and error messages\n- Focus on capture groups, resulting in **shallow parse trees**\n- **Minimal parse tree** data, fetch only what's needed on post-processing\n- Virtual stack, preventing overflows\n- Infinite **loop detection** (runtime)\n- Missing rule references detection (compile time)\n- **Streaming** support (experimental)\n\n## Add to project\n\nThe instructions for the latest version can be found here: [![Clojars Project](https://img.shields.io/clojars/v/nl.functionalbytes/crustimoney.svg)](https://clojars.org/nl.functionalbytes/crustimoney)\n\n## Quick!\n\nIn a hurry, and you just need to parse a small text where a regular expression just doesn't cut it?\nFor this there's the `crustimoney.quick/parse` function.\nIt takes a string- or data-driven parser definition plus a text.\nThe definition can use the [built-in parsers](#built-in-parsers).\nIt returns a conveniently transformed result if it matched.\nFor example:\n\n```clj\n(quick/parse '(\"alice\" (\" and \" (:who word))+)\n             \"alice and bob and eve\")\n\n=\u003e [nil \"alice and bob and eve\"\n    [:who \"bob\"]\n    [:who \"eve\"]]\n```\n\nAs you can see, the captured texts are directly availabe in the result.\n\nIf this is what you need - _right now!_ - you could skip directly to [string-based grammar](#string-based-grammar) or [data-based grammar](#data-based-grammar).\nFor all the other details, read on!\n\n## Combinator grammar\n\nThe combinators are at the heart of the library.\nEven though you may never use them directly, it is a good starting point.\nBelow is a list of available combinators, found in the `crustimoney.combinator-grammar` namespace.\n\nThe essentials:\n\n- `literal`, match an exact literal string\n- `chain`, chain multiple consecutive parsers\n- `choice`, match the first successful parsers\n- `repeat*`, eagerly match a parser as many times as possible\n- `negate`, succeed if the given parser does not\n\nThose are actually enough for parsing any unambiguous text.\nBut more combinators are provided, for ease of use, nicer result trees and better performance:\n\n- `regex`, match a regular expression\n- `repeat+`, same as `repeat*`, but require at least one match\n- `lookahead`, succeed if the given parser does, without advancing\n- `maybe`, try the given parser, succeed anyway\n- `eof`, succeed if there is no more input\n\nEach combinator returns a parser (model).\nSome combinators take one or more parsers, making them composable.\nFor example:\n\n```clj\n(chain (literal \"foo\") (regex \"ba(r|z)\"))\n```\n\nSuch a parser can be supplied to `core/parse`, together with the string to parse.\n\n## Parse results\n\nThe result is a \"hiccup\"-style parse tree, for example:\n\n```clj\n[:node {:start 0, :end 6}\n [:child-node {:start 0, :end 3}]\n [:child-node {:start 3, :end 6}]]\n```\n\nTo capture a node during parsing, it must be \"named\", such as `:node` or `:child-node` in above example.\nThis is done by wrapping a parser with `with-name` (or by other means, depending on the grammar type).\nResults without a name are filtered out, though its named children are kept.\nThe root node can be nameless (`nil`).\n\nOn failed parses, a set of errors is returned, which has the following structure:\n\n```clj\n#{{:key :expected-literal, :at 10, :detail {:literal \"foo\"}}\n  {:key :expected-match, :at 8, :detail {:regex \"alice|bob\"}}\n  {:key :unexpected-match, :at 8, :detail {:text \"eve\"}}}\n```\n\nIf you want to override the default key of an error, a parser can be wrapped with `with-error`.\nFor example:\n\n```clj\n(def parser\n  (with-error :number-required\n    (regex #\"\\d+\")))\n\n(core/parse parser \"nan\")\n=\u003e #{{:key :number-required, :at 0}}\n```\n\nTo work with these successes and errors, the functions in the `results` namespace can be used.\nThese allow you to get the text of a success node for example, or add `:line` and `:column` keys to the errors.\nIt also contains tools to walk and transform the tree (see [built-in transformer](#built-in-transformer)).\n\n## Recursive grammars\n\nComposing a single parser can be enough in some cases.\nMore complex texts need or are better expressed with a recursive grammar, i.e. named parsers that can refer to each other.\nFor example:\n\n```clj\n(def my-grammar\n   {:root (repeat+ (choice :foo :bax))\n    :foo  (literal \"foo\")\n    :bax  (regex \"ba(r|z)\")})\n```\n\nThis grammar can be used as follows:\n\n```clj\n(core/parse my-grammar \"foobaz\")\n=\u003e [nil {:start 0, :end 6}]\n```\n\nSuch a map requires a `:root` rule to be present.\n\n### Nested recursive grammars\n\nAn advanced feature is lexically-scoped nested recursive grammars.\nA contrived example of this is:\n\n```clj\n{:foo    (literal \"foo\")\n :bar    (literal \"wrong\")\n :foobar {:root (chain :foo :bar)\n          :bar  (literal \"bar\")}\n :root   (ref :foobar)}\n```\n\nInner maps can refer to rules in its own scope and the enclosing scopes.\nInner rules take precedence over outer rules with the same name.\nOuter scopes can not refer to inner scopes.\n\n## Compiling the grammar\n\nNote that the grammar model is compiled on-the-fly by `core/parse`.\nThis will check for a `:root` rule and dangling references.\n\nTo do this compiling beforehand, you can use `core/compile`.\nIt is recommended to do this in production code, as it speeds up consecutive parse calls considerably.\n\n## Auto-named rules\n\nThe example above shows that all success nodes are filtered out, except the root node.\nThis is because the results were nameless.\nThe parsers could be wrapped with `with-name`, but the names would probably be the same as the rule names in this case.\nAppending an `=` to the rule name will automatically wrap the parser with `with-name`.\nThis would update the grammar to:\n\n```clj\n{:root= (repeat+ (choice :foo :bax))\n :foo=  (literal \"foo\")\n :bax=  (regex \"ba(r|z)\")})\n```\n\nNote that the refernce keys are still without the postfix.\nParsing it again would yield the following result:\n\n```clj\n[:root {:start 0, :end 6}\n [:foo {:start 0, :end 3}]\n [:bax {:start 3, :end 6}]]\n```\n\nA word of caution though.\nIt is encouraged to be very intentional about which nodes should be captured and when.\nFor example, using the following grammar would _only_ yield a `:wrapped` node if the `:expr` is really wrapped in parentheses:\n\n```clj\n(choice (with-name :wrapped\n          (chain (literal \"(\")\n                 (ref :expr)\n                 (literal \")\")))\n        (ref :expr))\n```\n\nThis approach results in shallower result trees and thus less post-processing.\n\n## Cuts\n\nMost PEG parsers share a downside: they are memory hungry.\nThis is due to their packrat caching, that provides one of their upsides: linear parsing time.\n\n[This paper](https://www.researchgate.net/publication/221292729_Packrat_parsers_can_handle_practical_grammars_in_mostly_constant_space) describes adding _cuts_ to PEGs, a concept that is known from Prolog.\nCrustimoney expands on this by differentiating between _hard_ cuts and _soft_ cuts.\n\n### Hard cuts\n\nA hard cut tells the parser that it should never backtrack beyond the position where it is encountered.\nThis has two major benefits.\nThe first is better and more localized error messages.\nThe following example shows this, and also how to add a hard cut in the `chain` combinator.\n\n```clj\n(def example\n  (maybe (chain (literal \"(\")\n                hard-cut\n                (regex #\"\\d+\")\n                (literal \")\"))))\n\n(core/parse example \"(42\")\n=\u003e #{{:key :expected-literal, :at 3, :detail {:literal \")\"}}}\n```\n\nWithout the hard cut, the parse would be successful (because of the `maybe` combinator).\nBut, since the text clearly opens a parenthesis, it would be better to fail.\nThe hard cut enforces this, as the missing `\")\"` error cannot backtrack beyond it.\nSo from a user's standpoint, a cut can already be very beneficial.\n\nThe second major benefit is that the parser can release everything in its cache before the cut position.\nIt will never need this again.\nThis behaviour makes that well placed hard cuts can - especially when parsing repeating structures - alleviate the memory requirements to be constant.\n\nNote that a cut can only be used within a `chain`, and never as the first element.\nThe preceding elements should consume some input, and that input should only be valid for element at that point in the text.\n\nAn alias for the `hard-cut` is `\u003e\u003e`.\n\n### Soft cuts\n\nThere are situations that localized error messages are desired, but backtracking should still be possible.\nFor such situations a soft cut can be used.\nSuch a cut also disallows backtracking, but only while inside the `chain`.\nOnce the chain is successfully parsed, the soft cut has no effect anymore.\n\nConsider the expansion of the previous example:\n\n```clj\n(def example\n  (choice (chain\n            ;; --- same as before, but now with soft-cut\n            (maybe (chain (literal \"(\")\n                          soft-cut\n                          (regex #\"\\d+\")\n                          (literal \")\")))\n            ;; ---\n            (literal \"foo\"))\n          (literal \"bar\")))\n\n(core/parse example \"(42\")\n=\u003e #{{:key :expected-literal, :at 3, :detail {:literal \")\"}}}\n\n(core/parse example) \"(42)baz\")\n=\u003e #{{:key :expected-literal, :at 4, :detail {:literal \"foo\"}}\n     {:key :expected-literal, :at 0, :detail {:literal \"bar\"}}}\n```\n\nThe `hard-cut` has been replaced with a `soft-cut`, of which the alias would be `\u003e`.\nAs shown, this still shows a localized error for the missing `\")\"`, yet it also allows backtracking to try the `\"bar\"` choice.\n\nSince backtracking before the soft cut is still allowed outside of the chain's scope, the cache is not affected.\nHowever, soft and hard cuts can be combined in a grammar.\nWe could for instance extend the grammar a bit more:\n\n```clj\n(repeat+ (chain example hard-cut))\n```\n\nThis effectively says that after each finished `example`, we won't backtrack, that part is done.\nMany of such consecutive `example`s can be parsed, without memory requirements growing.\nThe parse tree does grow of course, though there is an experimental [`stream+`](#experimental-combinators) combinator.\n\nThe significance of cuts in PEGs must not be underestimated.\nTry to use them in your grammar on somewhat larger inputs.\nThe computing overhead is small, and is countered by faster cache lookups.\n\n## String-based grammar\n\nA parser or grammar can be defined in a string.\nWhile direct combinators have the most flexibility, a string-based definition is far denser.\nThe discussed combinators translate to this string-based grammar in the following way:\n\n```\nliteral   \u003c- 'foo'\nchain     \u003c- 'foo' 'bar'\nchoice    \u003c- 'bar' / 'baz'\n\nrepeat*   \u003c- 'foo'*\nrepeat+   \u003c- 'foo'+\nmaybe     \u003c- 'foo'?\n\nnegate    \u003c- !'foo'\nlookahead \u003c- \u0026'foo'\n\nregex     \u003c- #'ba(r|z)'\nchars     \u003c- [a-zA-Z]*\n\neof       \u003c- $\nref       \u003c- literal\n\ngroup     \u003c- ('foo' 'bar' / 'alice')\nnamed     \u003c- (:bax regex)\n\nsoft-cut  \u003c- \u003e\nhard-cut  \u003c- \u003e\u003e\n```\n\nThe function `create-parser` in the `crustimoney.string-grammar` is used to create a parser out of such a string.\nNote that above \"example\" has rules and thus describes a recursive grammar.\nTherefore a map is returned by `create-parser`.\nHowever, it is perfectly valid to define a single parser, such as:\n\n```\n'alice and ' !'eve' [a-z]+\n```\n\nThe syntax is flexible regarding whitespace.\nMultiple lines can be on the same line, and a `,` is also seen as whitespace.\n\nMultiple grammars can be merged, which can come in handy for parsers that are easier expressed in a different way:\n\n```clj\n(merge\n (create-parser \"root \u003c- 'Hello ' email\")\n {:email (regex #\"...\")})\n```\n\nAnd lastly, the names of the rules can have an `=` sign appended, for the auto-named feature discussed earlier.\n\nTo give an impression on how a string-based grammar looks, below is the same `example` parser that was used to explain soft-cuts.\n\n```\n( '(' \u003e [0-9]+ ')' )? 'foo' / 'bar'\n```\n\nAs you can see, it is far denser, and likely more readable.\nFor more examples, see the `examples` directory in the library's source.\n\n## Data-based grammar\n\nNext to the string-based definition, there is also a data-driven variant available.\nThe grammar below shows how such a definition is formed.\nIt is very similar to the string-based grammar.\n\n```clj\n'{literal    \"foo\"\n  character  \\f\n  regex      #\"ba(r|z)\"\n  regex-tag  #crusti/regex \"ba(r|z)\" ; EDN support\n\n  chain      (\"foo\" \"bar\")\n  choice     (\"bar\" / \"baz\")\n\n  repeat*    (\"foo\"*)\n  repeat+    (\"foo\"+)\n  maybe      (\"foo\"?)\n\n  negate     (!\"foo\")\n  lookahead  (\u0026\"foo\")\n\n  eof        $\n  ref        literal\n\n  group      (\"foo\" \"bar\" / \"alice\")\n  named      (:bax regex)\n\n  soft-cut   \u003e\n  hard-cut   \u003e\u003e\n\n  combinator-call   [:with-error {:key :fail} #crusti/parser (\"fooba\" #\"r|z\")]\n  custom-combinator [:my.app/my-combinator ...]}\n```\n\nThe function `create-parser` in the `crustimoney.data-grammar` is used to create a parser out of such a definition.\n\nThe data-based definition shares many properties with the string-based one.\nIt works the same way in supporting both recursive and non-recursive parsers, it also has auto-naming (the `=` postfix), and can be used as part of a bigger grammar.\n\nIt does have an extra feature: direct combinator calls, using vectors.\nThe first keyword in the vector determines the combinator.\nIf it is without a namespace, `crustimoney.combinators` is assumed (so not `crustimoney.combinator-grammar`!).\nThe other arguments are left as-is, except those tagged with `#crusti/parser`.\nWith that tag, the data is processed again as a parser definition.\n\nAnother possible benefit of the data-based grammar over the string-based one, is that is supports [nested grammars](#nested-recursive-grammars).\n\n### EDN support\n\nSince the grammar definition is data, it is perfectly feasible to use the EDN format.\nThe `#crusti/...` tags are not automatically supported by Clojure's EDN reader.\nThe following code makes it work:\n\n```clj\n(clojure.edn/read-string {:readers *data-readers*} ...)\n```\n\nNote that regular expressions are not supported in plain EDN.\nFor this you can use the `#crusti/regex` tag (see above example), although it could also be written as `[:regex {:pattern \"..\"}]`.\n\n## Vector-based grammar\n\nThe former section on data-based grammars describes that a vector is a valid data type, and that these translate to combinator calls.\nThat means that it is possible to write the entire grammar using vectors.\nThing is, _this is actually what the combinator-based, string-based and data-based parser generators do_.\nThey all use such vector model as their output format.\nThis allows the easy combining of multiple grammars and they can be debugged easily.\n\nAnother benefit is that the data-grammar can easily be extended.\nThe function `data-grammar/vector-model` is from the `DataGrammar` protocol.\nThis makes it possible to add support for other data types, using Clojure's `extend-type`.\nThe implementation simply returns a vector, possibly pointing to your own combinator (see further down below).\n\n## Built-in parsers\n\nThe library provides a couple of predefined parsers in the `built-ins` namespace, for parsing things like spaces, numbers, words and strings.\nIt also contains a map called `all`, containing all of the built-in parsers.\nThis map can be used as a basis for your own grammar, by merging them:\n\n```clj\n(merge built-ins/all (create-parser \"\n  root \u003c- (space? (:name word) blank (:id natural) space?)* $\n\"))\n```\n\n## Built-in transformer\n\nThe `results` namespace contains mostly basic functions for dealing with the parse results.\nThese include functions as `success-\u003etext` to get the matched text of a node, and `success-\u003echildren` to get its children.\nWhile not necessary (as the results tree is made of plain vectors), it does increase readability.\n\nWriting your own parse tree processor is easy, as again, it's just data.\nThat said, the `results` namespace has a `transform` function you can use.\nThis performs a postwalk, transforming the nodes based on their name, applying a function that receives the node and the full text.\nTwo accompanying helper macros are available, called `coerce` and `collect`.\nHere is an example:\n\n```clj\n(-\u003e (parse ... text)\n    (transform text\n      {:number    (coerce parse-long)\n       :operand   (coerce {\"+\" + \"-\" - \"*\" * \"/\" /})\n       :operation (collect [[v1 op v2]] (op v1 v2))\n       nil        (collect first)}))\n```\n\nIf the parse result is not a success, the `transform` returns the result as is.\n\nThe `coerce` macro creates a transformer, by applying a function to the node's matched text.\nInstead of a function, `coerce` can also take a binding vector and a body.\nSo the `:number` transformation above could be written as `(coerce [s] (parse-long s))`.\nIt could also be written without the macro as `(fn [node text] (parse-long (success-\u003etext node text)))`.\n\nThe `collect` macro also creates a transformation function, by applying a function to the node's children, as seen with the `nil` (root node) transformer above.\nInstead of a function, `collect` can also take a binding vector and a body, as seen with the `:operation` transformer.\n\nIf a transformer is missing, an exception is thrown.\nFor generic transformations (not rule-based), there's a `postwalk` function.\n\n## Experimental combinators\n\nLastly, there are a couple of experimental combinators.\nBeing experimental, they may get promoted, or changed, or dismissed.\n\n- `range`, like a `repeat`, requiring a minimum of matches and stops after a maximum of matches\n- `stream*` and `stream+`, like `repeat*`/`repeat+`, but does not keep its children\n- `with-callback`, fires (success) result of a parser to a callback function\n\nThese can be found in the `experimental.combinators` namespace, including more documentation on them.\nNote that these experimental combinators compile directly to a parser function, not to the vector model.\n\n## Writing your own combinator\n\nA parser combinator returns a function that takes a text input and a position.\nIt returns either a success or (a set of) errors.\nIt does this using the `results` namespace, which has functions like `-\u003esuccess` and `-\u003eerror`.\nSome combinators take other parser functions as their argument, making them composeable.\n\nHowever, the parsers returned by the combinators do not call other parsers directly.\nThis could lead to stack overflows.\nSo next to a `-\u003esuccess` or `-\u003eerror` result, it can also return a `-\u003epush` result.\nThis pushes another parser onto a virtual stack, together with an index and possibly some state.\n\nFor this reason, a parser function has the following signature:\n\n```clj\n(fn\n  ([text index]\n    ...)\n  ([text index result state]\n   ...))\n```\n\nThe 2-arity variant is called when the parser was pushed onto the stack.\nIt receives the entire text and the index it should begin parsing.\n\nIf it returns a \"push\" result, the 4-arity variant is called when that parser is done.\nIt receives the text and the original index, but also the result of the pushed parser and any state that was pushed with it.\nNow it can decide whether to return a success, a set of errors, or again a push.\n\nBefore you write your own combinator, do realise that the provided combinators are complete in the sense that they can parse any structured text.\n\n_That's it. As always, have fun!_ 🚀\n\n## License\n\nCopyright © 2022-2024 Arnout Roemers\n\nThis program and the accompanying materials are made available under the\nterms of the Eclipse Public License 2.0 which is available at\nhttp://www.eclipse.org/legal/epl-2.0.\n\nThis Source Code may also be made available under the following Secondary\nLicenses when the conditions for such availability set forth in the Eclipse\nPublic License, v. 2.0 are satisfied: GNU General Public License as published by\nthe Free Software Foundation, either version 2 of the License, or (at your\noption) any later version, with the GNU Classpath Exception which is available\nat https://www.gnu.org/software/classpath/license.html.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroemers%2Fcrustimoney","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faroemers%2Fcrustimoney","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faroemers%2Fcrustimoney/lists"}