{"id":13443090,"url":"https://github.com/pragdave/earmark","last_synced_at":"2026-02-18T21:02:44.818Z","repository":{"id":18332877,"uuid":"21512093","full_name":"pragdave/earmark","owner":"pragdave","description":"Markdown parser for Elixir","archived":false,"fork":false,"pushed_at":"2025-06-21T10:54:58.000Z","size":2039,"stargazers_count":896,"open_issues_count":26,"forks_count":148,"subscribers_count":11,"default_branch":"master","last_synced_at":"2025-10-21T19:03:54.314Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Elixir","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/pragdave.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":"2014-07-05T02:53:38.000Z","updated_at":"2025-10-12T16:49:15.000Z","dependencies_parsed_at":"2024-11-05T23:02:56.856Z","dependency_job_id":"3ee7ba89-78b5-4cf7-9a5d-ffcfbe423b7c","html_url":"https://github.com/pragdave/earmark","commit_stats":{"total_commits":930,"total_committers":67,"mean_commits":"13.880597014925373","dds":"0.20860215053763442","last_synced_commit":"8e19bd7701987d5f9452f5b85fcd108bf35ed29d"},"previous_names":[],"tags_count":69,"template":false,"template_full_name":null,"purl":"pkg:github/pragdave/earmark","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragdave%2Fearmark","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragdave%2Fearmark/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragdave%2Fearmark/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragdave%2Fearmark/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pragdave","download_url":"https://codeload.github.com/pragdave/earmark/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pragdave%2Fearmark/sbom","scorecard":{"id":743382,"data":{"date":"2025-08-11","repo":{"name":"github.com/pragdave/earmark","commit":"3f1c6273b34cefab49b14566ba0dc58a16fe386f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":4.5,"checks":[{"name":"Code-Review","score":6,"reason":"Found 19/30 approved changesets -- score normalized to 6","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/elixir.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":8,"reason":"8 commit(s) and 2 issue activity found in the last 90 days -- score normalized to 8","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:40: update your workflow using https://app.stepsecurity.io/secureworkflow/pragdave/earmark/elixir.yml/master?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/elixir.yml:41: update your workflow using https://app.stepsecurity.io/secureworkflow/pragdave/earmark/elixir.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/elixir.yml:51: update your workflow using https://app.stepsecurity.io/secureworkflow/pragdave/earmark/elixir.yml/master?enable=pin","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":8,"reason":"2 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-vq52-99r9-h5pw","Warn: Project is vulnerable to: GHSA-9fm9-hp7p-53mf"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 19 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-22T18:06:55.956Z","repository_id":18332877,"created_at":"2025-08-22T18:06:55.956Z","updated_at":"2025-08-22T18:06:55.956Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29596125,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T20:59:56.587Z","status":"ssl_error","status_checked_at":"2026-02-18T20:58:41.434Z","response_time":162,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T03:01:55.921Z","updated_at":"2026-02-18T21:02:44.801Z","avatar_url":"https://github.com/pragdave.png","language":"Elixir","readme":"\n\u003c!--\nDO NOT EDIT THIS FILE\nIt has been generated from the template `README.md.eex` by Extractly (https://github.com/RobertDober/extractly.git)\nand any changes you make in this file will most likely be lost\n--\u003e\n\n# Earmark—A Pure Elixir Markdown Processor\n\n[![CI](https://github.com/pragdave/earmark/actions/workflows/elixir.yml/badge.svg)](https://github.com/pragdave/earmark/actions/workflows/ci.yml)\n[![Coverage Status](https://coveralls.io/repos/github/pragdave/earmark/badge.svg?branch=master)](https://coveralls.io/github/pragdave/earmark?branch=master)\n[![Hex.pm](https://img.shields.io/hexpm/v/earmark.svg)](https://hex.pm/packages/earmark)\n[![Hex.pm](https://img.shields.io/hexpm/dw/earmark.svg)](https://hex.pm/packages/earmark)\n[![Hex.pm](https://img.shields.io/hexpm/dt/earmark.svg)](https://hex.pm/packages/earmark)\n\n\n**N.B.**\n\nThis README contains the docstrings and doctests from the code by means of [extractly](https://hex.pm/packages/extractly)\nand the following code examples are therefore verified with `ExUnit` doctests.\n\n## Table Of Content\n\n- [Table Of Content](#table-of-content)\n- [Options](#options)\n  - [Earmark.Cli.Implementation](#earmarkcliimplementation)\n  - [Earmark.Options](#earmarkoptions)\n  - [Earmark.Options.make_options/1](#earmarkoptionsmake_options1)\n  - [Earmark.Options.relative_filename/2](#earmarkoptionsrelative_filename2)\n  - [Earmark.Options.with_postprocessor/2](#earmarkoptionswith_postprocessor2)\n  - [Earmark.Internal](#earmarkinternal)\n  - [Earmark.Internal.as_ast!/2](#earmarkinternalas_ast2)\n  - [Earmark.Internal.from_file!/2](#earmarkinternalfrom_file2)\n  - [Earmark.Internal.include/2](#earmarkinternalinclude2)\n  - [Earmark.Transform](#earmarktransform)\n    - [Structure Conserving Transformers](#structure-conserving-transformers)\n    - [Postprocessors and Convenience Functions](#postprocessors-and-convenience-functions)\n    - [Structure Modifying Transformers](#structure-modifying-transformers)\n    - [Earmark.Restructure.walk_and_modify_ast/4](#earmarkrestructurewalk_and_modify_ast4)\n    - [Earmark.Restructure.split_by_regex/3](#earmarkrestructuresplit_by_regex3)\n- [Contributing](#contributing)\n- [Author](#author)\n\n## Options\n\n\n### Earmark.Cli.Implementation\n\nFunctional (with the exception of reading input files with `Earmark.File`) interface to the CLI\nreturning the device and the string to be output.\n\n\n### Earmark.Options\n\nThis is a superset of the options that need to be passed into `Earmark.Parser.as_ast/2`\n\nThe following options are proper to `Earmark` only and therefore explained in detail\n\n- `compact_output`: boolean indicating to avoid indentation and minimize whitespace\n- `eex`: Allows usage of an `EEx` template to be expanded to markdown before conversion\n- `file`: Name of file passed in from the CLI\n- `line`: 1 but might be set to an offset for better error messages in some integration cases\n- `smartypants`: boolean use [Smarty Pants](https://daringfireball.net/projects/smartypants/) in the output\n- `ignore_strings`, `postprocessor` and `registered_processors`: processors that modify the AST returned from\n\n   Earmark.Parser.as_ast/`2` before rendering (`post` because preprocessing is done on the markdown, e.g. `eex`)\n   Refer to the moduledoc of Earmark.`Transform` for details\n\nAll other options are passed onto Earmark.Parser.as_ast/`2`\n\n### Earmark.Options.make_options/1\n\nMake a legal and normalized Option struct from, maps or keyword lists\n\nWithout a param or an empty input we just get a new Option struct\n\niex(1)\u003e { make_options(), make_options(%{}) }\n{ {:ok, %Earmark.Options{}}, {:ok, %Earmark.Options{}} }\n\nThe same holds for the bang version of course\n\niex(2)\u003e { make_options!(), make_options!(%{}) }\n{ %Earmark.Options{}, %Earmark.Options{} }\n\n\nWe check for unallowed keys\n\niex(3)\u003e make_options(no_such_option: true)\n{:error, [{:warning, 0, \"Unrecognized option no_such_option: true\"}]}\n\nOf course we do not let our users discover one error after another\n\niex(4)\u003e make_options(no_such_option: true, gfm: false, still_not_an_option: 42)\n{:error, [{:warning, 0, \"Unrecognized option no_such_option: true\"}, {:warning, 0, \"Unrecognized option still_not_an_option: 42\"}]}\n\nAnd the bang version will raise an `Earmark.Error` as excepted (sic)\n\niex(5)\u003e make_options!(no_such_option: true, gfm: false, still_not_an_option: 42)\n** (Earmark.Error) [{:warning, 0, \"Unrecognized option no_such_option: true\"}, {:warning, 0, \"Unrecognized option still_not_an_option: 42\"}]\n\nSome values need to be numeric\n\niex(6)\u003e make_options(line: \"42\")\n{:error, [{:error, 0, \"line option must be numeric\"}]}\n\niex(7)\u003e make_options(%Earmark.Options{footnote_offset: \"42\"})\n{:error, [{:error, 0, \"footnote_offset option must be numeric\"}]}\n\niex(8)\u003e make_options(%{line: \"42\", footnote_offset: nil})\n{:error, [{:error, 0, \"footnote_offset option must be numeric\"}, {:error, 0, \"line option must be numeric\"}]}\n\n\n### Earmark.Options.relative_filename/2\n\nAllows to compute the path of a relative file name (starting with `\"./\"`) from the file in options\nand return an updated options struct\n\niex(9)\u003e options = %Earmark.Options{file: \"some/path/xxx.md\"}\n...(9)\u003e options_ = relative_filename(options, \"./local.md\")\n...(9)\u003e options_.file\n\"some/path/local.md\"\n\nFor your convenience you can just use a keyword list\n\niex(10)\u003e options = relative_filename([file: \"some/path/_.md\", breaks: true], \"./local.md\")\n...(10)\u003e {options.file, options.breaks}\n{\"some/path/local.md\", true}\n\nIf the filename is not absolute it just replaces the file in options\n\niex(11)\u003e options = %Earmark.Options{file: \"some/path/xxx.md\"}\n...(11)\u003e options_ = relative_filename(options, \"local.md\")\n...(11)\u003e options_.file\n\"local.md\"\n\nAnd there is a special case when processing stdin, meaning that `file: nil` we replace file\nverbatim in that case\n\niex(12)\u003e options = %Earmark.Options{}\n...(12)\u003e options_ = relative_filename(options, \"./local.md\")\n...(12)\u003e options_.file\n\"./local.md\"\n\n\n### Earmark.Options.with_postprocessor/2\n\nA convenience constructor\n\n\n\n### Earmark.Internal\n\nAll public functions that are internal to Earmark, so that **only** external API\nfunctions are public in `Earmark`\n\n### Earmark.Internal.as_ast!/2\n\nA wrapper to extract the AST from a call to `Earmark.Parser.as_ast` if a tuple `{:ok, result, []}` is returned,\nraise errors otherwise\n\n```elixir\n    iex(1)\u003e as_ast!([\"Hello %% annotated\"], annotations: \"%%\")\n    [{\"p\", [], [\"Hello \"], %{annotation: \"%% annotated\"}}]\n```\n\n```elixir\n    iex(2)\u003e as_ast!(\"===\")\n    ** (Earmark.Error) [{:warning, 1, \"Unexpected line ===\"}]\n```\n\n\n### Earmark.Internal.from_file!/2\n\nThis is a convenience method to read a file or pass it to `EEx.eval_file` if its name\nends in  `.eex`\n\nThe returned string is then passed to `as_html` this is used in the escript now and allows\nfor a simple inclusion mechanism, as a matter of fact an `include` function is passed\n\n\n### Earmark.Internal.include/2\n\nA utility function that will be passed as a partial capture to `EEx.eval_file` by\nproviding a value for the `options` parameter\n\n```elixir\n    EEx.eval(..., include: \u0026include(\u00261, options))\n```\n\nthusly allowing\n\n```eex\n  \u003c%= include.(some file) %\u003e\n```\n\nwhere `some file`  can be a relative path starting with `\"./\"`\n\nHere is an example using [these fixtures](https://github.com/pragdave/earmark/tree/master/test/fixtures)\n\n```elixir\n    iex(3)\u003e include(\"./include/basic.md.eex\", file: \"test/fixtures/does_not_matter\")\n    \"# Headline Level 1\\n\"\n```\n\nAnd here is how it is used inside a template\n\n```elixir\n    iex(4)\u003e options = [file: \"test/fixtures/does_not_matter\"]\n    ...(4)\u003e EEx.eval_string(~s{\u003c%= include.(\"./include/basic.md.eex\") %\u003e}, include: \u0026include(\u00261, options))\n    \"# Headline Level 1\\n\"\n```\n\n\n### Earmark.Transform\n\n#### Structure Conserving Transformers\n\nFor the convenience of processing the output of `Earmark.Parser.as_ast` we expose two structure conserving\nmappers.\n\n##### `map_ast`\n\nTraverses an AST using a mapper function.\n\nThe mapper function will be called for each node including text elements unless `map_ast` is called with\nthe third positional parameter `ignore_strings`, which is optional and defaults to `false`, set to `true`.\n\nDepending on the return value of the mapper function the traversal will either\n\n- `{new_tag, new_atts, ignored, new_meta}`\n\n  just replace the `tag`, `attribute` and `meta` values of the current node with the values of the returned\n  quadruple (ignoring `ignored` for facilitating nodes w/o transformation)\n  and then descend into the **original** content of the node.\n\n\n- `{:replace, node}`\n\n  replaces the current node with `node` and does not descend anymore, but continues traversal on sibblings.\n\n\n- {new_function, {new_tag, new_atts, ignored, new_meta}}\n\n  just replace the `tag`, `attribute` and `meta` values of the current node with the values of the returned\n  quadruple (ignoring `ignored` for facilitating nodes w/o transformation)\n  and then descend into the **original** content of the node but with the mapper function `new_function`\n  used for transformation of the AST.\n\n  **N.B.** The original mapper function will be used for transforming the sibbling nodes though.\n\ntakes a function that will be called for each node of the AST, where a leaf node is either a quadruple\nlike `{\"code\", [{\"class\", \"inline\"}], [\"some code\"], %{}}` or a text leaf like `\"some code\"`\n\nThe result of the function call must be\n\n- for nodes → as described above\n\n- for strings → strings or nodes\n\nAs an example let us transform an ast to have symbol keys\n\n```elixir\n      iex(1)\u003e input = [\n      ...(1)\u003e {\"h1\", [], [\"Hello\"], %{title: true}},\n      ...(1)\u003e {\"ul\", [], [{\"li\", [], [\"alpha\"], %{}}, {\"li\", [], [\"beta\"], %{}}], %{}}]\n      ...(1)\u003e map_ast(input, fn {t, a, _, m} -\u003e {String.to_atom(t), a, nil, m} end, true)\n      [ {:h1, [], [\"Hello\"], %{title: true}},\n        {:ul, [], [{:li, [], [\"alpha\"], %{}}, {:li, [], [\"beta\"], %{}}], %{}} ]\n```\n\n**N.B.** If this returning convention is not respected `map_ast` might not complain, but the resulting\ntransformation might not be suitable for `Earmark.Transform.transform` anymore. From this follows that\nany function passed in as value of the `postprocessor:` option must obey to these conventions.\n\n##### `map_ast_with`\n\nthis is like `map_ast` but like a reducer an accumulator can also be passed through.\n\nFor that reason the function is called with two arguments, the first element being the same value\nas in `map_ast` and the second the accumulator. The return values need to be equally augmented\ntuples.\n\nA simple example, annotating traversal order in the meta map's `:count` key, as we are not\ninterested in text nodes we use the fourth parameter `ignore_strings` which defaults to `false`\n\n```elixir\n       iex(2)\u003e  input = [\n       ...(2)\u003e  {\"ul\", [], [{\"li\", [], [\"one\"], %{}}, {\"li\", [], [\"two\"], %{}}], %{}},\n       ...(2)\u003e  {\"p\", [], [\"hello\"], %{}}]\n       ...(2)\u003e  counter = fn {t, a, _, m}, c -\u003e {{t, a, nil, Map.put(m, :count, c)}, c+1} end\n       ...(2)\u003e  map_ast_with(input, 0, counter, true)\n       {[ {\"ul\", [], [{\"li\", [], [\"one\"], %{count: 1}}, {\"li\", [], [\"two\"], %{count: 2}}], %{count: 0}},\n         {\"p\", [], [\"hello\"], %{count: 3}}], 4}\n```\n\nLet us describe an implementation of a real world use case taken from [Elixir Forum](https://elixirforum.com/t/how-to-extend-earmark/47406)\n\nSimplifying the exact parsing of the text node in this example we only want to replace a text node of the form `#elixir` with\na link to the Elixir home page _but_ only when inside a `{\"p\",....}` node\n\nWe can achieve this as follows\n\n```elixir\n      iex(3)\u003e elixir_home = {\"a\", [{\"href\", \"https://elixir-lang.org\"}], [\"Elixir\"], %{}}\n      ...(3)\u003e transformer = fn {\"p\", atts, _, meta}, _ -\u003e {{\"p\", atts, nil, meta}, true}\n      ...(3)\u003e                  \"#elixir\", true -\u003e {elixir_home, false}\n      ...(3)\u003e                  text, _ when is_binary(text) -\u003e {text, false}\n      ...(3)\u003e                  node, _ -\u003e  {node, false} end\n      ...(3)\u003e ast = [\n      ...(3)\u003e  {\"p\", [],[ \"#elixir\"], %{}}, {\"bold\", [],[ \"#elixir\"], %{}},\n      ...(3)\u003e  {\"ol\", [], [{\"li\", [],[ \"#elixir\"], %{}}, {\"p\", [],[ \"elixir\"], %{}}, {\"p\", [], [\"#elixir\"], %{}}], %{}}\n      ...(3)\u003e ]\n      ...(3)\u003e map_ast_with(ast, false, transformer)\n      {[\n       {\"p\", [],[{\"a\", [{\"href\", \"https://elixir-lang.org\"}], [\"Elixir\"], %{}}], %{}}, {\"bold\", [],[ \"#elixir\"], %{}},\n       {\"ol\", [], [{\"li\", [],[ \"#elixir\"], %{}}, {\"p\", [],[ \"elixir\"], %{}}, {\"p\", [], [{\"a\", [{\"href\", \"https://elixir-lang.org\"}], [\"Elixir\"], %{}}], %{}}], %{}}\n      ], false}\n```\n\nAn alternate, maybe more elegant solution would be to change the mapper function during AST traversal\nas demonstrated [here](https://github.com/pragdave/earmark/blob/master/test/acceptance/transform/map_ast_with_fnchange_test.exs)\n\n#### Postprocessors and Convenience Functions\n\nThese can be declared in the fields `postprocessor` and `registered_processors` in the `Options` struct,\n`postprocessor` is prepened to `registered_processors` and they are all applied to non string nodes (that\nis the quadtuples of the AST which are of the form `{tag, atts, content, meta}`\n\nAll postprocessors can just be functions on nodes or a `TagSpecificProcessors` struct which will group\nfunction applications depending on tags, as a convienience tuples of the form `{tag, function}` will be\ntransformed into a `TagSpecificProcessors` struct.\n\n```elixir\n    iex(4)\u003e add_class1 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class1\")\n    ...(4)\u003e m1 = Earmark.Options.make_options!(postprocessor: add_class1) |\u003e make_postprocessor()\n    ...(4)\u003e m1.({\"a\", [], nil, nil})\n    {\"a\", [{\"class\", \"class1\"}], nil, nil}\n```\n\nWe can also use the `registered_processors` field:\n\n```elixir\n    iex(5)\u003e add_class1 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class1\")\n    ...(5)\u003e m2 = Earmark.Options.make_options!(registered_processors: add_class1) |\u003e make_postprocessor()\n    ...(5)\u003e m2.({\"a\", [], nil, nil})\n    {\"a\", [{\"class\", \"class1\"}], nil, nil}\n```\n\nKnowing that values on the same attributes are added onto the front the following doctest demonstrates\nthe order in which the processors are executed\n\n```elixir\n    iex(6)\u003e add_class1 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class1\")\n    ...(6)\u003e add_class2 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class2\")\n    ...(6)\u003e add_class3 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class3\")\n    ...(6)\u003e m = Earmark.Options.make_options!(postprocessor: add_class1, registered_processors: [add_class2, {\"a\", add_class3}])\n    ...(6)\u003e |\u003e make_postprocessor()\n    ...(6)\u003e [{\"a\", [{\"class\", \"link\"}], nil, nil}, {\"b\", [], nil, nil}]\n    ...(6)\u003e |\u003e Enum.map(m)\n    [{\"a\", [{\"class\", \"class3 class2 class1 link\"}], nil, nil}, {\"b\", [{\"class\", \"class2 class1\"}], nil, nil}]\n```\n\nWe can see that the tuple form has been transformed into a tag specific transformation **only** as a matter of fact, the explicit definition would be:\n\n```elixir\n    iex(7)\u003e m = make_postprocessor(\n    ...(7)\u003e   %Earmark.Options{\n    ...(7)\u003e     registered_processors:\n    ...(7)\u003e       [Earmark.TagSpecificProcessors.new({\"a\", \u0026Earmark.AstTools.merge_atts_in_node(\u00261, target: \"_blank\")})]})\n    ...(7)\u003e [{\"a\", [{\"href\", \"url\"}], nil, nil}, {\"b\", [], nil, nil}]\n    ...(7)\u003e |\u003e Enum.map(m)\n    [{\"a\", [{\"href\", \"url\"}, {\"target\", \"_blank\"}], nil, nil}, {\"b\", [], nil, nil}]\n```\n\nWe can also define a tag specific transformer in one step, which might (or might not) solve potential performance issues\nwhen running too many processors\n\n```elixir\n    iex(8)\u003e add_class4 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class4\")\n    ...(8)\u003e add_class5 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class5\")\n    ...(8)\u003e add_class6 = \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"class6\")\n    ...(8)\u003e tsp = Earmark.TagSpecificProcessors.new([{\"a\", add_class5}, {\"b\", add_class5}])\n    ...(8)\u003e m = Earmark.Options.make_options!(\n    ...(8)\u003e       postprocessor: add_class4,\n    ...(8)\u003e       registered_processors: [tsp, add_class6])\n    ...(8)\u003e |\u003e make_postprocessor()\n    ...(8)\u003e [{\"a\", [], nil, nil}, {\"c\", [], nil, nil}, {\"b\", [], nil, nil}]\n    ...(8)\u003e |\u003e Enum.map(m)\n    [{\"a\", [{\"class\", \"class6 class5 class4\"}], nil, nil}, {\"c\", [{\"class\", \"class6 class4\"}], nil, nil}, {\"b\", [{\"class\", \"class6 class5 class4\"}], nil, nil}]\n```\n\nOf course the mechanics shown above is hidden if all we want is to trigger the postprocessor chain in `Earmark.as_html`, here goes a typical\nexample\n\n```elixir\n    iex(9)\u003e add_target = fn node -\u003e # This will only be applied to nodes as it will become a TagSpecificProcessors\n    ...(9)\u003e   if Regex.match?(~r{\\.x\\.com\\z}, Earmark.AstTools.find_att_in_node(node, \"href\", \"\")), do:\n    ...(9)\u003e     Earmark.AstTools.merge_atts_in_node(node, target: \"_blank\"), else: node end\n    ...(9)\u003e options = [\n    ...(9)\u003e registered_processors: [{\"a\", add_target}, {\"p\", \u0026Earmark.AstTools.merge_atts_in_node(\u00261, class: \"example\")}]]\n    ...(9)\u003e markdown = [\n    ...(9)\u003e   \"http://hello.x.com\",\n    ...(9)\u003e   \"\",\n    ...(9)\u003e   \"[some](url)\",\n    ...(9)\u003e  ]\n    ...(9)\u003e Earmark.as_html!(markdown, options)\n    \"\u003cp class=\\\"example\\\"\u003e\\n\u003ca href=\\\"http://hello.x.com\\\" target=\\\"_blank\\\"\u003ehttp://hello.x.com\u003c/a\u003e\u003c/p\u003e\\n\u003cp class=\\\"example\\\"\u003e\\n\u003ca href=\\\"url\\\"\u003esome\u003c/a\u003e\u003c/p\u003e\\n\"\n```\n\n##### Use case: Modification of Link Attributes depending on the URL\n\nThis would be done as follows\n\n```elixir\n        Earmark.as_html!(markdown, registered_processors: {\"a\", my_function_that_is_invoked_only_with_a_nodes})\n```\n\n##### Use case: Modification of the AST according to Annotations\n\n**N.B.** Annotation are an _experimental_ feature in 1.4.16-pre and are documented [here](https://github.com/RobertDober/earmark_parser/#annotations)\n\nBy annotating our markdown source we can then influence the rendering. In this example we will just\nadd some decoration\n\n```elixir\n    iex(10)\u003e markdown = [ \"A joke %% smile\", \"\", \"Charming %% in_love\" ]\n    ...(10)\u003e add_smiley = fn {_, _, _, meta} = quad, _acc -\u003e\n    ...(10)\u003e                case Map.get(meta, :annotation) do\n    ...(10)\u003e                  \"%% smile\"   -\u003e {quad, \"\\u1F601\"}\n    ...(10)\u003e                  \"%% in_love\" -\u003e {quad, \"\\u1F60d\"}\n    ...(10)\u003e                  _            -\u003e {quad, nil}\n    ...(10)\u003e                end\n    ...(10)\u003e                text, nil -\u003e {text, nil}\n    ...(10)\u003e                text, ann -\u003e {\"#{text} #{ann}\", nil}\n    ...(10)\u003e              end\n    ...(10)\u003e Earmark.as_ast!(markdown, annotations: \"%%\") |\u003e Earmark.Transform.map_ast_with(nil, add_smiley) |\u003e Earmark.transform\n    \"\u003cp\u003e\\nA joke  ὠ1\u003c/p\u003e\\n\u003cp\u003e\\nCharming  ὠd\u003c/p\u003e\\n\"\n```\n\n#### Structure Modifying Transformers\n\nFor structure modifications a tree traversal is needed and no clear pattern of how to assist this task with\ntools has emerged yet.\n\n\n\n\n#### Earmark.Restructure.walk_and_modify_ast/4\n\n\nWalks an AST and allows you to process it (storing details in acc) and/or\nmodify it as it is walked.\n\nitems is the AST you got from Earmark.Parser.as_ast()\n\nacc is the initial value of an accumulator that is passed to both\nprocess_item_fn and process_list_fn and accumulated. If your functions\ndo not need to use or store any state, you can pass nil.\n\nThe process_item_fn function is required. It takes two parameters, the\nsingle item to process (which will either be a string or a 4-tuple) and\nthe accumulator, and returns a tuple {processed_item, updated_acc}.\nReturning the empty list for processed_item will remove the item processed\nthe AST.\n\nThe process_list_fn function is optional and defaults to no modification of\nitems or accumulator. It takes two parameters, the list of items that\nare the sub-items of a given element in the AST (or the top-level list of\nitems), and the accumulator, and returns a tuple\n{processed_items_list, updated_acc}.\n\nThis function ends up returning {ast, acc}.\n\nHere is an example using a custom format to make `\u003cem\u003e` nodes and allowing\ncommented text to be left out\n\n```elixir\n    iex(1)\u003e is_comment? = fn item -\u003e is_binary(item) \u0026\u0026 Regex.match?(~r/\\A\\s*--/, item) end\n    ...(1)\u003e comment_remover =\n    ...(1)\u003e   fn items, acc -\u003e {Enum.reject(items, is_comment?), acc} end\n    ...(1)\u003e italics_maker = fn\n    ...(1)\u003e   item, acc when is_binary(item) -\u003e\n    ...(1)\u003e     new_item = Restructure.split_by_regex(\n    ...(1)\u003e       item,\n    ...(1)\u003e       ~r/\\/([[:graph:]].*?[[:graph:]]|[[:graph:]])\\//,\n    ...(1)\u003e       fn [_, content] -\u003e\n    ...(1)\u003e         {\"em\", [], [content], %{}}\n    ...(1)\u003e       end\n    ...(1)\u003e     )\n    ...(1)\u003e     {new_item, acc}\n    ...(1)\u003e   item, \"a\" -\u003e {item, nil}\n    ...(1)\u003e   {name, _, _, _}=item, _ -\u003e {item, name}\n    ...(1)\u003e end\n    ...(1)\u003e markdown = \"\"\"\n    ...(1)\u003e [no italics in links](http://example.io/some/path)\n    ...(1)\u003e but /here/\n    ...(1)\u003e\n    ...(1)\u003e -- ignore me\n    ...(1)\u003e\n    ...(1)\u003e text\n    ...(1)\u003e \"\"\"\n    ...(1)\u003e {:ok, ast, []} = Earmark.Parser.as_ast(markdown)\n    ...(1)\u003e Restructure.walk_and_modify_ast(ast, nil, italics_maker, comment_remover)\n    {[\n      {\"p\", [],\n        [\n          {\"a\", [{\"href\", \"http://example.io/some/path\"}], [\"no italics in links\"],\n          %{}},\n          \"\\nbut \",\n          {\"em\", [], [\"here\"], %{}},\n          \"\"\n        ], %{}},\n        {\"p\", [], [], %{}},\n        {\"p\", [], [\"text\"], %{}}\n      ], \"p\"}\n```\n\n\n\n#### Earmark.Restructure.split_by_regex/3\n\nUtility for creating a restructuring that parses text by splitting it into\nparts \"of interest\" vs. \"other parts\" using a regular expression.\nReturns a list of parts where the parts matching regex have been processed\nby invoking map_captures_fn on each part, and a list of remaining parts,\npreserving the order of parts from what it was in the plain text item.\n\n```elixir\n      iex(2)\u003e input = \"This is ::all caps::, right?\"\n      ...(2)\u003e split_by_regex(input, ~r/::(.*?)::/, fn [_, inner|_] -\u003e String.upcase(inner) end)\n      [\"This is \", \"ALL CAPS\", \", right?\"]\n```\n\n\n\n## Contributing\n\nPull Requests are happily accepted.\n\nPlease be aware of one _caveat_ when correcting/improving `README.md`.\n\nThe `README.md` is generated by `Extractly` as mentioned above and therefore contributors shall not modify it directly, but\n`README.md.eex` and the imported docs instead.\n\nYou need to run `mix xtra` after getting the dependencies to generate the `README.md` file.\nThank you all who have already helped with Earmark, your names are duly noted in [RELEASE.md](RELEASE.md).\n\n## Author\n\nCopyright © 2014,5,6,7,8,9, 2020,1,2 Dave Thomas, The Pragmatic Programmers \u0026 Robert Dober\n@/+pragdave,  dave@pragprog.com \u0026 robert.dober@gmail.com\n\n# LICENSE\n\nSame as Elixir, which is Apache License v2.0. Please refer to [LICENSE](LICENSE) for details.\n\n\u003c!-- SPDX-License-Identifier: Apache-2.0 --\u003e\n","funding_links":[],"categories":["Elixir","Markdown"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpragdave%2Fearmark","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpragdave%2Fearmark","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpragdave%2Fearmark/lists"}