├── .gitignore ├── _config.yml ├── LICENSE ├── contributing.md ├── metadata.py ├── toc.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .access-token 3 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Phodal Huang 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 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Please ensure your pull request adheres to the following guidelines: 4 | 5 | - Read [the awesome manifesto](https://github.com/sindresorhus/awesome/blob/master/awesome.md) and ensure your list complies. 6 | - Search previous suggestions before making a new one, as yours may be a duplicate. 7 | - Make sure your list is useful before submitting. That implies it having enough content and every item a good succinct description. 8 | - A link back to this list from yours, so users can discover more lists, would be appreciated. 9 | - Make an individual pull request for each suggestion. 10 | - Titles should be [capitalized](http://grammar.yourdictionary.com/capitalization/rules-for-capitalization-in-titles.html). 11 | - Use the following format: `[List Name](link)` 12 | - If you have a comment about the link, add it after a dash, start it with a capital letter, and end it with a full stop. Avoid starting your comment with an article like "A", "The". 13 | - Link additions should be added to the bottom of the relevant category. 14 | - New categories or improvements to the existing categorization are welcome. 15 | - Check your spelling and grammar. 16 | - Make sure your text editor is set to remove trailing whitespace. 17 | - The pull request and commit should have a useful title. 18 | 19 | Link format: 20 | 21 | ``` 22 | [Awesome resource name](resource link) - Performant REST client library by the famous John Doe. 23 | ^ ^ ^ 24 | dash capital letter, no article Full stop 25 | ``` 26 | 27 | Thank you for your suggestions! 28 | -------------------------------------------------------------------------------- /metadata.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # by Erik Osheim 5 | # 6 | # Reads README.md, and writes a README.md.new. If the format of 7 | # README.md changes, this script may need modifications. 8 | # 9 | # Currently it rewrites each section, doing the following: 10 | # 1. alphabetizing 11 | # 2. querying GitHub for stars and days since active 12 | # 3. formatting the link title to show this info 13 | # 4. bolding projects with lots of stars 14 | # 15 | # Once README.md has the stars/days info in the links, the 16 | # repo_regex will need slight modification. 17 | # 18 | # In order to use GH authentication, create a file in this directory 19 | # called .access-token, whose contents are: "$user:$token" where $user 20 | # is your github username, and $token is a Personal Access Token. 21 | 22 | import base64 23 | import datetime 24 | import json 25 | import os.path 26 | import random 27 | import re 28 | import shutil 29 | import sys 30 | import urllib2 31 | 32 | # we use these regexes when "parsing" README.md 33 | empty_regex = re.compile(r"^ *\n$") 34 | section_regex = re.compile(r"(^## (.+)\n$)|(^### (.+)\n$)") 35 | repo_regex = re.compile(r"^\* (?:\*\*)?\[?([^*★]+[^ ★])(?: ★ ([^ ]+) ⧗ ([^ *]+))?\]\((.+?)\)(?:\*\*)?(?: (?:-|—|–) (.+))?\n$") 36 | end_regex = re.compile(r"^# .+\n$") 37 | github_regex = re.compile(r"^https://github.com/(.+?)/(.+?)(?:/?)$") 38 | 39 | # some paths 40 | readme_path = 'README.md' 41 | temp_path = 'README.md.new' 42 | 43 | # these will be updated if .access-token exists. 44 | user = None 45 | token = None 46 | 47 | # use fake to avoid hitting github API 48 | fake = True 49 | 50 | # whether to query all projects, or just those lacking scores/days. 51 | full_update = False 52 | 53 | # right now. 54 | now = datetime.datetime.now() 55 | 56 | # ask github for the number of stargazers, and days since last 57 | # activity, for the given github project. 58 | def query(owner, name): 59 | if fake: 60 | print ' {0}/{1}: ok'.format(owner, name) 61 | return (random.randint(1, 1000), random.randint(1, 300)) 62 | else: 63 | try: 64 | req = urllib2.Request('https://api.github.com/repos/{0}/{1}'.format(owner, name)) 65 | if user is not None and token is not None: 66 | b64 = base64.encodestring('{0}:{1}'.format(user, token)).replace('\n', '') 67 | req.add_header("Authorization", "Basic {0}".format(b64)) 68 | u = urllib2.urlopen(req) 69 | j = json.load(u) 70 | t = datetime.datetime.strptime(j['updated_at'], "%Y-%m-%dT%H:%M:%SZ") 71 | days = max(int((now - t).days), 0) 72 | print ' {0}/{1}: ok'.format(owner, name) 73 | return (int(j['stargazers_count']), days) 74 | except urllib2.HTTPError, e: 75 | print ' {0}/{1}: FAILED'.format(owner, name) 76 | return (None, None) 77 | 78 | def output_repo(outf, name, stars, days, link, rdesc): 79 | popular = stars is not None and int(stars) >= 500 80 | if stars is None and days is None: 81 | title = name 82 | else: 83 | title = '%s ★ %s ⧗ %s' % (name, stars, days) 84 | if popular: 85 | outf.write('* **[{0}]({1})** - {2}\n'.format(title, link, rdesc)) 86 | else: 87 | outf.write('* [{0}]({1}) - {2}\n'.format(title, link, rdesc)) 88 | 89 | def flush_section(outf, section, sdesc, repos): 90 | print ' ' + section.strip() 91 | outf.write(section) 92 | outf.write('\n') 93 | if sdesc: 94 | outf.write(sdesc) 95 | outf.write('\n') 96 | repos.sort(key=lambda t: t[0].lower()) 97 | for name, stars, days, link, rdesc in repos: 98 | if not full_update and stars is not None and days is not None: 99 | output_repo(outf, name, stars, days, link, rdesc) 100 | continue 101 | 102 | m = github_regex.match(link) 103 | if not m: 104 | print ' {0}: not a repo'.format(link) 105 | output_repo(outf, name, stars, days, link, rdesc) 106 | continue 107 | 108 | stars, days = query(m.group(1), m.group(2)) 109 | output_repo(outf, name, stars, days, link, rdesc) 110 | outf.write('\n') 111 | 112 | def run(): 113 | if full_update: 114 | print 'querying for all entries' 115 | else: 116 | print 'querying for new entries only' 117 | 118 | if fake: 119 | print 'running in fake mode -- no GH queries will be made' 120 | 121 | if os.path.exists('.access-token'): 122 | global user, token 123 | user, token = open('.access-token').read().strip().split(':') 124 | print 'using Personal Access Token {0}:{1}'.format(user, token) 125 | else: 126 | print 'no Personal Access Token found in .access-token' 127 | 128 | inf = open(readme_path, 'r') 129 | lines = list(inf) 130 | inf.close() 131 | print 'read {0}'.format(readme_path) 132 | 133 | started = False 134 | finished = False 135 | section = None 136 | sdesc = None 137 | repos = [] 138 | outf = open(temp_path, 'w') 139 | 140 | total_repos = 0 141 | 142 | print 'writing {0}'.format(temp_path) 143 | for line in lines: 144 | if finished: 145 | outf.write(line) 146 | elif started: 147 | if end_regex.match(line): 148 | total_repos += len(repos) 149 | flush_section(outf, section, sdesc, repos) 150 | outf.write(line) 151 | finished = True 152 | elif empty_regex.match(line): 153 | continue 154 | elif section_regex.match(line): 155 | total_repos += len(repos) 156 | flush_section(outf, section, sdesc, repos) 157 | section = line 158 | sdesc = None 159 | repos = [] 160 | else: 161 | m = repo_regex.match(line) 162 | if m: 163 | name, stars, days, link, rdesc = m.groups() 164 | repos.append((name, stars, days, link, rdesc)) 165 | elif sdesc is None: 166 | sdesc = line 167 | else: 168 | raise Exception("cannot parse {0}".format(line)) 169 | else: 170 | if section_regex.match(line): 171 | section = line 172 | started = True 173 | else: 174 | outf.write(line) 175 | outf.close() 176 | print 'wrote {0} repos to {1}'.format(total_repos, temp_path) 177 | 178 | print 'moving {0} to {1}'.format(temp_path, readme_path) 179 | shutil.move(temp_path, readme_path) 180 | 181 | if __name__ == "__main__": 182 | #global fake, full_update 183 | 184 | from optparse import OptionParser 185 | 186 | parser = OptionParser() 187 | parser.add_option("-f", "--fake", action="store_true", dest="fake", 188 | default=False, help="don't query github, use fake data") 189 | parser.add_option("-u", "--update", action="store_true", dest="update", 190 | default=False, help="update all entries to newest data") 191 | 192 | opts, _ = parser.parse_args() 193 | fake = opts.fake 194 | full_update = opts.update 195 | run() 196 | -------------------------------------------------------------------------------- /toc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # 4 | # Sebastian Raschka 2014-2015 5 | # 6 | # Python script that inserts a table of contents 7 | # into markdown documents and creates the required 8 | # internal links. 9 | # 10 | # For more information about how internal links 11 | # in HTML and Markdown documents work, please see 12 | # 13 | # Creating a table of contents with internal links in 14 | # IPython Notebooks and Markdown documents: 15 | # http://sebastianraschka.com/Articles/2014_ipython_internal_links.html 16 | # 17 | # Updates for this script will be available at 18 | # https://github.com/rasbt/markdown-toclify 19 | # 20 | # for more information about the usage: 21 | # markdown-toclify.py --help 22 | # 23 | 24 | import argparse 25 | import re 26 | 27 | 28 | __version__ = '1.7.1' 29 | 30 | VALIDS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-&' 31 | 32 | 33 | def read_lines(in_file): 34 | """Returns a list of lines from a input markdown file.""" 35 | 36 | with open(in_file, 'r') as inf: 37 | in_contents = inf.read().split('\n') 38 | return in_contents 39 | 40 | 41 | def remove_lines(lines, remove=('[[back to top]', ' tags.""" 43 | 44 | if not remove: 45 | return lines[:] 46 | 47 | out = [] 48 | for l in lines: 49 | if l.startswith(remove): 50 | continue 51 | out.append(l) 52 | return out 53 | 54 | 55 | def dashify_headline(line): 56 | """ 57 | Takes a header line from a Markdown document and 58 | returns a tuple of the 59 | '#'-stripped version of the head line, 60 | a string version for anchor tags, 61 | and the level of the headline as integer. 62 | E.g., 63 | >>> dashify_headline('### some header lvl3') 64 | ('Some header lvl3', 'some-header-lvl3', 3) 65 | 66 | """ 67 | stripped_right = line.rstrip('#') 68 | stripped_both = stripped_right.lstrip('#') 69 | level = len(stripped_right) - len(stripped_both) 70 | stripped_wspace = stripped_both.strip() 71 | 72 | # character replacements 73 | replaced_colon = stripped_wspace.replace('.', '') 74 | replaced_slash = replaced_colon.replace('/', '') 75 | rem_nonvalids = ''.join([c if c in VALIDS 76 | else '-' for c in replaced_slash]) 77 | 78 | lowered = rem_nonvalids.lower() 79 | dashified = re.sub(r'(-)\1+', r'\1', lowered) # remove duplicate dashes 80 | dashified = dashified.strip('-') # strip dashes from start and end 81 | 82 | # exception '&' (double-dash in github) 83 | dashified = dashified.replace('-&-', '--') 84 | 85 | return [stripped_wspace, dashified, level] 86 | 87 | 88 | def tag_and_collect(lines, id_tag=True, back_links=False, exclude_h=None): 89 | """ 90 | Gets headlines from the markdown document and creates anchor tags. 91 | 92 | Keyword arguments: 93 | lines: a list of sublists where every sublist 94 | represents a line from a Markdown document. 95 | id_tag: if true, creates inserts a the tags (not req. by GitHub) 96 | back_links: if true, adds "back to top" links below each headline 97 | exclude_h: header levels to exclude. E.g., [2, 3] 98 | excludes level 2 and 3 headings. 99 | 100 | Returns a tuple of 2 lists: 101 | 1st list: 102 | A modified version of the input list where 103 | anchor tags where inserted 104 | above the header lines (if github is False). 105 | 106 | 2nd list: 107 | A list of 3-value sublists, where the first value 108 | represents the heading, the second value the string 109 | that was inserted assigned to the IDs in the anchor tags, 110 | and the third value is an integer that reprents the headline level. 111 | E.g., 112 | [['some header lvl3', 'some-header-lvl3', 3], ...] 113 | 114 | """ 115 | out_contents = [] 116 | headlines = [] 117 | for l in lines: 118 | saw_headline = False 119 | 120 | orig_len = len(l) 121 | l = l.lstrip() 122 | 123 | if l.startswith(('# ', '## ', '### ', '#### ', '##### ', '###### ')): 124 | 125 | # comply with new markdown standards 126 | 127 | # not a headline if '#' not followed by whitespace '##no-header': 128 | if not l.lstrip('#').startswith(' '): 129 | continue 130 | # not a headline if more than 6 '#': 131 | if len(l) - len(l.lstrip('#')) > 6: 132 | continue 133 | # headers can be indented by at most 3 spaces: 134 | if orig_len - len(l) > 3: 135 | continue 136 | 137 | # ignore empty headers 138 | if not set(l) - {'#', ' '}: 139 | continue 140 | 141 | saw_headline = True 142 | dashified = dashify_headline(l) 143 | 144 | if not exclude_h or not dashified[-1] in exclude_h: 145 | if id_tag: 146 | id_tag = ''\ 147 | % (dashified[1]) 148 | out_contents.append(id_tag) 149 | headlines.append(dashified) 150 | 151 | out_contents.append(l) 152 | if back_links and saw_headline: 153 | out_contents.append('[[back to top](#table-of-contents)]') 154 | return out_contents, headlines 155 | 156 | 157 | def positioning_headlines(headlines): 158 | """ 159 | Strips unnecessary whitespaces/tabs if first header is not left-aligned 160 | """ 161 | left_just = False 162 | for row in headlines: 163 | if row[-1] == 1: 164 | left_just = True 165 | break 166 | if not left_just: 167 | for row in headlines: 168 | row[-1] -= 1 169 | return headlines 170 | 171 | 172 | def create_toc(headlines, hyperlink=True, top_link=False, no_toc_header=False): 173 | """ 174 | Creates the table of contents from the headline list 175 | that was returned by the tag_and_collect function. 176 | 177 | Keyword Arguments: 178 | headlines: list of lists 179 | e.g., ['Some header lvl3', 'some-header-lvl3', 3] 180 | hyperlink: Creates hyperlinks in Markdown format if True, 181 | e.g., '- [Some header lvl1](#some-header-lvl1)' 182 | top_link: if True, add a id tag for linking the table 183 | of contents itself (for the back-to-top-links) 184 | no_toc_header: suppresses TOC header if True. 185 | 186 | Returns a list of headlines for a table of contents 187 | in Markdown format, 188 | e.g., [' - [Some header lvl3](#some-header-lvl3)', ...] 189 | 190 | """ 191 | processed = [] 192 | if not no_toc_header: 193 | if top_link: 194 | processed.append('\n') 195 | processed.append('# Table of Contents') 196 | 197 | for line in headlines: 198 | if hyperlink: 199 | item = '%s- [%s](#%s)' % ((line[2]-1)*' ', line[0], line[1]) 200 | else: 201 | item = '%s- %s' % ((line[2]-1)*' ', line[0]) 202 | processed.append(item) 203 | processed.append('\n') 204 | return processed 205 | 206 | 207 | def build_markdown(toc_headlines, body, spacer=0, placeholder=None): 208 | """ 209 | Returns a string with the Markdown output contents incl. 210 | the table of contents. 211 | 212 | Keyword arguments: 213 | toc_headlines: lines for the table of contents 214 | as created by the create_toc function. 215 | body: contents of the Markdown file including 216 | ID-anchor tags as returned by the 217 | tag_and_collect function. 218 | spacer: Adds vertical space after the table 219 | of contents. Height in pixels. 220 | placeholder: If a placeholder string is provided, the placeholder 221 | will be replaced by the TOC instead of inserting the TOC at 222 | the top of the document 223 | 224 | """ 225 | if spacer: 226 | spacer_line = ['\n
\n' % (spacer)] 227 | toc_markdown = "\n".join(toc_headlines + spacer_line) 228 | else: 229 | toc_markdown = "\n".join(toc_headlines) 230 | 231 | body_markdown = "\n".join(body).strip() 232 | 233 | if placeholder: 234 | markdown = body_markdown.replace(placeholder, toc_markdown) 235 | else: 236 | markdown = toc_markdown + body_markdown 237 | 238 | return markdown 239 | 240 | 241 | def output_markdown(markdown_cont, output_file): 242 | """ 243 | Writes to an output file if `outfile` is a valid path. 244 | 245 | """ 246 | if output_file: 247 | with open(output_file, 'w') as out: 248 | out.write(markdown_cont) 249 | 250 | 251 | def markdown_toclify(input_file, output_file=None, github=False, 252 | back_to_top=False, nolink=False, 253 | no_toc_header=False, spacer=0, placeholder=None, 254 | exclude_h=None): 255 | """ Function to add table of contents to markdown files. 256 | 257 | Parameters 258 | ----------- 259 | input_file: str 260 | Path to the markdown input file. 261 | 262 | output_file: str (defaul: None) 263 | Path to the markdown output file. 264 | 265 | github: bool (default: False) 266 | Uses GitHub TOC syntax if True. 267 | 268 | back_to_top: bool (default: False) 269 | Inserts back-to-top links below headings if True. 270 | 271 | nolink: bool (default: False) 272 | Creates the table of contents without internal links if True. 273 | 274 | no_toc_header: bool (default: False) 275 | Suppresses the Table of Contents header if True 276 | 277 | spacer: int (default: 0) 278 | Inserts horizontal space (in pixels) after the table of contents. 279 | 280 | placeholder: str (default: None) 281 | Inserts the TOC at the placeholder string instead 282 | of inserting the TOC at the top of the document. 283 | 284 | exclude_h: list (default None) 285 | Excludes header levels, e.g., if [2, 3], ignores header 286 | levels 2 and 3 in the TOC. 287 | 288 | Returns 289 | ----------- 290 | cont: str 291 | Markdown contents including the TOC. 292 | 293 | """ 294 | raw_contents = read_lines(input_file) 295 | cleaned_contents = remove_lines(raw_contents, remove=('[[back to top]', '