pax_global_header00006660000000000000000000000064145310426730014517gustar00rootroot0000000000000052 comment=25edb1eb96afff1def09f847387ab643eaf57e01 earmark_parser-1.4.39/000077500000000000000000000000001453104267300146135ustar00rootroot00000000000000earmark_parser-1.4.39/.github/000077500000000000000000000000001453104267300161535ustar00rootroot00000000000000earmark_parser-1.4.39/.github/workflows/000077500000000000000000000000001453104267300202105ustar00rootroot00000000000000earmark_parser-1.4.39/.github/workflows/elixir.yml000066400000000000000000000033321453104267300222300ustar00rootroot00000000000000name: Elixir CI on: pull_request: push: branches: - master jobs: mix_test_old: name: mix test (Elixir ${{matrix.elixir}} | OTP ${{matrix.otp}}) runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - elixir: 1.11.x otp: 22 os: ubuntu-20.04 - elixir: 1.11.x otp: 23 os: ubuntu-20.04 - elixir: 1.12.x otp: 23 os: ubuntu-20.04 - elixir: 1.13.x otp: 24 os: ubuntu-20.04 - elixir: 1.14.x otp: 25 os: ubuntu-latest # - elixir: 1.15.x # otp: 26 # os: ubuntu-latest # warnings_as_errors: true env: MIX_ENV: test steps: - uses: actions/checkout@v3 - uses: erlef/setup-beam@v1 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - name: Install Dependencies run: | mix local.hex --force mix local.rebar --force mix deps.get --only test - name: Cache build artifacts uses: actions/cache@v3 with: path: | ~/.hex ~/.mix _build key: ${{ matrix.otp }}-${{ matrix.elixir }}-build - run: mix compile --warnings-as-errors if: matrix.warnings_as_errors env: CC: gcc-10 CXX: g++-10 - run: mix test env: CC: gcc-10 CXX: g++-10 # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. earmark_parser-1.4.39/.gitignore000066400000000000000000000003411453104267300166010ustar00rootroot00000000000000/_build /deps erl_crash.dump *.ez tmp/* .tool-versions .mix_tasks notes/* /earmark *.beam doc/ # yecc, leex generated files src/*.erl cover lib/tools #*# /cover/ /doc/ /.fetch *.ez earmark_parser-*.tar .vscode/ test/debug/ earmark_parser-1.4.39/DEVNOTES.md000066400000000000000000000012161453104267300163640ustar00rootroot00000000000000# TODOS ## Release 1.5.0 ### For Lists - Change `Block.List.blocks` to `Block.List.list_items` - Remove `Block.List.lines` - Remove `Block.List.spaced?` - Refactor parser into a `ParagraphParser`? # Definitions ## [Paragraph Continuation Text](https://github.github.com/gfm/#paragraph-continuation-text) ...is text that will be parsed as part of the content of a paragraph, but does not occur at the beginning of the paragraph. Example: ```markdown This is **not** a PCT this is a PCT ``` ## [Thematic Break](https://github.github.com/gfm/#thematic-breaks) just what we scan as `Line.Ruler` earmark_parser-1.4.39/LICENSE000066400000000000000000000261351453104267300156270ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. earmark_parser-1.4.39/README.md000066400000000000000000000567671453104267300161170ustar00rootroot00000000000000 # EarmarkParser A Pure Elixir Markdown Parser [![CI](https://github.com/robertdober/earmark_parser/actions/workflows/elixir.yml/badge.svg)](https://github.com/robertdober/earmark_parser/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/github/RobertDober/earmark_parser/badge.svg?branch=master)](https://coveralls.io/github/RobertDober/earmark_parser?branch=master) [![Hex.pm](https://img.shields.io/hexpm/v/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) [![Hex.pm](https://img.shields.io/hexpm/dw/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) [![Hex.pm](https://img.shields.io/hexpm/dt/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) ## Table Of Contents - [Table Of Contents](#table-of-contents) - [Usage](#usage) - [EarmarkParser](#earmarkparser) - [API](#api) - [EarmarkParser.as_ast](#earmarkparseras_ast) - [Options](#options) - [Supports](#supports) - [Extensions](#extensions) - [Links](#links) - [Links supported by default](#links-supported-by-default) - [Autolinks](#autolinks) - [Additional link parsing via options](#additional-link-parsing-via-options) - [Pure links](#pure-links) - [Wikilinks...](#wikilinks) - [Sub and Sup HTML Elements](#sub-and-sup-html-elements) - [Mathematical expressions](#mathematical-expressions) - [Github Flavored Markdown](#github-flavored-markdown) - [Strike Through](#strike-through) - [GFM Tables](#gfm-tables) - [Syntax Highlighting](#syntax-highlighting) - [Footnotes](#footnotes) - [Breaks](#breaks) - [Enabling **all** options that are disabled by default](#enabling-all-options-that-are-disabled-by-default) - [Tables](#tables) - [HTML Blocks](#html-blocks) - [HTML Comments](#html-comments) - [Lists](#lists) - [Adding Attributes with the IAL extension](#adding-attributes-with-the-ial-extension) - [To block elements](#to-block-elements) - [To links or images](#to-links-or-images) - [Limitations](#limitations) - [Annotations](#annotations) - [Annotated Paragraphs](#annotated-paragraphs) - [Annotated HTML elements](#annotated-html-elements) - [Commenting your Markdown](#commenting-your-markdown) - [EarmarkParser.as_ast/2](#earmarkparseras_ast2) - [EarmarkParser.version/0](#earmarkparserversion0) - [Contributing](#contributing) - [Author](#author) - [LICENSE](#license) ## Usage ### EarmarkParser ### API #### EarmarkParser.as_ast This is the structure of the result of `as_ast`. {:ok, ast, []} = EarmarkParser.as_ast(markdown) {:ok, ast, deprecation_messages} = EarmarkParser.as_ast(markdown) {:error, ast, error_messages} = EarmarkParser.as_ast(markdown) For examples see the functiondoc below. #### Options Options can be passed into `as_ast/2` according to the documentation of `EarmarkParser.Options`. {status, ast, errors} = EarmarkParser.as_ast(markdown, options) ## Supports Standard [Gruber markdown][gruber]. [gruber]: ## Extensions ### Links #### Links supported by default ##### Oneline HTML Link tags ```elixir iex(1)> EarmarkParser.as_ast(~s{link}) {:ok, [{"a", [{"href", "href"}], ["link"], %{verbatim: true}}], []} ``` ##### Markdown links New style ... ```elixir iex(2)> EarmarkParser.as_ast(~s{[title](destination)}) {:ok, [{"p", [], [{"a", [{"href", "destination"}], ["title"], %{}}], %{}}], []} ``` and old style ```elixir iex(3)> EarmarkParser.as_ast("[foo]: /url \"title\"\n\n[foo]\n") {:ok, [{"p", [], [{"a", [{"href", "/url"}, {"title", "title"}], ["foo"], %{}}], %{}}], []} ``` #### Autolinks ```elixir iex(4)> EarmarkParser.as_ast("") {:ok, [{"p", [], [{"a", [{"href", "https://elixir-lang.com"}], ["https://elixir-lang.com"], %{}}], %{}}], []} ``` #### Additional link parsing via options #### Pure links **N.B.** that the `pure_links` option is `true` by default ```elixir iex(5)> EarmarkParser.as_ast("https://github.com") {:ok, [{"p", [], [{"a", [{"href", "https://github.com"}], ["https://github.com"], %{}}], %{}}], []} ``` But can be deactivated ```elixir iex(6)> EarmarkParser.as_ast("https://github.com", pure_links: false) {:ok, [{"p", [], ["https://github.com"], %{}}], []} ``` #### Wikilinks... are disabled by default ```elixir iex(7)> EarmarkParser.as_ast("[[page]]") {:ok, [{"p", [], ["[[page]]"], %{}}], []} ``` and can be enabled ```elixir iex(8)> EarmarkParser.as_ast("[[page]]", wikilinks: true) {:ok, [{"p", [], [{"a", [{"href", "page"}], ["page"], %{wikilink: true}}], %{}}], []} ``` ### Sub and Sup HTML Elements This feature is not enabled by default but can be enabled with the option `sub_sup: true` Therefore we will get ```elixir iex(9)> EarmarkParser.as_ast("H~2~O or a^n^ + b^n^ = c^n^") {:ok, [{"p", [], ["H~2~O or a^n^ + b^n^ = c^n^"], %{}}], []} ``` But by specifying `sub_sup: true` ```elixir iex(10)> EarmarkParser.as_ast("H~2~O or a^n^ + b^n^ = c^n^", sub_sup: true) {:ok, [{"p", [], ["H", {"sub", [], ["2"], %{}}, "O or a", {"sup", [], ["n"], %{}}, " + b", {"sup", [], ["n"], %{}}, " = c", {"sup", [], ["n"], %{}}], %{}}], []} ``` ### Mathematical expressions > Note: math syntax within Markdown is not standardized, so this option is a subject to change in future releases. This feature is not enabled by default but can be enabled with the option `math: true`. When enabled, LaTeX formatted math can be written within Markdown. For more information, see [LaTeX/Mathematics](https://en.wikibooks.org/wiki/LaTeX/Mathematics) in Wikibooks. #### Inline expressions Inline-style expression can be written by surrounding the expression with dollar symbols (`$`). ```elixir iex> EarmarkParser.as_ast("$x = 1$", math: true) {:ok, [{"p", [], [{"code", [{"class", "math-inline"}], ["x = 1"], %{line: 1}}], %{}}], []} ``` There must be no space between `$` and the surrounded expression. If you want to use a dollar sign in the same line as a math expression, you can escape the dollar with backslash (`\\$`). #### Expressions as blocks Display-style expression can be written by surrounding the expression with two dollar signs (`$$`). ```elixir iex> EarmarkParser.as_ast("$$x = 1$$", math: true) {:ok, [{"p", [], [{"code", [{"class", "math-display"}], ["x = 1"], %{line: 1}}], %{}}], []} ``` ### Github Flavored Markdown GFM is supported by default, however as GFM is a moving target and all GFM extension do not make sense in a general context, EarmarkParser does not support all of it, here is a list of what is supported: #### Strike Through ```elixir iex(11)> EarmarkParser.as_ast("~~hello~~") {:ok, [{"p", [], [{"del", [], ["hello"], %{}}], %{}}], []} ``` #### GFM Tables Are not enabled by default ```elixir iex(12)> as_ast("a|b\\n-|-\\nc|d\\n") {:ok, [{"p", [], ["a|b\\n-|-\\nc|d\\n"], %{}}], []} ``` But can be enabled with `gfm_tables: true` ```elixir iex(13)> as_ast("a|b\n-|-\nc|d\n", gfm_tables: true) {:ok, [ { "table", [], [ {"thead", [], [{"tr", [], [{"th", [{"style", "text-align: left;"}], ["a"], %{}}, {"th", [{"style", "text-align: left;"}], ["b"], %{}}], %{}}], %{}}, {"tbody", [], [{"tr", [], [{"td", [{"style", "text-align: left;"}], ["c"], %{}}, {"td", [{"style", "text-align: left;"}], ["d"], %{}}], %{}}], %{}} ], %{} } ], []} ``` #### Syntax Highlighting All backquoted or fenced code blocks with a language string are rendered with the given language as a _class_ attribute of the _code_ tag. For example: ```elixir iex(14)> [ ...(14)> "```elixir", ...(14)> " @tag :hello", ...(14)> "```" ...(14)> ] |> as_ast() {:ok, [{"pre", [], [{"code", [{"class", "elixir"}], [" @tag :hello"], %{}}], %{}}], []} ``` will be rendered as shown in the doctest above. If you want to integrate with a syntax highlighter with different conventions you can add more classes by specifying prefixes that will be put before the language string. Prism.js for example needs a class `language-elixir`. In order to achieve that goal you can add `language-` as a `code_class_prefix` to `EarmarkParser.Options`. In the following example we want more than one additional class, so we add more prefixes. ```elixir iex(15)> [ ...(15)> "```elixir", ...(15)> " @tag :hello", ...(15)> "```" ...(15)> ] |> as_ast(%EarmarkParser.Options{code_class_prefix: "lang- language-"}) {:ok, [{"pre", [], [{"code", [{"class", "elixir lang-elixir language-elixir"}], [" @tag :hello"], %{}}], %{}}], []} ``` #### Footnotes **N.B.** Footnotes are disabled by default, use `as_ast(..., footnotes: true)` to enable them Footnotes are now a **superset** of GFM Footnotes. This implies some changes - Footnote definitions (`[^footnote_id]`) must come at the end of your document (_GFM_) - Footnotes that are not referenced are not rendered anymore (_GFM_) - Footnote definitions can contain any markup with the exception of footnote definitions # iex(16)> markdown = [ # ...(16)> "My reference[^to_footnote]", # ...(16)> "", # ...(16)> "[^1]: I am not rendered", # ...(16)> "[^to_footnote]: Important information"] # ...(16)> {:ok, ast, []} = as_ast(markdown, footnotes: true) # ...(16)> ast # [ # {"p", [], ["My reference", # {"a", # [{"href", "#fn:to_footnote"}, {"id", "fnref:to_footnote"}, {"class", "footnote"}, {"title", "see footnote"}], # ["to_footnote"], %{}} # ], %{}}, # {"div", # [{"class", "footnotes"}], # [{"hr", [], [], %{}}, # {"ol", [], # [{"li", [{"id", "fn:to_footnote"}], # [{"a", [{"title", "return to article"}, {"class", "reversefootnote"}, {"href", "#fnref:to_footnote"}], ["↩"], %{}}, # {"p", [], ["Important information"], %{}}], %{}} # ], %{}}], %{}} # ] For more complex examples of footnotes, please refer to [these tests](https://github.com/RobertDober/earmark_parser/tree/master/test/acceptance/ast/footnotes/multiple_fn_test.exs) #### Breaks Hard linebreaks are disabled by default ```elixir iex(17)> ["* a"," b", "c"] ...(17)> |> as_ast() {:ok, [{"ul", [], [{"li", [], ["a\nb\nc"], %{}}], %{}}], []} ``` But can be enabled with `breaks: true` ```elixir iex(18)> ["* a"," b", "c"] ...(18)> |> as_ast(breaks: true) {:ok, [{"ul", [], [{"li", [], ["a", {"br", [], [], %{}}, "b", {"br", [], [], %{}}, "c"], %{}}], %{}}], []} ``` #### Enabling **all** options that are disabled by default Can be achieved with the `all: true` option ```elixir iex(19)> [ ...(19)> "a^n^", ...(19)> "b~2~", ...(19)> "[[wikilink]]"] ...(19)> |> as_ast(all: true) {:ok, [ {"p", [], ["a", {"sup", [], ["n"], %{}}, {"br", [], [], %{}}, "b", {"sub", [], ["2"], %{}}, {"br", [], [], %{}}, {"a", [{"href", "wikilink"}], ["wikilink"], %{wikilink: true}}], %{}} ], []} ``` #### Tables Are supported as long as they are preceded by an empty line. State | Abbrev | Capital ----: | :----: | ------- Texas | TX | Austin Maine | ME | Augusta Tables may have leading and trailing vertical bars on each line | State | Abbrev | Capital | | ----: | :----: | ------- | | Texas | TX | Austin | | Maine | ME | Augusta | Tables need not have headers, in which case all column alignments default to left. | Texas | TX | Austin | | Maine | ME | Augusta | Currently we assume there are always spaces around interior vertical unless there are exterior bars. However in order to be more GFM compatible the `gfm_tables: true` option can be used to interpret only interior vertical bars as a table if a separation line is given, therefore Language|Rating --------|------ Elixir | awesome is a table (if and only if `gfm_tables: true`) while Language|Rating Elixir | awesome never is. #### HTML Blocks HTML is not parsed recursively or detected in all conditions right now, though GFM compliance is a goal. But for now the following holds: A HTML Block defined by a tag starting a line and the same tag starting a different line is parsed as one HTML AST node, marked with %{verbatim: true} E.g. ```elixir iex(20)> lines = [ "
", "some", "
more text" ] ...(20)> EarmarkParser.as_ast(lines) {:ok, [{"div", [], ["", "some"], %{verbatim: true}}, "more text"], []} ``` And a line starting with an opening tag and ending with the corresponding closing tag is parsed in similar fashion ```elixir iex(21)> EarmarkParser.as_ast(["spaniel"]) {:ok, [{"span", [{"class", "superspan"}], ["spaniel"], %{verbatim: true}}], []} ``` What is HTML? We differ from strict GFM by allowing **all** tags not only HTML5 tags this holds for one liners.... ```elixir iex(22)> {:ok, ast, []} = EarmarkParser.as_ast(["", "better"]) ...(22)> ast [ {"stupid", [], [], %{verbatim: true}}, {"not", [], ["better"], %{verbatim: true}}] ``` and for multi line blocks ```elixir iex(23)> {:ok, ast, []} = EarmarkParser.as_ast([ "", "world", ""]) ...(23)> ast [{"hello", [], ["world"], %{verbatim: true}}] ``` #### HTML Comments Are recognized if they start a line (after ws and are parsed until the next `-->` is found all text after the next '-->' is ignored E.g. ```elixir iex(24)> EarmarkParser.as_ast(" text -->\nafter") {:ok, [{:comment, [], [" Comment", "comment line", "comment "], %{comment: true}}, {"p", [], ["after"], %{}}], []} ``` #### Lists Lists are pretty much GFM compliant, but some behaviors concerning the interpreation of the markdown inside a List Item's first paragraph seem not worth to be interpreted, examples are blockquote in a tight [list item](ttps://babelmark.github.io/?text=*+aa%0A++%3E+Second) which we can only have in a [loose one](https://babelmark.github.io/?text=*+aa%0A++%0A++%3E+Second) Or a headline in a [tight list item](https://babelmark.github.io/?text=*+bb%0A++%23+Headline) which, again is only available in the [loose version](https://babelmark.github.io/?text=*+bb%0A%0A++%23+Headline) in EarmarkParser. furthermore [this example](https://babelmark.github.io/?text=*+aa%0A++%60%60%60%0ASecond%0A++%60%60%60) demonstrates how weird and definitely not useful GFM's own interpretation can get. Therefore we stick to a more predictable approach. ```elixir iex(25)> markdown = [ ...(25)> "* aa", ...(25)> " ```", ...(25)> "Second", ...(25)> " ```" ] ...(25)> as_ast(markdown) {:ok, [{"ul", [], [{"li", [], ["aa", {"pre", [], [{"code", [], ["Second"], %{}}], %{}}], %{}}], %{}}], []} ``` Also we do support the immediate style of block content inside lists ```elixir iex(26)> as_ast("* > Nota Bene!") {:ok, [{"ul", [], [{"li", [], [{"blockquote", [], [{"p", [], ["Nota Bene!"], %{}}], %{}}], %{}}], %{}}], []} ``` or ```elixir iex(27)> as_ast("1. # Breaking...") {:ok, [{"ol", [], [{"li", [], [{"h1", [], ["Breaking..."], %{}}], %{}}], %{}}], []} ``` ### Adding Attributes with the IAL extension #### To block elements HTML attributes can be added to any block-level element. We use the Kramdown syntax: add the line `{:` _attrs_ `}` following the block. ```elixir iex(28)> markdown = ["# Headline", "{:.from-next-line}"] ...(28)> as_ast(markdown) {:ok, [{"h1", [{"class", "from-next-line"}], ["Headline"], %{}}], []} ``` Headers can also have the IAL string at the end of the line ```elixir iex(29)> markdown = ["# Headline{:.from-same-line}"] ...(29)> as_ast(markdown) {:ok, [{"h1", [{"class", "from-same-line"}], ["Headline"], %{}}], []} ``` A special use case is headers inside blockquotes which allow for some nifty styling in `ex_doc`* see [this PR](https://github.com/elixir-lang/ex_doc/pull/1400) if you are interested in the technical details ```elixir iex(30)> markdown = ["> # Headline{:.warning}"] ...(30)> as_ast(markdown) {:ok, [{"blockquote", [], [{"h1", [{"class", "warning"}], ["Headline"], %{}}], %{}}], []} ``` This also works for headers inside lists ```elixir iex(31)> markdown = ["- # Headline{:.warning}"] ...(31)> as_ast(markdown) {:ok, [{"ul", [], [{"li", [], [{"h1", [{"class", "warning"}], ["Headline"], %{}}], %{}}], %{}}], []} ``` It still works for inline code, as it did before ```elixir iex(32)> markdown = "`Enum.map`{:lang=elixir}" ...(32)> as_ast(markdown) {:ok, [{"p", [], [{"code", [{"class", "inline"}, {"lang", "elixir"}], ["Enum.map"], %{}}], %{}}], []} ``` _attrs_ can be one or more of: * `.className` * `#id` * name=value, name="value", or name='value' For example: # Warning {: .red} Do not turn off the engine if you are at altitude. {: .boxed #warning spellcheck="true"} #### To links or images It is possible to add IAL attributes to generated links or images in the following format. ```elixir iex(33)> markdown = "[link](url) {: .classy}" ...(33)> EarmarkParser.as_ast(markdown) { :ok, [{"p", [], [{"a", [{"class", "classy"}, {"href", "url"}], ["link"], %{}}], %{}}], []} ``` For both cases, malformed attributes are ignored and warnings are issued. ```elixir iex(34)> [ "Some text", "{:hello}" ] |> Enum.join("\n") |> EarmarkParser.as_ast() {:error, [{"p", [], ["Some text"], %{}}], [{:warning, 2,"Illegal attributes [\"hello\"] ignored in IAL"}]} ``` It is possible to escape the IAL in both forms if necessary ```elixir iex(35)> markdown = "[link](url)\\{: .classy}" ...(35)> EarmarkParser.as_ast(markdown) {:ok, [{"p", [], [{"a", [{"href", "url"}], ["link"], %{}}, "{: .classy}"], %{}}], []} ``` This of course is not necessary in code blocks or text lines containing an IAL-like string, as in the following example ```elixir iex(36)> markdown = "hello {:world}" ...(36)> EarmarkParser.as_ast(markdown) {:ok, [{"p", [], ["hello {:world}"], %{}}], []} ``` ## Limitations * Block-level HTML is correctly handled only if each HTML tag appears on its own line. So
hello
will work. However. the following won't
hello
* John Gruber's tests contain an ambiguity when it comes to lines that might be the start of a list inside paragraphs. One test says that This is the text * of a paragraph that I wrote is a single paragraph. The "*" is not significant. However, another test has * A list item * an another and expects this to be a nested list. But, in reality, the second could just be the continuation of a paragraph. I've chosen always to use the second interpretation—a line that looks like a list item will always be a list item. * Rendering of block and inline elements. Block or void HTML elements that are at the absolute beginning of a line end the preceding paragraph. Thusly mypara
Becomes

mypara


While mypara
will be transformed into

mypara


