├── .github ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE.md ├── workflows │ └── ci.yml └── CONTRIBUTING.md ├── .gitignore ├── .editorconfig ├── LICENSE └── tests └── check.exs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Correia-jpv -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | _build 3 | deps 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | charset = utf-8 12 | 13 | [{!*.md,!*.markdown}] 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Awesome CI 7 | 8 | on: 9 | push: 10 | branches: [ "master" ] 11 | pull_request: 12 | branches: [ "master" ] 13 | 14 | permissions: 15 | contents: read 16 | 17 | jobs: 18 | check-readme: 19 | name: Check README.md 20 | runs-on: ubuntu-22.04 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Set up Elixir 26 | uses: erlef/setup-beam@v1 27 | with: 28 | elixir-version: '1.14.2' # Define the elixir version [required] 29 | otp-version: '25.3' # Define the OTP version [required] 30 | 31 | - name: Run checks 32 | run: elixir tests/check.exs 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Julius Beckmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Awesome Elixir 2 | 3 | :+1: First off, thanks for taking the time to contribute! :+1: 4 | 5 | The following is a set of guidelines for contributing to Awesome Elixir. 6 | These are just guidelines, not rules, use your best judgment and feel free to propose changes to this document in a pull request. 7 | 8 | ## How Can I Contribute? 9 | 10 | ### Submitting Pull Requests 11 | 12 | Do you know any cool Elixir project that isn't listed here? Please submit a pull request, we will be happy to receive it. 13 | 14 | Feel free to take any open issue, just make sure that you follow the contribution guidelines. 15 | 16 | ### Improving the tests 17 | 18 | You are an awesome Elixir developer and think that we can improve our tests? Nice! Feel free to send your contributions. 19 | 20 | ## Contribution Guidelines 21 | 22 | * Create a pull request, not an issue. 23 | * Please search previous suggestions before making a new one, as yours may be a duplicate. 24 | * Libraries that are compatible with latest Elixir, hex-installable, tested and documented are preferred. 25 | * For packages use a link to the source repository like GitHub or Bitbucket. 26 | * Please make an individual pull request for each suggestion. 27 | * Prefix duplicate library names with their vendor or namespace followed by a space. 28 | * Keep descriptions short and simple. 29 | * End all descriptions with a full stop/period. 30 | * Check your spelling and grammar. 31 | * New categories, or improvements to the existing categorisation are welcome. 32 | * Category names have to be alphabetically sorted from a-z. 33 | * Entry names inside Categories have to be alphabetically sorted from a-z. 34 | * Use [EditorConfig](http://editorconfig.org) to ensure consistent line-endings, tabs/spaces, etc. or make sure manually that your Editor is configured 'the right way' (see `.editorconfig` for details). 35 | 36 | Thank you for your contribution and suggestions! 37 | -------------------------------------------------------------------------------- /tests/check.exs: -------------------------------------------------------------------------------- 1 | Mix.install([ 2 | {:earmark, "~> 1.2.2"} 3 | ]) 4 | 5 | # This script will check if the readme markdown file contains valid formatting. 6 | 7 | require Logger 8 | 9 | defmodule Awesome.Order do 10 | def check_string_list_in_order([first, second | tail]) do 11 | case String.downcase(first) < String.downcase(second) do 12 | false -> throw("Words not in order #{inspect(first)} and #{inspect(second)}") 13 | true -> check_string_list_in_order([second] ++ tail) 14 | end 15 | end 16 | 17 | def check_string_list_in_order([_one]) do 18 | true 19 | end 20 | 21 | def check_string_list_in_order([]) do 22 | true 23 | end 24 | end 25 | 26 | defmodule Awesome do 27 | import Awesome.Order 28 | 29 | # This is how a line has to look like. 30 | @line_regex ~r/^\[([^]]+)\]\(([^)]+)\) - (.+)([\.\!]+)$/ 31 | 32 | defp parse_line(line) do 33 | case Regex.run(@line_regex, line) do 34 | nil -> raise("Line does not match format: '#{line}' Is there a dot at the end?") 35 | [^line, name, link, description, _dot] -> [name, link, description] 36 | end 37 | end 38 | 39 | defp debug(message) do 40 | IO.puts("[debug] #{message}") 41 | end 42 | 43 | # ----- 44 | 45 | defp grab_link(line) do 46 | Regex.run(~r/https?:\/\/[^)]+\)/, line) 47 | |> Enum.map(fn x -> String.trim_trailing(x, ")") end) 48 | end 49 | 50 | def uniq_links(lines) do 51 | uniq_links(lines, %{}) 52 | end 53 | 54 | defp uniq_links([head | tail], linkcount) do 55 | link = 56 | case grab_link(head) do 57 | nil -> uniq_links(tail, linkcount) 58 | [h | _t] -> h 59 | end 60 | 61 | cnt = 62 | case Map.fetch(linkcount, link) do 63 | :error -> 64 | 1 65 | 66 | {:ok, c} -> 67 | IO.puts("Duplicate link: #{link}") 68 | c + 1 69 | end 70 | 71 | uniq_links(tail, Map.put(linkcount, link, cnt)) 72 | end 73 | 74 | defp uniq_links([], linkcount) do 75 | case Enum.any?(Map.values(linkcount), fn x -> x > 1 end) do 76 | true -> throw("Duplicate links found") 77 | false -> "" 78 | end 79 | end 80 | 81 | # ----- 82 | 83 | # Entry point 84 | def test_file(file) do 85 | lines = File.read!(file) 86 | debug("Using Earmark to parse to data structure we can work with.") 87 | {blocks, _links, _options} = Earmark.Parser.parse(String.split(lines, ~r{\r\n?|\n})) 88 | 89 | debug("Ensure that there is a header at first.") 90 | [%Earmark.Block.Heading{} | blocks] = blocks 91 | 92 | debug("Ensure that there is a introduction.") 93 | [_introduction | blocks] = blocks 94 | 95 | debug("Ensure that there is a +1 hint paragraph.") 96 | [_plusone | blocks] = blocks 97 | 98 | debug("Ensure that there is info about other curated lists of packages.") 99 | [_other_curated_lists | blocks] = blocks 100 | 101 | debug("Ensure that there is a table of content list.") 102 | [%Earmark.Block.List{blocks: tableOfContent} | blocksList] = blocks 103 | 104 | debug("Parse table of content to list of categories.") 105 | 106 | [%Earmark.Block.ListItem{blocks: [%Earmark.Block.Para{} | categories]} | tableOfContent] = 107 | tableOfContent 108 | 109 | [%Earmark.Block.List{blocks: categories}] = categories 110 | 111 | categories = 112 | for %Earmark.Block.ListItem{blocks: [%Earmark.Block.Para{lines: [name]}]} <- categories do 113 | {title, _link} = parse_markdown_link(name) 114 | title 115 | end 116 | 117 | # IO.inspect categories 118 | 119 | debug("Parse table of content to list of resources.") 120 | 121 | [%Earmark.Block.ListItem{blocks: [%Earmark.Block.Para{} | resources]} | _tableOfContent] = 122 | tableOfContent 123 | 124 | [%Earmark.Block.List{blocks: resources}] = resources 125 | 126 | resources = 127 | for %Earmark.Block.ListItem{blocks: [%Earmark.Block.Para{lines: [name]}]} <- resources do 128 | {title, _link} = parse_markdown_link(name) 129 | title 130 | end 131 | 132 | # IO.inspect resources 133 | 134 | IO.puts("--------- START") 135 | # Parse the main content 136 | iterate_content(blocksList) 137 | 138 | debug("Collect all headings.") 139 | headings = collect_headings(blocksList, [], []) 140 | # IO.inspect headings 141 | 142 | debug("Ensure headings are in alphabetic order.") 143 | for list <- headings, do: check_string_list_in_order(list) 144 | debug("Ensure headings are equal to the once in the tableOfContent.") 145 | [^categories, ^resources] = headings 146 | 147 | debug("Ensure entries are in alphabetic order.") 148 | 149 | for block <- blocksList do 150 | sorted_entries(block) 151 | end 152 | 153 | debug("Ensure links are unique.") 154 | 155 | String.split(lines, ~r{\r\n?|\n}) 156 | |> Enum.filter(fn x -> String.starts_with?(x, "* [") end) 157 | |> uniq_links 158 | end 159 | 160 | def parse_markdown_link(string) do 161 | [^string, title, link] = Regex.run(~r/\[(.+)\]\((.+)\)/, string) 162 | {title, link} 163 | end 164 | 165 | # ----- 166 | 167 | def sorted_entries(%Earmark.Block.List{blocks: entriesList}) do 168 | # Filter down to single lines 169 | entries = 170 | Enum.map(entriesList, fn %Earmark.Block.ListItem{ 171 | blocks: [%Earmark.Block.Para{lines: [line]}] 172 | } -> 173 | line 174 | end) 175 | 176 | names = 177 | Enum.map(entries, fn line -> 178 | line = line |> String.trim("~") 179 | [name | _rest] = parse_line(line) 180 | name 181 | end) 182 | 183 | check_string_list_in_order(names) 184 | end 185 | 186 | def sorted_entries(_) do 187 | end 188 | 189 | # ----- 190 | 191 | def collect_headings( 192 | [%Earmark.Block.Heading{content: heading, level: 2} | tail], 193 | found_headings, 194 | all_headings 195 | ) do 196 | collect_headings(tail, found_headings ++ [heading], all_headings) 197 | end 198 | 199 | def collect_headings( 200 | [%Earmark.Block.Heading{content: _heading, level: 1} | tail], 201 | found_headings, 202 | all_headings 203 | ) do 204 | check_string_list_in_order(found_headings) 205 | collect_headings(tail, [], all_headings ++ [found_headings]) 206 | end 207 | 208 | def collect_headings([_head | tail], found_headings, all_headings) do 209 | collect_headings(tail, found_headings, all_headings) 210 | end 211 | 212 | def collect_headings([], _found_headings, all_headings) do 213 | all_headings 214 | end 215 | 216 | # ----- 217 | 218 | def iterate_content([]) do 219 | IO.puts("--------- END") 220 | end 221 | 222 | # Find a level 2 headline, followed by a paragraph and the list of links. 223 | def iterate_content([ 224 | %Earmark.Block.Heading{content: heading, level: 2}, 225 | %Earmark.Block.Para{lines: _lines}, 226 | %Earmark.Block.List{blocks: blocks, type: :ul} | tail 227 | ]) do 228 | IO.puts("-- #{heading}") 229 | check_list(blocks) 230 | iterate_content(tail) 231 | end 232 | 233 | # Find a level 1 headline followed by a paragraph. 234 | def iterate_content([ 235 | %Earmark.Block.Heading{content: heading, level: 1}, 236 | %Earmark.Block.Para{lines: _lines} | tail 237 | ]) do 238 | IO.puts("- #{heading}") 239 | iterate_content(tail) 240 | end 241 | 242 | # Error handler, when not matching. 243 | def iterate_content([head | tail]) do 244 | IO.puts("\n\tERROR: Could not match on:") 245 | IO.inspect(head) 246 | iterate_content(tail) 247 | end 248 | 249 | # Iterate through all 250 | def check_list(list) do 251 | for listItem <- list, do: validate_list_item(listItem) 252 | end 253 | 254 | # Validate that the link as listitem is valid formatted. 255 | def validate_list_item(%Earmark.Block.ListItem{ 256 | blocks: [%Earmark.Block.Para{lines: [line]}], 257 | type: :ul 258 | }) do 259 | line = 260 | case String.starts_with?(line, "~~") and String.ends_with?(line, "~~") do 261 | true -> 262 | line |> String.trim_trailing() |> String.trim("~") 263 | 264 | false -> 265 | String.trim_trailing(line) 266 | end 267 | 268 | [name, link, description | _rest] = parse_line(line) 269 | IO.puts("\t'#{name}' #{link} '#{description}'") 270 | end 271 | end 272 | 273 | Awesome.test_file("README.md") 274 | --------------------------------------------------------------------------------