├── .gitignore
├── templates
├── partial_url.html
├── partial_search_link.html
├── partial_user.html
├── partial_menu_item.html
├── partial_link_selftext.html
├── partial_subreddit_pager_link.html
├── partial_index_subreddit.html
├── partial_comment.html
├── partial_link.html
├── partial_user_link.html
├── index.html
├── user.html
├── search.html
├── subreddit.html
└── link.html
├── screenshots
├── sub.jpg
└── post.jpg
├── r
└── static
│ ├── fonts
│ ├── S6uyw4BMUTPHjx4wXg.woff2
│ ├── S6uyw4BMUTPHjxAwXjeu.woff2
│ ├── S6u9w4BMUTPHh6UVSwiPGQ.woff2
│ ├── S6u9w4BMUTPHh7USSwiPGQ.woff2
│ └── S6u9w4BMUTPHh6UVSwaPGR_p.woff2
│ ├── js
│ ├── archive-comments.js
│ ├── bootstrap.min.js
│ └── jquery-3.3.1.slim.min.js
│ └── css
│ ├── lato.css
│ └── archive.css
├── LICENSE
├── README.md
├── fetch_links.py
└── write_html.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /data
2 | /r
3 | !/r/static
--------------------------------------------------------------------------------
/templates/partial_url.html:
--------------------------------------------------------------------------------
1 | #INNER_HTML#
--------------------------------------------------------------------------------
/templates/partial_search_link.html:
--------------------------------------------------------------------------------
1 | ###TITLE###
--------------------------------------------------------------------------------
/templates/partial_user.html:
--------------------------------------------------------------------------------
1 | ###AUTHOR###
--------------------------------------------------------------------------------
/templates/partial_menu_item.html:
--------------------------------------------------------------------------------
1 | ###SUB###
--------------------------------------------------------------------------------
/screenshots/sub.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/screenshots/sub.jpg
--------------------------------------------------------------------------------
/screenshots/post.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/screenshots/post.jpg
--------------------------------------------------------------------------------
/templates/partial_link_selftext.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/partial_subreddit_pager_link.html:
--------------------------------------------------------------------------------
1 | #TEXT#
--------------------------------------------------------------------------------
/r/static/fonts/S6uyw4BMUTPHjx4wXg.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/r/static/fonts/S6uyw4BMUTPHjx4wXg.woff2
--------------------------------------------------------------------------------
/r/static/fonts/S6uyw4BMUTPHjxAwXjeu.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/r/static/fonts/S6uyw4BMUTPHjxAwXjeu.woff2
--------------------------------------------------------------------------------
/templates/partial_index_subreddit.html:
--------------------------------------------------------------------------------
1 | #SUB# #NUM_LINKS#
--------------------------------------------------------------------------------
/r/static/fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/r/static/fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2
--------------------------------------------------------------------------------
/r/static/fonts/S6u9w4BMUTPHh7USSwiPGQ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/r/static/fonts/S6u9w4BMUTPHh7USSwiPGQ.woff2
--------------------------------------------------------------------------------
/r/static/fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/libertysoft3/reddit-html-archiver/HEAD/r/static/fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2
--------------------------------------------------------------------------------
/templates/partial_comment.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/partial_link.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/templates/partial_user_link.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 libertysoft3
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 |
--------------------------------------------------------------------------------
/r/static/js/archive-comments.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | $('a.score').click(function(){
3 | var $comment = $(this).closest('.comment');
4 | var depth = parseInt($comment.data('depth'));
5 | if ($comment.hasClass('collapsed')) {
6 | $comment.removeClass('collapsed');
7 | $check_comment = $comment.next('.comment');
8 | depth_sibling = false;
9 | while (!depth_sibling) {
10 | if ($check_comment.length == 0) {
11 | depth_sibling = true;
12 | }
13 | if ($check_comment.data('depth') == '' || parseInt($check_comment.data('depth')) <= depth) {
14 | depth_sibling = true;
15 | } else {
16 | $check_comment.removeClass('hidden');
17 | }
18 | $check_comment = $check_comment.next('.comment');
19 | }
20 | } else {
21 | $comment.addClass('collapsed');
22 | $check_comment = $comment.next('.comment');
23 | depth_sibling = false;
24 | while (!depth_sibling) {
25 | if ($check_comment.length == 0) {
26 | depth_sibling = true;
27 | }
28 | if ($check_comment.data('depth') == '' || parseInt($check_comment.data('depth')) <= depth) {
29 | depth_sibling = true;
30 | } else {
31 | $check_comment.addClass('hidden');
32 | }
33 | $check_comment = $check_comment.next('.comment');
34 | }
35 | }
36 | });
37 | });
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ###TITLE###
10 |
11 |
12 |
13 |
14 | ###TITLE###
15 |
16 |
17 |
18 |
29 |
30 |
31 |
32 |
33 | ###HTML_LINKS###
34 |
35 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/templates/user.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ###TITLE###
10 |
11 |
12 |
31 |
32 |
33 | ###HTML_LINKS###
34 |
35 |
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/r/static/css/lato.css:
--------------------------------------------------------------------------------
1 | /* latin-ext */
2 | @font-face {
3 | font-family: 'Lato';
4 | font-style: normal;
5 | font-weight: 300;
6 | src: local('Lato Light'), local('Lato-Light'), url(../fonts/S6u9w4BMUTPHh7USSwaPGR_p.woff2) format('woff2');
7 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
8 | }
9 | /* latin */
10 | @font-face {
11 | font-family: 'Lato';
12 | font-style: normal;
13 | font-weight: 300;
14 | src: local('Lato Light'), local('Lato-Light'), url(../fonts/S6u9w4BMUTPHh7USSwiPGQ.woff2) format('woff2');
15 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
16 | }
17 | /* latin-ext */
18 | @font-face {
19 | font-family: 'Lato';
20 | font-style: normal;
21 | font-weight: 400;
22 | src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
23 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
24 | }
25 | /* latin */
26 | @font-face {
27 | font-family: 'Lato';
28 | font-style: normal;
29 | font-weight: 400;
30 | src: local('Lato Regular'), local('Lato-Regular'), url(../fonts/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
31 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
32 | }
33 | /* latin-ext */
34 | @font-face {
35 | font-family: 'Lato';
36 | font-style: normal;
37 | font-weight: 700;
38 | src: local('Lato Bold'), local('Lato-Bold'), url(../fonts/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
39 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
40 | }
41 | /* latin */
42 | @font-face {
43 | font-family: 'Lato';
44 | font-style: normal;
45 | font-weight: 700;
46 | src: local('Lato Bold'), local('Lato-Bold'), url(../fonts/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
47 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
48 | }
49 |
--------------------------------------------------------------------------------
/r/static/css/archive.css:
--------------------------------------------------------------------------------
1 | footer {
2 | text-align: center;
3 | }
4 |
5 | /* markdown */
6 | .md {
7 | word-wrap: break-word;
8 | overflow-wrap: break-word;
9 | }
10 | .md p, .md ol, .md ul, .md blockquote {
11 | margin: 3px 0;
12 | }
13 | .md blockquote {
14 | border-left: 2px solid rgba(255, 255, 255, 0.4); /* text-muted */
15 | padding-left: 0.5rem;
16 | }
17 | .md blockquote, .md del {
18 | color: rgba(255, 255, 255, 0.4); /* text-muted */
19 | }
20 | .md code, .md pre {
21 | border: 1px solid #4E5D6C; /* alert-secondary */
22 | background: #4E5D6C; /* disabled form input color */
23 | }
24 | .md h1 {
25 | font-size: 1.5rem;
26 | }
27 | .md h2 {
28 | font-size: 1.4rem;
29 | }
30 | .md h3 {
31 | font-size: 1.3rem;
32 | }
33 | .md h4 {
34 | font-size: 1.2rem;
35 | }
36 | .md h5 {
37 | font-size: 1.1rem;
38 | }
39 | .md h6 {
40 | font-size: 1rem;
41 | font-weight: bold;
42 | }
43 |
44 | /* subreddit links */
45 | .links .title {
46 | line-height: 1.25;
47 | }
48 | .links .title a, .submission .title a {
49 | color: inherit;
50 | }
51 | .search .title {
52 | color: inherit;
53 | display: block;
54 | }
55 |
56 | /* link/post page */
57 | .op .author, .submission .author {
58 | color: #5bc0de; /* match bootstrap link color / badge-primary */
59 | }
60 | .submission .card-body {
61 | padding: 1rem;
62 | }
63 |
64 | /* comments */
65 | .comment {
66 | margin-bottom: 5px; /*1rem;*/
67 | }
68 | .comment .byline, .comment .byline a {
69 | line-height: 1;
70 | }
71 | .collapsed .md {
72 | display: none;
73 | }
74 | .hidden {
75 | display: none;
76 | }
77 | .to-top {
78 | display: block;
79 | }
80 | .comments .ml-1 {
81 | margin-left: 1rem !important;
82 | }
83 | .comments .ml-2 {
84 | margin-left: 2rem !important;
85 | }
86 | .comments .ml-3 {
87 | margin-left: 3rem !important;
88 | }
89 | .comments .ml-4 {
90 | margin-left: 4rem !important;
91 | }
92 | .comments .ml-5 {
93 | margin-left: 5rem !important;
94 | }
95 | .comments .ml-6 {
96 | margin-left: 6rem !important;
97 | }
98 | .comments .ml-7 {
99 | margin-left: 7rem !important;
100 | }
101 | .comments .ml-8 {
102 | margin-left: 8rem !important;
103 | }
--------------------------------------------------------------------------------
/templates/search.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | r/###SUB### ###TITLE###
10 |
11 |
12 |
43 |
44 |
45 | ###HTML_LINKS###
46 |
47 |
48 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/templates/subreddit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | r/###SUB### ###TITLE###
10 |
11 |
12 |
43 |
44 |
47 |
48 | ###HTML_LINKS###
49 |
50 |
53 |
54 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/templates/link.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | r/###SUB###: ###TITLE###
10 |
11 |
12 |
43 |
44 |
45 |
###HTML_TITLE###
46 |
###SCORE### ###DATE### by ###HTML_AUTHOR_URL###
47 | ###HTML_SELFTEXT###
48 |
49 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## reddit html archiver
2 |
3 | pulls reddit data from the [pushshift](https://github.com/pushshift/api) api and renders offline compatible html pages. uses the reddit markdown renderer.
4 |
5 | ### install
6 |
7 | requires python 3 on linux, OSX, or Windows.
8 |
9 | **warning:** if `$ python --version` outputs a python 2 version on your system, then you need to replace all occurances of `python` with `python3` in the commands below.
10 |
11 | $ sudo apt-get install pip
12 | $ pip install psaw -U
13 | $ git clone https://github.com/chid/snudown
14 | $ cd snudown
15 | $ sudo python setup.py install
16 | $ cd ..
17 | $ git clone [this repo]
18 | $ cd reddit-html-archiver
19 | $ chmod u+x *.py
20 |
21 | Windows users may need to run
22 |
23 | > chcp 65001
24 | > set PYTHONIOENCODING=utf-8
25 |
26 | before running `fetch_links.py` or `write_html.py` to resolve encoding errors such as 'codec can't encode character'.
27 |
28 | ### fetch reddit data
29 |
30 | fetch data by subreddit and date range, writing to csv files in `data`:
31 |
32 | $ python ./fetch_links.py politics 2017-1-1 2017-2-1
33 |
34 | or you can filter links/posts to download less data:
35 |
36 | $ python ./fetch_links.py --self_only --score "> 2000" politics 2015-1-1 2016-1-1
37 |
38 | to show all available options and filters run:
39 |
40 | $ python ./fetch_links.py -h
41 |
42 | decrease your date range or adjust `pushshift_rate_limit_per_minute` in `fetch_links.py` if you are getting connection errors.
43 |
44 | ### write web pages
45 |
46 | write html files for all subreddits to `r`:
47 |
48 | $ python ./write_html.py
49 |
50 | you can add some output filtering to have less empty postssmaller archive size
51 |
52 | $ python ./write_html.py --min-score 100 --min-comments 100 --hide-deleted-comments
53 |
54 | to show all available filters run:
55 |
56 | $ python ./write_html.py -h
57 |
58 | your html archive has been written to `r`. once you are satisfied with your archive feel free to copy/move the contents of `r` to elsewhere and to delete the git repos you have created. everything in `r` is fully self contained.
59 |
60 | to update an html archive, delete everything in `r` aside from `r/static` and re-run `write_html.py` to regenerate everything.
61 |
62 | ### hosting the archived pages
63 |
64 | copy the contents of the `r` directory to a web root or appropriately served git repo.
65 |
66 | ### potential improvements
67 |
68 | * fetch_links
69 | * num_comments filtering
70 | * thumbnails or thumbnail urls
71 | * media posts
72 | * score update
73 | * scores from reddit with [praw](https://github.com/praw-dev/praw)
74 | * real templating
75 | * choose [Bootswatch](https://bootswatch.com/) theme
76 | * specify subreddits to output
77 | * show link domain/post type
78 | * user pages
79 | * add pagination, posts sorted by score, comments, date, sub
80 | * too many files in one directory
81 | * view on reddit.com
82 | * js powered search page, show no links by default
83 | * js inline media embeds/expandos
84 | * archive.org links
85 |
86 | ### see also
87 |
88 | * [pushshift](https://github.com/pushshift/api), [r/pushshift](https://www.reddit.com/r/pushshift/)
89 | * [psaw](https://github.com/dmarx/psaw)
90 | * [snudown](https://github.com/reddit/snudown)
91 | * [redditsearch.io](https://redditsearch.io/)
92 | * [reddit post archiver](https://github.com/sJohnsonStoever/redditPostArchiver)
93 | * [reddit downloader](https://github.com/shadowmoose/RedditDownloader)
94 |
95 | ### screenshots
96 |
97 | 
98 | 
99 |
--------------------------------------------------------------------------------
/fetch_links.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | import time
3 | from time import mktime
4 | from datetime import datetime, timedelta
5 | import argparse
6 | from pprint import pprint
7 | import json
8 | import csv
9 | import os
10 | from psaw import PushshiftAPI
11 |
12 | pushshift_rate_limit_per_minute = 20
13 | max_comments_per_query = 150
14 | write_every = 10
15 |
16 | link_fields = ['author', 'created_utc', 'domain', 'id', 'is_self',
17 | 'num_comments', 'over_18', 'permalink', 'retrieved_on', 'score',
18 | 'selftext', 'stickied', 'subreddit_id', 'title', 'url']
19 | comment_fields = ['author', 'body', 'created_utc', 'id', 'link_id',
20 | 'parent_id', 'score', 'stickied', 'subreddit_id']
21 |
22 | def fetch_links(subreddit=None, date_start=None, date_stop=None, limit=None, score=None, self_only=False):
23 | if subreddit is None or date_start is None or date_stop is None:
24 | print('ERROR: missing required arguments')
25 | exit()
26 |
27 | api = PushshiftAPI(rate_limit_per_minute=pushshift_rate_limit_per_minute, detect_local_tz=False)
28 |
29 | # get links
30 | links = []
31 | print('fetching submissions %s to %s...' % (time.strftime('%Y-%m-%d', date_start), time.strftime('%Y-%m-%d', date_stop)))
32 | params = {
33 | 'after': int(mktime(date_start)) - 86400, # make date inclusive, adjust for UTC
34 | 'before': int(mktime(date_stop)) + 86400,
35 | 'subreddit': subreddit,
36 | 'filter': link_fields,
37 | 'sort': 'asc',
38 | 'sort_type': 'created_utc',
39 | }
40 | if limit:
41 | params['limit'] = int(limit)
42 | if score:
43 | params['score'] = score
44 | if self_only:
45 | params['is_self'] = True
46 | link_results = list(api.search_submissions(**params))
47 | print('processing %s links' % len(link_results))
48 | for s in link_results:
49 | # print('%s %s' % (datetime.utcfromtimestamp(int(s.d_['created_utc'])), s.d_['title']))
50 | # pprint(s)
51 |
52 | # get comment ids
53 | comments = []
54 | if s.d_['num_comments'] > 0 and not comment_data_exists(subreddit, s.d_['created_utc'], s.d_['id']):
55 | comment_ids = list(api._get_submission_comment_ids(s.d_['id']))
56 | # print('%s comment_ids: %s' % (data['id'], comment_ids))
57 |
58 | # get comments
59 | if (len(comment_ids) > 0):
60 | mychunks = []
61 | if len(comment_ids) > max_comments_per_query:
62 | mychunks = chunks(comment_ids, max_comments_per_query)
63 | else:
64 | mychunks = [comment_ids]
65 | for chunk in mychunks:
66 | comment_params = {
67 | 'filter': comment_fields,
68 | 'ids': ','.join(chunk),
69 | 'limit': max_comments_per_query,
70 | }
71 | comments_results = list(api.search_comments(**comment_params))
72 | print('%s fetch link %s comments %s/%s' % (datetime.utcfromtimestamp(int(s.d_['created_utc'])), s.d_['id'], len(comments_results), len(comment_ids)))
73 | for c in comments_results:
74 | comments.append(c.d_)
75 |
76 | s.d_['comments'] = comments
77 | links.append(s.d_)
78 |
79 | # write results
80 | if len(links) >= write_every:
81 | success = write_links(subreddit, links)
82 | if success:
83 | links = []
84 |
85 | # write remining results
86 | if len(links):
87 | write_links(subreddit, links)
88 |
89 | # csvs are not guaranteed to be sorted by date but you can resume broken runs
90 | # and change sort criteria later to add more posts without getting duplicates.
91 | # delete csvs and re-run to update existing posts
92 | def write_links(subreddit, links):
93 | if links and len(links) > 0:
94 | writing_day = None
95 | file = None
96 | writer = None
97 | existing_link_ids = []
98 | wrote_links = 0
99 | wrote_comments = 0
100 |
101 | for r in links:
102 | # print('%s link %s' % (r['id'], r['title']))
103 |
104 | # grab link comments
105 | existing_comment_ids = []
106 | comments = r['comments']
107 | # print('%s comments %s' % (r['id'], comments))
108 |
109 | created_ts = int(r['created_utc'])
110 | created = datetime.utcfromtimestamp(created_ts).strftime('%Y-%m-%d')
111 | created_path = datetime.utcfromtimestamp(created_ts).strftime('%Y/%m/%d')
112 | if created != writing_day:
113 | if file:
114 | file.close()
115 | writing_day = created
116 | path = 'data/' + subreddit + '/' + created_path
117 | os.makedirs(path, exist_ok=True)
118 |
119 | # create and parse existing links
120 | filename = 'links.csv'
121 | filepath = path + '/' + filename
122 | if not os.path.isfile(filepath):
123 | file = open(filepath, 'a', encoding='utf-8')
124 | writer = csv.DictWriter(file, fieldnames=link_fields)
125 | writer.writeheader()
126 | # print('created %s' % filepath)
127 | else:
128 | with open(filepath, 'r', encoding='utf-8') as file:
129 | reader = csv.DictReader(file)
130 | for row in reader:
131 | existing_link_ids.append(row['id'])
132 |
133 | file = open(filepath, 'a', encoding='utf-8')
134 | writer = csv.DictWriter(file, fieldnames=link_fields)
135 |
136 | # create and parse existing comments
137 | # writing empty comments csvs resuming and comment_data_exists()
138 | filename = r['id'] + '.csv'
139 | filepath = path + '/' + filename
140 | if not os.path.isfile(filepath):
141 | comments_file = open(filepath, 'a', encoding='utf-8')
142 | comments_writer = csv.DictWriter(comments_file, fieldnames=comment_fields)
143 | comments_writer.writeheader()
144 | # print('created %s' % filepath)
145 | else:
146 | with open(filepath, 'r', encoding='utf-8') as comments_file:
147 | reader = csv.DictReader(comments_file)
148 | for row in reader:
149 | existing_comment_ids.append(row['id'])
150 |
151 | comments_file = open(filepath, 'a', encoding='utf-8')
152 | comments_writer = csv.DictWriter(comments_file, fieldnames=comment_fields)
153 |
154 | # write link row
155 | if r['id'] not in existing_link_ids:
156 | for field in list(r):
157 | if field not in link_fields:
158 | del r[field]
159 |
160 | writer.writerow(r)
161 | wrote_links += 1
162 |
163 | # write comments
164 | for c in comments:
165 | if c['id'] not in existing_comment_ids:
166 | for field in list(c):
167 | if field not in comment_fields:
168 | del c[field]
169 | comments_writer.writerow(c)
170 | wrote_comments += 1
171 | comments_file.close()
172 |
173 |
174 | print('got %s links, wrote %s and %s comments' % (len(links), wrote_links, wrote_comments))
175 | return True
176 |
177 | def link_data_exists(subreddit, date):
178 | created_path = time.strftime('%Y/%m/%d', date)
179 | path = 'data/' + subreddit + '/' + created_path + '/links.csv'
180 | if not os.path.isfile(path):
181 | return False
182 | return True
183 |
184 | def comment_data_exists(subreddit, link_created_utc, link_id):
185 | created_ts = int(link_created_utc)
186 | created_path = datetime.utcfromtimestamp(created_ts).strftime('%Y/%m/%d')
187 | path = 'data/' + subreddit + '/' + created_path + '/' + link_id + '.csv'
188 | if os.path.isfile(path):
189 | return True
190 | return False
191 |
192 | def chunks(l, n):
193 | """Yield successive n-sized chunks from l."""
194 | for i in range(0, len(l), n):
195 | yield l[i:i + n]
196 |
197 | def mkdate(datestr):
198 | try:
199 | return time.strptime(datestr, '%Y-%m-%d')
200 | except ValueError:
201 | raise argparse.ArgumentTypeError(datestr + ' is not a proper date string')
202 |
203 | if __name__ == '__main__':
204 | parser=argparse.ArgumentParser()
205 | parser.add_argument('subreddit', help='subreddit to archive')
206 | parser.add_argument('date_start', type=mkdate, help='start archiving at date, e.g. 2005-1-1')
207 | parser.add_argument('date_stop', type=mkdate, help='stop archiving at date, inclusive, cannot be date_start')
208 | parser.add_argument('--limit', default=None, help='pushshift api limit param, default None')
209 | parser.add_argument('--score', default=None, help='pushshift api score param, e.g. "> 10", default None')
210 | parser.add_argument('--self_only', action="store_true", help='only fetch selftext submissions, default False')
211 | args=parser.parse_args()
212 |
213 | self_only = False
214 | if args.self_only:
215 | self_only = True
216 |
217 | args.subreddit = args.subreddit.lower()
218 |
219 | fetch_links(args.subreddit, args.date_start, args.date_stop, args.limit, args.score, self_only)
220 |
--------------------------------------------------------------------------------
/write_html.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | from datetime import datetime, date, timedelta
3 | import argparse
4 | import csv
5 | import os
6 | import re
7 | import snudown
8 | import psutil
9 |
10 | url_project = 'https://github.com/libertysoft3/reddit-html-archiver'
11 | links_per_page = 30
12 | pager_skip = 10
13 | pager_skip_long = 100
14 | start_date = date(2005, 1, 1)
15 | end_date = datetime.today().date() + timedelta(days=1)
16 | source_data_links = 'links.csv'
17 | max_comment_depth = 8 # mostly for mobile, which might be silly
18 | removed_content_identifiers = ['[deleted]','deleted','[removed]','removed']
19 | default_sort = 'score'
20 | sort_indexes = {
21 | 'score': {
22 | 'default': 1,
23 | 'slug': 'score'
24 | },
25 | 'num_comments': {
26 | 'default': 0,
27 | 'slug': 'comments',
28 | },
29 | 'created_utc': {
30 | 'default': 1000198000,
31 | 'slug': 'date',
32 | }
33 | }
34 | missing_comment_score_label = 'n/a'
35 |
36 | template_index = ''
37 | with open('templates/index.html', 'r', encoding='utf-8') as file:
38 | template_index = file.read()
39 |
40 | template_subreddit = ''
41 | with open('templates/subreddit.html', 'r', encoding='utf-8') as file:
42 | template_subreddit = file.read()
43 |
44 | template_link = ''
45 | with open('templates/link.html', 'r', encoding='utf-8') as file:
46 | template_link = file.read()
47 |
48 | template_comment = ''
49 | with open('templates/partial_comment.html', 'r', encoding='utf-8') as file:
50 | template_comment = file.read()
51 |
52 | template_search = ''
53 | with open('templates/search.html', 'r', encoding='utf-8') as file:
54 | template_search = file.read()
55 |
56 | template_user = ''
57 | with open('templates/user.html', 'r', encoding='utf-8') as file:
58 | template_user = file.read()
59 |
60 | template_sub_link = ''
61 | with open('templates/partial_menu_item.html', 'r', encoding='utf-8') as file:
62 | template_sub_link = file.read()
63 |
64 | template_user_url = ''
65 | with open('templates/partial_user.html', 'r', encoding='utf-8') as file:
66 | template_user_url = file.read()
67 |
68 | template_link_url = ''
69 | with open('templates/partial_link.html', 'r', encoding='utf-8') as file:
70 | template_link_url = file.read()
71 |
72 | template_search_link = ''
73 | with open('templates/partial_search_link.html', 'r', encoding='utf-8') as file:
74 | template_search_link = file.read()
75 |
76 | template_index_sub = ''
77 | with open('templates/partial_index_subreddit.html', 'r', encoding='utf-8') as file:
78 | template_index_sub = file.read()
79 |
80 | template_index_pager_link = ''
81 | with open('templates/partial_subreddit_pager_link.html', 'r', encoding='utf-8') as file:
82 | template_index_pager_link = file.read()
83 |
84 | template_selftext = ''
85 | with open('templates/partial_link_selftext.html', 'r', encoding='utf-8') as file:
86 | template_selftext = file.read()
87 |
88 | template_user_page_link = ''
89 | with open('templates/partial_user_link.html', 'r', encoding='utf-8') as file:
90 | template_user_page_link = file.read()
91 |
92 | teplate_url = ''
93 | with open('templates/partial_url.html', 'r', encoding='utf-8') as file:
94 | template_url = file.read()
95 |
96 | process = psutil.Process(os.getpid())
97 |
98 | def generate_html(min_score=0, min_comments=0, hide_deleted_comments=False):
99 | delta = timedelta(days=1)
100 | subs = get_subs()
101 | user_index = {}
102 | processed_subs = []
103 | stat_links = 0
104 | stat_filtered_links = 0
105 |
106 | for sub in subs:
107 | # write link pages
108 | # print('generate_html() processing %s %s kb' % (sub, int(int(process.memory_info().rss) / 1024)))
109 | stat_sub_links = 0
110 | stat_sub_filtered_links = 0
111 | stat_sub_comments = 0
112 | d = start_date
113 | while d <= end_date:
114 | raw_links = load_links(d, sub, True)
115 | stat_links += len(raw_links)
116 | stat_sub_links += len(raw_links)
117 | for l in raw_links:
118 | if validate_link(l, min_score, min_comments):
119 | write_link_page(subs, l, sub, hide_deleted_comments)
120 | stat_filtered_links += 1
121 | stat_sub_filtered_links += 1
122 | if 'comments' in l:
123 | stat_sub_comments += len(l['comments'])
124 | d += delta
125 | if stat_sub_filtered_links > 0:
126 | processed_subs.append({'name': sub, 'num_links': stat_sub_filtered_links})
127 | print('%s: %s links filtered to %s' % (sub, stat_sub_links, stat_sub_filtered_links))
128 |
129 | # write subreddit pages
130 | valid_sub_links = []
131 | d = start_date
132 | while d <= end_date:
133 | raw_links = load_links(d, sub)
134 | for l in raw_links:
135 | if validate_link(l, min_score, min_comments):
136 | valid_sub_links.append(l)
137 |
138 | # collect links for user pages
139 | # TODO: this is the least performant bit. load and generate user pages user by user instead.
140 | l['subreddit'] = sub
141 | if l['author'] not in user_index.keys():
142 | user_index[l['author']] = []
143 | user_index[l['author']].append(l)
144 | d += delta
145 | write_subreddit_pages(sub, subs, valid_sub_links, stat_sub_filtered_links, stat_sub_comments)
146 | write_subreddit_search_page(sub, subs, valid_sub_links, stat_sub_filtered_links, stat_sub_comments)
147 |
148 | # write user pages
149 | write_user_page(processed_subs, user_index)
150 |
151 | # write index page
152 | write_index(processed_subs)
153 | print('all done. %s links filtered to %s' % (stat_links, stat_filtered_links))
154 |
155 | def write_subreddit_pages(subreddit, subs, link_index, stat_sub_filtered_links, stat_sub_comments):
156 | if len(link_index) == 0:
157 | return True
158 |
159 | for sort in sort_indexes.keys():
160 | links = sorted(link_index, key=lambda k: (int(k[sort]) if k[sort] != '' else sort_indexes[sort]['default']), reverse=True)
161 | pages = list(chunks(links, links_per_page))
162 | page_num = 0
163 |
164 | sort_based_prefix = '../'
165 | if sort == default_sort:
166 | sort_based_prefix = ''
167 |
168 | # render subreddits list
169 | subs_menu_html = ''
170 | for sub in subs:
171 | sub_url = sort_based_prefix + '../' + sub + '/index.html'
172 | subs_menu_html += template_sub_link.replace('###URL_SUB###', sub_url).replace('###SUB###', sub)
173 |
174 | for page in pages:
175 | page_num += 1
176 | # print('%s page' % (page))
177 |
178 | links_html = ''
179 | for l in page:
180 | author_link_html = template_user_url
181 | author_url = sort_based_prefix + '../user/' + l['author'] + '.html'
182 | author_link_html = author_link_html.replace('###URL_AUTHOR###', author_url).replace('###AUTHOR###', l['author'])
183 |
184 | link_url = l['url']
185 | link_comments_url = sort_based_prefix + l['permalink'].lower().strip('/')
186 | link_comments_url = link_comments_url.replace('r/' + subreddit + '/', '')
187 | idpath = '/'.join(list(l['id']))
188 | link_comments_url = link_comments_url.replace(l['id'], idpath)
189 | link_comments_url += '.html'
190 | if l['is_self'] is True or l['is_self'] == 'True':
191 | link_url = link_comments_url
192 |
193 | index_link_data_map = {
194 | '###TITLE###': l['title'],
195 | '###URL###': link_url,
196 | '###URL_COMMENTS###': link_comments_url,
197 | '###SCORE###': str(l['score']),
198 | '###NUM_COMMENTS###': l['num_comments'] if int(l['num_comments']) > 0 else str(0),
199 | '###DATE###': datetime.utcfromtimestamp(int(l['created_utc'])).strftime('%Y-%m-%d'),
200 | '###LINK_DOMAIN###': '(self.' + subreddit + ')' if l['is_self'] is True or l['is_self'] == 'True' else '',
201 | '###HTML_AUTHOR_URL###': author_link_html,
202 | }
203 | link_html = template_link_url
204 | for key, value in index_link_data_map.items():
205 | link_html = link_html.replace(key, value)
206 | links_html += link_html + '\n'
207 |
208 | index_page_data_map = {
209 | '###INCLUDE_PATH###': sort_based_prefix + '../',
210 | '###TITLE###': 'by ' + sort_indexes[sort]['slug'] + ' page ' + str(page_num) + ' of ' + str(len(pages)),
211 | '###SUB###': subreddit,
212 | '###ARCH_NUM_POSTS###': str(stat_sub_filtered_links),
213 | '###ARCH_NUM_COMMENTS###': str(stat_sub_comments),
214 | '###URL_SUBS###': sort_based_prefix + '../index.html',
215 | '###URL_PROJECT###': url_project,
216 | '###URL_IDX_SCORE###': sort_based_prefix + 'index.html',
217 | '###URL_IDX_CMNT###': sort_based_prefix + 'index-' + sort_indexes['num_comments']['slug'] + '/index.html',
218 | '###URL_IDX_DATE###': sort_based_prefix + 'index-' + sort_indexes['created_utc']['slug'] + '/index.html',
219 | '###URL_SEARCH###': sort_based_prefix + 'search.html',
220 | '###URL_IDX_SCORE_CSS###': 'active' if sort == 'score' else '',
221 | '###URL_IDX_CMNT_CSS###': 'active' if sort == 'num_comments' else '',
222 | '###URL_IDX_DATE_CSS###': 'active' if sort == 'created_utc' else '',
223 | '###URL_SEARCH_CSS###': '',
224 | '###HTML_LINKS###': links_html,
225 | '###HTML_SUBS_MENU###': subs_menu_html,
226 | '###HTML_PAGER###': get_pager_html(page_num, len(pages)),
227 | }
228 | page_html = template_subreddit
229 | for key, value in index_page_data_map.items():
230 | page_html = page_html.replace(key, value)
231 |
232 |
233 | # write file
234 | suffix = '-' + str(page_num) + '.html'
235 | if page_num == 1:
236 | suffix = '.html'
237 | filename = 'index' + suffix
238 | if sort == default_sort:
239 | filepath = 'r/' + subreddit + '/' + filename
240 | else:
241 | filepath = 'r/' + subreddit + '/index-' + sort_indexes[sort]['slug'] + '/' + filename
242 | if not os.path.isfile(filepath):
243 | os.makedirs(os.path.dirname(filepath), exist_ok=True)
244 | with open(filepath, 'w', encoding='utf-8') as file:
245 | file.write(page_html)
246 | # print('wrote %s %s, %s links' % (sort, filepath, len(page)))
247 |
248 | return True
249 |
250 | def write_link_page(subreddits, link, subreddit='', hide_deleted_comments=False):
251 | # reddit: https://www.reddit.com/r/conspiracy/comments/8742iv/happening_now_classmate_former_friend_of/
252 | # archive: r/conspiracy/comments/8/7/4/2/i/v/happening_now_classmate_former_friend_of.html
253 | idpath = '/'.join(list(link['id']))
254 | filepath = link['permalink'].lower().strip('/') + '.html'
255 | filepath = filepath.replace(link['id'], idpath)
256 | if os.path.isfile(filepath):
257 | return True
258 |
259 | created = datetime.utcfromtimestamp(int(link['created_utc']))
260 | sorted_comments = []
261 | if len(link['comments']) > 0:
262 | sorted_comments = sort_comments(link['comments'], hide_deleted_comments)
263 |
264 | # traverse up to root dir, depends on id length
265 | static_include_path = ''
266 | for i in range(len(link['id']) + 2):
267 | static_include_path += '../'
268 |
269 | # render comments
270 | comments_html = ''
271 | for c in sorted_comments:
272 | css_classes = 'ml-' + (str(c['depth']) if int(c['depth']) <= max_comment_depth else str(max_comment_depth))
273 | if c['author'] == link['author'] and c['author'] not in removed_content_identifiers:
274 | css_classes += ' op'
275 | if c['stickied'].lower() == 'true' or c['stickied'] is True:
276 | css_classes += ' stickied'
277 |
278 | # author link
279 | url = static_include_path + 'user/' + c['author'] + '.html'
280 | author_link_html = template_user_url.replace('###URL_AUTHOR###', url).replace('###AUTHOR###', c['author'])
281 |
282 | comment_data_map = {
283 | '###ID###': c['id'],
284 | '###PARENT_ID###': c['parent_id'],
285 | '###DEPTH###': str(c['depth']),
286 | '###DATE###': created.strftime('%Y-%m-%d'),
287 | '###SCORE###': str(c['score']) if len(str(c['score'])) > 0 else missing_comment_score_label,
288 | '###BODY###': snudown.markdown(c['body'].replace('>','>')),
289 | '###CSS_CLASSES###': css_classes,
290 | '###CLASS_SCORE###': 'badge-danger' if len(c['score']) > 0 and int(c['score']) < 1 else 'badge-secondary',
291 | '###HTML_AUTHOR_URL###': author_link_html,
292 | }
293 | comment_html = template_comment
294 | for key, value in comment_data_map.items():
295 | comment_html = comment_html.replace(key, value)
296 | comments_html += comment_html + '\n'
297 |
298 | # render subreddits list
299 | subs_menu_html = ''
300 | for sub in subreddits:
301 | sub_url = static_include_path + sub + '/index.html'
302 | subs_menu_html += template_sub_link.replace('###URL_SUB###', sub_url).replace('###SUB###', sub)
303 |
304 | # render selftext
305 | selftext_html = ''
306 | if len(link['selftext']) > 0:
307 | selftext_html = template_selftext.replace('###SELFTEXT###', snudown.markdown(link['selftext'].replace('>','>')))
308 |
309 | # author link
310 | url = static_include_path + 'user/' + link['author'] + '.html'
311 | author_link_html = template_user_url.replace('###URL_AUTHOR###', url).replace('###AUTHOR###', link['author'])
312 |
313 | html_title = template_url.replace('#HREF#', link['url']).replace('#INNER_HTML#', link['title'])
314 | if link['is_self'] is True or link['is_self'].lower() == 'true':
315 | html_title = link['title']
316 |
317 | # render link page
318 | link_data_map = {
319 | '###INCLUDE_PATH###': static_include_path,
320 | '###SUB###': subreddit,
321 | '###TITLE###': link['title'],
322 | '###ID###': link['id'],
323 | '###DATE###': created.strftime('%Y-%m-%d'),
324 | '###ARCHIVE_DATE###': datetime.utcfromtimestamp(int(link['retrieved_on'])).strftime('%Y-%m-%d') if link['retrieved_on'] != '' else 'n/a',
325 | '###SCORE###': str(link['score']),
326 | '###NUM_COMMENTS###': str(link['num_comments']),
327 | '###URL_PROJECT###': url_project,
328 | '###URL_SUBS###': static_include_path + 'index.html',
329 | '###URL_SUB###': static_include_path + subreddit + '/index.html',
330 | '###URL_SUB_CMNT###': static_include_path + subreddit + '/index-' + sort_indexes['num_comments']['slug'] + '/index.html',
331 | '###URL_SUB_DATE###': static_include_path + subreddit + '/index-' + sort_indexes['created_utc']['slug'] + '/index.html',
332 | '###URL_SEARCH###': static_include_path + subreddit + '/search.html',
333 | '###HTML_SUBS_MENU###': subs_menu_html,
334 | '###HTML_SELFTEXT###': selftext_html,
335 | '###HTML_COMMENTS###': comments_html,
336 | '###HTML_AUTHOR_URL###': author_link_html,
337 | '###HTML_TITLE###': html_title,
338 | }
339 | html = template_link
340 | for key, value in link_data_map.items():
341 | html = html.replace(key, value)
342 |
343 | # write html
344 | # reddit: https://www.reddit.com/r/conspiracy/comments/8742iv/happening_now_classmate_former_friend_of/
345 | # archive: r/conspiracy/comments/8/7/4/2/i/v/happening_now_classmate_former_friend_of.html
346 | idpath = '/'.join(list(link['id']))
347 | filepath = link['permalink'].lower().strip('/') + '.html'
348 | filepath = filepath.replace(link['id'], idpath)
349 | if not os.path.isfile(filepath):
350 | os.makedirs(os.path.dirname(filepath), exist_ok=True)
351 | with open(filepath, 'w', encoding='utf-8') as file:
352 | file.write(html)
353 | # print('wrote %s %s' % (created.strftime('%Y-%m-%d'), filepath))
354 |
355 | return True
356 |
357 | def write_subreddit_search_page(subreddit, subs, link_index, stat_sub_filtered_links, stat_sub_comments):
358 | if len(link_index) == 0:
359 | return True
360 |
361 | # name sort?
362 | links = sorted(link_index, key=lambda k: re.sub(r'\W+', '', k['title']).lower())
363 |
364 | # render subreddits list
365 | subs_menu_html = ''
366 | for sub in subs:
367 | sub_url = '../' + sub + '/index.html'
368 | subs_menu_html += template_sub_link.replace('###URL_SUB###', sub_url).replace('###SUB###', sub)
369 |
370 | links_html = ''
371 | for l in links:
372 | link_comments_url = l['permalink'].lower().strip('/').replace('r/' + subreddit + '/', '')
373 | idpath = '/'.join(list(l['id']))
374 | link_comments_url = link_comments_url.replace(l['id'], idpath)
375 | link_comments_url += '.html'
376 | index_link_data_map = {
377 | '###TITLE###': l['title'],
378 | '###URL###': link_comments_url,
379 | }
380 | link_html = template_search_link
381 | for key, value in index_link_data_map.items():
382 | link_html = link_html.replace(key, value)
383 | links_html += link_html + '\n'
384 |
385 | index_page_data_map = {
386 | '###INCLUDE_PATH###': '../',
387 | '###TITLE###': 'search',
388 | '###SUB###': subreddit,
389 | '###ARCH_NUM_POSTS###': str(stat_sub_filtered_links),
390 | '###ARCH_NUM_COMMENTS###': str(stat_sub_comments),
391 | '###URL_SUBS###': '../index.html',
392 | '###URL_PROJECT###': url_project,
393 | '###URL_IDX_SCORE###': 'index.html',
394 | '###URL_IDX_CMNT###': 'index-' + sort_indexes['num_comments']['slug'] + '/index.html',
395 | '###URL_IDX_DATE###': 'index-' + sort_indexes['created_utc']['slug'] + '/index.html',
396 | '###URL_SEARCH###': 'search.html',
397 | '###URL_IDX_SCORE_CSS###': '',
398 | '###URL_IDX_CMNT_CSS###': '',
399 | '###URL_IDX_DATE_CSS###': '',
400 | '###URL_SEARCH_CSS###': 'active',
401 | '###HTML_LINKS###': links_html,
402 | '###HTML_SUBS_MENU###': subs_menu_html,
403 | }
404 | page_html = template_search
405 | for key, value in index_page_data_map.items():
406 | page_html = page_html.replace(key, value)
407 |
408 | # write file
409 | filename = 'search.html'
410 | filepath = 'r/' + subreddit + '/' + filename
411 | if not os.path.isfile(filepath):
412 | os.makedirs(os.path.dirname(filepath), exist_ok=True)
413 | with open(filepath, 'w', encoding='utf-8') as file:
414 | file.write(page_html)
415 | # print('wrote %s, %s links' % (filepath, len(links)))
416 | return True
417 |
418 | def write_user_page(subs, user_index):
419 | if len(user_index.keys()) == 0:
420 | return False
421 |
422 | # subreddits list
423 | subs_menu_html = ''
424 | for sub in subs:
425 | sub_url = '../' + sub['name'] + '/index.html'
426 | subs_menu_html += template_sub_link.replace('###URL_SUB###', sub_url).replace('###SUB###', sub['name'])
427 |
428 | for user in user_index.keys():
429 | links = user_index[user]
430 | links.sort(key=lambda k: (int(k['score']) if k['score'] != '' else sort_indexes['score']['default']), reverse=True)
431 |
432 | links_html = ''
433 | for l in links:
434 |
435 | author_link_html = template_user_url
436 | author_url = l['author'] + '.html'
437 | author_link_html = author_link_html.replace('###URL_AUTHOR###', author_url).replace('###AUTHOR###', l['author'])
438 |
439 | link_comments_url = l['permalink'].lower().replace('/r/', '').strip('/')
440 | link_comments_url = '../' + link_comments_url
441 | idpath = '/'.join(list(l['id']))
442 | link_comments_url = link_comments_url.replace(l['id'], idpath)
443 | link_comments_url += '.html'
444 | link_url = l['url']
445 | if l['is_self'] is True or l['is_self'] == 'True':
446 | link_url = link_comments_url
447 |
448 | link_data_map = {
449 | '###TITLE###': l['title'],
450 | '###URL###': link_url,
451 | '###URL_COMMENTS###': link_comments_url,
452 | '###SCORE###': str(l['score']),
453 | '###NUM_COMMENTS###': str(l['num_comments']) if int(l['num_comments']) > 0 else str(0),
454 | '###DATE###': datetime.utcfromtimestamp(int(l['created_utc'])).strftime('%Y-%m-%d'),
455 | '###SUB###': l['subreddit'],
456 | '###SUB_URL###': '../' + l['subreddit'] + '/index.html',
457 | '###HTML_AUTHOR_URL###': author_link_html,
458 | }
459 | link_html = template_user_page_link
460 | for key, value in link_data_map.items():
461 | link_html = link_html.replace(key, value)
462 | links_html += link_html + '\n'
463 |
464 | page_data_map = {
465 | '###INCLUDE_PATH###': '../',
466 | '###TITLE###': 'user/' + user,
467 | '###ARCH_NUM_POSTS###': str(len(links)),
468 | '###URL_USER###': user + '.html',
469 | '###URL_SUBS###': '../index.html',
470 | '###URL_PROJECT###': url_project,
471 | '###HTML_LINKS###': links_html,
472 | '###HTML_SUBS_MENU###': subs_menu_html,
473 | }
474 | page_html = template_user
475 | for key, value in page_data_map.items():
476 | page_html = page_html.replace(key, value)
477 |
478 | filepath = 'r/user/' + user + '.html'
479 | if not os.path.isfile(filepath):
480 | os.makedirs(os.path.dirname(filepath), exist_ok=True)
481 | with open(filepath, 'w', encoding='utf-8') as file:
482 | file.write(page_html)
483 | # print('wrote %s' % (filepath))
484 |
485 | return True
486 |
487 | def write_index(subs):
488 | if len(subs) == 0:
489 | return False
490 | subs.sort(key=lambda k: k['name'].casefold())
491 |
492 | stat_num_links = 0
493 | links_html = ''
494 | subs_menu_html = ''
495 | for sub in subs:
496 | sub_url = sub['name'] + '/index.html'
497 | links_html += template_index_sub.replace('#URL_SUB#', sub_url).replace('#SUB#', sub['name']).replace('#NUM_LINKS#', str(sub['num_links']))
498 | subs_menu_html += template_sub_link.replace('###URL_SUB###', sub_url).replace('###SUB###', sub['name'])
499 | stat_num_links += sub['num_links']
500 |
501 | index_page_data_map = {
502 | '###INCLUDE_PATH###': '',
503 | '###TITLE###': 'subreddits',
504 | '###URL_SUBS###': 'index.html',
505 | '###URL_PROJECT###': url_project,
506 | '###ARCH_NUM_POSTS###': str(stat_num_links),
507 | '###HTML_LINKS###': links_html,
508 | '###HTML_SUBS_MENU###': subs_menu_html,
509 | }
510 | page_html = template_index
511 | for key, value in index_page_data_map.items():
512 | page_html = page_html.replace(key, value)
513 |
514 | filepath = 'r/index.html'
515 | if not os.path.isfile(filepath):
516 | os.makedirs(os.path.dirname(filepath), exist_ok=True)
517 | with open(filepath, 'w', encoding='utf-8') as file:
518 | file.write(page_html)
519 | # print('wrote %s' % (filepath))
520 |
521 | return True
522 |
523 | # a 'top' comments sort with orphaned comments (incomplete data) rendered last
524 | def sort_comments(comments, hide_deleted_comments=False):
525 | sorted_comments = []
526 | if len(comments) == 0:
527 | return sorted_comments
528 | parent_map = {}
529 | id_map = {}
530 | top_level_comments = []
531 | link_id = comments[0]['link_id']
532 | depth = 0
533 |
534 | for c in comments:
535 | c['depth'] = depth
536 | id_map[c['id']] = c
537 | parent_map[c['id']] = c['parent_id']
538 | # add stickied comments
539 | if c['stickied'].lower() == 'true':
540 | sorted_comments.append(c)
541 | # store top level comments
542 | elif c['parent_id'] == c['link_id']:
543 | top_level_comments.append(c)
544 |
545 | # sort non stickied top level comments
546 | if len(top_level_comments) > 0:
547 | top_level_comments = sorted(top_level_comments, key=lambda k: (int(k['score']) if k['score'] != '' else 1), reverse=True)
548 | sorted_comments += top_level_comments
549 |
550 | # add each top level comment's child comments
551 | sorted_linear_comments = []
552 | for c in sorted_comments:
553 | # only remove deleted comments if no children
554 | if hide_deleted_comments and c['body'] in removed_content_identifiers and 't1_' + c['id'] not in parent_map.values():
555 | pass
556 | else:
557 | sorted_linear_comments.append(c)
558 | child_comments = get_comment_tree_list([], depth + 1, c, id_map, parent_map, hide_deleted_comments)
559 | if len(child_comments) > 0:
560 | sorted_linear_comments += child_comments
561 |
562 | # add orphaned comments
563 | for c in comments:
564 | if c['parent_id'] != link_id and c['parent_id'].replace('t1_', '') not in id_map.keys():
565 | if hide_deleted_comments and c['body'] in removed_content_identifiers:
566 | continue
567 | sorted_linear_comments.append(c)
568 |
569 | # print('sort_comments() in %s out %s show deleted: %s' % (len(comments), len(sorted_comments), hide_deleted_comments))
570 | return sorted_linear_comments
571 |
572 | def get_comment_tree_list(tree, depth, parent_comment, id_map, parent_map, hide_deleted_comments):
573 | parent_id = 't1_' + parent_comment['id']
574 | child_comments = []
575 | for key, value in parent_map.items():
576 | if value == parent_id:
577 | if hide_deleted_comments and id_map[key]['body'] in removed_content_identifiers and 't1_' + key not in parent_map.values():
578 | pass
579 | else:
580 | child_comments.append(id_map[key])
581 |
582 | # sort children by score
583 | # TODO: sort by score and # of child comments
584 | if len(child_comments) > 0:
585 | child_comments = sorted(child_comments, key=lambda k: (int(k['score']) if k['score'] != '' else 1), reverse=True)
586 | for child_comment in child_comments:
587 | child_comment['depth'] = depth
588 | tree.append(child_comment)
589 | tree = get_comment_tree_list(tree, depth + 1, child_comment, id_map, parent_map, hide_deleted_comments)
590 | return tree
591 |
592 | def validate_link(link, min_score=0, min_comments=0):
593 | if not link:
594 | return False
595 | elif not 'id' in link.keys():
596 | return False
597 | # apply multiple conditions as an OR, keep high score low comments and high comment low score links/posts
598 | # TODO this should be configurable
599 | if min_score > 0 and min_comments > 0:
600 | if int(link['score']) < min_score and int(link['num_comments']) < min_comments:
601 | return False
602 | else:
603 | if min_score > 0 and int(link['score']) < min_score:
604 | return False
605 | if min_comments > 0 and int(link['num_comments']) < min_comments:
606 | return False
607 |
608 | return True
609 |
610 | def load_links(date, subreddit, with_comments=False):
611 | links = []
612 | if not date or not subreddit:
613 | return links
614 |
615 | date_path = date.strftime("%Y/%m/%d")
616 | daily_path = 'data/' + subreddit + '/' + date_path
617 | daily_links_path = daily_path + '/' + source_data_links
618 | if os.path.isfile(daily_links_path):
619 | links = []
620 | with open(daily_links_path, 'r', encoding='utf-8') as links_file:
621 | reader = csv.DictReader(links_file)
622 | for link_row in reader:
623 | if with_comments and 'id' in link_row.keys():
624 | comments = []
625 | comments_file_path = daily_path + '/' + link_row['id'] + '.csv'
626 | if os.path.isfile(comments_file_path):
627 | with open(comments_file_path, 'r', encoding='utf-8') as comments_file:
628 | reader = csv.DictReader(comments_file)
629 | for comment_row in reader:
630 | comments.append(comment_row)
631 | link_row['comments'] = comments
632 | links.append(link_row)
633 | return links
634 |
635 | def get_subs():
636 | subs = []
637 | if not os.path.isdir('data'):
638 | print('ERROR: no data, run fetch_links.py first')
639 | return subs
640 | for d in os.listdir('data'):
641 | if os.path.isdir('data' + '/' + d):
642 | subs.append(d.lower())
643 | return subs
644 |
645 | def get_pager_html(page_num=1, pages=1):
646 | html_pager = ''
647 |
648 | # previous
649 | css = ''
650 | if page_num == 1:
651 | css = 'disabled'
652 | url = 'index'
653 | if page_num - 1 > 1:
654 | url += '-' + str(page_num - 1)
655 | url += '.html'
656 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', '‹').replace('#CSS_CLASS#', css)
657 |
658 | # skip back
659 | css = ''
660 | prev_skip = page_num - pager_skip
661 | if prev_skip < 1:
662 | prev_skip = 1
663 | if page_num == 1:
664 | css = 'disabled'
665 | url = 'index'
666 | if prev_skip > 1:
667 | url += '-' + str(prev_skip)
668 | url += '.html'
669 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', '‹‹').replace('#CSS_CLASS#', css)
670 |
671 | # skip back far
672 | css = ''
673 | prev_skip = page_num - pager_skip_long
674 | if prev_skip < 1:
675 | prev_skip = 1
676 | if page_num == 1:
677 | css += ' disabled'
678 | url = 'index'
679 | if prev_skip > 1:
680 | url += '-' + str(prev_skip)
681 | url += '.html'
682 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', '‹‹‹').replace('#CSS_CLASS#', css)
683 |
684 | # n-1
685 | start = -2
686 | if page_num + 1 > pages:
687 | start -= 1
688 | if page_num + 2 > pages:
689 | start -= 1
690 | for prev_page_num in range(start,0):
691 | if page_num + prev_page_num > 0:
692 | css = ''
693 | url = 'index'
694 | if page_num + prev_page_num > 1:
695 | url += '-' + str(page_num + prev_page_num)
696 | url += '.html'
697 | if prev_page_num < -1:
698 | css = 'd-none d-sm-block'
699 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', str(page_num + prev_page_num)).replace('#CSS_CLASS#', css)
700 | # n
701 | url = 'index'
702 | if page_num > 1:
703 | url += '-' + str(page_num)
704 | url += '.html'
705 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', str(page_num)).replace('#CSS_CLASS#', 'active')
706 | # n + 1
707 | css = ''
708 | end = 3
709 | if page_num -1 < 1:
710 | end += 1
711 | if page_num - 2 < 1:
712 | end += 1
713 | for next_page_num in range(1,end):
714 | if page_num + next_page_num <= pages:
715 | if next_page_num > 1:
716 | css = 'd-none d-sm-block'
717 | html_pager += template_index_pager_link.replace('#URL#', 'index' + '-' + str(page_num + next_page_num) + '.html').replace('#TEXT#', str(page_num + next_page_num)).replace('#CSS_CLASS#', css)
718 |
719 | # skip forward far
720 | next_skip = page_num + pager_skip_long
721 | css = ''
722 | if page_num == pages:
723 | css += ' disabled'
724 | if next_skip > pages:
725 | next_skip = pages
726 | url = 'index'
727 | if next_skip > 1:
728 | url += '-' + str(next_skip)
729 | url += '.html'
730 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', '›››').replace('#CSS_CLASS#', css)
731 |
732 | # skip forward
733 | next_skip = page_num + pager_skip
734 | css = ''
735 | if page_num == pages:
736 | css = 'disabled'
737 | if next_skip > pages:
738 | next_skip = pages
739 | url = 'index'
740 | if next_skip > 1:
741 | url += '-' + str(next_skip)
742 | url += '.html'
743 | html_pager += template_index_pager_link.replace('#URL#', url).replace('#TEXT#', '››').replace('#CSS_CLASS#', css)
744 |
745 | # next
746 | css = ''
747 | next_num = page_num + 1
748 | if page_num == pages:
749 | css = 'disabled'
750 | next_num = pages
751 | html_pager += template_index_pager_link.replace('#URL#', 'index' + '-' + str(next_num) + '.html').replace('#TEXT#', '›').replace('#CSS_CLASS#', css)
752 |
753 | return html_pager
754 |
755 | def chunks(l, n):
756 | """Yield successive n-sized chunks from l."""
757 | for i in range(0, len(l), n):
758 | yield l[i:i + n]
759 |
760 | if __name__ == '__main__':
761 | parser=argparse.ArgumentParser()
762 | parser.add_argument('--min-score', default=0, help='limit post rendering, default 0')
763 | parser.add_argument('--min-comments', default=0, help='limit post rendering, default 0')
764 | parser.add_argument('--hide-deleted-comments', action='store_true', help='exclude deleted and removed comments where possible')
765 | args=parser.parse_args()
766 |
767 | hide_deleted_comments = False
768 | if args.hide_deleted_comments:
769 | hide_deleted_comments = True
770 |
771 | args.min_score = int(args.min_score)
772 | args.min_comments = int(args.min_comments)
773 |
774 | generate_html(args.min_score, args.min_comments, hide_deleted_comments)
775 |
--------------------------------------------------------------------------------
/r/static/js/bootstrap.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v4.1.3 (https://getbootstrap.com/)
3 | * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
5 | */
6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e(t.bootstrap={},t.jQuery,t.Popper)}(this,function(t,e,h){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)P(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!(Ie={AUTO:"auto",TOP:"top",RIGHT:"right",BOTTOM:"bottom",LEFT:"left"}),selector:!(Se={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)"}),placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},we="out",Ne={HIDE:"hide"+Ee,HIDDEN:"hidden"+Ee,SHOW:(De="show")+Ee,SHOWN:"shown"+Ee,INSERTED:"inserted"+Ee,CLICK:"click"+Ee,FOCUSIN:"focusin"+Ee,FOCUSOUT:"focusout"+Ee,MOUSEENTER:"mouseenter"+Ee,MOUSELEAVE:"mouseleave"+Ee},Oe="fade",ke="show",Pe=".tooltip-inner",je=".arrow",He="hover",Le="focus",Re="click",xe="manual",We=function(){function i(t,e){if("undefined"==typeof h)throw new TypeError("Bootstrap tooltips require Popper.js (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=pe(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(pe(this.getTipElement()).hasClass(ke))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),pe.removeData(this.element,this.constructor.DATA_KEY),pe(this.element).off(this.constructor.EVENT_KEY),pe(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&pe(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===pe(this.element).css("display"))throw new Error("Please use show on visible elements");var t=pe.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){pe(this.element).trigger(t);var n=pe.contains(this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!n)return;var i=this.getTipElement(),r=Fn.getUID(this.constructor.NAME);i.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&pe(i).addClass(Oe);var o="function"==typeof this.config.placement?this.config.placement.call(this,i,this.element):this.config.placement,s=this._getAttachment(o);this.addAttachmentClass(s);var a=!1===this.config.container?document.body:pe(document).find(this.config.container);pe(i).data(this.constructor.DATA_KEY,this),pe.contains(this.element.ownerDocument.documentElement,this.tip)||pe(i).appendTo(a),pe(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new h(this.element,i,{placement:s,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:je},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){e._handlePopperPlacementChange(t)}}),pe(i).addClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().on("mouseover",null,pe.noop);var l=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,pe(e.element).trigger(e.constructor.Event.SHOWN),t===we&&e._leave(null,e)};if(pe(this.tip).hasClass(Oe)){var c=Fn.getTransitionDurationFromElement(this.tip);pe(this.tip).one(Fn.TRANSITION_END,l).emulateTransitionEnd(c)}else l()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=pe.Event(this.constructor.Event.HIDE),r=function(){e._hoverState!==De&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),pe(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(pe(this.element).trigger(i),!i.isDefaultPrevented()){if(pe(n).removeClass(ke),"ontouchstart"in document.documentElement&&pe(document.body).children().off("mouseover",null,pe.noop),this._activeTrigger[Re]=!1,this._activeTrigger[Le]=!1,this._activeTrigger[He]=!1,pe(this.tip).hasClass(Oe)){var o=Fn.getTransitionDurationFromElement(n);pe(n).one(Fn.TRANSITION_END,r).emulateTransitionEnd(o)}else r();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){pe(this.getTipElement()).addClass(Te+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||pe(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(pe(t.querySelectorAll(Pe)),this.getTitle()),pe(t).removeClass(Oe+" "+ke)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?pe(e).parent().is(t)||t.empty().append(e):t.text(pe(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getAttachment=function(t){return Ie[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)pe(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xe){var e=t===He?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===He?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;pe(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}pe(i.element).closest(".modal").on("hide.bs.modal",function(){return i.hide()})}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Le:He]=!0),pe(e.getTipElement()).hasClass(ke)||e._hoverState===De?e._hoverState=De:(clearTimeout(e._timeout),e._hoverState=De,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===De&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||pe(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),pe(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Le:He]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=we,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===we&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,pe(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),Fn.typeCheckConfig(ve,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=pe(this.getTipElement()),e=t.attr("class").match(be);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(pe(t).removeClass(Oe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=pe(this).data(ye),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),pe(this).data(ye,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}},{key:"Default",get:function(){return Ae}},{key:"NAME",get:function(){return ve}},{key:"DATA_KEY",get:function(){return ye}},{key:"Event",get:function(){return Ne}},{key:"EVENT_KEY",get:function(){return Ee}},{key:"DefaultType",get:function(){return Se}}]),i}(),pe.fn[ve]=We._jQueryInterface,pe.fn[ve].Constructor=We,pe.fn[ve].noConflict=function(){return pe.fn[ve]=Ce,We._jQueryInterface},We),Jn=(qe="popover",Ke="."+(Fe="bs.popover"),Me=(Ue=e).fn[qe],Qe="bs-popover",Be=new RegExp("(^|\\s)"+Qe+"\\S+","g"),Ve=l({},zn.Default,{placement:"right",trigger:"click",content:"",template:''}),Ye=l({},zn.DefaultType,{content:"(string|element|function)"}),ze="fade",Ze=".popover-header",Ge=".popover-body",$e={HIDE:"hide"+Ke,HIDDEN:"hidden"+Ke,SHOW:(Je="show")+Ke,SHOWN:"shown"+Ke,INSERTED:"inserted"+Ke,CLICK:"click"+Ke,FOCUSIN:"focusin"+Ke,FOCUSOUT:"focusout"+Ke,MOUSEENTER:"mouseenter"+Ke,MOUSELEAVE:"mouseleave"+Ke},Xe=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var r=i.prototype;return r.isWithContent=function(){return this.getTitle()||this._getContent()},r.addAttachmentClass=function(t){Ue(this.getTipElement()).addClass(Qe+"-"+t)},r.getTipElement=function(){return this.tip=this.tip||Ue(this.config.template)[0],this.tip},r.setContent=function(){var t=Ue(this.getTipElement());this.setElementContent(t.find(Ze),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Ge),e),t.removeClass(ze+" "+Je)},r._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},r._cleanTipClass=function(){var t=Ue(this.getTipElement()),e=t.attr("class").match(Be);null!==e&&0=this._offsets[r]&&("undefined"==typeof this._offsets[r+1]||t li > .active",xn='[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',Wn=".dropdown-toggle",Un="> .dropdown-menu .active",qn=function(){function i(t){this._element=t}var t=i.prototype;return t.show=function(){var n=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&bn(this._element).hasClass(Nn)||bn(this._element).hasClass(On))){var t,i,e=bn(this._element).closest(Hn)[0],r=Fn.getSelectorFromElement(this._element);if(e){var o="UL"===e.nodeName?Rn:Ln;i=(i=bn.makeArray(bn(e).find(o)))[i.length-1]}var s=bn.Event(Dn.HIDE,{relatedTarget:this._element}),a=bn.Event(Dn.SHOW,{relatedTarget:i});if(i&&bn(i).trigger(s),bn(this._element).trigger(a),!a.isDefaultPrevented()&&!s.isDefaultPrevented()){r&&(t=document.querySelector(r)),this._activate(this._element,e);var l=function(){var t=bn.Event(Dn.HIDDEN,{relatedTarget:n._element}),e=bn.Event(Dn.SHOWN,{relatedTarget:i});bn(i).trigger(t),bn(n._element).trigger(e)};t?this._activate(t,t.parentNode,l):l()}}},t.dispose=function(){bn.removeData(this._element,Sn),this._element=null},t._activate=function(t,e,n){var i=this,r=("UL"===e.nodeName?bn(e).find(Rn):bn(e).children(Ln))[0],o=n&&r&&bn(r).hasClass(kn),s=function(){return i._transitionComplete(t,r,n)};if(r&&o){var a=Fn.getTransitionDurationFromElement(r);bn(r).one(Fn.TRANSITION_END,s).emulateTransitionEnd(a)}else s()},t._transitionComplete=function(t,e,n){if(e){bn(e).removeClass(Pn+" "+Nn);var i=bn(e.parentNode).find(Un)[0];i&&bn(i).removeClass(Nn),"tab"===e.getAttribute("role")&&e.setAttribute("aria-selected",!1)}if(bn(t).addClass(Nn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),Fn.reflow(t),bn(t).addClass(Pn),t.parentNode&&bn(t.parentNode).hasClass(wn)){var r=bn(t).closest(jn)[0];if(r){var o=[].slice.call(r.querySelectorAll(Wn));bn(o).addClass(Nn)}t.setAttribute("aria-expanded",!0)}n&&n()},i._jQueryInterface=function(n){return this.each(function(){var t=bn(this),e=t.data(Sn);if(e||(e=new i(this),t.data(Sn,e)),"string"==typeof n){if("undefined"==typeof e[n])throw new TypeError('No method named "'+n+'"');e[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.1.3"}}]),i}(),bn(document).on(Dn.CLICK_DATA_API,xn,function(t){t.preventDefault(),qn._jQueryInterface.call(bn(this),"show")}),bn.fn.tab=qn._jQueryInterface,bn.fn.tab.Constructor=qn,bn.fn.tab.noConflict=function(){return bn.fn.tab=An,qn._jQueryInterface},qn);!function(t){if("undefined"==typeof t)throw new TypeError("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1===e[0]&&9===e[1]&&e[2]<1||4<=e[0])throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(e),t.Util=Fn,t.Alert=Kn,t.Button=Mn,t.Carousel=Qn,t.Collapse=Bn,t.Dropdown=Vn,t.Modal=Yn,t.Popover=Jn,t.Scrollspy=Zn,t.Tab=Gn,t.Tooltip=zn,Object.defineProperty(t,"__esModule",{value:!0})});
7 | //# sourceMappingURL=bootstrap.min.js.map
--------------------------------------------------------------------------------
/r/static/js/jquery-3.3.1.slim.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v3.3.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector | (c) JS Foundation and other contributors | jquery.org/license */
2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){"use strict";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,u=n.push,s=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,d=f.toString,p=d.call(Object),h={},g=function e(t){return"function"==typeof t&&"number"!=typeof t.nodeType},v=function e(t){return null!=t&&t===t.window},y={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement("script");if(o.text=e,n)for(i in y)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function b(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?l[c.call(e)]||"object":typeof e}var x="3.3.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-event/ajax,-effects,-effects/Tween,-effects/animatedSelector",w=function(e,t){return new w.fn.init(e,t)},C=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g;w.fn=w.prototype={jquery:x,constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,u,s,l,c,f,d,p,h,g,v,y,m,b,x="sizzle"+1*new Date,w=e.document,C=0,T=0,E=ae(),N=ae(),k=ae(),A=function(e,t){return e===t&&(f=!0),0},D={}.hasOwnProperty,S=[],L=S.pop,j=S.push,q=S.push,O=S.slice,P=function(e,t){for(var n=0,r=e.length;n+~]|"+I+")"+I+"*"),_=new RegExp("="+I+"*([^\\]'\"]*?)"+I+"*\\]","g"),U=new RegExp(M),V=new RegExp("^"+R+"$"),X={ID:new RegExp("^#("+R+")"),CLASS:new RegExp("^\\.("+R+")"),TAG:new RegExp("^("+R+"|[*])"),ATTR:new RegExp("^"+B),PSEUDO:new RegExp("^"+M),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+I+"*(even|odd|(([+-]|)(\\d*)n|)"+I+"*(?:([+-]|)"+I+"*(\\d+)|))"+I+"*\\)|)","i"),bool:new RegExp("^(?:"+H+")$","i"),needsContext:new RegExp("^"+I+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+I+"*((?:-\\d)?\\d*)"+I+"*\\)|)(?=[^-]|$)","i")},Q=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,G=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,J=/[+~]/,Z=new RegExp("\\\\([\\da-f]{1,6}"+I+"?|("+I+")|.)","ig"),ee=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)},te=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ne=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},re=function(){d()},ie=me(function(e){return!0===e.disabled&&("form"in e||"label"in e)},{dir:"parentNode",next:"legend"});try{q.apply(S=O.call(w.childNodes),w.childNodes),S[w.childNodes.length].nodeType}catch(e){q={apply:S.length?function(e,t){j.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function oe(e,t,r,i){var o,u,l,c,f,h,y,m=t&&t.ownerDocument,C=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==C&&9!==C&&11!==C)return r;if(!i&&((t?t.ownerDocument||t:w)!==p&&d(t),t=t||p,g)){if(11!==C&&(f=K.exec(e)))if(o=f[1]){if(9===C){if(!(l=t.getElementById(o)))return r;if(l.id===o)return r.push(l),r}else if(m&&(l=m.getElementById(o))&&b(t,l)&&l.id===o)return r.push(l),r}else{if(f[2])return q.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return q.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!k[e+" "]&&(!v||!v.test(e))){if(1!==C)m=t,y=e;else if("object"!==t.nodeName.toLowerCase()){(c=t.getAttribute("id"))?c=c.replace(te,ne):t.setAttribute("id",c=x),u=(h=a(e)).length;while(u--)h[u]="#"+c+" "+ye(h[u]);y=h.join(","),m=J.test(e)&&ge(t.parentNode)||t}if(y)try{return q.apply(r,m.querySelectorAll(y)),r}catch(e){}finally{c===x&&t.removeAttribute("id")}}}return s(e.replace($,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function ue(e){return e[x]=!0,e}function se(e){var t=p.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function le(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function ce(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function fe(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function de(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pe(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ie(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return ue(function(t){return t=+t,ue(function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}n=oe.support={},o=oe.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},d=oe.setDocument=function(e){var t,i,a=e?e.ownerDocument||e:w;return a!==p&&9===a.nodeType&&a.documentElement?(p=a,h=p.documentElement,g=!o(p),w!==p&&(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",re,!1):i.attachEvent&&i.attachEvent("onunload",re)),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=G.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=x,!p.getElementsByName||!p.getElementsByName(x).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(Z,ee);return function(e){var n="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&g)return t.getElementsByClassName(e)},y=[],v=[],(n.qsa=G.test(p.querySelectorAll))&&(se(function(e){h.appendChild(e).innerHTML=" ",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+I+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+I+"*(?:value|"+H+")"),e.querySelectorAll("[id~="+x+"-]").length||v.push("~="),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+x+"+*").length||v.push(".#.+[+~]")}),se(function(e){e.innerHTML=" ";var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+I+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(n.matchesSelector=G.test(m=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=m.call(e,"*"),m.call(e,"[s!='']:x"),y.push("!=",M)}),v=v.length&&new RegExp(v.join("|")),y=y.length&&new RegExp(y.join("|")),t=G.test(h.compareDocumentPosition),b=t||G.test(h.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},A=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e===p||e.ownerDocument===w&&b(w,e)?-1:t===p||t.ownerDocument===w&&b(w,t)?1:c?P(c,e)-P(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],u=[t];if(!i||!o)return e===p?-1:t===p?1:i?-1:o?1:c?P(c,e)-P(c,t):0;if(i===o)return ce(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)u.unshift(n);while(a[r]===u[r])r++;return r?ce(a[r],u[r]):a[r]===w?-1:u[r]===w?1:0},p):p},oe.matches=function(e,t){return oe(e,null,null,t)},oe.matchesSelector=function(e,t){if((e.ownerDocument||e)!==p&&d(e),t=t.replace(_,"='$1']"),n.matchesSelector&&g&&!k[t+" "]&&(!y||!y.test(t))&&(!v||!v.test(t)))try{var r=m.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){}return oe(t,p,null,[e]).length>0},oe.contains=function(e,t){return(e.ownerDocument||e)!==p&&d(e),b(e,t)},oe.attr=function(e,t){(e.ownerDocument||e)!==p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&D.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},oe.escape=function(e){return(e+"").replace(te,ne)},oe.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},oe.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(A),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return c=null,e},i=oe.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=oe.selectors={cacheLength:50,createPseudo:ue,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Z,ee),e[3]=(e[3]||e[4]||e[5]||"").replace(Z,ee),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||oe.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&oe.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return X.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&U.test(n)&&(t=a(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Z,ee).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+I+")"+e+"("+I+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=oe.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(W," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),u="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,s){var l,c,f,d,p,h,g=o!==a?"nextSibling":"previousSibling",v=t.parentNode,y=u&&t.nodeName.toLowerCase(),m=!s&&!u,b=!1;if(v){if(o){while(g){d=t;while(d=d[g])if(u?d.nodeName.toLowerCase()===y:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?v.firstChild:v.lastChild],a&&m){b=(p=(l=(c=(f=(d=v)[x]||(d[x]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&l[1])&&l[2],d=p&&v.childNodes[p];while(d=++p&&d&&d[g]||(b=p=0)||h.pop())if(1===d.nodeType&&++b&&d===t){c[e]=[C,p,b];break}}else if(m&&(b=p=(l=(c=(f=(d=t)[x]||(d[x]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&l[1]),!1===b)while(d=++p&&d&&d[g]||(b=p=0)||h.pop())if((u?d.nodeName.toLowerCase()===y:1===d.nodeType)&&++b&&(m&&((c=(f=d[x]||(d[x]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[C,b]),d===t))break;return(b-=i)===r||b%r==0&&b/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||oe.error("unsupported pseudo: "+e);return i[x]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ue(function(e,n){var r,o=i(e,t),a=o.length;while(a--)e[r=P(e,o[a])]=!(n[r]=o[a])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ue(function(e){var t=[],n=[],r=u(e.replace($,"$1"));return r[x]?ue(function(e,t,n,i){var o,a=r(e,null,i,[]),u=e.length;while(u--)(o=a[u])&&(e[u]=!(t[u]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ue(function(e){return function(t){return oe(e,t).length>0}}),contains:ue(function(e){return e=e.replace(Z,ee),function(t){return(t.textContent||t.innerText||i(t)).indexOf(e)>-1}}),lang:ue(function(e){return V.test(e||"")||oe.error("unsupported lang: "+e),e=e.replace(Z,ee).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:pe(!1),disabled:pe(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return Q.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function xe(e,t,n){for(var r=0,i=t.length;r-1&&(o[l]=!(a[l]=f))}}else y=we(y===a?y.splice(h,y.length):y),i?i(null,a,y,s):q.apply(a,y)})}function Te(e){for(var t,n,i,o=e.length,a=r.relative[e[0].type],u=a||r.relative[" "],s=a?1:0,c=me(function(e){return e===t},u,!0),f=me(function(e){return P(t,e)>-1},u,!0),d=[function(e,n,r){var i=!a&&(r||n!==l)||((t=n).nodeType?c(e,n,r):f(e,n,r));return t=null,i}];s1&&be(d),s>1&&ye(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace($,"$1"),n,s0,i=e.length>0,o=function(o,a,u,s,c){var f,h,v,y=0,m="0",b=o&&[],x=[],w=l,T=o||i&&r.find.TAG("*",c),E=C+=null==w?1:Math.random()||.1,N=T.length;for(c&&(l=a===p||a||c);m!==N&&null!=(f=T[m]);m++){if(i&&f){h=0,a||f.ownerDocument===p||(d(f),u=!g);while(v=e[h++])if(v(f,a||p,u)){s.push(f);break}c&&(C=E)}n&&((f=!v&&f)&&y--,o&&b.push(f))}if(y+=m,n&&m!==y){h=0;while(v=t[h++])v(b,x,a,u);if(o){if(y>0)while(m--)b[m]||x[m]||(x[m]=L.call(s));x=we(x)}q.apply(s,x),c&&!o&&x.length>0&&y+t.length>1&&oe.uniqueSort(s)}return c&&(C=E,l=w),b};return n?ue(o):o}return u=oe.compile=function(e,t){var n,r=[],i=[],o=k[e+" "];if(!o){t||(t=a(e)),n=t.length;while(n--)(o=Te(t[n]))[x]?r.push(o):i.push(o);(o=k(e,Ee(i,r))).selector=e}return o},s=oe.select=function(e,t,n,i){var o,s,l,c,f,d="function"==typeof e&&e,p=!i&&a(e=d.selector||e);if(n=n||[],1===p.length){if((s=p[0]=p[0].slice(0)).length>2&&"ID"===(l=s[0]).type&&9===t.nodeType&&g&&r.relative[s[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(Z,ee),t)||[])[0]))return n;d&&(t=t.parentNode),e=e.slice(s.shift().value.length)}o=X.needsContext.test(e)?0:s.length;while(o--){if(l=s[o],r.relative[c=l.type])break;if((f=r.find[c])&&(i=f(l.matches[0].replace(Z,ee),J.test(s[0].type)&&ge(t.parentNode)||t))){if(s.splice(o,1),!(e=i.length&&ye(s)))return q.apply(n,i),n;break}}}return(d||u(e,p))(i,t,!g,n,!t||J.test(e)&&ge(t.parentNode)||t),n},n.sortStable=x.split("").sort(A).join("")===x,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("fieldset"))}),se(function(e){return e.innerHTML=" ","#"===e.firstChild.getAttribute("href")})||le("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML=" ",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||le("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||le(H,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),oe}(e);w.find=E,w.expr=E.selectors,w.expr[":"]=w.expr.pseudos,w.uniqueSort=w.unique=E.uniqueSort,w.text=E.getText,w.isXMLDoc=E.isXML,w.contains=E.contains,w.escapeSelector=E.escape;var N=function(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&w(e).is(n))break;r.push(e)}return r},k=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},A=w.expr.match.needsContext;function D(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var S=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function L(e,t,n){return g(t)?w.grep(e,function(e,r){return!!t.call(e,r,e)!==n}):t.nodeType?w.grep(e,function(e){return e===t!==n}):"string"!=typeof t?w.grep(e,function(e){return s.call(t,e)>-1!==n}):w.filter(t,e,n)}w.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?w.find.matchesSelector(r,e)?[r]:[]:w.find.matches(e,w.grep(t,function(e){return 1===e.nodeType}))},w.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(w(e).filter(function(){for(t=0;t1?w.uniqueSort(n):n},filter:function(e){return this.pushStack(L(this,e||[],!1))},not:function(e){return this.pushStack(L(this,e||[],!0))},is:function(e){return!!L(this,"string"==typeof e&&A.test(e)?w(e):e||[],!1).length}});var j,q=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(w.fn.init=function(e,t,n){var i,o;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(i="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:q.exec(e))||!i[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(i[1]){if(t=t instanceof w?t[0]:t,w.merge(this,w.parseHTML(i[1],t&&t.nodeType?t.ownerDocument||t:r,!0)),S.test(i[1])&&w.isPlainObject(t))for(i in t)g(this[i])?this[i](t[i]):this.attr(i,t[i]);return this}return(o=r.getElementById(i[2]))&&(this[0]=o,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):g(e)?void 0!==n.ready?n.ready(e):e(w):w.makeArray(e,this)}).prototype=w.fn,j=w(r);var O=/^(?:parents|prev(?:Until|All))/,P={children:!0,contents:!0,next:!0,prev:!0};w.fn.extend({has:function(e){var t=w(e,this),n=t.length;return this.filter(function(){for(var e=0;e-1:1===n.nodeType&&w.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?w.uniqueSort(o):o)},index:function(e){return e?"string"==typeof e?s.call(w(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(w.uniqueSort(w.merge(this.get(),w(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}});function H(e,t){while((e=e[t])&&1!==e.nodeType);return e}w.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return N(e,"parentNode")},parentsUntil:function(e,t,n){return N(e,"parentNode",n)},next:function(e){return H(e,"nextSibling")},prev:function(e){return H(e,"previousSibling")},nextAll:function(e){return N(e,"nextSibling")},prevAll:function(e){return N(e,"previousSibling")},nextUntil:function(e,t,n){return N(e,"nextSibling",n)},prevUntil:function(e,t,n){return N(e,"previousSibling",n)},siblings:function(e){return k((e.parentNode||{}).firstChild,e)},children:function(e){return k(e.firstChild)},contents:function(e){return D(e,"iframe")?e.contentDocument:(D(e,"template")&&(e=e.content||e),w.merge([],e.childNodes))}},function(e,t){w.fn[e]=function(n,r){var i=w.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=w.filter(r,i)),this.length>1&&(P[e]||w.uniqueSort(i),O.test(e)&&i.reverse()),this.pushStack(i)}});var I=/[^\x20\t\r\n\f]+/g;function R(e){var t={};return w.each(e.match(I)||[],function(e,n){t[n]=!0}),t}w.Callbacks=function(e){e="string"==typeof e?R(e):w.extend({},e);var t,n,r,i,o=[],a=[],u=-1,s=function(){for(i=i||e.once,r=t=!0;a.length;u=-1){n=a.shift();while(++u-1)o.splice(n,1),n<=u&&u--}),this},has:function(e){return e?w.inArray(e,o)>-1:o.length>0},empty:function(){return o&&(o=[]),this},disable:function(){return i=a=[],o=n="",this},disabled:function(){return!o},lock:function(){return i=a=[],n||t||(o=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],a.push(n),t||s()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l};function B(e){return e}function M(e){throw e}function W(e,t,n,r){var i;try{e&&g(i=e.promise)?i.call(e).done(t).fail(n):e&&g(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}w.extend({Deferred:function(t){var n=[["notify","progress",w.Callbacks("memory"),w.Callbacks("memory"),2],["resolve","done",w.Callbacks("once memory"),w.Callbacks("once memory"),0,"resolved"],["reject","fail",w.Callbacks("once memory"),w.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},"catch":function(e){return i.then(null,e)},pipe:function(){var e=arguments;return w.Deferred(function(t){w.each(n,function(n,r){var i=g(e[r[4]])&&e[r[4]];o[r[1]](function(){var e=i&&i.apply(this,arguments);e&&g(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)})}),e=null}).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var u=this,s=arguments,l=function(){var e,l;if(!(t=o&&(r!==M&&(u=void 0,s=[e]),n.rejectWith(u,s))}};t?c():(w.Deferred.getStackHook&&(c.stackTrace=w.Deferred.getStackHook()),e.setTimeout(c))}}return w.Deferred(function(e){n[0][3].add(a(0,e,g(i)?i:B,e.notifyWith)),n[1][3].add(a(0,e,g(t)?t:B)),n[2][3].add(a(0,e,g(r)?r:M))}).promise()},promise:function(e){return null!=e?w.extend(e,i):i}},o={};return w.each(n,function(e,t){var a=t[2],u=t[5];i[t[1]]=a.add,u&&a.add(function(){r=u},n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+"With"](this===o?void 0:this,arguments),this},o[t[0]+"With"]=a.fireWith}),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),i=o.call(arguments),a=w.Deferred(),u=function(e){return function(n){r[e]=this,i[e]=arguments.length>1?o.call(arguments):n,--t||a.resolveWith(r,i)}};if(t<=1&&(W(e,a.done(u(n)).resolve,a.reject,!t),"pending"===a.state()||g(i[n]&&i[n].then)))return a.then();while(n--)W(i[n],u(n),a.reject);return a.promise()}});var $=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;w.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&$.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},w.readyException=function(t){e.setTimeout(function(){throw t})};var F=w.Deferred();w.fn.ready=function(e){return F.then(e)["catch"](function(e){w.readyException(e)}),this},w.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--w.readyWait:w.isReady)||(w.isReady=!0,!0!==e&&--w.readyWait>0||F.resolveWith(r,[w]))}}),w.ready.then=F.then;function z(){r.removeEventListener("DOMContentLoaded",z),e.removeEventListener("load",z),w.ready()}"complete"===r.readyState||"loading"!==r.readyState&&!r.documentElement.doScroll?e.setTimeout(w.ready):(r.addEventListener("DOMContentLoaded",z),e.addEventListener("load",z));var _=function(e,t,n,r,i,o,a){var u=0,s=e.length,l=null==n;if("object"===b(n)){i=!0;for(u in n)_(e,t,u,n[u],!0,o,a)}else if(void 0!==r&&(i=!0,g(r)||(a=!0),l&&(a?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(w(e),n)})),t))for(;u1,null,!0)},removeData:function(e){return this.each(function(){J.remove(this,e)})}}),w.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=K.get(e,t),n&&(!r||Array.isArray(n)?r=K.access(e,t,w.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=w.queue(e,t),r=n.length,i=n.shift(),o=w._queueHooks(e,t),a=function(){w.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return K.get(e,n)||K.access(e,n,{empty:w.Callbacks("once memory").add(function(){K.remove(e,[t+"queue",n])})})}}),w.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]+)/i,he=/^$|^module$|\/(?:java|ecma)script/i,ge={option:[1,""," "],thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};ge.optgroup=ge.option,ge.tbody=ge.tfoot=ge.colgroup=ge.caption=ge.thead,ge.th=ge.td;function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&D(e,t)?w.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n-1)i&&i.push(o);else if(l=w.contains(o.ownerDocument,o),a=ve(f.appendChild(o),"script"),l&&ye(a),n){c=0;while(o=a[c++])he.test(o.type||"")&&n.push(o)}return f}!function(){var e=r.createDocumentFragment().appendChild(r.createElement("div")),t=r.createElement("input");t.setAttribute("type","radio"),t.setAttribute("checked","checked"),t.setAttribute("name","t"),e.appendChild(t),h.checkClone=e.cloneNode(!0).cloneNode(!0).lastChild.checked,e.innerHTML="",h.noCloneChecked=!!e.cloneNode(!0).lastChild.defaultValue}();var xe=r.documentElement,we=/^key/,Ce=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,Te=/^([^.]*)(?:\.(.+)|)/;function Ee(){return!0}function Ne(){return!1}function ke(){try{return r.activeElement}catch(e){}}function Ae(e,t,n,r,i,o){var a,u;if("object"==typeof t){"string"!=typeof n&&(r=r||n,n=void 0);for(u in t)Ae(e,u,n,r,t[u],o);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ne;else if(!i)return e;return 1===o&&(a=i,(i=function(e){return w().off(e),a.apply(this,arguments)}).guid=a.guid||(a.guid=w.guid++)),e.each(function(){w.event.add(this,t,i,r,n)})}w.event={global:{},add:function(e,t,n,r,i){var o,a,u,s,l,c,f,d,p,h,g,v=K.get(e);if(v){n.handler&&(n=(o=n).handler,i=o.selector),i&&w.find.matchesSelector(xe,i),n.guid||(n.guid=w.guid++),(s=v.events)||(s=v.events={}),(a=v.handle)||(a=v.handle=function(t){return"undefined"!=typeof w&&w.event.triggered!==t.type?w.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(I)||[""]).length;while(l--)p=g=(u=Te.exec(t[l])||[])[1],h=(u[2]||"").split(".").sort(),p&&(f=w.event.special[p]||{},p=(i?f.delegateType:f.bindType)||p,f=w.event.special[p]||{},c=w.extend({type:p,origType:g,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&w.expr.match.needsContext.test(i),namespace:h.join(".")},o),(d=s[p])||((d=s[p]=[]).delegateCount=0,f.setup&&!1!==f.setup.call(e,r,h,a)||e.addEventListener&&e.addEventListener(p,a)),f.add&&(f.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),w.event.global[p]=!0)}},remove:function(e,t,n,r,i){var o,a,u,s,l,c,f,d,p,h,g,v=K.hasData(e)&&K.get(e);if(v&&(s=v.events)){l=(t=(t||"").match(I)||[""]).length;while(l--)if(u=Te.exec(t[l])||[],p=g=u[1],h=(u[2]||"").split(".").sort(),p){f=w.event.special[p]||{},d=s[p=(r?f.delegateType:f.bindType)||p]||[],u=u[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),a=o=d.length;while(o--)c=d[o],!i&&g!==c.origType||n&&n.guid!==c.guid||u&&!u.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(o,1),c.selector&&d.delegateCount--,f.remove&&f.remove.call(e,c));a&&!d.length&&(f.teardown&&!1!==f.teardown.call(e,h,v.handle)||w.removeEvent(e,p,v.handle),delete s[p])}else for(p in s)w.event.remove(e,p+t[l],n,r,!0);w.isEmptyObject(s)&&K.remove(e,"handle events")}},dispatch:function(e){var t=w.event.fix(e),n,r,i,o,a,u,s=new Array(arguments.length),l=(K.get(this,"events")||{})[t.type]||[],c=w.event.special[t.type]||{};for(s[0]=t,n=1;n=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(o=[],a={},n=0;n-1:w.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&u.push({elem:l,handlers:o})}return l=this,s\x20\t\r\n\f]*)[^>]*)\/>/gi,Se=/
###NUM_COMMENTS### comments
51 | ###HTML_COMMENTS### 52 |