## Annotations **N.B.** this is an experimental feature from v1.4.16-pre on and might change or be removed again The idea is that each markdown line can be annotated, as such annotations change the semantics of Markdown they have to be enabled with the `annotations` option. If the `annotations` option is set to a string (only one string is supported right now, but a list might be implemented later on, hence the name), the last occurrence of that string in a line and all text following it will be added to the line as an annotation. Depending on how that line will eventually be parsed, this annotation will be added to the meta map (the 4th element in an AST quadruple) with the key `:annotation` In the current version the annotation will only be applied to verbatim HTML tags and paragraphs Let us show some examples now: ### Annotated Paragraphs ```elixir iex(37)> as_ast("hello %> annotated", annotations: "%>") {:ok, [{"p", [], ["hello "], %{annotation: "%> annotated"}}], []} ``` If we annotate more than one line in a para the first annotation takes precedence ```elixir iex(38)> as_ast("hello %> annotated\nworld %> discarded", annotations: "%>") {:ok, [{"p", [], ["hello \nworld "], %{annotation: "%> annotated"}}], []} ``` ### Annotated HTML elements In one line ```elixir iex(39)> as_ast("One Line // a span", annotations: "//") {:ok, [{"span", [], ["One Line"], %{annotation: "// a span", verbatim: true}}], []} ``` or block elements ```elixir iex(40)> [ ...(40)> "
: annotation", ...(40)> " text", ...(40)> "
: discarded" ...(40)> ] |> as_ast(annotations: " : ") {:ok, [{"div", [], [" text"], %{annotation: " : annotation", verbatim: true}}], []} ``` ### Commenting your Markdown Although many markdown elements do not support annotations yet, they can be used to comment your markdown, w/o cluttering the generated AST with comments ```elixir iex(41)> [ ...(41)> "# Headline --> first line", ...(41)> "- item1 --> a list item", ...(41)> "- item2 --> another list item", ...(41)> "", ...(41)> " --> do not go there" ...(41)> ] |> as_ast(annotations: "-->") {:ok, [ {"h1", [], ["Headline"], %{}}, {"ul", [], [{"li", [], ["item1 "], %{}}, {"li", [], ["item2 "], %{}}], %{}}, {"p", [], [{"a", [{"href", "http://somewhere/to/go"}], ["http://somewhere/to/go"], %{}}, " "], %{annotation: "--> do not go there"}} ], [] } ``` ### EarmarkParser.as_ast/2 iex(42)> markdown = "My `code` is **best**" ...(42)> {:ok, ast, []} = EarmarkParser.as_ast(markdown) ...(42)> ast [{"p", [], ["My ", {"code", [{"class", "inline"}], ["code"], %{}}, " is ", {"strong", [], ["best"], %{}}], %{}}] ```elixir iex(43)> markdown = "```elixir\nIO.puts 42\n```" ...(43)> {:ok, ast, []} = EarmarkParser.as_ast(markdown, code_class_prefix: "lang-") ...(43)> ast [{"pre", [], [{"code", [{"class", "elixir lang-elixir"}], ["IO.puts 42"], %{}}], %{}}] ``` **Rationale**: The AST is exposed in the spirit of [Floki's](https://hex.pm/packages/floki). ### EarmarkParser.version/0 Accesses current hex version of the `EarmarkParser` application. Convenience for `iex` usage. ## Contributing Pull Requests are happily accepted. Please be aware of one _caveat_ when correcting/improving `README.md`. The `README.md` is generated by the mix task `readme` from `README.template` and docstrings by means of `%moduledoc` or `%functiondoc` directives. Please identify the origin of the generated text you want to correct and then apply your changes there. Then issue the mix task `readme`, this is important to have a correctly updated `README.md` after the merge of your PR. Thank you all who have already helped with Earmark/EarmarkParser, your names are duely noted in [RELEASE.md](RELEASE.md). ## Author Copyright © 2014,5,6,7,8,9;2020 Dave Thomas, The Pragmatic Programmers @/+pragdave, dave@pragprog.com Copyright © 2020,1,2,3 Robert Dober robert.dober@gmail.com ## LICENSE Same as Elixir, which is Apache License v2.0. Please refer to [LICENSE](LICENSE) for details. earmark_parser-1.4.39/README.md.eex000066400000000000000000000035211453104267300166530ustar00rootroot00000000000000 # EarmarkParser A Pure Elixir Markdown Parser [![CI](https://github.com/robertdober/earmark_parser/actions/workflows/elixir.yml/badge.svg)](https://github.com/robertdober/earmark_parser/actions/workflows/elixir.yml) [![Coverage Status](https://coveralls.io/repos/github/RobertDober/earmark_parser/badge.svg?branch=master)](https://coveralls.io/github/RobertDober/earmark_parser?branch=master) [![Hex.pm](https://img.shields.io/hexpm/v/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) [![Hex.pm](https://img.shields.io/hexpm/dw/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) [![Hex.pm](https://img.shields.io/hexpm/dt/earmark_parser.svg)](https://hex.pm/packages/earmark_parser) ## Table Of Contents <%= xtra.toc :self, gh_links: true, min_level: 2, max_level: 4, remove_gaps: true %> ## Usage <%= xtra.moduledoc "EarmarkParser", include: :all, wrap_code_blocks: "elixir", headline: 3 %> ## Contributing Pull Requests are happily accepted. Please be aware of one _caveat_ when correcting/improving `README.md`. The `README.md` is generated by the mix task `readme` from `README.template` and docstrings by means of `%moduledoc` or `%functiondoc` directives. Please identify the origin of the generated text you want to correct and then apply your changes there. Then issue the mix task `readme`, this is important to have a correctly updated `README.md` after the merge of your PR. Thank you all who have already helped with Earmark/EarmarkParser, your names are duely noted in [RELEASE.md](RELEASE.md). ## Author Copyright © 2014,5,6,7,8,9;2020 Dave Thomas, The Pragmatic Programmers @/+pragdave, dave@pragprog.com Copyright © 2020,1,2,3 Robert Dober robert.dober@gmail.com ## LICENSE Same as Elixir, which is Apache License v2.0. Please refer to [LICENSE](LICENSE) for details. earmark_parser-1.4.39/RELEASE.md000066400000000000000000000725751453104267300162350ustar00rootroot00000000000000 ## 1.5.0 2023-??-?? - [Depreacting message to be passed in as an array in options, and fixing it](https://github.com/robertdober/earmark_parser/issues/86) - [Parsing HTML] ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.38 2023-11-10 - [Add metadata line to inline code spans](https://github.com/RobertDober/earmark_parser/pull/140) Kudos to [Wojtek Mach](https://github.com/wojtekmach) ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.37 2023-10-01 - [José fixed more deprecation warnings for Elixir 1.6](https://github.com/RobertDober/earmark_parser/pull/138) - [José namespaced yecc and leex files for me <3](https://github.com/RobertDober/earmark_parser/pull/137) ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.36 2023-09-22 - Correting deprection version for smarty_pants - Checking for result type `EarmarkParser.t` of `EarmarkParser.as_ast` ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.35 2023-09-12 - Better error messages for bad data passed into `EarmarkParser.as_ast` - Using minipeg instead of leaky string_lexer ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.34 2023-09-11 - PR [Strip spaces in front of code blocks](https://github.com/RobertDober/earmark_parser/pull/132) Kudos to [Lukas Larsson](https://github.com/garazdawi) ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.33 2023-07-04 - PR [Avoid warnings in String#slice/2 with Elixir 1.16](https://github.com/RobertDober/earmark_parser/pull/128) Kudos to [José Valim](https://github.com/josevalim) ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.32 2023-04-29 - PR [Fix of a crash on unquoted html attributes](https://github.com/RobertDober/earmark_parser/pull/127) Kudos to [Sebastian Seilund](https://github.com/sebastianseilund) ## [EarmarkParser](https://hex.pm/packages/earmark_parser) 1.4.31 2023-03-03 - PR [Fix catastrophic backtracking in IAL regex](https://github.com/RobertDober/earmark_parser/pull/125) Special Kudos for spotting **and** fixing this [Alex Martsinovich](https://github.com/martosaur) - Bugfix for [Strikethrough not working if option `breaks: true`](https://github.com/RobertDober/earmark_parser/issues/123) Kudos to [Mayel de Borniol](https://github.com/mayel) for providing tests ## 1.4.30 2023-01-27 - [Fixed a problem with headers that close with # but have a # inside too ](https://github.com/RobertDober/earmark_parser/pull/122) Kudos to [Alex Martsinovich](https://github.com/martosaur) - Adding a non regression test for `~` inside links (was broken earlier) Kudos to [Faried Nawaz](https://github.com/faried) ## 1.4.29 2022-10-20 - Bugfix for [Strike Through Only Working on start of line #115](https://github.com/RobertDober/earmark_parser/issues/115) ## 1.4.28 2022-10-01 - [Do not wrap labelled wikilinks in `

` tags](https://github.com/RobertDober/earmark_parser/pull/112) Kudos to [Ben Olive](https://github.com/sionide21) - Add option `all: true` enabling all options which are disabled by default, which are: `breaks`, `footnotes`, `gfm_tables`, `sub_sup`, `wikilinks` - Fix bug for `a^n^` not being parsed as sup ## 1.4.27 2022-09-30 - [Nice addition of sub and sup elements](https://github.com/RobertDober/earmark_parser/tree/i108-sub-and-sup) Needs to be enabled with the option `sub_sup: true` renders `~x~` inside `` and `^x^` inside `` Kudos to [manuel-rubio](https://github.com/manuel-rubio) - Optimisation in the inline renderer - Removal of compiler warnings ## 1.4.26 2022-06-15 - Allow unquoted values for HTML attributes - [Accept valueless HTML attributes](https://github.com/RobertDober/earmark_parser/pull/106) Kudos to [Tom Conroy](https://github.com/tomconroy) ## 1.4.25 2022-03-24 - [Two PRs to assure lookahead scanning is applied on the top level, where needed most](https://github.com/robertdober/earmark_parser/issues/100) [and corresponding performance test](https://github.com/robertdober/earmark_parser/issues/101) Kudos to [jonatanklosko](https://github.com/jonatanklosko) ## 1.4.24 2022-03-20 - Single worded footnote definitions where shadowed by ID Definitions, the tiebreak was resolved in favor of Footnotes as ID Definitions do not need, and might as a matter of fact almost never, to start with a `^` [Related Issue](https://github.com/RobertDober/earmark_parser/issues/99) - Unused import warning removed ## 1.4.23 2022-03-16 Two more list regressions fixed - multi line inline code was ignored in the body parts of lists - spaced lists with inline code in their bodies (single and multiline) were rendered tightly (no surrounding `

...

` ## 1.4.22 2022-03-14 Fixes all List Regressions introduced in 1.4.19 GFM support for lists remain limited (spaced and tight lists are not 100% compliant) but is better than in 1.4.18 ## 1.4.21 2022-03-13 - [Paragraph context lost after indented code blocks](https://github.com/robertdober/earmark_parser/issues/98) ## 1.4.20 2022-02-21 - [Preserve newlines inside HTML code](https://github.com/RobertDober/earmark_parser/pull/97) Kudos to [José Valim](https://github.com/josevalim) - [Do not remove ial on blockquote inside triple quoted](https://github.com/RobertDober/earmark_parser/pull/96) Kudos to [José Valim](https://github.com/josevalim) - Removed support for Elixir 1.10 (following `ex_doc`'s lead) - [Correct pure link regex to reject invalid characters](https://github.com/RobertDober/earmark_parser/pull/91) Kudos to [Akash Hiremath](https://github.com/akash-akya) - [Intensive work to make pure links GFM Spec compliant](https://github.com/RobertDober/earmark_parser/pull/92) Kudos to [Akash Hiremath](https://github.com/akash-akya) ## 1.4.19 2022-01-07 - [Fix stop condition on closing HTML in scanners lookup algo](https://github.com/robertdober/earmark_parser/pull/79) Kudos to [José Valim](https://github.com/josevalim) - [Typos](https://github.com/robertdober/earmark_parser/pull/78) Kudos to [kianmeng](https://github.com/kianmeng) - [Footnotes fixed and upgraded(#26)](https://github.com/robertdober/earmark_parser/pull/76) Footnotes are now a **superset** of GFM Footnotes. This implies some changes - Footnote definitions (`[^footnote_id]`) must come at the end of your document (_GFM_) - Footnotes that are not referenced are not rendered anymore (_GFM_) - Footnote definitions can contain any markup with the exception of footnote definitions ## 1.4.18 2021-12-04 - [Deprecate options not useful anymore after the removal of parallel scanning (#72)](https://github.com/robertdober/earmark_parser/pull/72) - [Do not turn off lookahead on indented fences and check for indent only once (#71)](https://github.com/robertdober/earmark_parser/pull/71) Kudos to [José Valim](https://github.com/josevalim) - [Add lookahead for fenced code blocks (#70)](https://github.com/robertdober/earmark_parser/pull/70) * Do lookaheads for fenced code blocks Prior to this commit, we were trying to parse all lines between fenced code blocks, which could be very expensive. Therefore we lookahead fences and convert them to text. * Do not scan lines in parallel anymore * Remove unused code blocks * Handle fenced blocks wrapped in tags * Clean up regex * More corner cases * Optimize text creation * Optimize length checks Kudos to [José Valim](https://github.com/josevalim) - [5 Whitespace after closing ATX Headers](https://github.com/robertdober/earmark_parser/issues/5) - [40 Footnotes in lists](https://github.com/robertdober/earmark_parser/issues/40) - [65 Bad line numbers in spaced lists](https://github.com/robertdober/earmark_parser/issues/65) - [61 Fixes some double URI encoding in links](https://github.com/robertdober/earmark_parser/issues/61) - [Deprecate Pedantic Option](https://github.com/robertdober/earmark_parser/pull/60) ## 1.4.17 2021-10-29 - [44 Multiple Query Params in pure links](https://github.com/robertdober/earmark_parser/issues/44) - [28 Side By Side Reference Style Links fixed](https://github.com/robertdober/earmark_parser/issues/28) - [52 Deprecate Smartypants Options](https://github.com/robertdober/earmark_parser/issues/52) ## 1.4.16 2021/10/07 - [Inline IALs for headers, rulers and blockquotes](https://github.com/robertdober/earmark_parser/pull/56) - [Use Extractly instead of homemade readme task → Syntax Highlightening for iex> code blocks](https://github.com/robertdober/earmark_parser/pull/51) - [Refactoring and Dead Code Elimination in Context](https://github.com/robertdober/earmark_parser/pull/50) - [Annotations for Paragraphs and verbatim HTML](https://github.com/robertdober/earmark_parser/issues/47) - [46-fixing-typos](https://github.com/RobertDober/earmark_parser/pull/46) Kudos to [kianmeng](https://github.com/kianmeng) ## 1.4.15 2021/08/12 - [43-add-option-to-disable-inline-parsing](https://github.com/RobertDober/earmark_parser/pull/43) Kudos to [jonatanklosko](https://github.com/jonatanklosko) ## 1.4.13 2021/04/18 - [37-ial-in-li-raised-key-error](https://github.com/RobertDober/earmark_parser/pull/37) - [35-clearer-doc-iff-replacement](https://github.com/RobertDober/earmark_parser/pull/35) Kudos to [ream88](https://github.com/ream88) - [33-fix-for-bad-multiclass-ial-rendering](https://github.com/RobertDober/earmark_parser/pull/33) Kudos to [myrrlyn](https://github.com/myrrlyn) ## 1.4.12 2020/11/27 - [29-broken-changelog-link](https://github.com/robertdober/earmark_parser/pull/29) Kudos to [optikfluffel](https://github.com/optikfluffel) - [18-support-wikilinks](https://github.com/robertdober/earmark_parser/pull/18) Kudos to [sionide21](https://github.com/sionide21) ## 1.4.11 2020/11/26 - [24-treat-single-dash-as-text](https://github.com/robertdober/earmark_parser/issues/24) Kudos to [Ben Olive](https://github.com/sionide21) - [22-missing-ws-before-links](https://github.com/robertdober/earmark_parser/issues/22) Kudos to [Ben Olive](https://github.com/sionide21) ## 1.4.10 2020/07/18 - [1-text-of-footnote-definitions-not-converted](https://github.com/robertdober/earmark_parser/issues/1) - [19-use-spdx-in-hex](https://github.com/robertdober/earmark_parser/issues/19) Kudos to [Chulki Lee](https://github.com/chulkilee) - [10-Missing-space-between-whitspace-separated-items](https://github.com/robertdober/earmark_parser/issues/10) - [15-hide-private-module](https://github.com/robertdober/earmark_parser/issues/15) Kudos to [Wojtek Mach](https://github.com/wojtekmach) - [14-remove-application-from-mix.exs](https://github.com/robertdober/earmark_parser/issues/14) Kudos to [Wojtek Mach](https://github.com/wojtekmach) - [13-fix-github-link](https://githuhttps://github.com/RobertDober/earmark_parser/issues?q=is%3Apr+author%3Awojtekmachb.com/robertdober/earmark_parser/issues/13) Kudos to [Wojtek Mach](https://github.com/wojtekmach) ## 1.4.9 2020/07/01 - [2-accept-any-struct-as-option](https://github.com/pragdave/earmark/issues/2) Allow client code of Earmark to replace their calls to `Earmark.as_ast` with `EarmarkParser.as_ast` w/o any changes ## 1.4.8 2020/06/29 This marks the first release of the parser isolated from the rest of Earmark. It is feature identical to the 1.4.7 release of Earmark. All releases below were Earmark, all releases above are only EarmarkParser. # Earmark ## 1.4.7 2020/06/29 - [371-still-spurious-ws-after-inline-tags](https://github.com/pragdave/earmark/issues/371) ## 1.4.6 2020/06/28 - [350-some-complicated-autolinks-cut](https://github.com/pragdave/earmark/issues/350) - [359-unexpected-ws-in-html](https://github.com/pragdave/earmark/issues/359) - [337-quadruple-ast-format](https://github.com/pragdave/earmark/issues/337) - [366-simplify-transform](https://github.com/pragdave/earmark/issues/366) Kudos to [Eksperimental](https://github.com/eksperimental) - [353-oneline-html-tags](https://github.com/pragdave/earmark/issues/353) - [351-html-tags-without-newlines](https://github.com/pragdave/earmark/issues/351) - [335-content-inside-table-cells-reversed](https://github.com/pragdave/earmark/issues/335) - [348-no-crashes-for-invalid-URIs](https://github.com/pragdave/earmark/issues/348) Kudos to José Valim - [347-dialyxir-errors](https://github.com/pragdave/earmark/issues/347) Fixed some of them, alas not all ## 1.4.5 2020/06/06 This is mostly a bugfix release, as there were edge cases that resulted in Earmark crashing, notably - Bare IAL - unquoted attributes in html tags Also autolinks (GFM extension) delivered incorrect URLS where parenthesis were involved, for better GFM compatibility we therefore - Fixed broken parenthesis links (99% of all cases) - introduced the same URL encoding/decoding in links and link names of autolinks as GFM does And last but not least all numeric options in the CLI can now be written with underlines for readability. - [343-error-parsing-unquoted-atts](https://github.com/pragdave/earmark/issues/343) - [342 parens in pure links](https://github.com/pragdave/earmark/issues/342) - [340 IAL might cause error](https://github.com/pragdave/earmark/issues/340) - [339 Typos fix](ihttps://github.com/pragdave/earmark/pull/339) Kudos to [Ondrej Pinka](https://github.com/onpikono) - [336 Smartypants: Convert three hyphens to em dash](https://github.com/pragdave/earmark/pull/336) Kudos to [Jony Stoten](https://github.com/jonnystoten) - [324 Fix AST for links with nested elements](https://github.com/pragdave/earmark/pull/324) Kudos to [Wojtek Mach](https://github.com/wojtekmach) - [320 Nested Blockquotes](https://github.com/pragdave/earmark/issues/320) ## 1.4.4 2020/05/01 - [338 Deprecation warnings in mixfile removed](https://github.com/pragdave/earmark/issues/338) ## 1.4.3 2019/11/23 - [309 fenced code allows for more than 3 backticks/tildes now](https://github.com/pragdave/earmark/issues/309) - [302 Earmark.version returned a charlist, now a string](https://github.com/pragdave/earmark/issues/302) - [298 Blockquotes nested in lists only work with an indentation of 2 spaces](https://github.com/pragdave/earmark/issues/298) ## 1.4.2 2019/10/14 - [296 code for tasks removed from package](https://github.com/pragdave/earmark/issues/296) The additional tasks are only needed for dev and have been removed from the hex package. **Finally** - [PR#293 Nice fix for broken TOC links in README](https://github.com/pragdave/earmark/pull/293) Kudos to Ray Gesualdo [raygesualdo](https://github.com/raygesualdo) - [291 Transformer whitespace inside / around <code> <pre> tags](https://github.com/pragdave/earmark/issues/291) The spurious whitespace has been removed - [289 HTML Problem](https://github.com/pragdave/earmark/issues/289) The AST parser can now correctly distinguish between _generated_ AST (from md) and _parsed_ AST (from HTML) - [288 Metadata allowed to be added to the AST](https://github.com/pragdave/earmark/issues/288) The default HTML Transformer ignores metadata in the form of a map with the exception of `%{meta: ...}` ## 1.4.1 2019/09/24 - [282 Always create a `` in tables](https://github.com/pragdave/earmark/issues/282) Although strictly speaking a `` is only needed when there is a ``, semantic HTML suggests the presence of `` anyway. - [281 Urls in links were URL endoded, that is actually a bug ](https://github.com/pragdave/earmark/issues/281) It is the markdown author's responsibility to url encode her urls, if she does so correctly we double encoded the url before this fix. - [279 Languages in code blocks were limited to alphanum names, thus excluding, e.g. C# ](https://github.com/pragdave/earmark/issues/279) - [278 Implementing better GFM Table support ](https://github.com/pragdave/earmark/issues/278) Because of compatility issues we use a new option `gfm_tables` defaulting to `false` for this. Using this option `Earmark` will implement its own table extension **+** GFM tables at the same time. - [277 Expose an AST to HTML Transformer](https://github.com/pragdave/earmark/issues/277) While it should be faster to call `to_ast|>transform` it cannot be used instead of `as_html` yet as the API is not yet stable and some subtle differences in the output need to be addressed. ## 1.4.0 2019/09/05 - [145 Expose AST for output manipulation]( https://github.com/pragdave/earmark/issues/145) - [238 Pure Links are default now]( https://github.com/pragdave/earmark/issues/238) - [256 Align needed Elixir Version with ex_doc (>= 1.7)]( https://github.com/pragdave/earmark/issues/256) - [259 Deprecated option `sanitize` removed]( https://github.com/pragdave/earmark/issues/259) - [261 Deprecated Plugins removed]( https://github.com/pragdave/earmark/issues/261) - [265 Make deprecated `Earmark.parse/2` private]( https://github.com/pragdave/earmark/issues/265) ## 1.3.6 2019/08/30 Hopefully the last patch release of 1.3 before the structural changes of 1.4. - [#270]( https://github.com/pragdave/earmark/issues/270) Error messages during parsing of table cells were duplicated in a number, exponential to the number of table cells. - [#268]( https://github.com/pragdave/earmark/issues/268) Deprecation warnings concerning pure links showed fixed link to https://github.com/pragdave/earmark, at least a reasonable choice ;), instead of the text of the link. - [#266]( https://github.com/pragdave/earmark/issues/266) According to HTML5 Style Guide better XHTML compatibility by closing void tags e.g. `
` --> `
` ## 1.3.5 2019/08/01 - [#264]( https://github.com/pragdave/earmark/issues/264) Expose `Earmark.parse/2` but deprecate it. - [#262]( https://github.com/pragdave/earmark/issues/262) Remove non XHTML tags and - [#236]( https://github.com/pragdave/earmark/issues/236) Deprecation of plugins. - [#257]( https://github.com/pragdave/earmark/issues/257) Deprecation of `sanitize` option. ## 1.3.4 2019/07/29 - [#254 pure links inside links](https://github.com/pragdave/earmark/issues/254) ## 1.3.3 2019/07/23 ### Bugs - [#240 code blocks in lists](https://github.com/pragdave/earmark/issues/240) Bad reindentation inside list items led to code blocks not being verabtim =&rt; Badly formatted hexdoc for Earmark - [#243 errors in unicode link names](https://github.com/pragdave/earmark/issues/243) Regexpression was not UTF, thus some links were not correctly parsed Fixed in PR [244](https://github.com/pragdave/earmark/pull/244) Thank you [Stéphane ROBINO](https://github.com/StephaneRob) ### Features - [#158 some pure links implemented](https://github.com/pragdave/earmark/issues/158) This GFM like behavior is more and more expected, I will issue a PR for `ex_doc` on this as discussed with [José Valim](https://github.com/josevalim) Deprecation Warnings are issued by default, but will be suppressed for `ex_doc` in said PR. - Minor improvements on documentation In PR [235](https://github.com/pragdave/earmark/pull/235) Thank you - [Jason Axelson](https://github.com/axelson) ### Other - Refactoring c.f. PR [246](https://github.com/pragdave/earmark/pull/246) - Added Elixir version 1.9.0 for Travis c.f. PR #248 ### Minor improvements on documentation ### PRs - [244](https://github.com/pragdave/earmark/pull/244) - [235](https://github.com/pragdave/earmark/pull/235) ### Kudos: - [Jason Axelson](https://github.com/axelson) - [Stéphane ROBINO](https://github.com/StephaneRob) ## 1.3.2 2019/03/23 * Fix for issues - [#224 titles might be extracted from outside link]( https://github.com/pragdave/earmark/issues/224 ) - [#220 render only first link title always correctly]( https://github.com/pragdave/earmark/issues/220 ) - [#218 replaced iff with longer but clearer if and only if ]( https://github.com/pragdave/earmark/issues/218 ) ### Kudos: [niku](https://github.com/niku) for #218 [Rich Morin](https://github.com/RichMorin) for #220 & #224 as well as discussions ## 1.3.1 2018/12/21 - [#212 spaces at line end force line break]( https://github.com/pragdave/earmark/issues/212 ) - [#211 documentation explaining error messages]( https://github.com/pragdave/earmark/issues/211 ) ## 1.3.0 2018/11/15 * Fix for issues - [#208 Inline code made Commonmark compatible]( https://github.com/pragdave/earmark/issues/208 ) - [#203 escript does not report filename in error messages]( https://github.com/pragdave/earmark/issues/203 ) - [#90 Parsing "...' or '..." as link titles removed]( https://github.com/pragdave/earmark/issues/90 ) ### Dev dependencies updated * credo -> 0.10 ## 1.2.7 Not released Milestone merged into 1.3 Special KUDOS for [pareehonos](https://github.com/pareeohnos) for a huge PR concerning the major Feature Request [#145](https://github.com/pragdave/earmark/issues/145) This cannot be merged yet but certainly is a great contribution to our codebase. ## 1.2.6 2018/08/21 * Fix for issues - [#198 Escapes inside link texts are ignored]( https://github.com/pragdave/earmark/issues/198 ) - [#197 README task broken in Elixir 1.7]( https://github.com/pragdave/earmark/issues/197 ) - [#191 Allow configurable timeout for parallel map]( https://github.com/pragdave/earmark/issues/191 ) - [#190 do not include generated src/*.erl in the package]( https://github.com/pragdave/earmark/issues/190 ) * [#195 incorrect HTML for inline code blocks and IAL specified classes](https://github.com/pragdave/earmark/issues/195) from [Benjamin Milde]( https://github.com/LostKobrakai ) ## 1.2.5 2018/04/02 * Fix for issues - [#161]( https://github.com/pragdave/earmark/issues/161 ) - [#168]( https://github.com/pragdave/earmark/issues/168 ) - [#172]( https://github.com/pragdave/earmark/issues/172 ) - [#175]( https://github.com/pragdave/earmark/issues/175 ) - [#181]( https://github.com/pragdave/earmark/issues/181 ) * [#178](https://github.com/pragdave/earmark/pull/178) from [jwworth](https://github.com/jwworth) ### Kudos: [jwworth](https://github.com/jwworth) ## 1.2.4 2017/11/28 * Fix for issue - [#166]( https://github.com/pragdave/earmark/issues/166 ) * [PR160](https://github.com/pragdave/earmark/pull/160) from [simonwebdesign](https://github.com/simonewebdesign) * [PR163](https://github.com/pragdave/earmark/pull/163) from [nscyclone](https://github.com/nscyclone) * [PR164](https://github.com/pragdave/earmark/pull/164) from [joshsmith](https://github.com/joshsmith) * [PR165](https://github.com/pragdave/earmark/pull/165) from [asummers](https://github.com/asummers) ### Kudos: [simonwebdesign](https://github.com/simonewebdesign), [nscyclone](https://github.com/nscyclone), [joshsmith](https://github.com/joshsmith), [asummers](https://github.com/asummers) ## 1.2.3 2017/07/26 * [PR151](https://github.com/pragdave/earmark/pull/151) from [joshuawscott](https://github.com/joshuawscott) * Fixes for issues - [#150](https://github.com/pragdave/earmark/issues/150) - [#147](https://github.com/pragdave/earmark/issues/147) ### Kudos: [joshuawscott](https://github.com/joshuawscott) ## 1.2.2 2017/05/11 * [PR #144](https://github.com/pragdave/earmark/pull/144) from [KronicDeth](https://github.com/KronicDeth) ### Kudos: [KronicDeth](https://github.com/KronicDeth) ## 1.2.1 2017/05/03 * [PR #136](https://github.com/pragdave/earmark/pull/136) from [chrisalley](https://github.com/chrisalley) * Fixes for issues - [#139](https://github.com/pragdave/earmark/issues/139) ### Kudos: [chrisalley](https://github.com/chrisalley) ## 1.2 2017/03/10 * [PR #130](https://github.com/pragdave/earmark/pull/130) from [eksperimental](https://github.com/eksperimental) * [PR #129](https://github.com/pragdave/earmark/pull/129) from [Alekx](https://github.com/alkx) * [PR #125](//https://github.com/pragdave/earmark/pull/125) from [vyachkonovalov](https://github.com/vyachkonovalov) * Fixes for issues - #127 - #131 ### Kudos: [vyachkonovalov](https://github.com/vyachkonovalov), [Alekx](https://github.com/alkx), [eksperimental](https://github.com/eksperimental) ## 1.1.1 2017/02/03 * PR from Natronium pointing out issue #123 * Fixes for issues - #123 ### Kudos: [Natronium](https://github.com/Natronium) ## 1.1.0 2017/01/22 * PR from Michael Pope * PR from Pragdave * PR from christopheradams * PR from [AndrewDryga](https://github.com/AndrewDryga) * Fixes for issues - #106 - #110 - #114 ### Kudos: [AndrewDryga](https://github.com/AndrewDryga), [Christopher Adams](https://github.com/christopheradams), [Michael Pope](https://github.com/amorphid) ## 1.0.3 2016/11/02 * PR from TBK145 with some dead code elimination. * Implementation of command line switches for the `earmark` executable. Now any `%Earmark.Options{}` key can be passed in. * Fixes for issues - #99 - #96 - #95 - #103 ### Kudos: Thijs Klaver (TBK145) ## 1.0.2 2016/10/10 * PR from pdebelak with a fix of #55 * PR from jonnystorm with a fix for a special case in issue #85 * test coverage at 100% * PR from michalmuskala * Fixed remaining compiler warnings from 1.0.1 (Elixir 1.3) * PR from pdebelak to fix a factual error in the README * Fixes for issues - #55 - #86 - #88 - #89 - #93 ### Kudos: Jonathan Storm (jonnystorm), Michal Muskala (michalmuskala) & Peter Debelak (pdebelak) ## 1.0.1 2016/06/07 * fixing issue #81 by pushing this updated Changelog.md :) * PR from mschae fixing issue #80 broken hex package ### Kudos: Michael Schaefermeyer (mschae) & Tobias Pfeiffer (PragTob) ## 1.0.0 2016/06/07 * --version | -v switch for `earmark` escript. * added security notice about XSS to docs thanks to remiq * PR from alakra (issue #59) to allow Hyphens and Unicode in fenced code block names * PR from sntran to fix unsafe conditional variables from PR * PR from riacataquian to use maps instead of dicts * PR from gmile to remove duplicate tests * PR from gmile to upgrade poison dependency * PR from whatyouhide to fix warnings for Elixir 1.4 with additional help from milmazz * Travis for 1.2.x and 1.3.1 as well as OTP 19 * Fixes for issues: - #61 - #66 - #70 - #71 - #72 - #77 - #78 ### Kudos: Remigiusz Jackowski (remiq), Angelo Lakra (alakra), Son Tran-Nguyen (sntran), Mike Kreuzer (mikekreuzer), Ria Cataquian (riacataquian), Eugene Pirogov (gmile), Andrea Leopardi (whatyouhide) & Milton Mazzarri (milmazz) ## 0.2.1 2016/01/15 * Added 1.2 for Travis * PR from mneudert to fix HTMLOneLine detection ### Kudos: Marc Neudert (mneudert) ## 0.2.0 2015/12/28 * PR from eksperimental guaranteeing 100% HTML5 * PR from imbriaco to decouple parsing and html generation and whitespace removal * Fixes for issues: - #40 - #41 - #43 - #48 - #50 - #51 * Explicit LICENSE change to Apache 2.0 (#42) * Loading of test support files only in test environment thanks to José Valim * IO Capture done correctly thanks to whatyouhide * Warning for Elixir 1.2 fixed by mschae ### Kudos: Eksperimental (eksperimental), Mark Imbriaco (imbriaco), Andrea Leopardi(whatyouhide), José Valim & Michael Schaefermeyer (mschae) ## 0.1.19 2015/10/27 * Fix | in implicit lists, and restructur the parse a little. Many thanks to Robert Dober ## 0.1.17 2015/05/18 * Add strikethrough support to the HTML renderer. Thanks to Michael Schaefermeyer (mschae) ## 0.1.16 2015/05/08 * Another fix from José, this time for & in code blocks. ## 0.1.15 2015/03/25 * Allow numbered lists to start anywhere in the first four columns. (This was previously allowed for unnumbered lists). Fixes #13. ## 0.1.14 2015/03/25 * Fixed a problem where a malformed text heading caused a crash. We now report what appears to be malformed Markdown and continue, processing the line as text. Fixes #17. ## 0.1.13 2015/01/31 * José fixed a bug in Regex that revealed a problem with some Earmark replacement strings. As he's a true gentleman, he then fixed Earmark. ## 0.1.11 2014/08/18 * Matthew Lyon contributed footnote support. the answer is clearly 42.[^fn-why] In this case we need to… [^fn-why]: 42 is the only two digit number with the digits 4 and 2 that starts with a 4. For now, consider it experimental. For that reason, you have to enable it by passing the `footnotes: true` option. ## 0.1.10 2014/08/13 * The spec is ambiguous when it comes to setext headings. I assumed that they needed a blank line after them, but common practice says no. Changed the parser to treat them as headings if there's no blank. ## 0.1.9 2014/08/05 * Bug fix—extra blank lines could be appended to code blocks. * Tidied up code block HTML ## 0.1.7 2014/07/26 * Block rendering is now performed in parallel ## 0.1.6 07/25/14 * Added support for Kramdown-style attribute annotators for all block elements, so you can write # Warning {: .red} Do not turn off the engine if you are at altitude. {: .boxed #warning spellcheck="true"} and generate

Warning

Do not turn off the engine if you are at altitude.

## 0.1.5 07/20/14 * Merged two performance improvements from José Valim * Support escaping of pipes in tables, so a | b c | d \| e has two columns, not three. ## 0.1.4 07/14/14 * Allow list bullets to be indented, and deal with potential subsequent additional indentation of the body of an item. ## 0.1.3 07/14/14 * Added tasks to the Hex file list ## 0.1.2 07/14/14 * Add support for GFM tables ## 0.1.1 07/09/14 * Move readme generation task out of mix.exs and into tasks/ * Fix bug if setext heading started on first line ## 0.1.0 07/09/14 * Initial Release earmark_parser-1.4.39/coveralls.json000066400000000000000000000000571453104267300175020ustar00rootroot00000000000000{ "skip_files": [ "test/support", "src/" ] } earmark_parser-1.4.39/lib/000077500000000000000000000000001453104267300153615ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser.ex000066400000000000000000000543731453104267300207310ustar00rootroot00000000000000defmodule EarmarkParser do @type ast_meta :: map() @type ast_tag :: binary() @type ast_attribute_name :: binary() @type ast_attribute_value :: binary() @type ast_attribute :: {ast_attribute_name(), ast_attribute_value()} @type ast_attributes :: list(ast_attribute()) @type ast_tuple :: {ast_tag(), ast_attributes(), ast(), ast_meta()} @type ast_node :: binary() | ast_tuple() @type ast :: list(ast_node()) @type error :: {atom(), non_neg_integer(), binary()} @type errors :: list(error()) @type t :: {:ok, ast(), []} | {:error, ast(), errors()} @moduledoc ~S""" ### API #### EarmarkParser.as_ast This is the structure of the result of `as_ast`. {:ok, ast, []} = EarmarkParser.as_ast(markdown) {:ok, ast, deprecation_messages} = EarmarkParser.as_ast(markdown) {:error, ast, error_messages} = EarmarkParser.as_ast(markdown) For examples see the functiondoc below. #### Options Options can be passed into `as_ast/2` according to the documentation of `EarmarkParser.Options`. {status, ast, errors} = EarmarkParser.as_ast(markdown, options) ## Supports Standard [Gruber markdown][gruber]. [gruber]: ## Extensions ### Links #### Links supported by default ##### Oneline HTML Link tags iex(1)> EarmarkParser.as_ast(~s{link}) {:ok, [{"a", [{"href", "href"}], ["link"], %{verbatim: true}}], []} ##### Markdown links New style ... iex(2)> EarmarkParser.as_ast(~s{[title](destination)}) {:ok, [{"p", [], [{"a", [{"href", "destination"}], ["title"], %{}}], %{}}], []} and old style iex(3)> EarmarkParser.as_ast("[foo]: /url \"title\"\n\n[foo]\n") {:ok, [{"p", [], [{"a", [{"href", "/url"}, {"title", "title"}], ["foo"], %{}}], %{}}], []} #### Autolinks iex(4)> EarmarkParser.as_ast("") {:ok, [{"p", [], [{"a", [{"href", "https://elixir-lang.com"}], ["https://elixir-lang.com"], %{}}], %{}}], []} #### Additional link parsing via options #### Pure links **N.B.** that the `pure_links` option is `true` by default iex(5)> EarmarkParser.as_ast("https://github.com") {:ok, [{"p", [], [{"a", [{"href", "https://github.com"}], ["https://github.com"], %{}}], %{}}], []} But can be deactivated iex(6)> EarmarkParser.as_ast("https://github.com", pure_links: false) {:ok, [{"p", [], ["https://github.com"], %{}}], []} #### Wikilinks... are disabled by default iex(7)> EarmarkParser.as_ast("[[page]]") {:ok, [{"p", [], ["[[page]]"], %{}}], []} and can be enabled iex(8)> EarmarkParser.as_ast("[[page]]", wikilinks: true) {:ok, [{"p", [], [{"a", [{"href", "page"}], ["page"], %{wikilink: true}}], %{}}], []} ### Sub and Sup HTML Elements This feature is not enabled by default but can be enabled with the option `sub_sup: true` Therefore we will get iex(9)> EarmarkParser.as_ast("H~2~O or a^n^ + b^n^ = c^n^") {:ok, [{"p", [], ["H~2~O or a^n^ + b^n^ = c^n^"], %{}}], []} But by specifying `sub_sup: true` iex(10)> EarmarkParser.as_ast("H~2~O or a^n^ + b^n^ = c^n^", sub_sup: true) {:ok, [{"p", [], ["H", {"sub", [], ["2"], %{}}, "O or a", {"sup", [], ["n"], %{}}, " + b", {"sup", [], ["n"], %{}}, " = c", {"sup", [], ["n"], %{}}], %{}}], []} ### Mathematical expressions > Note: math syntax within Markdown is not standardized, so this option is a subject to change in future releases. This feature is not enabled by default but can be enabled with the option `math: true`. When enabled, LaTeX formatted math can be written within Markdown. For more information, see [LaTeX/Mathematics](https://en.wikibooks.org/wiki/LaTeX/Mathematics) in Wikibooks. #### Inline expressions Inline-style expression can be written by surrounding the expression with dollar symbols (`$`). iex> EarmarkParser.as_ast("$x = 1$", math: true) {:ok, [{"p", [], [{"code", [{"class", "math-inline"}], ["x = 1"], %{line: 1}}], %{}}], []} There must be no space between `$` and the surrounded expression. If you want to use a dollar sign in the same line as a math expression, you can escape the dollar with backslash (`\\$`). #### Expressions as blocks Display-style expression can be written by surrounding the expression with two dollar signs (`$$`). iex> EarmarkParser.as_ast("$$x = 1$$", math: true) {:ok, [{"p", [], [{"code", [{"class", "math-display"}], ["x = 1"], %{line: 1}}], %{}}], []} ### Github Flavored Markdown GFM is supported by default, however as GFM is a moving target and all GFM extension do not make sense in a general context, EarmarkParser does not support all of it, here is a list of what is supported: #### Strike Through iex(11)> EarmarkParser.as_ast("~~hello~~") {:ok, [{"p", [], [{"del", [], ["hello"], %{}}], %{}}], []} #### GFM Tables Are not enabled by default iex(12)> as_ast("a|b\\n-|-\\nc|d\\n") {:ok, [{"p", [], ["a|b\\n-|-\\nc|d\\n"], %{}}], []} But can be enabled with `gfm_tables: true` iex(13)> as_ast("a|b\n-|-\nc|d\n", gfm_tables: true) {:ok, [ { "table", [], [ {"thead", [], [{"tr", [], [{"th", [{"style", "text-align: left;"}], ["a"], %{}}, {"th", [{"style", "text-align: left;"}], ["b"], %{}}], %{}}], %{}}, {"tbody", [], [{"tr", [], [{"td", [{"style", "text-align: left;"}], ["c"], %{}}, {"td", [{"style", "text-align: left;"}], ["d"], %{}}], %{}}], %{}} ], %{} } ], []} #### Syntax Highlighting All backquoted or fenced code blocks with a language string are rendered with the given language as a _class_ attribute of the _code_ tag. For example: iex(14)> [ ...(14)> "```elixir", ...(14)> " @tag :hello", ...(14)> "```" ...(14)> ] |> as_ast() {:ok, [{"pre", [], [{"code", [{"class", "elixir"}], [" @tag :hello"], %{}}], %{}}], []} will be rendered as shown in the doctest above. If you want to integrate with a syntax highlighter with different conventions you can add more classes by specifying prefixes that will be put before the language string. Prism.js for example needs a class `language-elixir`. In order to achieve that goal you can add `language-` as a `code_class_prefix` to `EarmarkParser.Options`. In the following example we want more than one additional class, so we add more prefixes. iex(15)> [ ...(15)> "```elixir", ...(15)> " @tag :hello", ...(15)> "```" ...(15)> ] |> as_ast(%EarmarkParser.Options{code_class_prefix: "lang- language-"}) {:ok, [{"pre", [], [{"code", [{"class", "elixir lang-elixir language-elixir"}], [" @tag :hello"], %{}}], %{}}], []} #### Footnotes **N.B.** Footnotes are disabled by default, use `as_ast(..., footnotes: true)` to enable them Footnotes are now a **superset** of GFM Footnotes. This implies some changes - Footnote definitions (`[^footnote_id]`) must come at the end of your document (_GFM_) - Footnotes that are not referenced are not rendered anymore (_GFM_) - Footnote definitions can contain any markup with the exception of footnote definitions # iex(16)> markdown = [ # ...(16)> "My reference[^to_footnote]", # ...(16)> "", # ...(16)> "[^1]: I am not rendered", # ...(16)> "[^to_footnote]: Important information"] # ...(16)> {:ok, ast, []} = as_ast(markdown, footnotes: true) # ...(16)> ast # [ # {"p", [], ["My reference", # {"a", # [{"href", "#fn:to_footnote"}, {"id", "fnref:to_footnote"}, {"class", "footnote"}, {"title", "see footnote"}], # ["to_footnote"], %{}} # ], %{}}, # {"div", # [{"class", "footnotes"}], # [{"hr", [], [], %{}}, # {"ol", [], # [{"li", [{"id", "fn:to_footnote"}], # [{"a", [{"title", "return to article"}, {"class", "reversefootnote"}, {"href", "#fnref:to_footnote"}], ["↩"], %{}}, # {"p", [], ["Important information"], %{}}], %{}} # ], %{}}], %{}} # ] For more complex examples of footnotes, please refer to [these tests](https://github.com/RobertDober/earmark_parser/tree/master/test/acceptance/ast/footnotes/multiple_fn_test.exs) #### Breaks Hard linebreaks are disabled by default iex(17)> ["* a"," b", "c"] ...(17)> |> as_ast() {:ok, [{"ul", [], [{"li", [], ["a\nb\nc"], %{}}], %{}}], []} But can be enabled with `breaks: true` iex(18)> ["* a"," b", "c"] ...(18)> |> as_ast(breaks: true) {:ok, [{"ul", [], [{"li", [], ["a", {"br", [], [], %{}}, "b", {"br", [], [], %{}}, "c"], %{}}], %{}}], []} #### Enabling **all** options that are disabled by default Can be achieved with the `all: true` option iex(19)> [ ...(19)> "a^n^", ...(19)> "b~2~", ...(19)> "[[wikilink]]"] ...(19)> |> as_ast(all: true) {:ok, [ {"p", [], ["a", {"sup", [], ["n"], %{}}, {"br", [], [], %{}}, "b", {"sub", [], ["2"], %{}}, {"br", [], [], %{}}, {"a", [{"href", "wikilink"}], ["wikilink"], %{wikilink: true}}], %{}} ], []} #### Tables Are supported as long as they are preceded by an empty line. State | Abbrev | Capital ----: | :----: | ------- Texas | TX | Austin Maine | ME | Augusta Tables may have leading and trailing vertical bars on each line | State | Abbrev | Capital | | ----: | :----: | ------- | | Texas | TX | Austin | | Maine | ME | Augusta | Tables need not have headers, in which case all column alignments default to left. | Texas | TX | Austin | | Maine | ME | Augusta | Currently we assume there are always spaces around interior vertical unless there are exterior bars. However in order to be more GFM compatible the `gfm_tables: true` option can be used to interpret only interior vertical bars as a table if a separation line is given, therefore Language|Rating --------|------ Elixir | awesome is a table (if and only if `gfm_tables: true`) while Language|Rating Elixir | awesome never is. #### HTML Blocks HTML is not parsed recursively or detected in all conditions right now, though GFM compliance is a goal. But for now the following holds: A HTML Block defined by a tag starting a line and the same tag starting a different line is parsed as one HTML AST node, marked with %{verbatim: true} E.g. iex(20)> lines = [ "
", "some", "
more text" ] ...(20)> EarmarkParser.as_ast(lines) {:ok, [{"div", [], ["", "some"], %{verbatim: true}}, "more text"], []} And a line starting with an opening tag and ending with the corresponding closing tag is parsed in similar fashion iex(21)> EarmarkParser.as_ast(["spaniel"]) {:ok, [{"span", [{"class", "superspan"}], ["spaniel"], %{verbatim: true}}], []} What is HTML? We differ from strict GFM by allowing **all** tags not only HTML5 tags this holds for one liners.... iex(22)> {:ok, ast, []} = EarmarkParser.as_ast(["", "better"]) ...(22)> ast [ {"stupid", [], [], %{verbatim: true}}, {"not", [], ["better"], %{verbatim: true}}] and for multi line blocks iex(23)> {:ok, ast, []} = EarmarkParser.as_ast([ "", "world", ""]) ...(23)> ast [{"hello", [], ["world"], %{verbatim: true}}] #### HTML Comments Are recognized if they start a line (after ws and are parsed until the next `-->` is found all text after the next '-->' is ignored E.g. iex(24)> EarmarkParser.as_ast(" text -->\nafter") {:ok, [{:comment, [], [" Comment", "comment line", "comment "], %{comment: true}}, {"p", [], ["after"], %{}}], []} #### Lists Lists are pretty much GFM compliant, but some behaviors concerning the interpreation of the markdown inside a List Item's first paragraph seem not worth to be interpreted, examples are blockquote in a tight [list item](ttps://babelmark.github.io/?text=*+aa%0A++%3E+Second) which we can only have in a [loose one](https://babelmark.github.io/?text=*+aa%0A++%0A++%3E+Second) Or a headline in a [tight list item](https://babelmark.github.io/?text=*+bb%0A++%23+Headline) which, again is only available in the [loose version](https://babelmark.github.io/?text=*+bb%0A%0A++%23+Headline) in EarmarkParser. furthermore [this example](https://babelmark.github.io/?text=*+aa%0A++%60%60%60%0ASecond%0A++%60%60%60) demonstrates how weird and definitely not useful GFM's own interpretation can get. Therefore we stick to a more predictable approach. iex(25)> markdown = [ ...(25)> "* aa", ...(25)> " ```", ...(25)> "Second", ...(25)> " ```" ] ...(25)> as_ast(markdown) {:ok, [{"ul", [], [{"li", [], ["aa", {"pre", [], [{"code", [], ["Second"], %{}}], %{}}], %{}}], %{}}], []} Also we do support the immediate style of block content inside lists iex(26)> as_ast("* > Nota Bene!") {:ok, [{"ul", [], [{"li", [], [{"blockquote", [], [{"p", [], ["Nota Bene!"], %{}}], %{}}], %{}}], %{}}], []} or iex(27)> as_ast("1. # Breaking...") {:ok, [{"ol", [], [{"li", [], [{"h1", [], ["Breaking..."], %{}}], %{}}], %{}}], []} ### Adding Attributes with the IAL extension #### To block elements HTML attributes can be added to any block-level element. We use the Kramdown syntax: add the line `{:` _attrs_ `}` following the block. iex(28)> markdown = ["# Headline", "{:.from-next-line}"] ...(28)> as_ast(markdown) {:ok, [{"h1", [{"class", "from-next-line"}], ["Headline"], %{}}], []} Headers can also have the IAL string at the end of the line iex(29)> markdown = ["# Headline{:.from-same-line}"] ...(29)> as_ast(markdown) {:ok, [{"h1", [{"class", "from-same-line"}], ["Headline"], %{}}], []} A special use case is headers inside blockquotes which allow for some nifty styling in `ex_doc`* see [this PR](https://github.com/elixir-lang/ex_doc/pull/1400) if you are interested in the technical details iex(30)> markdown = ["> # Headline{:.warning}"] ...(30)> as_ast(markdown) {:ok, [{"blockquote", [], [{"h1", [{"class", "warning"}], ["Headline"], %{}}], %{}}], []} This also works for headers inside lists iex(31)> markdown = ["- # Headline{:.warning}"] ...(31)> as_ast(markdown) {:ok, [{"ul", [], [{"li", [], [{"h1", [{"class", "warning"}], ["Headline"], %{}}], %{}}], %{}}], []} It still works for inline code, as it did before iex(32)> markdown = "`Enum.map`{:lang=elixir}" ...(32)> as_ast(markdown) {:ok, [{"p", [], [{"code", [{"class", "inline"}, {"lang", "elixir"}], ["Enum.map"], %{line: 1}}], %{}}], []} _attrs_ can be one or more of: * `.className` * `#id` * name=value, name="value", or name='value' For example: # Warning {: .red} Do not turn off the engine if you are at altitude. {: .boxed #warning spellcheck="true"} #### To links or images It is possible to add IAL attributes to generated links or images in the following format. iex(33)> markdown = "[link](url) {: .classy}" ...(33)> EarmarkParser.as_ast(markdown) { :ok, [{"p", [], [{"a", [{"class", "classy"}, {"href", "url"}], ["link"], %{}}], %{}}], []} For both cases, malformed attributes are ignored and warnings are issued. iex(34)> [ "Some text", "{:hello}" ] |> Enum.join("\n") |> EarmarkParser.as_ast() {:error, [{"p", [], ["Some text"], %{}}], [{:warning, 2,"Illegal attributes [\"hello\"] ignored in IAL"}]} It is possible to escape the IAL in both forms if necessary iex(35)> markdown = "[link](url)\\{: .classy}" ...(35)> EarmarkParser.as_ast(markdown) {:ok, [{"p", [], [{"a", [{"href", "url"}], ["link"], %{}}, "{: .classy}"], %{}}], []} This of course is not necessary in code blocks or text lines containing an IAL-like string, as in the following example iex(36)> markdown = "hello {:world}" ...(36)> EarmarkParser.as_ast(markdown) {:ok, [{"p", [], ["hello {:world}"], %{}}], []} ## Limitations * Block-level HTML is correctly handled only if each HTML tag appears on its own line. So
hello
will work. However. the following won't
hello
* John Gruber's tests contain an ambiguity when it comes to lines that might be the start of a list inside paragraphs. One test says that This is the text * of a paragraph that I wrote is a single paragraph. The "*" is not significant. However, another test has * A list item * an another and expects this to be a nested list. But, in reality, the second could just be the continuation of a paragraph. I've chosen always to use the second interpretation—a line that looks like a list item will always be a list item. * Rendering of block and inline elements. Block or void HTML elements that are at the absolute beginning of a line end the preceding paragraph. Thusly mypara
Becomes

mypara


While mypara
will be transformed into

mypara


## Annotations **N.B.** this is an experimental feature from v1.4.16-pre on and might change or be removed again The idea is that each markdown line can be annotated, as such annotations change the semantics of Markdown they have to be enabled with the `annotations` option. If the `annotations` option is set to a string (only one string is supported right now, but a list might be implemented later on, hence the name), the last occurrence of that string in a line and all text following it will be added to the line as an annotation. Depending on how that line will eventually be parsed, this annotation will be added to the meta map (the 4th element in an AST quadruple) with the key `:annotation` In the current version the annotation will only be applied to verbatim HTML tags and paragraphs Let us show some examples now: ### Annotated Paragraphs iex(37)> as_ast("hello %> annotated", annotations: "%>") {:ok, [{"p", [], ["hello "], %{annotation: "%> annotated"}}], []} If we annotate more than one line in a para the first annotation takes precedence iex(38)> as_ast("hello %> annotated\nworld %> discarded", annotations: "%>") {:ok, [{"p", [], ["hello \nworld "], %{annotation: "%> annotated"}}], []} ### Annotated HTML elements In one line iex(39)> as_ast("One Line // a span", annotations: "//") {:ok, [{"span", [], ["One Line"], %{annotation: "// a span", verbatim: true}}], []} or block elements iex(40)> [ ...(40)> "
: annotation", ...(40)> " text", ...(40)> "
: discarded" ...(40)> ] |> as_ast(annotations: " : ") {:ok, [{"div", [], [" text"], %{annotation: " : annotation", verbatim: true}}], []} ### Commenting your Markdown Although many markdown elements do not support annotations yet, they can be used to comment your markdown, w/o cluttering the generated AST with comments iex(41)> [ ...(41)> "# Headline --> first line", ...(41)> "- item1 --> a list item", ...(41)> "- item2 --> another list item", ...(41)> "", ...(41)> " --> do not go there" ...(41)> ] |> as_ast(annotations: "-->") {:ok, [ {"h1", [], ["Headline"], %{}}, {"ul", [], [{"li", [], ["item1 "], %{}}, {"li", [], ["item2 "], %{}}], %{}}, {"p", [], [{"a", [{"href", "http://somewhere/to/go"}], ["http://somewhere/to/go"], %{}}, " "], %{annotation: "--> do not go there"}} ], [] } """ alias EarmarkParser.Options import EarmarkParser.Message, only: [sort_messages: 1] @doc """ iex(42)> markdown = "My `code` is **best**" ...(42)> {:ok, ast, []} = EarmarkParser.as_ast(markdown) ...(42)> ast [{"p", [], ["My ", {"code", [{"class", "inline"}], ["code"], %{line: 1}}, " is ", {"strong", [], ["best"], %{}}], %{}}] iex(43)> markdown = "```elixir\\nIO.puts 42\\n```" ...(43)> {:ok, ast, []} = EarmarkParser.as_ast(markdown, code_class_prefix: "lang-") ...(43)> ast [{"pre", [], [{"code", [{"class", "elixir lang-elixir"}], ["IO.puts 42"], %{}}], %{}}] **Rationale**: The AST is exposed in the spirit of [Floki's](https://hex.pm/packages/floki). """ @spec as_ast(binary()|list(binary()), any()) :: t() def as_ast(lines, options \\ %Options{}) def as_ast(lines, %Options{} = options) do context = _as_ast(lines, options) messages = sort_messages(context) messages1 = Options.add_deprecations(options, messages) status = case Enum.any?(messages1, fn {severity, _, _} -> severity == :error || severity == :warning end) do true -> :error _ -> :ok end {status, context.value, messages1} end def as_ast(lines, options) when is_list(options) do as_ast(lines, struct(Options, options)) end def as_ast(lines, options) when is_map(options) do as_ast(lines, struct(Options, options |> Map.delete(:__struct__) |> Enum.into([]))) end def as_ast(_, options) do raise ArgumentError, "#{inspect options} not a legal options map or keyword list" end defp _as_ast(lines, options) do {blocks, context} = EarmarkParser.Parser.parse_markdown(lines, Options.normalize(options)) EarmarkParser.AstRenderer.render(blocks, context) end @doc """ Accesses current hex version of the `EarmarkParser` application. Convenience for `iex` usage. """ def version() do with {:ok, version} = :application.get_key(:earmark_parser, :vsn), do: to_string(version) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/000077500000000000000000000000001453104267300203575ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/ast/000077500000000000000000000000001453104267300211465ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/ast/emitter.ex000066400000000000000000000015231453104267300231560ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Emitter do @moduledoc false def emit(tag, content \\ [], atts \\ [], meta \\ %{}) def emit(tag, content, atts, meta) when is_binary(content) or is_tuple(content) do {tag, _to_atts(atts), [content], meta} end def emit(tag, content, atts, meta) do {tag, _to_atts(atts), content, meta} end defp _to_atts(atts) defp _to_atts(nil), do: [] defp _to_atts(atts) when is_map(atts) do atts |> Enum.into([]) |> Enum.map(fn {name, value} -> {to_string(name), _to_string(value)} end) end defp _to_atts(atts) do atts |> Enum.map(fn {name, value} -> {to_string(name), _to_string(value)} end) end defp _to_string(value) defp _to_string(value) when is_list(value), do: Enum.join(value, " ") defp _to_string(value), do: to_string(value) end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/ast/inline.ex000066400000000000000000000326141453104267300227700ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Inline do @moduledoc false alias EarmarkParser.{Context, Message, Parser} alias EarmarkParser.Helpers.PureLinkHelpers alias Parser.LinkParser import EarmarkParser.Ast.Emitter import EarmarkParser.Ast.Renderer.AstWalker import EarmarkParser.Helpers import EarmarkParser.Helpers.AttrParser import EarmarkParser.Helpers.StringHelpers, only: [behead: 2] import EarmarkParser.Helpers.AstHelpers import Context, only: [set_value: 2] @typep conversion_data :: {String.t(), non_neg_integer(), EarmarkParser.Context.t(), boolean()} def convert(src, lnb, context) def convert(list, lnb, context) when is_list(list) do _convert(Enum.join(list, "\n"), lnb, context, true) end def convert(src, lnb, context) do _convert(src, lnb, context, true) end defp _convert(src, current_lnb, context, use_linky?) defp _convert(src, _, %{options: %{parse_inline: false}} = context, _) do prepend(context, src) end defp _convert("", _, context, _), do: context defp _convert(src, current_lnb, context, use_linky?) do {src1, lnb1, context1, use_linky1?} = _convert_next(src, current_lnb, context, use_linky?) _convert(src1, lnb1, context1, use_linky1?) end defp all_converters do [ converter_for_escape: &converter_for_escape/1, converter_for_autolink: &converter_for_autolink/1, # only if use_linky? converter_for_link_and_image: &converter_for_link_and_image/1, converter_for_reflink: &converter_for_reflink/1, converter_for_footnote: &converter_for_footnote/1, converter_for_nolink: &converter_for_nolink/1, # converter_for_strikethrough_gfm: &converter_for_strikethrough_gfm/1, converter_for_strong: &converter_for_strong/1, converter_for_em: &converter_for_em/1, # only for option sub_sup converter_for_sub: &converter_for_sub/1, converter_for_sup: &converter_for_sup/1, # converter_for_math_display: &converter_for_math_display/1, converter_for_math_inline: &converter_for_math_inline/1, # converter_for_code: &converter_for_code/1, converter_for_br: &converter_for_br/1, converter_for_inline_ial: &converter_for_inline_ial/1, converter_for_pure_link: &converter_for_pure_link/1, converter_for_text: &converter_for_text/1 ] end defp _convert_next(src, lnb, context, use_linky?) do _find_and_execute_converter({src, lnb, context, use_linky?}) end defp _find_and_execute_converter({src, lnb, context, use_linky?}) do all_converters() |> Enum.find_value(fn {_converter_name, converter} -> converter.({src, lnb, context, use_linky?}) end) end ###################### # # Converters # ###################### @escape_rule ~r{^\\([\\`*\{\}\[\]()\#+\-.!_>$])} def converter_for_escape({src, lnb, context, use_linky?}) do if match = Regex.run(@escape_rule, src) do [match, escaped] = match {behead(src, match), lnb, prepend(context, escaped), use_linky?} end end @autolink_rgx ~r{^<([^ >]+(@|:\/)[^ >]+)>} def converter_for_autolink({src, lnb, context, use_linky?}) do if match = Regex.run(@autolink_rgx, src) do [match, link, protocol] = match {href, text} = convert_autolink(link, protocol) out = render_link(href, text) {behead(src, match), lnb, prepend(context, out), use_linky?} end end def converter_for_pure_link({src, lnb, context, use_linky?}) do if context.options.pure_links do case PureLinkHelpers.convert_pure_link(src) do {ast, length} -> {behead(src, length), lnb, prepend(context, ast), use_linky?} _ -> nil end end end def converter_for_link_and_image({src, lnb, context, use_linky?}) do if use_linky? do match = LinkParser.parse_link(src, lnb) if match do {match1, text, href, title, link_or_img} = match out = case link_or_img do :link -> output_link(context, text, href, title, lnb) :wikilink -> maybe_output_wikilink(context, text, href, title, lnb) :image -> render_image(text, href, title) end if out do {behead(src, match1), lnb, prepend(context, out), use_linky?} end end end end @link_text ~S{(?:\[[^]]*\]|[^][]|\])*} @reflink ~r{^!?\[(#{@link_text})\]\s*\[([^]]*)\]}x def converter_for_reflink({src, lnb, context, use_linky?}) do if use_linky? do if match = Regex.run(@reflink, src) do {match_, alt_text, id} = case match do [match__, id, ""] -> {match__, id, id} [match__, alt_text, id] -> {match__, alt_text, id} end case reference_link(context, match_, alt_text, id, lnb) do {:ok, out} -> {behead(src, match_), lnb, prepend(context, out), use_linky?} _ -> nil end end end end def converter_for_footnote({src, lnb, context, use_linky?}) do if use_linky? do case Regex.run(context.rules.footnote, src) do [match, id] -> case footnote_link(context, match, id) do {:ok, out} -> {behead(src, match), lnb, _prepend_footnote(context, out, id), use_linky?} _ -> converter_for_text( {src, lnb, Message.add_message( context, {:error, lnb, "footnote #{id} undefined, reference to it ignored"} ), use_linky?} ) end _ -> nil end end end @nolink ~r{^!?\[((?:\[[^]]*\]|[^][])*)\]} def converter_for_nolink({src, lnb, context, use_linky?}) do if use_linky? do case Regex.run(@nolink, src) do [match, id] -> case reference_link(context, match, id, id, lnb) do {:ok, out} -> {behead(src, match), lnb, prepend(context, out), use_linky?} _ -> nil end _ -> nil end end end ################################ # Simple Tags: em, strong, del # ################################ @strikethrough_rgx ~r{\A~~(?=\S)([\s\S]*?\S)~~} def converter_for_strikethrough_gfm({src, _, _, _} = conv_tuple) do if match = Regex.run(@strikethrough_rgx, src) do _converter_for_simple_tag(conv_tuple, match, "del") end end @strong_rgx ~r{\A__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)} def converter_for_strong({src, _, _, _} = conv_tuple) do if match = Regex.run(@strong_rgx, src) do _converter_for_simple_tag(conv_tuple, match, "strong") end end @emphasis_rgx ~r{\A\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)} def converter_for_em({src, _, _, _} = conv_tuple) do if match = Regex.run(@emphasis_rgx, src) do _converter_for_simple_tag(conv_tuple, match, "em") end end @sub_rgx ~r{\A~(?=\S)(.*?\S)~} def converter_for_sub({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do if match = Regex.run(@sub_rgx, src) do _converter_for_simple_tag(conv_tuple, match, "sub") end end def converter_for_sub(_), do: nil @sup_rgx ~r{\A\^(?=\S)(.*?\S)\^} def converter_for_sup({src, _, %{options: %{sub_sup: true}}, _} = conv_tuple) do if match = Regex.run(@sup_rgx, src) do _converter_for_simple_tag(conv_tuple, match, "sup") end end def converter_for_sup(_), do: nil @math_inline_rgx ~r{\A\$(?=[^\s$])([\s\S]*?[^\s\\])\$} def converter_for_math_inline({src, lnb, %{options: %{math: true}} = context, use_linky?}) do if match = Regex.run(@math_inline_rgx, src) do [match, content] = match content = String.trim(content) out = math_inline(content, lnb) {behead(src, match), lnb, prepend(context, out), use_linky?} end end def converter_for_math_inline(_), do: nil @math_display_rgx ~r{\A\$\$([\s\S]+?)\$\$} def converter_for_math_display({src, lnb, %{options: %{math: true}} = context, use_linky?}) do if match = Regex.run(@math_display_rgx, src) do [match, content] = match content = String.trim(content) out = math_display(content, lnb) {behead(src, match), lnb, prepend(context, out), use_linky?} end end def converter_for_math_display(_), do: nil @squash_ws ~r{\s+} @code ~r{^ (`+) # $1 = Opening run of ` (.+?) # $2 = The code block (? String.trim() |> String.replace(@squash_ws, " ") out = codespan(content1, lnb) {behead(src, match), lnb, prepend(context, out), use_linky?} end end @inline_ial ~r<^\s*\{:\s*(.*?)\s*}> def converter_for_inline_ial({src, lnb, context, use_linky?}) do if match = Regex.run(@inline_ial, src) do [match, ial] = match {context1, ial_attrs} = parse_attrs(context, ial, lnb) new_tags = augment_tag_with_ial(context.value, ial_attrs) {behead(src, match), lnb, set_value(context1, new_tags), use_linky?} end end def converter_for_br({src, lnb, context, use_linky?}) do if match = Regex.run(context.rules.br, src, return: :index) do [{0, match_len}] = match {behead(src, match_len), lnb, prepend(context, emit("br")), use_linky?} end end @line_ending ~r{\r\n?|\n} @spec converter_for_text(conversion_data()) :: conversion_data() def converter_for_text({src, lnb, context, _}) do matched = case Regex.run(context.rules.text, src) do [match] -> match end line_count = matched |> String.split(@line_ending) |> Enum.count() ast = hard_line_breaks(matched, context.options.gfm) ast = walk_ast(ast, &gruber_line_breaks/1) {behead(src, matched), lnb + line_count - 1, prepend(context, ast), true} end ###################### # # Helpers # ###################### defp _converter_for_simple_tag({src, lnb, context, use_linky?}, match, for_tag) do {match1, content} = case match do [m, _, c] -> {m, c} [m, c] -> {m, c} end context1 = _convert(content, lnb, set_value(context, []), use_linky?) {behead(src, match1), lnb, prepend(context, emit(for_tag, context1.value |> Enum.reverse())), use_linky?} end defp _prepend_footnote(context, out, id) do context |> Map.update!(:referenced_footnote_ids, &MapSet.put(&1, id)) |> prepend(out) end defp convert_autolink(link, separator) defp convert_autolink(link, _separator = "@") do link = if String.at(link, 6) == ":", do: behead(link, 7), else: link text = link href = "mailto:" <> text {href, text} end defp convert_autolink(link, _separator) do {link, link} end @gruber_line_break Regex.compile!(" {2,}(?>\n)", "m") defp gruber_line_breaks(text) do text |> String.split(@gruber_line_break) |> Enum.intersperse(emit("br")) |> _remove_leading_empty() end @gfm_hard_line_break ~r{\\\n} defp hard_line_breaks(text, gfm) defp hard_line_breaks(text, false), do: text defp hard_line_breaks(text, nil), do: text defp hard_line_breaks(text, _) do text |> String.split(@gfm_hard_line_break) |> Enum.intersperse(emit("br")) |> _remove_leading_empty() end defp output_image_or_link(context, link_or_image, text, href, title, lnb) defp output_image_or_link(_context, "!" <> _, text, href, title, _lnb) do render_image(text, href, title) end defp output_image_or_link(context, _, text, href, title, lnb) do output_link(context, text, href, title, lnb) end defp output_link(context, text, href, title, lnb) do context1 = %{context | options: %{context.options | pure_links: false}} context2 = _convert(text, lnb, set_value(context1, []), String.starts_with?(text, "!")) if title do emit("a", Enum.reverse(context2.value), href: href, title: title) else emit("a", Enum.reverse(context2.value), href: href) end end defp maybe_output_wikilink(context, text, href, title, lnb) do if context.options.wikilinks do {tag, attrs, content, meta} = output_link(context, text, href, title, lnb) {tag, attrs, content, Map.put(meta, :wikilink, true)} end end defp reference_link(context, match, alt_text, id, lnb) do id = id |> replace(~r{\s+}, " ") |> String.downcase() case Map.fetch(context.links, id) do {:ok, link} -> {:ok, output_image_or_link(context, match, alt_text, link.url, link.title, lnb)} _ -> nil end end defp footnote_link(context, _match, id) do case Map.fetch(context.footnotes, id) do {:ok, _} -> {:ok, render_footnote_link("fn:#{id}", "fnref:#{id}", id)} _ -> nil end end defp prepend(%Context{} = context, prep) do _prepend(context, prep) end defp _prepend(context, value) defp _prepend(context, [bin | rest]) when is_binary(bin) do _prepend(_prepend(context, bin), rest) end defp _prepend(%Context{value: [str | rest]} = context, prep) when is_binary(str) and is_binary(prep) do %{context | value: [str <> prep | rest]} end defp _prepend(%Context{value: value} = context, prep) when is_list(prep) do %{context | value: Enum.reverse(prep) ++ value} end defp _prepend(%Context{value: value} = context, prep) do %{context | value: [prep | value]} end defp _remove_leading_empty(list) defp _remove_leading_empty(["" | rest]), do: rest defp _remove_leading_empty(list), do: list end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/ast/renderer/000077500000000000000000000000001453104267300227545ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/ast/renderer/ast_walker.ex000066400000000000000000000035571453104267300254600ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Renderer.AstWalker do @moduledoc false def walk(anything, fun, ignore_map_keys \\ false), do: _walk(anything, fun, ignore_map_keys, false) def walk_ast(ast, fun), do: _walk_ast(ast, fun, []) defp _walk(ast, fun, ignore_map_keys, child_of_map) defp _walk([], _fun, _ignore_map_keys, _child_of_map), do: [] defp _walk(list, fun, ignore_map_keys, _child_of_map) when is_list(list) do Enum.map(list, &(_walk(&1, fun, ignore_map_keys, false))) end defp _walk(map, fun, ignore_map_keys, _child_of_map) when is_map(map) do map |> Enum.into(%{}, &(_walk(&1, fun, ignore_map_keys, true))) end defp _walk(tuple, fun, ignore_map_keys, child_of_map) when is_tuple(tuple) do if child_of_map && ignore_map_keys do _walk_map_element(tuple, fun, ignore_map_keys) else tuple |> Tuple.to_list |> Enum.map(&(_walk(&1, fun, ignore_map_keys, false))) |> List.to_tuple end end defp _walk(ele, fun, _ignore_map_keys, _child_of_map), do: fun.(ele) defp _walk_map_element({key, value}, fun, ignore_map_keys) do {key, _walk(value, fun, ignore_map_keys, false)} end defp _walk_ast(ast, fun, res) defp _walk_ast([], _fun, res), do: Enum.reverse(res) defp _walk_ast(stringy, fun, res) when is_binary(stringy), do: _walk_ast([stringy], fun, res) defp _walk_ast([stringy|rest], fun, res) when is_binary(stringy) do res1 = case fun.(stringy) do [] -> res [_|_]=trans -> List.flatten([Enum.reverse(trans)|res]) stringy1 -> [stringy1|res] end _walk_ast(rest, fun, res1) end defp _walk_ast([{tag, atts, content, meta}|rest], fun, res) do _walk_ast(rest, fun, [{tag, atts, _walk_ast(content, fun, []), meta}|res]) end defp _walk_ast([list|rest], fun, res) when is_list(list) do _walk_ast(rest, fun, [_walk_ast(list, fun, [])|res]) end end earmark_parser-1.4.39/lib/earmark_parser/ast/renderer/footnote_renderer.ex000066400000000000000000000026241453104267300270410ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Renderer.FootnoteRenderer do import EarmarkParser.Ast.Emitter alias EarmarkParser.{AstRenderer, Block, Context, Message} import Context, only: [clear_value: 1, prepend: 2] @moduledoc false @empty_set MapSet.new([]) def render_defined_fns(%Block.FnList{blocks: footnotes}, context) do {elements, errors} = render_footnote_blocks(footnotes, context) ast = emit( "div", [ emit("hr"), emit("ol", elements) ], class: "footnotes" ) prepend(context, ast) |> Message.add_messages(errors) end defp _render_footnote_def(%Block.FnDef{blocks: blocks, id: id}, {ast, errors, context}=acc) do if MapSet.member?(context.referenced_footnote_ids, id) do context1 = AstRenderer.render(blocks, clear_value(context)) a_attrs = %{title: "return to article", class: "reversefootnote", href: "#fnref:#{id}"} footnote_li_ast = emit("li", [emit("a", ["↩"], a_attrs) | context1.value], id: "fn:#{id}") {[footnote_li_ast|ast], MapSet.union(errors, context1.options.messages), context} else acc end end defp render_footnote_blocks(footnotes, context) do {elements, errors, _} = footnotes |> Enum.reduce({[], @empty_set, context}, &_render_footnote_def/2) {elements|>Enum.reverse, errors} end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/ast/renderer/html_renderer.ex000066400000000000000000000016551453104267300261530ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Renderer.HtmlRenderer do import EarmarkParser.Context, only: [prepend: 2] import EarmarkParser.Helpers.HtmlParser import EarmarkParser.Helpers.AstHelpers, only: [annotate: 2] @moduledoc false # Structural Renderer for html blocks def render_html_block(lines, context, annotation) def render_html_block(lines, context, annotation) do [tag] = parse_html(lines) tag_ = if annotation, do: annotate(tag, annotation), else: tag prepend(context, tag_) end def render_html_oneline([line|_], context, annotation \\ []) do [tag|rest] = parse_html([line]) tag_ = if annotation, do: annotate(tag, annotation), else: tag prepend(context, [tag_|rest]) end @html_comment_start ~r{\A\s*.*\z} def render_html_comment_line(line) do line |> String.replace(@html_comment_start, "") |> String.replace(@html_comment_end, "") end end earmark_parser-1.4.39/lib/earmark_parser/ast/renderer/table_renderer.ex000066400000000000000000000027271453104267300262770ustar00rootroot00000000000000defmodule EarmarkParser.Ast.Renderer.TableRenderer do @moduledoc false alias EarmarkParser.Ast.Inline alias EarmarkParser.Context import EarmarkParser.Ast.Emitter def render_header(header, lnb, aligns, context) do {th_ast, context1} = header |> Enum.zip(aligns) |> Enum.map_reduce(context, &_render_col(&1, &2, lnb, "th")) {emit("thead", emit("tr", th_ast)), context1} end def render_rows(rows, lnb, aligns, context) do {rows1, context1} = rows |> Enum.zip(Stream.iterate(lnb, &(&1 + 1))) |> Enum.map_reduce(context, &_render_row(&1, &2, aligns)) {[emit("tbody", rows1)], context1} end defp _render_cols(row, lnb, aligns, context, coltype \\ "td") do row |> Enum.zip(aligns) |> Enum.map_reduce(context, &_render_col(&1, &2, lnb, coltype)) end defp _render_col({col, align}, context, lnb, coltype) do context1 = Inline.convert(col, lnb, Context.clear_value(context)) {emit(coltype, context1.value |> Enum.reverse, _align_to_style(align)), context1} end defp _render_row({row, lnb}, context, aligns) do {ast, context1} = _render_cols(row, lnb, aligns, context) {emit("tr", ast), context1} end defp _align_to_style(align) defp _align_to_style(:left), do: [{"style", "text-align: left;"}] defp _align_to_style(:right), do: [{"style", "text-align: right;"}] defp _align_to_style(:center), do: [{"style", "text-align: center;"}] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/ast_renderer.ex000066400000000000000000000157341453104267300234040ustar00rootroot00000000000000defmodule EarmarkParser.AstRenderer do alias EarmarkParser.Block alias EarmarkParser.Context alias EarmarkParser.Options import Context, only: [clear_value: 1, modify_value: 2, prepend: 2, prepend: 3] import EarmarkParser.Ast.Emitter import EarmarkParser.Ast.Inline, only: [convert: 3] import EarmarkParser.Helpers.AstHelpers import EarmarkParser.Ast.Renderer.{HtmlRenderer, FootnoteRenderer, TableRenderer} @moduledoc false def render(blocks, context = %Context{options: %Options{}}, loose? \\ true) do _render(blocks, context, loose?) end defp _render(blocks, context, loose?) defp _render([], context, _loose?), do: context defp _render([block | blocks], context, loose?) do context1 = render_block(block, clear_value(context), loose?) _render(blocks, prepend(context1, context), loose?) end defp render_block(block, context, loose?) ############# # Paragraph # ############# defp render_block(%Block.Para{lnb: lnb, lines: lines, attrs: attrs} = para, context, _loose?) do context1 = convert(lines, lnb, context) value = context1.value |> Enum.reverse() ast = emit("p", value, attrs) |> annotate(para) prepend(context, ast, context1) end ######## # Html # ######## defp render_block(%Block.Html{annotation: annotation, html: html}, context, _loose?) do render_html_block(html, context, annotation) end defp render_block(%Block.HtmlOneline{annotation: annotation, html: html}, context, _loose?) do render_html_oneline(html, context, annotation) end defp render_block(%Block.HtmlComment{lines: lines}, context, _loose?) do lines1 = lines |> Enum.map(&render_html_comment_line/1) prepend(context, emit(:comment, lines1, [], %{comment: true})) end ######### # Ruler # ######### defp render_block(%Block.Ruler{type: "-", attrs: attrs}, context, _loose?) do prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "thin"}))) end defp render_block(%Block.Ruler{type: "_", attrs: attrs}, context, _loose?) do prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "medium"}))) end defp render_block(%Block.Ruler{type: "*", attrs: attrs}, context, _loose?) do prepend(context, emit("hr", [], merge_attrs(attrs, %{"class" => "thick"}))) end ########### # Heading # ########### defp render_block( %Block.Heading{lnb: lnb, level: level, content: content, attrs: attrs}, context, _loose? ) do context1 = convert(content, lnb, clear_value(context)) modify_value( context1, fn _ -> [ emit( "h#{level}", context1.value |> Enum.reverse(), attrs) ] end ) end ############## # Blockquote # ############## defp render_block(%Block.BlockQuote{blocks: blocks, attrs: attrs}, context, _loose?) do context1 = render(blocks, clear_value(context)) modify_value(context1, fn ast -> [emit("blockquote", ast, attrs)] end) end ######### # Table # ######### defp render_block( %Block.Table{lnb: lnb, header: header, rows: rows, alignments: aligns, attrs: attrs}, context, _loose? ) do header_offset = if header do 2 # 1 line for header text, 1 line for header separator else 0 end {rows_ast, context1} = render_rows(rows, lnb + header_offset, aligns, context) {rows_ast1, context2} = if header do {header_ast, context3} = render_header(header, lnb, aligns, context1) {[header_ast | rows_ast], context3} else {rows_ast, context1} end prepend( clear_value(context2), emit("table", rows_ast1, attrs) ) end ######## # Code # ######## defp render_block( %Block.Code{language: language, attrs: attrs} = block, context = %Context{options: options}, _loose? ) do classes = if language && language != "", do: [code_classes(language, options.code_class_prefix)], else: [] lines = render_code(block) prepend( context, emit("pre", emit("code", lines, classes), attrs) ) end ######### # Lists # ######### @start_rgx ~r{\A\d+} defp render_block( %Block.List{type: type, bullet: bullet, blocks: items, attrs: attrs}, context, _loose? ) do context1 = render(items, clear_value(context)) start_map = case bullet && Regex.run(@start_rgx, bullet) do nil -> %{} ["1"] -> %{} [start1] -> %{start: _normalize_start(start1)} end prepend( context, emit(to_string(type), context1.value, merge_attrs(attrs, start_map)), context1 ) end # format a spaced list item defp render_block( %Block.ListItem{blocks: blocks, attrs: attrs, loose?: loose?}, context, _loose? ) do context1 = render(blocks, clear_value(context), loose?) prepend( context, emit("li", context1.value, attrs), context1 ) end ######## # Text # ######## defp render_block(%Block.Text{line: line, lnb: lnb}, context, loose?) do context1 = convert(line, lnb, clear_value(context)) ast = context1.value |> Enum.reverse() if loose? do modify_value(context1, fn _ -> [emit("p", ast)] end) else modify_value(context1, fn _ -> ast end) end end ################## # Footnote Block # ################## @empty_set MapSet.new([]) defp render_block(%Block.FnList{}=fn_list, context, _loose?) do if MapSet.equal?(context.referenced_footnote_ids, @empty_set) do context else render_defined_fns(fn_list, context) end end ####################################### # Isolated IALs are rendered as paras # ####################################### defp render_block(%Block.Ial{verbatim: verbatim}, context, _loose?) do prepend(context, emit("p", "{:#{verbatim}}")) end #################### # IDDef is ignored # #################### defp render_block(%Block.IdDef{}, context, _loose?), do: context # Helpers # ------- # Seems to be dead code but as GFM list handling is broken maybe we have a bug # that does not call this correctly, anyhow AST triplets do not exits anymore # so this code would break if called # defp _fix_text_lines(ast, loose?) # defp _fix_text_lines(ast, false), do: Enum.map(ast, &_fix_tight_text_line/1) # defp _fix_text_lines(ast, true), do: Enum.map(ast, &_fix_loose_text_line/1) # defp _fix_loose_text_line(node) # defp _fix_loose_text_line({:text, _, lines}), do: emit("p", lines) # defp _fix_loose_text_line(node), do: node # defp _fix_tight_text_line(node) # defp _fix_tight_text_line({:text, _, lines}), do: lines # defp _fix_tight_text_line(node), do: node # INLINE CANDIDATE defp _normalize_start(start) do case String.trim_leading(start, "0") do "" -> "0" start1 -> start1 end end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/000077500000000000000000000000001453104267300214515ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/block/block_quote.ex000066400000000000000000000002461453104267300243200ustar00rootroot00000000000000defmodule EarmarkParser.Block.BlockQuote do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, blocks: [] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/code.ex000066400000000000000000000002561453104267300227240ustar00rootroot00000000000000defmodule EarmarkParser.Block.Code do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, lines: [], language: nil end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/fn_def.ex000066400000000000000000000002671453104267300232350ustar00rootroot00000000000000defmodule EarmarkParser.Block.FnDef do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, id: nil, number: nil, blocks: [] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/fn_list.ex000066400000000000000000000002531453104267300234450ustar00rootroot00000000000000defmodule EarmarkParser.Block.FnList do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: ".footnotes", blocks: [] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/heading.ex000066400000000000000000000002611453104267300234050ustar00rootroot00000000000000defmodule EarmarkParser.Block.Heading do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, content: nil, level: nil end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/html.ex000066400000000000000000000002501453104267300227500ustar00rootroot00000000000000defmodule EarmarkParser.Block.Html do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, html: [], tag: nil end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/html_comment.ex000066400000000000000000000002461453104267300244770ustar00rootroot00000000000000defmodule EarmarkParser.Block.HtmlComment do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, lines: [] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/html_oneline.ex000066400000000000000000000002451453104267300244650ustar00rootroot00000000000000defmodule EarmarkParser.Block.HtmlOneline do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, html: "" end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/ial.ex000066400000000000000000000002571453104267300225600ustar00rootroot00000000000000defmodule EarmarkParser.Block.Ial do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, content: nil, verbatim: "" end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/id_def.ex000066400000000000000000000002641453104267300232230ustar00rootroot00000000000000defmodule EarmarkParser.Block.IdDef do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, id: nil, url: nil, title: nil end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/list.ex000066400000000000000000000006201453104267300227600ustar00rootroot00000000000000defmodule EarmarkParser.Block.List do @moduledoc false defstruct annotation: nil, attrs: nil, blocks: [], lines: [], bullet: "-", indent: 0, lnb: 0, loose?: false, pending: {nil, 0}, spaced?: false, start: "", type: :ul end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/list_item.ex000066400000000000000000000004551453104267300240040ustar00rootroot00000000000000defmodule EarmarkParser.Block.ListItem do @moduledoc false defstruct attrs: nil, blocks: [], bullet: "", lnb: 0, annotation: nil, loose?: false, spaced?: true, type: :ul end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/para.ex000066400000000000000000000002371453104267300227340ustar00rootroot00000000000000defmodule EarmarkParser.Block.Para do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, lines: [] end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/ruler.ex000066400000000000000000000002401453104267300231340ustar00rootroot00000000000000defmodule EarmarkParser.Block.Ruler do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, type: nil end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/table.ex000066400000000000000000000004341453104267300230770ustar00rootroot00000000000000defmodule EarmarkParser.Block.Table do @moduledoc false defstruct lnb: 0, annotation: nil, attrs: nil, rows: [], header: nil, alignments: [] def new_for_columns(n) do %__MODULE__{alignments: Elixir.List.duplicate(:left, n)} end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/block/text.ex000066400000000000000000000002361453104267300227740ustar00rootroot00000000000000defmodule EarmarkParser.Block.Text do @moduledoc false defstruct attrs: nil, lnb: 0, annotation: nil, line: "" end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/context.ex000066400000000000000000000077701453104267300224140ustar00rootroot00000000000000defmodule EarmarkParser.Context do @moduledoc false alias EarmarkParser.Options @type t :: %__MODULE__{ options: EarmarkParser.Options.t(), links: map(), footnotes: map(), referenced_footnote_ids: MapSet.t(String.t()), value: String.t() | [String.t()] } defstruct options: %EarmarkParser.Options{}, links: Map.new(), rules: nil, footnotes: Map.new(), referenced_footnote_ids: MapSet.new([]), value: [] ############################################################################## # Handle adding option specific rules and processors # ############################################################################## @doc false def modify_value(%__MODULE__{value: value} = context, fun) do nv = fun.(value) %{context | value: nv} end @doc false def prepend(context1, ast_or_context, context2_or_nil \\ nil) def prepend(%__MODULE__{} = context1, %__MODULE__{} = context2, nil) do context1 |> _merge_contexts(context2) |> _prepend(context2.value) end def prepend(%__MODULE__{} = context1, ast, nil) do context1 |> _prepend(ast) end def prepend(%__MODULE__{} = context1, ast, %__MODULE__{} = context2) do context1 |> _merge_contexts(context2) |> _prepend(ast) end defp _merge_contexts( %__MODULE__{referenced_footnote_ids: orig} = context1, %__MODULE__{referenced_footnote_ids: new} = context2 ) do context_ = _merge_messages(context1, context2) %{context_| referenced_footnote_ids: MapSet.union(orig, new)} end defp _merge_messages(context, context_or_messages) defp _merge_messages(context, %__MODULE__{options: %Options{messages: messages}}) do _merge_messages(context, messages) end defp _merge_messages(context, messages) do %{context | options: %{context.options|messages: MapSet.union(context.options.messages, messages)}} end defp _prepend(ctxt, []), do: ctxt defp _prepend(%{value: value} = ctxt, {:comment, _, _, _} = ct), do: %{ctxt | value: [ct | value]} defp _prepend(%{value: value} = ctxt, tuple) when is_tuple(tuple) do %{ctxt | value: [tuple | value] |> List.flatten()} end defp _prepend(%{value: value} = ctxt, list) when is_list(list), do: %{ctxt | value: List.flatten(list ++ value)} @doc """ Convenience method to prepend to the value list """ def set_value(%__MODULE__{} = ctx, value) do %{ctx | value: value} end def clear_value(%__MODULE__{} = ctx), do: %{ctx | value: []} # this is called by the command line processor to update # the inline-specific rules in light of any options def update_context(context = %EarmarkParser.Context{options: options}) do %{context | rules: rules_for(options)} end # ( "[" .*? "]"n or anything w/o {"[", "]"}* or "]" ) * @link_text ~S{(?:\[[^]]*\]|[^][]|\])*} # " # @href ~S{\s*?(?:\s+['"](.*?)['"])?\s*} defp basic_rules do [ br: ~r<^ {2,}\n(?!\s*$)>, text: ~r<^[\s\S]+?(?=[\\ ] end defp rules_for(options) do subsup = if options.sub_sup do "~^" else "" end math = if options.math do "$" else "" end rule_updates = if options.gfm do rules = [ text: ~r{^[\s\S]+?(?=~~|[\\ Enum.into(%{}) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/enum/000077500000000000000000000000001453104267300213235ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/enum/ext.ex000066400000000000000000000037311453104267300224650ustar00rootroot00000000000000defmodule EarmarkParser.Enum.Ext do @moduledoc ~S""" Some extensions of Enum functions """ @doc ~S""" `reduce_with_end` is like `Enum.reduce` for lists, but the reducer function is called for each element of the list with the tuple `{:element, element}` and the accumulator and once more at the end with `:end` and the accumulator iex(1)> reducer = ...(1)> fn {:element, nil}, {partial, result} -> {[], [Enum.sum(partial)|result]} ...(1)> {:element, val}, {partial, result} -> {[val|partial], result} ...(1)> :end, {partial, result} -> [Enum.sum(partial)|result] |> Enum.reverse ...(1)> end ...(1)> [1, 2, nil, 4, 1, 0, nil, 3, 2, 2] ...(1)> |> reduce_with_end({[], []}, reducer) [3, 5, 7] **N.B.** that in the treatment of `:end` we can change the shape of the accumulator w/o any penalty concerning the complexity of the reducer function """ def reduce_with_end(collection, initial_acc, reducer_fn) def reduce_with_end([], acc, reducer_fn) do reducer_fn.(:end, acc) end def reduce_with_end([ele|rest], acc, reducer_fn) do reduce_with_end(rest, reducer_fn.({:element, ele}, acc), reducer_fn) end @doc ~S""" Like map_reduce but reversing the list iex(2)> replace_nil_and_count = fn ele, acc -> ...(2)> if ele, do: {ele, acc}, else: {"", acc + 1} ...(2)> end ...(2)> ["y", nil, "u", nil, nil, "a", nil] |> reverse_map_reduce(0, replace_nil_and_count) { ["", "a", "", "", "u", "", "y"], 4 } """ def reverse_map_reduce(list, initial, fun) do _reverse_map_reduce(list, initial, [], fun) end # Helpers {{{ defp _reverse_map_reduce(list, acc, result, fun) defp _reverse_map_reduce([], acc, result, _fun), do: {result, acc} defp _reverse_map_reduce([fst|rst], acc, result, fun) do {new_ele, new_acc} = fun.(fst, acc) _reverse_map_reduce(rst, new_acc, [new_ele|result], fun) end # }}} end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers.ex000066400000000000000000000045421453104267300223640ustar00rootroot00000000000000defmodule EarmarkParser.Helpers do @moduledoc false @doc """ Expand tabs to multiples of 4 columns """ def expand_tabs(line) do Regex.replace(~r{(.*?)\t}, line, &expander/2) end @trailing_ial_rgx ~r< (?x @doc ~S""" Returns a tuple containing a potentially present IAL and the line w/o the IAL iex(1)> extract_ial("# A headline") {nil, "# A headline"} iex(2)> extract_ial("# A classy headline{:.classy}") {".classy", "# A classy headline"} An IAL line, remains an IAL line though iex(3)> extract_ial("{:.line-ial}") {nil, "{:.line-ial}"} """ def extract_ial(line) do case Regex.split(@trailing_ial_rgx, line, include_captures: true, parts: 2, on: [:ial]) do [_] -> {nil, line} [line_, "{:" <> ial, _] -> ial_ = ial |> String.trim_trailing("}") |> String.trim() {ial_, String.trim_trailing(line_)} end end defp expander(_, leader) do extra = 4 - rem(String.length(leader), 4) leader <> pad(extra) end @doc """ Remove newlines at end of line and optionally annotations """ # def remove_line_ending(line, annotation \\ nil) def remove_line_ending(line, nil) do _trim_line({line, nil}) end def remove_line_ending(line, annotation) do case Regex.run(annotation, line) do nil -> _trim_line({line, nil}) match -> match |> tl() |> List.to_tuple |> _trim_line() end end defp _trim_line({line, annot}), do: {line |> String.trim_trailing("\n") |> String.trim_trailing("\r"), annot} defp pad(1), do: " " defp pad(2), do: " " defp pad(3), do: " " defp pad(4), do: " " @doc """ `Regex.replace` with the arguments in the correct order """ def replace(text, regex, replacement, options \\ []) do Regex.replace(regex, text, replacement, options) end @doc """ Replace <, >, and quotes with the corresponding entities. If `encode` is true, convert ampersands, too, otherwise only convert non-entity ampersands. """ @amp_rgx ~r{&(?!#?\w+;)} def escape(html), do: _escape(Regex.replace(@amp_rgx, html, "&")) defp _escape(html) do html |> String.replace("<", "<") |> String.replace(">", ">") |> String.replace("\"", """) |> String.replace("'", "'") end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/000077500000000000000000000000001453104267300220215ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/helpers/ast_helpers.ex000066400000000000000000000063071453104267300246760ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.AstHelpers do @moduledoc false import EarmarkParser.Ast.Emitter import EarmarkParser.Helpers alias EarmarkParser.Block @doc false def annotate(node, from_block) def annotate(node, %{annotation: nil}), do: node def annotate({tag, atts, children, meta}, %{annotation: annotation}), do: {tag, atts, children, Map.put(meta, :annotation, annotation)} def annotate({tag, atts, children, meta}, annotation), do: {tag, atts, children, Map.put(meta, :annotation, annotation)} @doc false def attrs_to_string_keys(key_value_pair) def attrs_to_string_keys({k, vs}) when is_list(vs) do {to_string(k), Enum.join(vs, " ")} end def attrs_to_string_keys({k, vs}) do {to_string(k),to_string(vs)} end @doc false def augment_tag_with_ial(tags, ial) def augment_tag_with_ial([{t, a, c, m}|tags], atts) do [{t, merge_attrs(a, atts), c, m}|tags] end def augment_tag_with_ial([], _atts) do [] end @doc false def code_classes(language, prefix) do classes = ["" | String.split(prefix || "")] |> Enum.map(fn pfx -> "#{pfx}#{language}" end) {"class", classes |> Enum.join(" ")} end @doc false def math_inline(text, lnb) do emit("code", text, [class: "math-inline"], %{line: lnb}) end def math_display(text, lnb) do emit("code", text, [class: "math-display"], %{line: lnb}) end @doc false def codespan(text, lnb) do emit("code", text, [class: "inline"], %{line: lnb}) end @doc false def render_footnote_link(ref, backref, number) do emit("a", to_string(number), href: "##{ref}", id: backref, class: "footnote", title: "see footnote") end @doc false def render_code(%Block.Code{lines: lines}) do lines |> Enum.join("\n") end @remove_escapes ~r{ \\ (?! \\ ) }x @doc false def render_image(text, href, title) do alt = text |> escape() |> String.replace(@remove_escapes, "") if title do emit("img", [], src: href, alt: alt, title: title) else emit("img", [], src: href, alt: alt) end end @doc false def render_link(url, text) do emit("a", text, href: _encode(url)) end ############################################## # add attributes to the outer tag in a block # ############################################## @verbatims ~r<%[\da-f]{2}>i defp _encode(url) do url |> String.split(@verbatims, include_captures: true) |> Enum.chunk_every(2) |> Enum.map(&_encode_chunk/1) |> IO.chardata_to_string end defp _encode_chunk([encodable, verbatim]), do: [URI.encode(encodable), verbatim] defp _encode_chunk([encodable]), do: URI.encode(encodable) @doc false def merge_attrs(maybe_atts, new_atts) def merge_attrs(nil, new_atts), do: new_atts def merge_attrs(atts, new) when is_list(atts) do atts |> Enum.into(%{}) |> merge_attrs(new) end def merge_attrs(atts, new) do atts |> Map.merge(new, &_value_merger/3) |> Enum.into([]) |> Enum.map(&attrs_to_string_keys/1) end defp _value_merger(key, val1, val2) defp _value_merger(_, val1, val2) when is_list(val1) do val1 ++ [val2] end defp _value_merger(_, val1, val2) do [val1, val2] end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/attr_parser.ex000066400000000000000000000041421453104267300247060ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.AttrParser do @moduledoc false import EarmarkParser.Helpers.StringHelpers, only: [ behead: 2 ] import EarmarkParser.Message, only: [add_message: 2] @type errorlist :: list(String.t) def parse_attrs(context, attrs, lnb) do { attrs, errors } = _parse_attrs(%{}, attrs, [], lnb) { add_errors(context, errors, lnb), attrs } end defp _parse_attrs(dict, attrs, errors, lnb) do cond do Regex.match?(~r{^\s*$}, attrs) -> {dict, errors} match = Regex.run(~r{^\.(\S+)\s*}, attrs) -> [ leader, class ] = match Map.update(dict, "class", [ class ], &[ class | &1]) |> _parse_attrs(behead(attrs, leader), errors, lnb) match = Regex.run(~r{^\#(\S+)\s*}, attrs) -> [ leader, id ] = match Map.update(dict, "id", [ id ], &[ id | &1]) |> _parse_attrs(behead(attrs, leader), errors, lnb) # Might we being running into escape issues here too? match = Regex.run(~r{^(\S+)=\'([^\']*)'\s*}, attrs) -> #' [ leader, name, value ] = match Map.update(dict, name, [ value ], &[ value | &1]) |> _parse_attrs(behead(attrs, leader), errors, lnb) # Might we being running into escape issues here too? match = Regex.run(~r{^(\S+)=\"([^\"]*)"\s*}, attrs) -> #" [ leader, name, value ] = match Map.update(dict, name, [ value ], &[ value | &1]) |> _parse_attrs(behead(attrs, leader), errors, lnb) match = Regex.run(~r{^(\S+)=(\S+)\s*}, attrs) -> [ leader, name, value ] = match Map.update(dict, name, [ value ], &[ value | &1]) |> _parse_attrs(behead(attrs, leader), errors, lnb) match = Regex.run(~r{^(\S+)\s*(.*)}, attrs) -> [ _, incorrect, rest ] = match _parse_attrs(dict, rest, [ incorrect | errors ], lnb) :otherwise -> {dict, [attrs | errors ]} end end defp add_errors(context, [], _lnb), do: context defp add_errors(context, errors, lnb), do: add_message(context, {:warning, lnb, "Illegal attributes #{inspect errors} ignored in IAL"}) end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/html_parser.ex000066400000000000000000000053501453104267300247020ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.HtmlParser do @moduledoc false import EarmarkParser.Helpers.StringHelpers, only: [behead: 2] import EarmarkParser.LineScanner, only: [void_tag?: 1] def parse_html(lines) def parse_html([tag_line|rest]) do case _parse_tag(tag_line) do { :ok, tag, "" } -> [_parse_rest(rest, tag, [])] { :ok, tag, suffix } -> [_parse_rest(rest, tag, [suffix])] { :ext, tag, "" } -> [_parse_rest(rest, tag, [])] { :ext, tag, suffix } -> [_parse_rest(rest, tag, []), [suffix]] end end # Parse One Tag # ------------- @quoted_attr ~r{\A ([-\w]+) \s* = \s* (["']) (.*?) \2 \s*}x @unquoted_attr ~r{\A ([-\w]+) (?: \s* = \s* ([^&\s>]*))? \s*}x defp _parse_atts(string, tag, atts) do case Regex.run(@quoted_attr, string) do [all, name, _delim, value] -> _parse_atts(behead(string, all), tag, [{name, value}|atts]) _ -> case Regex.run(@unquoted_attr, string) do [all, name, value] -> _parse_atts(behead(string, all), tag, [{name, value}|atts]) [all, name] -> _parse_atts(behead(string, all), tag, [{name, name}|atts]) _ -> _parse_tag_tail(string, tag, atts) end end end # Are leading and trailing "-"s ok? @tag_head ~r{\A \s* <([-\w]+) \s*}x defp _parse_tag(string) do case Regex.run(@tag_head, string) do [all, tag] -> _parse_atts(behead(string, all), tag, []) end end @tag_tail ~r{\A .*? (/?)> \s* (.*) \z}x defp _parse_tag_tail(string, tag, atts) do case Regex.run(@tag_tail, string) do [_, closing, suffix] -> suffix1 = String.replace(suffix, ~r{\s*.*}, "") _close_tag_tail(tag, atts, closing != "", suffix1) end end defp _close_tag_tail(tag, atts, closing?, suffix) do if closing? || void_tag?(tag) do {:ext, {tag, Enum.reverse(atts)}, suffix } else {:ok, {tag, Enum.reverse(atts)}, suffix } end end # Iterate over lines inside a tag # ------------------------------- @verbatim %{verbatim: true} defp _parse_rest(rest, tag_tpl, lines) defp _parse_rest([], tag_tpl, lines) do tag_tpl |> Tuple.append(Enum.reverse(lines)) |> Tuple.append(@verbatim) end defp _parse_rest([last_line], {tag, _}=tag_tpl, lines) do case Regex.run(~r{\A\s*\s*(.*)}, last_line) do nil -> tag_tpl |> Tuple.append(Enum.reverse([last_line|lines])) |> Tuple.append(@verbatim) [_, ""] -> tag_tpl |> Tuple.append(Enum.reverse(lines)) |> Tuple.append(@verbatim) [_, suffix] -> [tag_tpl |> Tuple.append(Enum.reverse(lines)) |> Tuple.append(@verbatim), suffix] end end defp _parse_rest([inner_line|rest], tag_tpl, lines) do _parse_rest(rest, tag_tpl, [inner_line|lines]) end end earmark_parser-1.4.39/lib/earmark_parser/helpers/leex_helpers.ex000066400000000000000000000014001453104267300250310ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.LeexHelpers do @moduledoc false @doc """ Allows to lex an Elixir string with a leex lexer and returns the tokens as needed for a yecc parser. """ def lex text, with: lexer do case text |> String.to_charlist() |> lexer.string() do {:ok, tokens, _} -> tokens end end def tokenize line, with: lexer do {:ok, tokens, _} = line |> to_charlist() |> lexer.string() elixirize_tokens(tokens,[]) |> Enum.reverse() end defp elixirize_tokens(tokens, rest) defp elixirize_tokens([], result), do: result defp elixirize_tokens([{token, _, text}|rest], result), do: elixirize_tokens(rest, [{token,to_string(text)}|result]) end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/line_helpers.ex000066400000000000000000000017101453104267300250270ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.LineHelpers do @moduledoc false alias EarmarkParser.Line def blank?(%Line.Blank{}), do: true def blank?(_), do: false def blockquote_or_text?(%Line.BlockQuote{}), do: true def blockquote_or_text?(struct), do: text?(struct) def indent_or_blank?(%Line.Indent{}), do: true def indent_or_blank?(line), do: blank?(line) # Gruber's tests have # # para text... # * and more para text # # So list markers inside paragraphs are ignored. But he also has # # * line # * line # # And expects it to be a nested list. These seem to be in conflict # # I think the second is a better interpretation, so I commented # out the 2nd match below. def text?(%Line.Text{}), do: true def text?(%Line.TableLine{}), do: true # def text?(%Line.ListItem{}), do: true def text?(_), do: false end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/lookahead_helpers.ex000066400000000000000000000050141453104267300260300ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.LookaheadHelpers do @moduledoc false import EarmarkParser.Helpers.LeexHelpers @doc """ Indicates if the _numbered_line_ passed in leaves an inline code block open. If so returns a tuple whre the first element is the opening sequence of backticks, and the second the linenumber of the _numbered_line_ Otherwise `{nil, 0}` is returned """ def opens_inline_code(%{line: line, lnb: lnb}) do case tokenize(line, with: :earmark_parser_string_lexer) |> has_still_opening_backtix(nil) do nil -> {nil, 0} {_, btx} -> {btx, lnb} end end @doc """ returns false if and only if the line closes a pending inline code *without* opening a new one. The opening backtix are passed in as second parameter. If the function does not return false it returns the (new or original) opening backtix """ # (#{},{_,_}) -> {_,_} def still_inline_code(%{line: line, lnb: lnb}, old = {pending, _pending_lnb}) do case tokenize(line, with: :earmark_parser_string_lexer) |> has_still_opening_backtix({:old, pending}) do nil -> {nil, 0} {:new, btx} -> {btx, lnb} {:old, _} -> old end end # A tokenized line {:verabtim, text} | {:backtix, ['``+]} is analyzed for # if it is closed (-> nil), not closed (-> {:old, btx}) or reopened (-> {:new, btx}) # concerning backtix defp has_still_opening_backtix(tokens, opened_so_far) # Empty, done, but take care of tangeling escape (\) defp has_still_opening_backtix([], :force_outside), do: nil defp has_still_opening_backtix([], open), do: open # Outside state, represented by nil defp has_still_opening_backtix([{:other, _} | rest], nil), do: has_still_opening_backtix(rest, nil) defp has_still_opening_backtix([{:backtix, btx} | rest], nil), do: has_still_opening_backtix(rest, {:new, btx}) defp has_still_opening_backtix([{:escape, _} | rest], nil), do: has_still_opening_backtix(rest, :force_outside) # Next state forced outside, represented by :force_outside defp has_still_opening_backtix([_ | rest], :force_outside), do: has_still_opening_backtix(rest, nil) # Inside state, represented by { :old | :new, btx } defp has_still_opening_backtix([{:backtix, btx} | rest], open = {_, openedbtx}) do if btx == openedbtx do has_still_opening_backtix(rest, nil) else has_still_opening_backtix(rest, open) end end defp has_still_opening_backtix([_ | rest], open = {_, _}), do: has_still_opening_backtix(rest, open) end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/pure_link_helpers.ex000066400000000000000000000047441453104267300261020ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.PureLinkHelpers do @moduledoc false import EarmarkParser.Helpers.AstHelpers, only: [render_link: 2] @pure_link_rgx ~r{ \A (\s*) ( (?:https?://|www\.) [^\s<>]* [^\s<>?!.,:*_~] ) }ux def convert_pure_link(src) do case Regex.run(@pure_link_rgx, src) do [_match, spaces, link_text] -> if String.ends_with?(link_text, ")") do remove_trailing_closing_parens(spaces, link_text) else make_result(spaces, link_text) end _ -> nil end end @split_at_ending_parens ~r{ (.*?) (\)*) \z}x defp remove_trailing_closing_parens(leading_spaces, link_text) do [_, link_text, trailing_parens] = Regex.run(@split_at_ending_parens, link_text) trailing_paren_count = String.length(trailing_parens) # try to balance parens from the rhs unbalanced_count = balance_parens(String.reverse(link_text), trailing_paren_count) balanced_parens = String.slice(trailing_parens, 0, trailing_paren_count - unbalanced_count) make_result(leading_spaces, link_text <> balanced_parens) end defp make_result(leading_spaces, link_text) do link = if String.starts_with?(link_text, "www.") do render_link("http://" <> link_text, link_text) else render_link(link_text, link_text) end if leading_spaces == "" do {link, String.length(link_text)} else {[leading_spaces, link], String.length(leading_spaces) + String.length(link_text)} end end # balance parens and return unbalanced *trailing* paren count defp balance_parens(reverse_text, trailing_count, non_trailing_count \\ 0) defp balance_parens(<<>>, trailing_paren_count, _non_trailing_count), do: trailing_paren_count defp balance_parens(_reverse_text, 0, _non_trailing_count), do: 0 defp balance_parens(")" <> rest, trailing_paren_count, non_trailing_count) do balance_parens(rest, trailing_paren_count, non_trailing_count + 1) end defp balance_parens("(" <> rest, trailing_paren_count, non_trailing_count) do # non-trailing paren must be balanced before trailing paren if non_trailing_count > 0 do balance_parens(rest, trailing_paren_count, non_trailing_count - 1) else balance_parens(rest, trailing_paren_count - 1, non_trailing_count) end end defp balance_parens(<<_::utf8,rest::binary>>, trailing_paren_count, non_trailing_count) do balance_parens(rest, trailing_paren_count, non_trailing_count) end end earmark_parser-1.4.39/lib/earmark_parser/helpers/reparse_helpers.ex000066400000000000000000000013531453104267300255440ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.ReparseHelpers do @moduledoc false alias EarmarkParser.Line @doc """ Extract the verbatim text of `%EarmarkParser.Line.t` elements with less alignment so that it can be reparsed (as elements of footnotes or indented code) """ # Add additional spaces for any indentation past level 1 def properly_indent(%Line.Indent{level: level, content: content}, target_level) when level == target_level do content end def properly_indent(%Line.Indent{level: level, content: content}, target_level) when level > target_level do String.duplicate(" ", level-target_level) <> content end def properly_indent(line, _) do line.content end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/string_helpers.ex000066400000000000000000000005671453104267300254170ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.StringHelpers do @moduledoc false @doc """ Remove the leading part of a string """ def behead(str, ignore) when is_integer(ignore) do {_pre, post} = String.split_at(str, ignore) post end def behead(str, leading_string) do behead(str, String.length(leading_string)) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/helpers/yecc_helpers.ex000066400000000000000000000007461453104267300250330ustar00rootroot00000000000000defmodule EarmarkParser.Helpers.YeccHelpers do @moduledoc false import EarmarkParser.Helpers.LeexHelpers, only: [lex: 2] def parse!( text, lexer: lexer, parser: parser ) do case parse(text, lexer: lexer, parser: parser) do {:ok, ast} -> ast {:error, _} -> nil end end def parse( text, lexer: lexer, parser: parser ) do with tokens <- lex(text, with: lexer) do parser.parse(tokens) end end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/line.ex000066400000000000000000000050251453104267300216460ustar00rootroot00000000000000defmodule EarmarkParser.Line do @moduledoc false defmodule Blank do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, content: "") end defmodule Ruler do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, type: "- or * or _") end defmodule Heading do @moduledoc false defstruct(annotation: nil, ial: nil, lnb: 0, line: "", indent: -1, level: 1, content: "inline text") end defmodule BlockQuote do @moduledoc false defstruct(annotation: nil, ial: nil, lnb: 0, line: "", indent: -1, content: "text") end defmodule Indent do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, level: 0, content: "text") end defmodule Fence do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, delimiter: "~ or `", language: nil) end defmodule HtmlOpenTag do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, tag: "", content: "") end defmodule HtmlCloseTag do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, tag: "<... to eol") end defmodule HtmlComment do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, complete: true) end defmodule HtmlOneLine do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, tag: "", content: "") end defmodule IdDef do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, id: nil, url: nil, title: nil) end defmodule FnDef do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, id: nil, content: "text") end defmodule ListItem do @moduledoc false defstruct( annotation: nil, ial: nil, lnb: 0, type: :ul, line: "", indent: -1, bullet: "* or -", content: "text", initial_indent: 0, list_indent: 0 ) end defmodule SetextUnderlineHeading do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, level: 1) end defmodule TableLine do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, content: "", columns: 0, is_header: false, needs_header: false) end defmodule Ial do @moduledoc false defstruct(annotation: nil, ial: nil, lnb: 0, line: "", indent: -1, attrs: "", verbatim: "") end defmodule Text do @moduledoc false defstruct(annotation: nil, lnb: 0, line: "", indent: -1, content: "") end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/line_scanner.ex000066400000000000000000000227571453104267300233720ustar00rootroot00000000000000defmodule EarmarkParser.LineScanner do @moduledoc false alias EarmarkParser.{Helpers, Line, Options} # This is the re that matches the ridiculous "[id]: url title" syntax @id_title_part ~S""" (?| " (.*) " # in quotes | ' (.*) ' # | \( (.*) \) # in parens ) """ @id_re ~r''' ^\[([^^].*?)\]: # [someid]: \s+ (?| < (\S+) > # url in <>s | (\S+) # or without ) (?: \s+ # optional title #{@id_title_part} )? \s* $ '''x @indent_re ~r''' \A ( (?: \s{4})+ ) (\s*) # 4 or more leading spaces (.*) # the rest '''x @void_tags ~w{area br hr img wbr} @void_tag_rgx ~r''' ^<( #{Enum.join(@void_tags, "|")} ) .*? > '''x @doc false def void_tag?(tag), do: Regex.match?(@void_tag_rgx, "<#{tag}>") def scan_lines(lines, options, recursive) do _lines_with_count(lines, options.line - 1) |> _with_lookahead(options, recursive) end def type_of(line, recursive) when is_boolean(recursive), do: type_of(line, %Options{}, recursive) def type_of({line, lnb}, options = %Options{annotations: annotations}, recursive) when is_binary(line) do {line1, annotation} = line |> Helpers.expand_tabs() |> Helpers.remove_line_ending(annotations) %{_type_of(line1, options, recursive) | annotation: annotation, lnb: lnb} end def type_of({line, lnb}, _, _) do raise ArgumentError, "line number #{lnb} #{inspect line} is not a binary" end defp _type_of(line, options = %Options{}, recursive) do {ial, stripped_line} = Helpers.extract_ial(line) {content, indent} = _count_indent(line, 0) lt_four? = indent < 4 cond do content == "" -> _create_text(line, content, indent) lt_four? && !recursive && Regex.run(~r/\A \z/x, content) -> %Line.HtmlComment{complete: true, indent: indent, line: line} lt_four? && !recursive && Regex.run(~r/\A /u, options, recursive)] other -> [other | _with_lookahead(lines, options, recursive)] end end defp _with_lookahead([], _options, _recursive), do: [] defp _lookahead_until_match([], _, _, _), do: [] defp _lookahead_until_match([{line, lnb} | lines], regex, options, recursive) do if line =~ regex do [type_of({line, lnb}, options, recursive) | _with_lookahead(lines, options, recursive)] else [ %{_create_text(line) | lnb: lnb} | _lookahead_until_match(lines, regex, options, recursive) ] end end @column_rgx ~r{\A[\s|:-]+\z} defp _determine_if_header(columns) do columns |> Enum.all?(fn col -> Regex.run(@column_rgx, col) end) end defp _split_table_columns(line) do line |> String.split(~r{(? Enum.map(&String.trim/1) |> Enum.map(fn col -> Regex.replace(~r{\\\|}, col, "|") end) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/message.ex000066400000000000000000000017321453104267300223440ustar00rootroot00000000000000defmodule EarmarkParser.Message do @moduledoc false alias EarmarkParser.Context alias EarmarkParser.Options @type message_type :: :error | :warning @type t :: {message_type, number, binary} @type ts :: list(t) @type container_type :: Options.t() | Context.t() def add_messages(container, messages), do: Enum.reduce(messages, container, &add_message(&2, &1)) def add_message(container, message) def add_message(options = %Options{}, message) do %{options | messages: MapSet.put(options.messages, message)} end def add_message(context = %Context{}, message) do %{context | options: add_message(context.options, message)} end def get_messages(container) def get_messages(%Context{options: %{messages: messages}}), do: messages @doc """ For final output """ def sort_messages(container) do container |> get_messages() |> Enum.sort(fn {_, l, _}, {_, r, _} -> r >= l end) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/options.ex000066400000000000000000000104051453104267300224100ustar00rootroot00000000000000defmodule EarmarkParser.Options do # What we use to render defstruct renderer: EarmarkParser.HtmlRenderer, # Inline style options all: false, gfm: true, gfm_tables: false, breaks: false, footnotes: false, footnote_offset: 1, wikilinks: false, parse_inline: true, # allow for annotations annotations: nil, # additional prefies for class of code blocks code_class_prefix: nil, # Filename and initial line number of the markdown block passed in # for meaningful error messages file: "", line: 1, # [{:error|:warning, lnb, text},...] messages: MapSet.new([]), pure_links: true, sub_sup: false, math: false, # deprecated pedantic: false, smartypants: false, timeout: nil @type t :: %__MODULE__{ all: boolean(), gfm: boolean(), gfm_tables: boolean(), breaks: boolean(), footnotes: boolean(), footnote_offset: non_neg_integer(), wikilinks: boolean(), parse_inline: boolean(), # allow for annotations annotations: nil | binary(), # additional prefies for class of code blocks code_class_prefix: nil | binary(), # Filename and initial line number of the markdown block passed in # for meaningful error messages file: binary(), line: number(), # [{:error|:warning, lnb, text},...] messages: MapSet.t, pure_links: boolean(), sub_sup: boolean(), # deprecated pedantic: boolean(), smartypants: boolean(), timeout: nil | non_neg_integer() } @doc false def add_deprecations(options, messages) def add_deprecations(%__MODULE__{smartypants: true} = options, messages) do add_deprecations( %{options | smartypants: false}, [ {:deprecated, 0, "The smartypants option has no effect anymore and will be removed in EarmarkParser 1.5"} | messages ] ) end def add_deprecations(%__MODULE__{timeout: timeout} = options, messages) when timeout != nil do add_deprecations( %{options | timeout: nil}, [ {:deprecated, 0, "The timeout option has no effect anymore and will be removed in EarmarkParser 1.5"} | messages ] ) end def add_deprecations(%__MODULE__{pedantic: true} = options, messages) do add_deprecations( %{options | pedantic: false}, [ {:deprecated, 0, "The pedantic option has no effect anymore and will be removed in EarmarkParser 1.5"} | messages ] ) end def add_deprecations(_options, messages), do: messages @doc ~S""" Use normalize before passing it into any API function iex(1)> options = normalize(annotations: "%%") ...(1)> options.annotations ~r{\A(.*)(%%.*)} """ def normalize(options) def normalize(%__MODULE__{} = options) do case options.annotations do %Regex{} -> options nil -> options _ -> %{ options | annotations: Regex.compile!("\\A(.*)(#{Regex.escape(options.annotations)}.*)") } end |> _set_all_if_applicable() |> _deprecate_old_messages() end def normalize(options), do: struct(__MODULE__, options) |> normalize() defp _deprecate_old_messages(opitons) defp _deprecate_old_messages(%__MODULE__{messages: %MapSet{}} = options), do: options defp _deprecate_old_messages(%__MODULE__{} = options) do %{ options | messages: MapSet.new([ {:deprecated, 0, "messages is an internal option that is ignored and will be removed from the API in v1.5"} ]) } end defp _set_all_if_applicable(options) defp _set_all_if_applicable(%{all: true} = options) do %{options | breaks: true, footnotes: true, gfm_tables: true, sub_sup: true, wikilinks: true} end defp _set_all_if_applicable(options), do: options end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/parser.ex000066400000000000000000000467361453104267300222310ustar00rootroot00000000000000defmodule EarmarkParser.Parser do @moduledoc false alias EarmarkParser.{Block, Line, LineScanner, Options} import EarmarkParser.Helpers.{AttrParser, LineHelpers, ReparseHelpers} import EarmarkParser.Helpers.LookaheadHelpers, only: [opens_inline_code: 1, still_inline_code: 2] import EarmarkParser.Message, only: [add_message: 2, add_messages: 2] import EarmarkParser.Parser.FootnoteParser, only: [parse_fn_defs: 3] import EarmarkParser.Parser.ListParser, only: [parse_list: 3] @doc """ Given a markdown document (as either a list of lines or a string containing newlines), return a parse tree and the context necessary to render the tree. The options are a `%EarmarkParser.Options{}` structure. See `as_html!` for more details. """ def parse_markdown(lines, options) def parse_markdown(lines, options = %Options{}) when is_list(lines) do {blocks, links, footnotes, options1} = parse(lines, options, false) context = %EarmarkParser.Context{options: options1, links: links} |> EarmarkParser.Context.update_context() context = put_in(context.footnotes, footnotes) context = put_in(context.options, options1) {blocks, context} end def parse_markdown(lines, options) when is_binary(lines) do lines |> String.split(~r{\r\n?|\n}) |> parse_markdown(options) end def parse_markdown(lines, _) do raise ArgumentError, "#{inspect lines} not a binary, nor a list of binaries" end def parse(text_lines, options = %Options{}, recursive) do ["" | text_lines ++ [""]] |> LineScanner.scan_lines(options, recursive) |> parse_lines(options, recursive) end @doc false # Given a list of `Line.xxx` structs, group them into related blocks. # Then extract any id definitions, and build a map from them. Not # for external consumption. def parse_lines(lines, options, recursive) do {blocks, footnotes, options} = lines |> remove_trailing_blank_lines() |> lines_to_blocks(options, recursive) links = links_from_blocks(blocks) {blocks, links, footnotes, options} end defp lines_to_blocks(lines, options, recursive) do {blocks, footnotes, options1} = _parse(lines, [], options, recursive) {blocks |> assign_attributes_to_blocks([]), footnotes, options1} end defp _parse(input, result, options, recursive) defp _parse([], result, options, _recursive), do: {result, %{}, options} ################### # setext headings # ################### # 1 step defp _parse( [ %Line.Blank{}, %Line.Text{content: heading, lnb: lnb}, %Line.SetextUnderlineHeading{annotation: annotation, level: level} | rest ], result, options, recursive ) do _parse( rest, [%Block.Heading{annotation: annotation, content: heading, level: level, lnb: lnb} | result], options, recursive ) end # 1 step defp _parse( [ %Line.Blank{}, %Line.Text{content: heading, lnb: lnb}, %Line.Ruler{type: "-"} | rest ], result, options, recursive ) do _parse( rest, [%Block.Heading{content: heading, level: 2, lnb: lnb} | result], options, recursive ) end ################# # Other heading # ################# # 1 step defp _parse( [%Line.Heading{content: content, ial: ial, level: level, lnb: lnb} | rest], result, options, recursive ) do {options1, result1} = prepend_ial( options, ial, lnb, [%Block.Heading{content: content, level: level, lnb: lnb} | result] ) _parse(rest, result1, options1, recursive) end ######### # Ruler # ######### # 1 step defp _parse([%Line.Ruler{type: type, lnb: lnb} | rest], result, options, recursive) do _parse(rest, [%Block.Ruler{type: type, lnb: lnb} | result], options, recursive) end ############### # Block Quote # ############### # split and parse defp _parse(lines = [%Line.BlockQuote{lnb: lnb} | _], result, options, recursive) do {quote_lines, rest} = Enum.split_while(lines, &blockquote_or_text?/1) lines = for line <- quote_lines, do: line.content {blocks, _, _, options1} = parse(lines, %{options | line: lnb}, true) _parse(rest, [%Block.BlockQuote{blocks: blocks, lnb: lnb} | result], options1, recursive) end ######### # Table # ######### # read and add verbatim defp _parse( lines = [ %Line.TableLine{columns: cols1, lnb: lnb1, needs_header: false}, %Line.TableLine{columns: cols2} | _rest ], result, options, recursive ) when length(cols1) == length(cols2) do columns = length(cols1) {table, rest} = read_table(lines, columns, []) table1 = %{table | lnb: lnb1} _parse(rest, [table1 | result], options, recursive) end defp _parse( lines = [ %Line.TableLine{columns: cols1, lnb: lnb1, needs_header: true}, %Line.TableLine{columns: cols2, is_header: true} | _rest ], result, options, recursive ) when length(cols1) == length(cols2) do columns = length(cols1) {table, rest} = read_table(lines, columns, []) table1 = %{table | lnb: lnb1} _parse(rest, [table1 | result], options, recursive) end ############# # Paragraph # ############# # split and add verbatim defp _parse(lines = [%Line.TableLine{lnb: lnb} | _], result, options, recursive) do {para_lines, rest} = Enum.split_while(lines, &text?/1) line_text = for line <- para_lines, do: line.line _parse(rest, [%Block.Para{lines: line_text, lnb: lnb + 1} | result], options, recursive) end # read and parse defp _parse(lines = [%Line.Text{lnb: lnb} | _], result, options, recursive) do {reversed_para_lines, rest, pending, annotation} = consolidate_para(lines) options1 = case pending do {nil, _} -> options {pending, lnb1} -> add_message( options, {:warning, lnb1, "Closing unclosed backquotes #{pending} at end of input"} ) end line_text = for line <- reversed_para_lines |> Enum.reverse(), do: line.line if recursive == :list do _parse(rest, [%Block.Text{line: line_text, lnb: lnb} | result], options1, recursive) else _parse( rest, [%Block.Para{annotation: annotation, lines: line_text, lnb: lnb} | result], options1, recursive ) end end defp _parse( [%Line.SetextUnderlineHeading{line: line, lnb: lnb, level: 2} | rest], result, options, recursive ) do _parse([%Line.Text{line: line, lnb: lnb} | rest], result, options, recursive) end ######### # Lists # ######### # We handle lists in two passes. In the first, we build list items, # in the second we combine adjacent items into lists. This is pass one defp _parse([%Line.ListItem{} | _] = input, result, options, recursive) do {with_prepended_lists, rest, options1} = parse_list(input, result, options) _parse([%Line.Blank{lnb: 0} | rest], with_prepended_lists, options1, recursive) end ################# # Indented code # ################# defp _parse(list = [%Line.Indent{lnb: lnb} | _], result, options, recursive) do {code_lines, rest} = Enum.split_while(list, &indent_or_blank?/1) code_lines = remove_trailing_blank_lines(code_lines) code = for line <- code_lines, do: properly_indent(line, 1) _parse([%Line.Blank{}|rest], [%Block.Code{lines: code, lnb: lnb} | result], options, recursive) end ############### # Fenced code # ############### defp _parse( [%Line.Fence{delimiter: delimiter, language: language, lnb: lnb} | rest], result, options, recursive ) do {code_lines, rest} = Enum.split_while(rest, fn line -> !match?(%Line.Fence{delimiter: ^delimiter, language: _}, line) end) {rest1, options1} = _check_closing_fence(rest, lnb, delimiter, options) code = for line <- code_lines, do: line.line _parse( rest1, [%Block.Code{lines: code, language: language, lnb: lnb} | result], options1, recursive ) end ############## # HTML block # ############## defp _parse( [opener = %Line.HtmlOpenTag{annotation: annotation, tag: tag, lnb: lnb} | rest], result, options, recursive ) do {html_lines, rest1, unclosed, annotation} = _html_match_to_closing(opener, rest, annotation) options1 = add_messages( options, unclosed |> Enum.map(fn %{lnb: lnb1, tag: tag} -> {:warning, lnb1, "Failed to find closing <#{tag}>"} end) ) html = Enum.reverse(html_lines) _parse( rest1, [%Block.Html{tag: tag, html: html, lnb: lnb, annotation: annotation} | result], options1, recursive ) end #################### # HTML on one line # #################### defp _parse( [%Line.HtmlOneLine{annotation: annotation, line: line, lnb: lnb} | rest], result, options, recursive ) do _parse( rest, [%Block.HtmlOneline{annotation: annotation, html: [line], lnb: lnb} | result], options, recursive ) end ################ # HTML Comment # ################ defp _parse( [line = %Line.HtmlComment{complete: true, lnb: lnb} | rest], result, options, recursive ) do _parse(rest, [%Block.HtmlComment{lines: [line.line], lnb: lnb} | result], options, recursive) end defp _parse( lines = [%Line.HtmlComment{complete: false, lnb: lnb} | _], result, options, recursive ) do {html_lines, rest} = Enum.split_while(lines, fn line -> !(line.line =~ ~r/-->/) end) {html_lines, rest} = if rest == [] do {html_lines, rest} else {html_lines ++ [hd(rest)], tl(rest)} end html = for line <- html_lines, do: line.line _parse(rest, [%Block.HtmlComment{lines: html, lnb: lnb} | result], options, recursive) end ################# # ID definition # ################# defp _parse([defn = %Line.IdDef{lnb: lnb} | rest], result, options, recursive) do _parse( rest, [%Block.IdDef{id: defn.id, url: defn.url, title: defn.title, lnb: lnb} | result], options, recursive ) end ####################### # Footnote Definition # ####################### # Starting from 1.5.0 Footnote Definitions are always at the end of the document (GFM) meaning that the # `_parse` iteration can now end and we will trigger `_parse_fn_defs` # this has the advantage that we can make the assumption that the top of the `result` # list contains a `Block.FnList` element defp _parse([%Line.FnDef{} | _] = input, result, options, _recursive) do parse_fn_defs(input, result, options) end #################### # IAL (attributes) # #################### defp _parse( [%Line.Ial{attrs: attrs, lnb: lnb, verbatim: verbatim} | rest], result, options, recursive ) do {options1, attributes} = parse_attrs(options, attrs, lnb) _parse( rest, [%Block.Ial{attrs: attributes, content: attrs, lnb: lnb, verbatim: verbatim} | result], options1, recursive ) end ############### # Blank Lines # ############### # We've reached the point where empty lines are no longer significant defp _parse([%Line.Blank{} | rest], result, options, recursive) do _parse(rest, result, options, recursive) end ############################################################## # Anything else... we warn, then treat it as if it were text # ############################################################## defp _parse([anything = %{lnb: lnb} | rest], result, options, recursive) do _parse( [%Line.Text{content: anything.line, lnb: lnb} | rest], result, add_message(options, {:warning, anything.lnb, "Unexpected line #{anything.line}"}), recursive ) end ####################################################### # Assign attributes that follow a block to that block # ####################################################### defp assign_attributes_to_blocks([], result), do: result defp assign_attributes_to_blocks([%Block.Ial{attrs: attrs}, block | rest], result) do assign_attributes_to_blocks(rest, [%{block | attrs: attrs} | result]) end defp assign_attributes_to_blocks([block | rest], result) do assign_attributes_to_blocks(rest, [block | result]) end defp _check_closing_fence(rest, lnb, delimiter, options) defp _check_closing_fence([], lnb, delimiter, options) do {[], add_message(options, {:error, lnb, "Fenced Code Block opened with #{delimiter} not closed at end of input"})} end defp _check_closing_fence([_|rest], _lnb, _delimiter, options) do {rest, options} end ############################################################ # Consolidate multiline inline code blocks into an element # ############################################################ @not_pending {nil, 0} # ([#{},...]) -> {[#{}],[#{}],{'nil' | binary(),number()}} # @spec consolidate_para( ts ) :: { ts, ts, {nil | String.t, number} } defp consolidate_para(lines), do: _consolidate_para(lines, [], @not_pending, nil) defp _consolidate_para([], result, pending, annotation) do {result, [], pending, annotation} end defp _consolidate_para([line | rest] = lines, result, pending, annotation) do case _inline_or_text?(line, pending) do %{pending: still_pending, continue: true} -> _consolidate_para(rest, [line | result], still_pending, annotation || line.annotation) _ -> {result, lines, @not_pending, annotation} end end ################################################## # Read in a table (consecutive TableLines with # the same number of columns) defp read_table(lines, col_count, rows) defp read_table( [%Line.TableLine{columns: cols} | rest], col_count, rows ) when length(cols) == col_count do read_table(rest, col_count, [cols | rows]) end defp read_table(rest, col_count, rows) do rows = Enum.reverse(rows) table = Block.Table.new_for_columns(col_count) table = case look_for_alignments(rows) do nil -> %Block.Table{table | rows: rows} aligns -> %Block.Table{table | alignments: aligns, header: hd(rows), rows: tl(tl(rows))} end {table, [%Line.Blank{lnb: 0} | rest]} end defp look_for_alignments([_first, second | _rest]) do if Enum.all?(second, fn row -> row =~ ~r{^:?-+:?$} end) do second |> Enum.map(fn row -> Regex.replace(~r/-+/, row, "-") end) |> Enum.map(fn row -> case row do ":-:" -> :center ":-" -> :left "-" -> :left "-:" -> :right end end) else nil end end ##################################################### # Traverse the block list and build a list of links # ##################################################### defp links_from_blocks(blocks) do visit(blocks, Map.new(), &link_extractor/2) end defp link_extractor(item = %Block.IdDef{id: id}, result) do Map.put(result, String.downcase(id), item) end defp link_extractor(_, result), do: result ################################## # Visitor pattern for each block # ################################## defp visit([], result, _func), do: result # Structural node BlockQuote -> descend defp visit([item = %Block.BlockQuote{blocks: blocks} | rest], result, func) do result = func.(item, result) result = visit(blocks, result, func) visit(rest, result, func) end # Structural node List -> descend defp visit([item = %Block.List{blocks: blocks} | rest], result, func) do result = func.(item, result) result = visit(blocks, result, func) visit(rest, result, func) end # Structural node ListItem -> descend defp visit([item = %Block.ListItem{blocks: blocks} | rest], result, func) do result = func.(item, result) result = visit(blocks, result, func) visit(rest, result, func) end # Leaf, leaf it alone defp visit([item | rest], result, func) do result = func.(item, result) visit(rest, result, func) end ################################################################### # Consume HTML, taking care of nesting. Assumes one tag per line. # ################################################################### defp _html_match_to_closing(opener, rest, annotation), do: _find_closing_tags([opener], rest, [opener.line], [], annotation) defp _find_closing_tags(needed, input, html_lines, text_lines, annotation) # No more open tags, happy case defp _find_closing_tags([], rest, html_lines, [], annotation), do: {html_lines, rest, [], annotation} # run out of input, unhappy case defp _find_closing_tags(needed, [], html_lines, text_lines, annotation), do: {_add_text_lines(html_lines, text_lines), [], needed, annotation} # still more lines, still needed closing defp _find_closing_tags( needed = [needed_hd | needed_tl], [rest_hd | rest_tl], html_lines, text_lines, annotation ) do cond do _closes_tag?(rest_hd, needed_hd) -> _find_closing_tags( needed_tl, rest_tl, [rest_hd.line | _add_text_lines(html_lines, text_lines)], [], _override_annotation(annotation, rest_hd) ) _opens_tag?(rest_hd) -> _find_closing_tags( [rest_hd | needed], rest_tl, [rest_hd.line | _add_text_lines(html_lines, text_lines)], [], annotation ) true -> _find_closing_tags(needed, rest_tl, html_lines, [rest_hd.line | text_lines], annotation) end end defp _add_text_lines(html_lines, []), do: html_lines defp _add_text_lines(html_lines, text_lines), do: [text_lines |> Enum.reverse() |> Enum.join("\n") | html_lines] ########### # Helpers # ########### defp _closes_tag?(%Line.HtmlCloseTag{tag: ctag}, %Line.HtmlOpenTag{tag: otag}) do ctag == otag end defp _closes_tag?(_, _), do: false defp _opens_tag?(%Line.HtmlOpenTag{}), do: true defp _opens_tag?(_), do: false defp _inline_or_text?(line, pending) defp _inline_or_text?(line = %Line.Text{}, @not_pending) do pending = opens_inline_code(line) %{pending: pending, continue: true} end defp _inline_or_text?(line = %Line.TableLine{}, @not_pending) do pending = opens_inline_code(line) %{pending: pending, continue: true} end defp _inline_or_text?(_line, @not_pending), do: %{pending: @not_pending, continue: false} defp _inline_or_text?(line, pending) do pending = still_inline_code(line, pending) %{pending: pending, continue: true} end defp _override_annotation(annotation, line), do: annotation || line.annotation defp remove_trailing_blank_lines(lines) do lines |> Enum.reverse() |> Enum.drop_while(&blank?/1) |> Enum.reverse() end def prepend_ial(context, maybeatts, lnb, result) def prepend_ial(context, nil, _lnb, result), do: {context, result} def prepend_ial(context, ial, lnb, result) do {context1, attributes} = parse_attrs(context, ial, lnb) {context1, [%Block.Ial{attrs: attributes, content: ial, lnb: lnb, verbatim: ial} | result]} end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/parser/000077500000000000000000000000001453104267300216535ustar00rootroot00000000000000earmark_parser-1.4.39/lib/earmark_parser/parser/footnote_parser.ex000066400000000000000000000035751453104267300254340ustar00rootroot00000000000000defmodule EarmarkParser.Parser.FootnoteParser do alias EarmarkParser.{Block, Enum.Ext, Line} @moduledoc false def parse_fn_defs([fn_def | rest], result, options) do acc = {[fn_def.content], [%Block.FnList{blocks: [_block_fn_def(fn_def)]} | result], %{}, options} rest |> Ext.reduce_with_end(acc, &_parse_fn_def_reduce/2) end defp _parse_fn_def_reduce(ele_or_end, acc) defp _parse_fn_def_reduce({:element, %Line.FnDef{content: content}=fn_def}, acc) do {result1, footnotes, options1} = _complete_fn_def_block(acc, fn_def) {[content], result1, footnotes, options1} end defp _parse_fn_def_reduce({:element, %{line: line}}, acc) do _prepend_to_first_in4(line, acc) end defp _parse_fn_def_reduce(:end, acc) do {[fn_list | rest], footnotes, options} = _complete_fn_def_block(acc) {[%{fn_list | blocks: Enum.reverse(fn_list.blocks)} | rest], footnotes, options} end defp _prepend_to_first_in4(element, {a, b, c, d}) do {[element | a], b, c, d} end defp _block_fn_def(%Line.FnDef{} = fn_def) do %Block.FnDef{id: fn_def.id, lnb: fn_def.lnb} end defp _complete_fn_def_block( {input, [%Block.FnList{blocks: [open_fn | closed_fns]} | rest], footnotes, options}, new_fn_def \\ nil ) do # `_footnotes1` should be empty but let us not change the shape of parse depending # on options or the value of recursive? {inner_blocks, _links, _footnotes1, options1} = EarmarkParser.Parser.parse(Enum.reverse(input), options, true) closed_fn = %{open_fn | blocks: inner_blocks} footnotes1 = Map.put(footnotes, closed_fn.id, closed_fn) fn_blocks = if new_fn_def do [_block_fn_def(new_fn_def), closed_fn | closed_fns] else [closed_fn | closed_fns] end {[%Block.FnList{blocks: fn_blocks} | rest], footnotes1, options1} end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/parser/link_parser.ex000066400000000000000000000111101453104267300245140ustar00rootroot00000000000000defmodule EarmarkParser.Parser.LinkParser do @moduledoc false import EarmarkParser.Helpers.LeexHelpers, only: [tokenize: 2] import EarmarkParser.Helpers.YeccHelpers, only: [parse!: 2] import EarmarkParser.Helpers.StringHelpers, only: [behead: 2] # Hopefully this will go away in v1.3 # ********************************** # # Right now it needs to parse the url part of strings according to the following grammar # # url -> ( inner_url ) # url -> ( inner_url title ) # # inner_url -> ( inner_url ) # inner_url -> [ inner_url ] # inner_url -> url_char* # # url_char -> . - quote - ( - ) - [ - ] # # title -> quote .* quote ;; not LALR-k here # # quote -> " # quote -> ' ;; yep allowing '...." for now # # non_quote -> . - quote @doc false def parse_link(src, lnb) do case parse!(src, lexer: :earmark_parser_link_text_lexer, parser: :earmark_parser_link_text_parser) do {link_or_img, link_text, parsed_text} -> beheaded = behead(src, to_string(parsed_text)) tokens = tokenize(beheaded, with: :earmark_parser_link_text_lexer) p_url(tokens, lnb) |> make_result(to_string(link_text), to_string(parsed_text), link_or_img) _ -> nil end end defp p_url([{:open_paren, _} | ts], lnb), do: url(ts, {[], [], nil}, [:close_paren], lnb) defp p_url(_, _), do: nil # push one level defp url([{:open_paren, text} | ts], result, needed, lnb), do: url(ts, add(result, text), [:close_paren | needed], lnb) # pop last level defp url([{:close_paren, _} | _], result, [:close_paren], _lnb), do: result # pop inner level defp url([{:close_paren, text} | ts], result, [:close_paren | needed], lnb), do: url(ts, add(result, text), needed, lnb) # A quote on level 0 -> bailing out if there is a matching quote defp url(ts_all = [{:open_title, text} | ts], result, [:close_paren], lnb) do case bail_out_to_title(ts_all, result) do nil -> url(ts, add(result, text), [:close_paren], lnb) res -> res end end # All these are just added to the url defp url([{:open_bracket, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) defp url([{:close_bracket, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) defp url([{:any_quote, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) defp url([{:verbatim, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) defp url([{:ws, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) defp url([{:escaped, text} | ts], result, needed, lnb), do: url(ts, add(result, text), needed, lnb) # That is not good, actually this is not a legal url part of a link defp url(_, _, _, _), do: nil defp bail_out_to_title(ts, result) do with remaining_text <- ts |> Enum.map(&text_of_token/1) |> Enum.join("") do case title(remaining_text) do nil -> nil {title_text, inner_title} -> add_title(result, {title_text, inner_title}) end end end defp text_of_token(token) defp text_of_token({:escaped, text}), do: "\\#{text}" defp text_of_token({_, text}) do text end # sic!!! Greedy and not context aware, matching '..." and "...' for backward comp @title_rgx ~r{\A\s+(['"])(.*?)\1(?=\))} defp title(remaining_text) do case Regex.run(@title_rgx, remaining_text) do nil -> nil [parsed, _, inner] -> {parsed, inner} end end @wikilink_rgx ~r{\A\[\[([^\]\|]+)(?:\|([^\]]+))?\]\]\Z} defp make_result(nil, _, parsed_text, :link) do case Regex.run(@wikilink_rgx, parsed_text) do nil -> nil [_, wikilink] -> make_wikilink(parsed_text, wikilink, wikilink) [_, wikilink, link_text] -> make_wikilink(parsed_text, wikilink, link_text) end end defp make_result(nil, _, _, _), do: nil defp make_result({parsed, url, title}, link_text, parsed_text, link_or_img) do {"#{parsed_text}(#{list_to_text(parsed)})", link_text, list_to_text(url), title, link_or_img} end defp add({parsed_text, url_text, nil}, text), do: {[text | parsed_text], [text | url_text], nil} defp add_title({parsed_text, url_text, _}, {parsed, inner}), do: {[parsed | parsed_text], url_text, inner} defp make_wikilink(parsed_text, target, link_text) do {parsed_text, String.trim(link_text), String.trim(target), nil, :wikilink} end defp list_to_text(lst), do: lst |> Enum.reverse() |> Enum.join("") end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/parser/list_info.ex000066400000000000000000000023151453104267300242000ustar00rootroot00000000000000defmodule EarmarkParser.Parser.ListInfo do import EarmarkParser.Helpers.LookaheadHelpers, only: [opens_inline_code: 1, still_inline_code: 2] @moduledoc false @not_pending {nil, 0} defstruct( indent: 0, lines: [], loose?: false, pending: @not_pending, options: %EarmarkParser.Options{}, width: 0 ) def new(%EarmarkParser.Line.ListItem{initial_indent: ii, list_indent: width}=item, options) do pending = opens_inline_code(item) %__MODULE__{indent: ii, lines: [item.content], options: options, pending: pending, width: width} end def update_list_info(list_info, line, pending_line, loose? \\ false) do prepend_line(list_info, line) |> _update_rest(pending_line, loose?) end def prepend_line(%__MODULE__{lines: lines}=list_info, line) do %{list_info|lines: [line|lines]} end defp _update_rest(%{pending: @not_pending}=list_info, line, loose?) do pending = opens_inline_code(line) %{list_info | pending: pending, loose?: loose?} end defp _update_rest(%{pending: pending}=list_info, line, loose?) do pending1 = still_inline_code(line, pending) %{list_info | pending: pending1, loose?: loose?} end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/lib/earmark_parser/parser/list_parser.ex000066400000000000000000000166571453104267300245570ustar00rootroot00000000000000defmodule EarmarkParser.Parser.ListParser do alias EarmarkParser.{Block, Line, Options} alias EarmarkParser.Parser.ListInfo import EarmarkParser.Helpers.StringHelpers, only: [behead: 2] import EarmarkParser.Message, only: [add_message: 2] import ListInfo @moduledoc false @not_pending {nil, 0} def parse_list(lines, result, options \\ %Options{}) do {items, rest, options1} = _parse_list_items_init(lines, [], options) list = _make_list(items, _empty_list(items) ) {[list|result], rest, options1} end defp _parse_list_items_init([item|rest], list_items, options) do options1 = %{options|line: item.lnb} _parse_list_items_start(rest, _make_and_prepend_list_item(item, list_items), new(item, options1)) end defp _parse_list_items_spaced(input, items, list_info) defp _parse_list_items_spaced(input, items, %{pending: @not_pending}=list_info) do _parse_list_items_spaced_np(input, items, list_info) end defp _parse_list_items_spaced(input, items, list_info) do _parse_list_items_spaced_pdg(input, items, list_info) end defp _parse_list_items_spaced_np([%Line.Blank{}|rest], items, list_info) do list_info1 = %{list_info|lines: [""|list_info.lines], options: %{list_info.options|line: list_info.options.line + 1}} _parse_list_items_spaced_np(rest, items, list_info1) end defp _parse_list_items_spaced_np([%Line.Ruler{}|_]=lines, items, list_info) do _finish_list_items(lines, items, false, list_info) end defp _parse_list_items_spaced_np([%Line.ListItem{indent: ii}=item|_]=input, list_items, %{width: w}=list_info) when ii < w do if _starts_list?(item, list_items) do _finish_list_items(input, list_items, false, list_info) else {items1, options1} = _finish_list_item(list_items, false, _loose(list_info)) _parse_list_items_init(input, items1, options1) end end defp _parse_list_items_spaced_np([%Line.Indent{indent: ii}=item|rest], list_items, %{width: w}=list_info) when ii >= w do indented = _behead_spaces(item.line, w) _parse_list_items_spaced(rest, list_items, update_list_info(list_info, indented, item, true)) end defp _parse_list_items_spaced_np([%Line.ListItem{}=line|rest], items, list_info) do indented = _behead_spaces(line.line, list_info.width) _parse_list_items_start(rest, items, update_list_info(list_info, indented, line)) end # BUG: Still do not know how much to indent here??? defp _parse_list_items_spaced_np([%{indent: indent, line: str_line}=line|rest], items, %{width: width}=list_info) when indent >= width do _parse_list_items_spaced(rest, items, update_list_info(list_info, behead(str_line, width), line, true)) end defp _parse_list_items_spaced_np(input, items, list_info) do _finish_list_items(input ,items, false, list_info) end defp _parse_list_items_spaced_pdg(input, items, list_info) defp _parse_list_items_spaced_pdg([], items, %{pending: {pending, lnb}}=list_info) do options1 = add_message(list_info.options, {:warning, lnb, "Closing unclosed backquotes #{pending} at end of input"}) _finish_list_items([], items, false, %{list_info| options: options1}) end defp _parse_list_items_spaced_pdg([line|rest], items, list_info) do indented = _behead_spaces(line.line, list_info.width) _parse_list_items_spaced(rest, items, update_list_info(list_info, indented, line, true)) end defp _parse_list_items_start(input, list_items, list_info) defp _parse_list_items_start(input, list_items, %{pending: @not_pending}=list_info) do _parse_list_items_start_np(input, list_items, list_info) end defp _parse_list_items_start(input, list_items, list_info) do _parse_list_items_start_pdg(input, list_items, list_info) end defp _parse_list_items_start_np(input, list_items, list_info) defp _parse_list_items_start_np([%Line.Blank{}|input], items, list_info) do _parse_list_items_spaced(input, items, prepend_line(list_info, "")) end defp _parse_list_items_start_np([], list_items, list_info) do _finish_list_items([], list_items, true, list_info) end defp _parse_list_items_start_np([%Line.Ruler{}|_]=input, list_items, list_info) do _finish_list_items(input, list_items, true, list_info) end defp _parse_list_items_start_np([%Line.Heading{}|_]=input, list_items, list_info) do _finish_list_items(input, list_items, true, list_info) end defp _parse_list_items_start_np([%Line.ListItem{indent: ii}=item|_]=input, list_items, %{width: w}=list_info) when ii < w do if _starts_list?(item, list_items) do _finish_list_items(input, list_items, true, list_info) else {items1, options1} = _finish_list_item(list_items, true, list_info) _parse_list_items_init(input, items1, options1) end end # Slurp in everything else before a first blank line defp _parse_list_items_start_np([%{line: str_line}=line|rest], items, list_info) do indented = _behead_spaces(str_line, list_info.width) _parse_list_items_start(rest, items, update_list_info(list_info, indented, line)) end defp _parse_list_items_start_pdg(input, items, list_info) defp _parse_list_items_start_pdg([], items, list_info) do _finish_list_items([], items, true, list_info) end defp _parse_list_items_start_pdg([%{line: str_line}=line|rest], items, list_info) do indented = _behead_spaces(str_line, list_info.width) _parse_list_items_start(rest, items, update_list_info(list_info, indented, line)) end defp _behead_spaces(str, len) do Regex.replace(~r/\A\s{1,#{len}}/, str, "") end # INLINE CANDIDATE defp _empty_list([%Block.ListItem{loose?: loose?, type: type}|_]) do %Block.List{loose?: loose?, type: type} end @start_number_rgx ~r{\A0*(\d+)\.} defp _extract_start(%{bullet: bullet}) do case Regex.run(@start_number_rgx, bullet) do nil -> "" [_, "1"] -> "" [_, start] -> ~s{ start="#{start}"} end end defp _finish_list_item([%Block.ListItem{}=item|items], _at_start?, list_info) do {blocks, _, _, options1} = list_info.lines |> Enum.reverse |> EarmarkParser.Parser.parse(%{list_info.options|line: item.lnb}, :list) loose1? = _already_loose?(items) || list_info.loose? {[%{item | blocks: blocks, loose?: loose1?}|items], options1} end defp _finish_list_items(input, items, at_start?, list_info) do {items1, options1} = _finish_list_item(items, at_start?, list_info) {items1, input, options1} end defp _make_and_prepend_list_item(%Line.ListItem{bullet: bullet, lnb: lnb, type: type}, list_items) do [%Block.ListItem{bullet: bullet, lnb: lnb, spaced?: false, type: type}|list_items] end defp _make_list(items, list) defp _make_list([%Block.ListItem{bullet: bullet, lnb: lnb}=item], %Block.List{loose?: loose?}=list) do %{list | blocks: [%{item | loose?: loose?}|list.blocks], bullet: bullet, lnb: lnb, start: _extract_start(item)} end defp _make_list([%Block.ListItem{}=item|rest], %Block.List{loose?: loose?}=list) do _make_list(rest, %{list | blocks: [%{item | loose?: loose?}|list.blocks]}) end defp _already_loose?(items) defp _already_loose?([]), do: false defp _already_loose?([%{loose?: loose?}|_]), do: loose? defp _loose(list_info), do: %{list_info|loose?: true} defp _starts_list?(%{bullet: bullet1}, [%Block.ListItem{bullet: bullet2}|_]) do String.last(bullet1) != String.last(bullet2) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/mix.exs000066400000000000000000000043361453104267300161370ustar00rootroot00000000000000defmodule EarmarkParser.MixProject do use Mix.Project @version "1.4.39" @url "https://github.com/RobertDober/earmark_parser" @deps [ {:dialyxir, "~> 1.4.1", only: [:dev]}, {:earmark_ast_dsl, "~> 0.3.7", only: [:test]}, {:excoveralls, "~> 0.14.4", only: [:test]}, {:extractly, "~> 0.5.3", only: [:dev]}, {:floki, "~> 0.32", only: [:dev, :test]} ] def project do [ app: :earmark_parser, version: @version, compilers: [:leex, :yecc] ++ Mix.compilers(), elixir: "~> 1.11", elixirc_paths: elixirc_paths(Mix.env()), deps: @deps, description: "AST parser and generator for Markdown", package: package(), preferred_cli_env: [ coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test ], test_coverage: [tool: ExCoveralls], aliases: [docs: &build_docs/1] ] end defp package do [ files: [ "lib", "src/*.xrl", "src/*.yrl", "mix.exs", "README.md", "RELEASE.md", "LICENSE" ], maintainers: [ "Robert Dober " ], licenses: [ "Apache-2.0" ], links: %{ "Changelog" => "#{@url}/blob/master/RELEASE.md", "GitHub" => @url } ] end defp elixirc_paths(:test), do: ["lib", "test/support", "dev"] defp elixirc_paths(:dev), do: ["lib", "bench", "dev"] defp elixirc_paths(_), do: ["lib"] @module "EarmarkParser" defp build_docs(_) do Mix.Task.run("compile") ex_doc = Path.join(Mix.path_for(:escripts), "ex_doc") Mix.shell().info("Using escript: #{ex_doc} to build the docs") unless File.exists?(ex_doc) do raise "cannot build docs because escript for ex_doc is not installed, " <> "make sure to run `mix escript.install hex ex_doc` before" end args = [@module, @version, Mix.Project.compile_path()] opts = ~w[--main #{@module} --source-ref v#{@version} --source-url #{@url}] Mix.shell().info("Running: #{ex_doc} #{inspect(args ++ opts)}") System.cmd(ex_doc, args ++ opts) Mix.shell().info("Docs built successfully") end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/mix.lock000066400000000000000000000105531453104267300162660ustar00rootroot00000000000000%{ "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, "earmark_ast_dsl": {:hex, :earmark_ast_dsl, "0.3.7", "5b8cacc7169c9404237a98e1232fdfd8e1ea6e54c8cc2a0497b1fd52f9c6b7c3", [:mix], [], "hexpm", "12d26fa74797e2facfc53f1660efe67218e3c5c87cd7de7a5f377c26091020b1"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, "extractly": {:hex, :extractly, "0.5.3", "ca6afc3430e63cc9017d50eb03bdf1a3499c7e888037d2279b0ec0ea393af390", [:mix], [], "hexpm", "858f7a285ffb767937e75bb8866f03fecb3b7e5b42728a9677d2ebb6ea885503"}, "floki": {:hex, :floki, "0.32.0", "f915dc15258bc997d49be1f5ef7d3992f8834d6f5695270acad17b41f5bcc8e2", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "1c5a91cae1fd8931c26a4826b5e2372c284813904c8bacb468b5de39c7ececbd"}, "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "traverse": {:hex, :traverse, "1.0.1", "5e9b04428e1a82a7de8857ffeecdc37afc94cd702140690c4de2a5e68cbc483d", [:mix], [], "hexpm", "0cf9ec9a974caf36d2d70476af88f6fda469c5d1c08e6341c5cf517b02e01c71"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } earmark_parser-1.4.39/src/000077500000000000000000000000001453104267300154025ustar00rootroot00000000000000earmark_parser-1.4.39/src/earmark_parser_link_text_lexer.xrl000066400000000000000000000021071453104267300244070ustar00rootroot00000000000000Definitions. ESCAPED = \\. ESCAPE = \\ EXCLAMATION_MARK = [!] OPEN_PAREN = \( CLOSE_PAREN = \) OPEN_BRACKET = \[ CLOSE_BRACKET = \] OPEN_TITLE = \s+['"] ANY_QUOTE = ['"] WS = \s+ ANY = [^]\\"'()[\s]+ Rules. {ESCAPED} : {token, {escaped, TokenLine, dismiss_backslash(TokenChars)}}. {EXCLAMATION_MARK} : {token, {exclamation_mark, TokenLine, TokenChars}}. {OPEN_PAREN} : {token, {open_paren, TokenLine, TokenChars}}. {CLOSE_PAREN} : {token, {close_paren, TokenLine, TokenChars}}. {OPEN_BRACKET} : {token, {open_bracket, TokenLine, TokenChars}}. {CLOSE_BRACKET} : {token, {close_bracket, TokenLine, TokenChars}}. {OPEN_TITLE} : {token, {open_title, TokenLine, TokenChars}}. {ANY_QUOTE} : {token, {any_quote, TokenLine, TokenChars}}. {ESCAPE} : {token, {verbatim, TokenLine, TokenChars}}. {WS} : {token, {ws, TokenLine, TokenChars}}. {ANY} : {token, {verbatim, TokenLine, TokenChars}}. Erlang code. dismiss_backslash([$\\|Chars]) -> Chars. earmark_parser-1.4.39/src/earmark_parser_link_text_parser.yrl000066400000000000000000000047511453104267300245740ustar00rootroot00000000000000Nonterminals link_or_image link rest inside_brackets inside_brackets_part anything. Terminals any_quote open_bracket open_title close_bracket open_paren close_paren verbatim escaped exclamation_mark ws. Rootsymbol link_or_image. link_or_image -> exclamation_mark link : make_image_tuple('$2'). link_or_image -> link : '$1'. link -> open_bracket close_bracket : {link, "", "[]"}. link -> open_bracket close_bracket rest : {link, "", "[]"}. link -> open_bracket inside_brackets close_bracket : title_tuple('$2'). link -> open_bracket inside_brackets close_bracket rest : title_tuple('$2'). inside_brackets -> inside_brackets_part : '$1'. inside_brackets -> inside_brackets_part inside_brackets : concat_tuple('$1', '$2'). inside_brackets_part -> exclamation_mark : extract_token('$1'). inside_brackets_part -> verbatim : extract_token('$1'). inside_brackets_part -> ws : extract_token('$1'). inside_brackets_part -> open_title : extract_token('$1'). inside_brackets_part -> open_paren : {"(", "("}. inside_brackets_part -> close_paren : {")", ")"}. inside_brackets_part -> any_quote : extract_token('$1'). inside_brackets_part -> escaped : escaped_token('$1'). inside_brackets_part -> open_bracket close_bracket : {"[]", "[]"}. inside_brackets_part -> open_bracket inside_brackets close_bracket : concat_3t("[", '$2', "]"). rest -> anything. rest -> anything rest. anything -> exclamation_mark. anything -> ws. anything -> verbatim. anything -> open_paren. anything -> close_paren. anything -> open_bracket. anything -> close_bracket. anything -> any_quote. anything -> escaped. anything -> open_title. Erlang code. concat_tuple({LT, LP}, {RT, RP}) -> {string:concat(LT, RT), string:concat(LP, RP)}. concat_3t(L, {MT, MP}, R) -> {string:join([L, MT, R], ""), string:join([ L, MP, R ], "")}. escaped_token({_Token, _Line, Value}) -> {string:concat("\\", Value), string:concat("\\", Value)}. extract_token({_Token, _Line, Value}) -> {Value, Value}. make_image_tuple({_Link, L, R}) -> {image, L, string:concat("!", R)}. title_tuple({Title, Parsed}) -> {link, Title, string:join(["[", Parsed, "]"], "")}. %% SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/src/earmark_parser_string_lexer.xrl000066400000000000000000000003771453104267300237230ustar00rootroot00000000000000Definitions. OTHER = [^`\\]+ ESCAPE = \\ BACKTIX = `+ Rules. {OTHER} : {token, {other, TokenLine, TokenChars}}. {ESCAPE} : {token, {escape, TokenLine, TokenChars}}. {BACKTIX} : {token, {backtix, TokenLine, TokenChars}}. Erlang code. earmark_parser-1.4.39/test/000077500000000000000000000000001453104267300155725ustar00rootroot00000000000000earmark_parser-1.4.39/test/acceptance/000077500000000000000000000000001453104267300176605ustar00rootroot00000000000000earmark_parser-1.4.39/test/acceptance/ast/000077500000000000000000000000001453104267300204475ustar00rootroot00000000000000earmark_parser-1.4.39/test/acceptance/ast/atx_headers_test.exs000066400000000000000000000060301453104267300245150ustar00rootroot00000000000000defmodule Acceptance.Ast.AtxHeadersTest do use ExUnit.Case, async: true import Support.Helpers, only: [as_ast: 1] import EarmarkAstDsl describe "ATX headers" do test "from one to six" do markdown = "# foo\n## foo\n### foo\n#### foo\n##### foo\n###### foo\n" ast = (1..6) |> Enum.map(&tag("h#{&1}", "foo")) messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "from one to six with closing" do markdown = "# foo #\n## foo ##\n### foo ###\n#### foo #####\n##### foo #####\n###### foo ######\n" ast = (1..6) |> Enum.map(&tag("h#{&1}", "foo")) messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "from one to six with closing and trailing ws" do markdown = "# foo # \n## foo ## \n### foo ### \n#### foo ##### \n##### foo ##### \n###### foo ###### \n" ast = (1..6) |> Enum.map(&tag("h#{&1}", "foo")) messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "seven? kidding, right?" do markdown = "####### foo\n" ast = p("####### foo") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "sticky (better than to have no glue)" do markdown = "#5 bolt\n\n#foobar\n" ast = [ p("#5 bolt"), p("#foobar") ] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "close escape" do markdown = "\\## foo\n" ast = p("## foo") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "position is so important" do markdown = "# foo *bar* \\*baz\\*\n" ast = tag("h1", ["foo ", tag("em", "bar"), " *baz*"]) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "spacy" do markdown = "# foo \n" ast = tag("h1", "foo") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "code comes first" do markdown = " # foo\nnext" ast = [pre_code("# foo"), p("next")] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "some prefer to close their headers" do markdown = "# foo#\n" ast = tag("h1", "foo") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "closing headers can get creative" do markdown = "### foo######\n" ast = tag("h3", "foo") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "hash can still be used in a header" do markdown = "# C# #\n" ast = tag("h1", "C#") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "closing header with hash" do markdown = "# C# (language)#\n" ast = tag("h1", "C# (language)") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/test/acceptance/ast/block_ial_test.exs000066400000000000000000000047651453104267300241620ustar00rootroot00000000000000defmodule Acceptance.Ast.BlockIalTest do use ExUnit.Case, async: true import Support.Helpers, only: [as_ast: 1] import EarmarkAstDsl describe "IAL" do test "Not associated" do markdown = "{:hello=world}" ast = p("{:hello=world}") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "Not associated means verbatim" do markdown = "{: hello=world }" ast = p("{: hello=world }") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "Not associated and incorrect" do markdown = "{:hello}" ast = tag("p", "{:hello}") messages = [{:warning, 1, "Illegal attributes [\"hello\"] ignored in IAL" }] assert as_ast(markdown) == {:error, [ast], messages} end test "Associated" do markdown = "Before\n{:hello=world}" ast = tag("p", "Before", hello: "world") messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "Associated in between" do markdown = "Before\n{:hello=world}\nAfter" ast = [p("Before", hello: "world"), p("After")] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "Associated and incorrect" do markdown = "Before\n{:hello}" ast = tag("p", "Before") messages = [{:warning, 2, "Illegal attributes [\"hello\"] ignored in IAL" }] assert as_ast(markdown) == {:error, [ast], messages} end test "Associated and partly incorrect" do markdown = "Before\n{:hello title=world}" ast = p("Before", title: "world") messages = [{:warning, 2, "Illegal attributes [\"hello\"] ignored in IAL" }] assert as_ast(markdown) == {:error, [ast], messages} end test "Associated and partly incorrect and shortcuts" do markdown = "Before\n{:#hello .alpha hello title=world .beta class=\"gamma\" title='class'}" ast = p("Before", class: "gamma beta alpha", id: "hello", title: "class world") messages = [{:warning, 2, "Illegal attributes [\"hello\"] ignored in IAL" }] assert as_ast(markdown) == {:error, [ast], messages} end # https://github.com/pragdave/earmark/issues/367 @tag :wip test "In tight lists?" do markdown = "- Before\n{:.alpha .beta}" ast = tag("ul", tag("li", "Before", class: "beta alpha")) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/test/acceptance/ast/block_quotes_test.exs000066400000000000000000000076031453104267300247270ustar00rootroot00000000000000defmodule Acceptance.Ast.BlockQuotesTest do use ExUnit.Case, async: true import Support.Helpers, only: [as_ast: 1, as_ast: 2] import EarmarkAstDsl @moduletag :ast describe "with breaks: true" do test "acceptance test 490 with breaks" do markdown = "> bar\nbaz\n> foo\n" ast = bq(p(brlist(~w[bar baz foo]))) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "acceptance test 580 with breaks" do markdown = "1. foo\nbar" ast = tag("ol", [tag("li", brlist(~w[foo bar]))]) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "acceptance test 581 with breaks" do markdown = "* a\n b\nc" ast = tag("ul", tag("li", brlist(~w[a b c]))) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "acceptance test 582 with breaks" do markdown = "* x\n a\n| A | B |" ast = tag("ul", tag("li", brlist(["x", "a", "| A | B |"]))) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "acceptance test 583 with breaks" do markdown = "* x\n | A | B |" ast = tag("ul", tag("li", brlist(["x", "| A | B |"]))) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "acceptance test 630 with breaks" do markdown = "\\*not emphasized\\*\n\\[not a link](/foo)\n\\`not code\\`\n1\\. not a list\n\\* not a list\n\\# not a header\n\\[foo]: /url \"not a reference\"\n" ast = p(brlist([ "*not emphasized*", "[not a link](/foo)", "`not code`", "1. not a list", "* not a list", "# not a header", "[foo]: /url \"not a reference\""])) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end test "more" do markdown = "> # Foo\n> bar\n> baz\n" ast = bq([tag("h1", "Foo"), p(brlist(~w[bar baz]))]) messages = [] assert as_ast(markdown, breaks: true) == {:ok, [ast], messages} end end describe "Block Quotes" do test "quote my block" do markdown = "> Foo" ast = bq(p("Foo")) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "and block my quotes" do markdown = "> # Foo\n> bar\n> baz\n" ast = bq([tag("h1", "Foo"), p("bar\nbaz")]) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "linient we are" do markdown = "> bar\nbaz\n> foo\n" ast = bq(p("bar\nbaz\nfoo")) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "lists in blockquotes? Coming up Sir" do markdown = "> - foo\n- bar\n" ast = [bq(tag("ul", tag("li", "foo"))), tag("ul", tag("li", "bar"))] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end test "indented case" do markdown = " > - foo\n- bar\n" ast = [bq(tag("ul", tag("li", "foo"))), tag("ul", tag("li", "bar"))] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end end describe "Nested blockquotes" do test "just two blocks" do markdown = ">>foo\n> bar\n" ast = bq(bq(p("foo\nbar"))) messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "attached list" do markdown = " >- foo\n- bar\n" ast = [bq(tag("ul", tag("li", "foo"))), tag("ul", tag("li", "bar"))] messages = [] assert as_ast(markdown) == {:ok, ast, messages} end end defp bq(content, atts \\ []) do tag("blockquote", content, atts) end defp br, do: void_tag("br") defp brlist(elements) do elements |> Enum.intersperse(br()) end end # SPDX-License-Identifier: Apache-2.0 earmark_parser-1.4.39/test/acceptance/ast/comment_test.exs000066400000000000000000000026271453104267300237000ustar00rootroot00000000000000defmodule Acceptance.Ast.CommentTest do use ExUnit.Case, async: true import Support.Helpers, only: [as_ast: 1] describe "HTML Comments" do test "one line" do markdown = "" ast = {:comment, [], [" Hello "], %{comment: true}} messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "more lines" do markdown = "" ast = {:comment, [], [" Hello", " World"], %{comment: true}} messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "what about the closing" do markdown = "garbish" ast = {:comment, [], [" Hello", " World "], %{comment: true}} messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "what about the fenced closing" do markdown = """ garbish """ ast = {:comment, [], [" Hello", "```elixir", " World "], %{comment: true}} messages = [] assert as_ast(markdown) == {:ok, [ast], messages} end test "fence at the end" do markdown = """ ", %Line.HtmlComment{complete: true}, nil}, {"", %Line.HtmlComment{complete: true}, "does it make sense?"}, {"