bflw
						commit
						0925786f0d
					
				| @ -0,0 +1,14 @@ | ||||
| Single Word | ||||
| Flabby Words | ||||
| Power Words | ||||
| Prompt | ||||
| Word of the Day | ||||
| 2020 | ||||
| pH | ||||
| C | ||||
| David Mullich | ||||
| Pieper | ||||
| ite | ||||
| Chris | ||||
| Chris Keegan | ||||
| Beautiful | ||||
											
												Binary file not shown.
											
										
									
								| @ -0,0 +1,5 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| scp -r lazywiki_uneditable vps: | ||||
| ssh vps docker build -t lazywiki lazywiki_uneditable | ||||
| ssh vps ./wiki.sh | ||||
| @ -0,0 +1,5 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| scp ~/bflw/lazy_wiki.sqlite3 vps: | ||||
| ssh vps docker cp lazy_wiki.sqlite3 lazywiki:/db/lazy_wiki.sqlite3 | ||||
| ssh vps docker restart lazywiki | ||||
| @ -0,0 +1,4 @@ | ||||
| *.pyc | ||||
| *.swp | ||||
| venv/ | ||||
| *.egg-info/ | ||||
| @ -0,0 +1,8 @@ | ||||
| FROM python | ||||
| WORKDIR /app | ||||
| COPY . . | ||||
| RUN pip install . | ||||
| EXPOSE 8080 | ||||
| VOLUME /db | ||||
| ENTRYPOINT lazywiki /db | ||||
| 
 | ||||
| @ -0,0 +1 @@ | ||||
| include lazy_wiki/views/* | ||||
| @ -0,0 +1,9 @@ | ||||
| from . import web | ||||
| 
 | ||||
| def main(): | ||||
| 
 | ||||
|     web.app.run(host = '0.0.0.0', port = 8080) | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     print("444444") | ||||
|     main() | ||||
| @ -0,0 +1,88 @@ | ||||
| from sqlalchemy import create_engine | ||||
| from sqlalchemy.sql import select, update, insert | ||||
| from sqlalchemy.sql.expression import literal | ||||
| from .schema import metadata, articles | ||||
| from markdown import markdown | ||||
| from string import whitespace, punctuation | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| db_file = os.path.join(sys.argv[1], 'lazy_wiki.sqlite3') | ||||
| regex = re.compile(r'^\W+|^\W*\w+\W*') | ||||
| 
 | ||||
| # instantiate an engine for connecting to a database | ||||
| engine = create_engine('sqlite:///{}'.format(db_file)) | ||||
| # create tables if they don't exist | ||||
| metadata.create_all(engine) | ||||
| # connect to the database | ||||
| dbc = engine.connect() | ||||
| 
 | ||||
| def select_longest_keyword_string_starts_with(string): | ||||
|     ''' | ||||
|     Fetch the longest keyword that the given string starts with. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(literal(string).ilike(articles.c.title + '%')) | ||||
|     # may be bottleneck | ||||
|     results = [dict(u) for u in dbc.execute(query).fetchall()] | ||||
|     if not results: | ||||
|         return None | ||||
|     return max(results, key = lambda keyword : len(keyword['title'])) | ||||
| 
 | ||||
| def select_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(articles.c.title == literal(keyword)) | ||||
|     try: | ||||
|         return dict(dbc.execute(query).fetchone()) | ||||
|     except: | ||||
|         return None | ||||
| 
 | ||||
| def select_formatted_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword, | ||||
|     add hyperlinks to it, and format it to HTML. | ||||
|     ''' | ||||
| 
 | ||||
|     # get article content | ||||
|     article = select_article(keyword) | ||||
|     raw = article['content'] | ||||
|     # add hyperlinks to the content | ||||
|     formatted = '' | ||||
|     # until the raw content is empty | ||||
|     while raw: | ||||
|         # if the remaining raw content starts with a keyword | ||||
|         word = select_longest_keyword_string_starts_with(raw) | ||||
|         if word and raw[len(word['title'])] in punctuation + whitespace: | ||||
|             # use original capitalization for hyperlink text | ||||
|             original = raw[:len(word['title'])] | ||||
|             # create a Markdown hyperlink | ||||
|             word = '[{}](/view/{})'.format(original, word['title']) | ||||
|             # cut off the start of the raw content | ||||
|             raw = raw[len(original):] | ||||
|         else: | ||||
|             # cut a word off the start of the raw content | ||||
|             word = regex.search(raw).group() | ||||
|             raw = raw[len(word):] | ||||
|         # add the hyperlink or word to the formatted content | ||||
|         formatted += word | ||||
| 
 | ||||
|     article['content'] = markdown(formatted) | ||||
|     return article | ||||
| 
 | ||||
| def insert_article(**kwargs): | ||||
| 
 | ||||
|     query = insert(articles).values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| 
 | ||||
| def update_article(title, **kwargs): | ||||
| 
 | ||||
|     query = update(articles) \ | ||||
|         .where(articles.c.title == literal(title)) \ | ||||
|         .values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| @ -0,0 +1,10 @@ | ||||
| from sqlalchemy import Table, Column, Integer, String, MetaData | ||||
| 
 | ||||
| metadata = MetaData() | ||||
| 
 | ||||
| articles = Table ( | ||||
|     'article', metadata, | ||||
|     Column('id', Integer, primary_key = True), | ||||
|     Column('content', String, nullable = False), | ||||
|     Column('title', String, nullable = False) | ||||
| ) | ||||
| @ -0,0 +1,19 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = article['title']) | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 			<h1>Delete {{article['title']}}?</h1> | ||||
|             <hr> | ||||
|             <a href = '/edit/{{article['title']}}'>-yes-</a> • <a href = '/edit/{{article['title']}}'>-no-</a> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,25 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = 'Edit Article') | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 		<form action = '/article' method = 'post'> | ||||
| 			<div class = 'title'> | ||||
|             <h1><input type = 'text' placeholder = 'title' name = 'title' value = '{{article['title']}}'/></h1> | ||||
|             Synonyms: <input type = 'synonyms' value = 'synonyms'/><br> | ||||
|             <p><i>Separate synonyms with bullets: cat•cats•catting•catted</i></p> | ||||
|             </div> | ||||
| 			<p><textarea rows = 35 cols = 80 name = 'content'>{{article['content']}}</textarea></p> | ||||
| 			<p><input type = 'submit' value = 'submit'/> • <input type = 'delete' value = 'delete'/></p> | ||||
| 		</form> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,7 @@ | ||||
| <head> | ||||
| 	<title>LazyWiki - {{title}}</title> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <!--    <link rel="stylesheet" type="text/css" href="/static/css/reset.css">--> | ||||
|     <link rel="stylesheet" type="text/css" href="/static/css/view.css"> | ||||
| </head> | ||||
| @ -0,0 +1,31 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = article['title']) | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<h1>{{article['title']}}</h1> | ||||
|             <div class = 'edit'> | ||||
|             <a href = '/edit/{{article['title']}}'>-edit-</a> | ||||
|             </div> | ||||
|             <hr> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<div class = 'col'> | ||||
|                 <div class = 'content'> | ||||
| 				{{!article['content']}} | ||||
|                 </div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,69 @@ | ||||
| from . import db | ||||
| import bottle | ||||
| from urllib.parse import unquote, quote | ||||
| import os | ||||
| import pathlib | ||||
| 
 | ||||
| static_dir = os.path.join(os.path.expanduser('~'), 'lazy_wiki') | ||||
| bottle.TEMPLATE_PATH = [os.path.join(os.path.dirname(__file__), 'views')] | ||||
| app = bottle.Bottle() | ||||
| 
 | ||||
| # Serve CSS | ||||
| @app.get('/static/css/<filename:path>') | ||||
| def serve_css(filename): | ||||
|     return bottle.static_file(filename, root=pathlib.Path(__file__).parent / 'static/css') | ||||
| 
 | ||||
| @app.get('/favicon.ico', method='GET') | ||||
| def get_favicon(): | ||||
|     return bottle.static_file('favicon.ico', root=pathlib.Path(__file__).parent / 'static/img') | ||||
| 
 | ||||
| 
 | ||||
| @app.get('/edit/') | ||||
| def new_article(): | ||||
|     ''' | ||||
|     Write a new article. | ||||
|     ''' | ||||
| 
 | ||||
|     return bottle.template('edit', article = {'title' : '', 'content' : ''}) | ||||
| 
 | ||||
| @app.get('/delete/<keyword>') | ||||
| def delete_article(keyword): | ||||
|     ''' | ||||
|     Delete an article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_formatted_article(unquote(keyword)) | ||||
|     return bottle.template('view', article = article) | ||||
| 
 | ||||
| @app.get('/edit/<keyword>') | ||||
| def edit_article(keyword): | ||||
|     ''' | ||||
|     Edit an existing article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_article(unquote(keyword)) | ||||
|     return bottle.template('edit', article = article) | ||||
| 
 | ||||
| @app.get('/view/<keyword>') | ||||
| def get_article(keyword): | ||||
|     ''' | ||||
|     Get an article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_formatted_article(unquote(keyword)) | ||||
|     return bottle.template('view', article = article) | ||||
| 
 | ||||
| @app.post('/article') | ||||
| def post_article(): | ||||
|     ''' | ||||
|     Post a new article. | ||||
|     ''' | ||||
| 
 | ||||
|     POST = bottle.request.POST.decode() | ||||
|     article = db.select_article(POST['title']) | ||||
|     if article: | ||||
|         db.update_article(**POST) | ||||
|     else: | ||||
|         db.insert_article(**POST) | ||||
| 
 | ||||
|     bottle.redirect('/view/{}'.format(quote(POST['title']))) | ||||
| @ -0,0 +1,9 @@ | ||||
| from . import web | ||||
| import sys | ||||
| 
 | ||||
| def main(): | ||||
| 
 | ||||
|     web.app.run(host = '0.0.0.0', port = int(sys.argv[2])) | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     main() | ||||
| @ -0,0 +1,87 @@ | ||||
| from sqlalchemy import create_engine | ||||
| from sqlalchemy.sql import select, update, insert | ||||
| from sqlalchemy.sql.expression import literal | ||||
| from .schema import metadata, articles | ||||
| from markdown import markdown | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| db_file = os.path.join(sys.argv[1], 'lazy_wiki.sqlite3') | ||||
| regex = re.compile(r'^\W+|^\W*\w+\W*') | ||||
| 
 | ||||
| # instantiate an engine for connecting to a database | ||||
| engine = create_engine('sqlite:///{}'.format(db_file)) | ||||
| # create tables if they don't exist | ||||
| metadata.create_all(engine) | ||||
| # connect to the database | ||||
| dbc = engine.connect() | ||||
| 
 | ||||
| def select_longest_keyword_string_starts_with(string): | ||||
|     ''' | ||||
|     Fetch the longest keyword that the given string starts with. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(literal(string).ilike(articles.c.title + '%')) | ||||
|     # may be bottleneck | ||||
|     results = [dict(u) for u in dbc.execute(query).fetchall()] | ||||
|     if not results: | ||||
|         return None | ||||
|     return max(results, key = lambda keyword : len(keyword['title'])) | ||||
| 
 | ||||
| def select_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(articles.c.title == literal(keyword)) | ||||
|     try: | ||||
|         return dict(dbc.execute(query).fetchone()) | ||||
|     except: | ||||
|         return None | ||||
| 
 | ||||
| def select_formatted_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword, | ||||
|     add hyperlinks to it, and format it to HTML. | ||||
|     ''' | ||||
| 
 | ||||
|     # get article content | ||||
|     article = select_article(keyword) | ||||
|     raw = article['content'] | ||||
|     # add hyperlinks to the content | ||||
|     formatted = '' | ||||
|     # until the raw content is empty | ||||
|     while raw: | ||||
|         # if the remaining raw content starts with a keyword | ||||
|         word = select_longest_keyword_string_starts_with(raw) | ||||
|         if word: | ||||
|             # use original capitalization for hyperlink text | ||||
|             original = raw[:len(word['title'])] | ||||
|             # create a Markdown hyperlink | ||||
|             word = '[{}](/view/{})'.format(original, word['title']) | ||||
|             # cut off the start of the raw content | ||||
|             raw = raw[len(original):] | ||||
|         else: | ||||
|             # cut a word off the start of the raw content | ||||
|             word = regex.search(raw).group() | ||||
|             raw = raw[len(word):] | ||||
|         # add the hyperlink or word to the formatted content | ||||
|         formatted += word | ||||
| 
 | ||||
|     article['content'] = markdown(formatted) | ||||
|     return article | ||||
| 
 | ||||
| def insert_article(**kwargs): | ||||
| 
 | ||||
|     query = insert(articles).values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| 
 | ||||
| def update_article(title, **kwargs): | ||||
| 
 | ||||
|     query = update(articles) \ | ||||
|         .where(articles.c.title == literal(title)) \ | ||||
|         .values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| @ -0,0 +1,10 @@ | ||||
| from sqlalchemy import Table, Column, Integer, String, MetaData | ||||
| 
 | ||||
| metadata = MetaData() | ||||
| 
 | ||||
| articles = Table ( | ||||
|     'article', metadata, | ||||
|     Column('id', Integer, primary_key = True), | ||||
|     Column('content', String, nullable = False), | ||||
|     Column('title', String, nullable = False) | ||||
| ) | ||||
| @ -0,0 +1,48 @@ | ||||
| /* http://meyerweb.com/eric/tools/css/reset/  | ||||
|    v2.0 | 20110126 | ||||
|    License: none (public domain) | ||||
| */ | ||||
| 
 | ||||
| html, body, div, span, applet, object, iframe, | ||||
| h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||||
| a, abbr, acronym, address, big, cite, code, | ||||
| del, dfn, em, img, ins, kbd, q, s, samp, | ||||
| small, strike, strong, sub, sup, tt, var, | ||||
| b, u, i, center, | ||||
| dl, dt, dd, ol, ul, li, | ||||
| fieldset, form, label, legend, | ||||
| table, caption, tbody, tfoot, thead, tr, th, td, | ||||
| article, aside, canvas, details, embed,  | ||||
| figure, figcaption, footer, header, hgroup,  | ||||
| menu, nav, output, ruby, section, summary, | ||||
| time, mark, audio, video { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
| 	border: 0; | ||||
| 	font-size: 100%; | ||||
| 	font: inherit; | ||||
| 	vertical-align: baseline; | ||||
| } | ||||
| /* HTML5 display-role reset for older browsers */ | ||||
| article, aside, details, figcaption, figure,  | ||||
| footer, header, hgroup, menu, nav, section { | ||||
| 	display: block; | ||||
| } | ||||
| body { | ||||
| 	line-height: 1; | ||||
| } | ||||
| ol, ul { | ||||
| 	list-style: none; | ||||
| } | ||||
| blockquote, q { | ||||
| 	quotes: none; | ||||
| } | ||||
| blockquote:before, blockquote:after, | ||||
| q:before, q:after { | ||||
| 	content: ''; | ||||
| 	content: none; | ||||
| } | ||||
| table { | ||||
| 	border-collapse: collapse; | ||||
| 	border-spacing: 0; | ||||
| } | ||||
| @ -0,0 +1,103 @@ | ||||
| * { | ||||
|     padding:0; | ||||
|     margin:0; | ||||
|     font-family: sans-serif; | ||||
|     background-color: #171321; | ||||
|     font-size: 1em; | ||||
|     color: #d2c1e5; | ||||
|     font-family: Ubuntu; | ||||
| } | ||||
| html, body {padding:0; margin:0; height:100%;} | ||||
| footer { | ||||
| } | ||||
| a, a:link, a:visited, a:hover, a:active, b, p, ul, ol, li, h1, h2, h3, h4, h5, img, i, hr, pre, code { | ||||
|     background-color: transparent;  | ||||
| } | ||||
| a, a:link, a:visited, a:hover, a:active { | ||||
|     text-decoration: none; | ||||
| } | ||||
| a:link, a:visited { | ||||
|     color: #A68BA7; | ||||
| } | ||||
| a:hover, a:active { | ||||
|     color: #FCECC9;  | ||||
| } | ||||
| p { | ||||
|     text-indent: 2em; | ||||
| } | ||||
| ul, ol { | ||||
|     list-style-type: disc; | ||||
|     list-style-position: inside; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| li { | ||||
|     padding-left: 2em; | ||||
| } | ||||
| h1 { | ||||
|     font-size: 1.5em; | ||||
| } | ||||
| h1 a, h1 a:visited, h1 a:hover, h1 a:active { | ||||
| } | ||||
| h2 a:hover, h2 a:active { | ||||
| } | ||||
| h2 { | ||||
|     font-size: 1.4em; | ||||
| } | ||||
| h3 { | ||||
|     font-size: 1.3em; | ||||
| } | ||||
| h4 { | ||||
|     font-size: 1.2em; | ||||
| } | ||||
| h4 { | ||||
|     font-size: 1.1em; | ||||
| } | ||||
| p { | ||||
|     line-height: 2em; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| pre, code { | ||||
|     font-size: 0.9em; | ||||
|     margin-top: 1.5em; | ||||
|     margin-bottom: 1.5em; | ||||
|     padding-left: 0.5em; | ||||
|     padding-right: 0.5em; | ||||
|     padding-bottom: 0.1em; | ||||
| } | ||||
| img { | ||||
|     margin-top: 1.5em; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| 
 | ||||
| input, textarea { | ||||
|     padding: .5em; | ||||
|     border: 2px solid #3b3748; | ||||
| } | ||||
| ::-moz-selection { /* Code for Firefox */ | ||||
|   background: #c76199; | ||||
| } | ||||
| 
 | ||||
| ::selection { | ||||
|   background: #c76199; | ||||
| } | ||||
| 
 | ||||
| .container { | ||||
|     margin: 2em; | ||||
| } | ||||
| .title { | ||||
|     margin: 0em; | ||||
| } | ||||
| .edit { | ||||
|     font-size: .8em; | ||||
|     color: #3b3748; | ||||
|     margin: 0em; | ||||
| } | ||||
| .edit a { | ||||
|     color: #3b3748; | ||||
| } | ||||
| .content { | ||||
|     width: 80%; | ||||
|     max-width: 75ch; | ||||
|     margin: auto; | ||||
|     margin-top: 1em; | ||||
| } | ||||
											
												Binary file not shown.
											
										
									
								| After Width: | Height: | Size: 1.4 KiB | 
| @ -0,0 +1,19 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = article['title']) | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 			<h1>Delete {{article['title']}}?</h1> | ||||
|             <hr> | ||||
|             <a href = '/edit/{{article['title']}}'>-yes-</a> • <a href = '/edit/{{article['title']}}'>-no-</a> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,25 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = 'Edit Article') | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 		<form action = '/article' method = 'post'> | ||||
| 			<div class = 'title'> | ||||
|             <h1><input type = 'text' placeholder = 'title' name = 'title' value = '{{article['title']}}'/></h1> | ||||
|             <!--Synonyms: <input type = 'synonyms' value = 'synonyms'/><br> | ||||
|             <p><i>Separate synonyms with bullets: cat•cats•catting•catted</i></p>--> | ||||
|             </div> | ||||
| 			<p><textarea rows = 35 cols = 80 name = 'content'>{{article['content']}}</textarea></p> | ||||
| 			<p><input type = 'submit' value = 'submit'/><!-- • <input type = 'delete' value = 'delete'/>--></p> | ||||
| 		</form> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,7 @@ | ||||
| <head> | ||||
| 	<title>LazyWiki - {{title}}</title> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <!--    <link rel="stylesheet" type="text/css" href="/static/css/reset.css">--> | ||||
|     <link rel="stylesheet" type="text/css" href="/static/css/view.css"> | ||||
| </head> | ||||
| @ -0,0 +1,31 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = article['title']) | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<h1>{{article['title']}}</h1> | ||||
|             <div class = 'edit'> | ||||
|             <a href = '/edit/{{article['title']}}'>-edit-</a> | ||||
|             </div> | ||||
|             <hr> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<div class = 'col'> | ||||
|                 <div class = 'content'> | ||||
| 				{{!article['content']}} | ||||
|                 </div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,69 @@ | ||||
| from . import db | ||||
| import bottle | ||||
| from urllib.parse import unquote, quote | ||||
| import os | ||||
| import pathlib | ||||
| 
 | ||||
| static_dir = os.path.join(os.path.expanduser('~'), 'lazy_wiki') | ||||
| bottle.TEMPLATE_PATH = [os.path.join(os.path.dirname(__file__), 'views')] | ||||
| app = bottle.Bottle() | ||||
| 
 | ||||
| # Serve CSS | ||||
| @app.get('/static/css/<filename:path>') | ||||
| def serve_css(filename): | ||||
|     return bottle.static_file(filename, root=pathlib.Path(__file__).parent / 'static/css') | ||||
| 
 | ||||
| @app.get('/favicon.ico', method='GET') | ||||
| def get_favicon(): | ||||
|     return bottle.static_file('favicon.ico', root=pathlib.Path(__file__).parent / 'static/img') | ||||
| 
 | ||||
| 
 | ||||
| @app.get('/edit/') | ||||
| def new_article(): | ||||
|     ''' | ||||
|     Write a new article. | ||||
|     ''' | ||||
| 
 | ||||
|     return bottle.template('edit', article = {'title' : '', 'content' : ''}) | ||||
| 
 | ||||
| @app.get('/delete/<keyword>') | ||||
| def delete_article(keyword): | ||||
|     ''' | ||||
|     Delete an article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_formatted_article(unquote(keyword)) | ||||
|     return bottle.template('view', article = article) | ||||
| 
 | ||||
| @app.get('/edit/<keyword>') | ||||
| def edit_article(keyword): | ||||
|     ''' | ||||
|     Edit an existing article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_article(unquote(keyword)) | ||||
|     return bottle.template('edit', article = article) | ||||
| 
 | ||||
| @app.get('/view/<keyword>') | ||||
| def get_article(keyword): | ||||
|     ''' | ||||
|     Get an article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_formatted_article(unquote(keyword)) | ||||
|     return bottle.template('view', article = article) | ||||
| 
 | ||||
| @app.post('/article') | ||||
| def post_article(): | ||||
|     ''' | ||||
|     Post a new article. | ||||
|     ''' | ||||
| 
 | ||||
|     POST = bottle.request.POST.decode() | ||||
|     article = db.select_article(POST['title']) | ||||
|     if article: | ||||
|         db.update_article(**POST) | ||||
|     else: | ||||
|         db.insert_article(**POST) | ||||
| 
 | ||||
|     bottle.redirect('/view/{}'.format(quote(POST['title']))) | ||||
| @ -0,0 +1,20 @@ | ||||
| #!/usr/bin/env python | ||||
| 
 | ||||
| from setuptools import setup, find_packages | ||||
| 
 | ||||
| setup ( | ||||
|     name = 'Lazy Wiki', | ||||
|     version = '0.0.2', | ||||
|     packages = find_packages(), | ||||
|     include_package_data = True, | ||||
|     entry_points = { | ||||
|         'console_scripts' : [ | ||||
|             'lazywiki=lazy_wiki.__main__:main' | ||||
|         ] | ||||
|     }, | ||||
|     install_requires = [ | ||||
|         'sqlalchemy==1.4.42', | ||||
|         'bottle', | ||||
|         'markdown' | ||||
|     ] | ||||
| ) | ||||
| @ -0,0 +1,4 @@ | ||||
| *.pyc | ||||
| *.swp | ||||
| venv/ | ||||
| *.egg-info/ | ||||
| @ -0,0 +1,8 @@ | ||||
| FROM python | ||||
| WORKDIR /app | ||||
| COPY . . | ||||
| RUN pip install . | ||||
| EXPOSE 8080 | ||||
| VOLUME /db | ||||
| ENTRYPOINT lazywiki /db | ||||
| 
 | ||||
| @ -0,0 +1 @@ | ||||
| include lazy_wiki/views/* | ||||
| @ -0,0 +1,9 @@ | ||||
| from . import web | ||||
| 
 | ||||
| def main(): | ||||
| 
 | ||||
|     web.app.run(host = '0.0.0.0', port = 8080) | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     print("33333") | ||||
|     main() | ||||
| @ -0,0 +1,87 @@ | ||||
| from sqlalchemy import create_engine | ||||
| from sqlalchemy.sql import select, update, insert | ||||
| from sqlalchemy.sql.expression import literal | ||||
| from .schema import metadata, articles | ||||
| from markdown import markdown | ||||
| import re | ||||
| import os | ||||
| import sys | ||||
| 
 | ||||
| db_file = os.path.join(sys.argv[1], 'lazy_wiki.sqlite3') | ||||
| regex = re.compile(r'^\W+|^\W*\w+\W*') | ||||
| 
 | ||||
| # instantiate an engine for connecting to a database | ||||
| engine = create_engine('sqlite:///{}'.format(db_file)) | ||||
| # create tables if they don't exist | ||||
| metadata.create_all(engine) | ||||
| # connect to the database | ||||
| dbc = engine.connect() | ||||
| 
 | ||||
| def select_longest_keyword_string_starts_with(string): | ||||
|     ''' | ||||
|     Fetch the longest keyword that the given string starts with. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(literal(string).ilike(articles.c.title + '%')) | ||||
|     # may be bottleneck | ||||
|     results = [dict(u) for u in dbc.execute(query).fetchall()] | ||||
|     if not results: | ||||
|         return None | ||||
|     return max(results, key = lambda keyword : len(keyword['title'])) | ||||
| 
 | ||||
| def select_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword. | ||||
|     ''' | ||||
| 
 | ||||
|     query = select([articles]) \ | ||||
|         .where(articles.c.title == literal(keyword)) | ||||
|     try: | ||||
|         return dict(dbc.execute(query).fetchone()) | ||||
|     except: | ||||
|         return None | ||||
| 
 | ||||
| def select_formatted_article(keyword): | ||||
|     ''' | ||||
|     Fetch an article associated with the given keyword, | ||||
|     add hyperlinks to it, and format it to HTML. | ||||
|     ''' | ||||
| 
 | ||||
|     # get article content | ||||
|     article = select_article(keyword) | ||||
|     raw = article['content'] | ||||
|     # add hyperlinks to the content | ||||
|     formatted = '' | ||||
|     # until the raw content is empty | ||||
|     while raw: | ||||
|         # if the remaining raw content starts with a keyword | ||||
|         word = select_longest_keyword_string_starts_with(raw) | ||||
|         if word: | ||||
|             # use original capitalization for hyperlink text | ||||
|             original = raw[:len(word['title'])] | ||||
|             # create a Markdown hyperlink | ||||
|             word = '[{}](/view/{})'.format(original, word['title']) | ||||
|             # cut off the start of the raw content | ||||
|             raw = raw[len(original):] | ||||
|         else: | ||||
|             # cut a word off the start of the raw content | ||||
|             word = regex.search(raw).group() | ||||
|             raw = raw[len(word):] | ||||
|         # add the hyperlink or word to the formatted content | ||||
|         formatted += word | ||||
| 
 | ||||
|     article['content'] = markdown(formatted) | ||||
|     return article | ||||
| 
 | ||||
| def insert_article(**kwargs): | ||||
| 
 | ||||
|     query = insert(articles).values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| 
 | ||||
| def update_article(title, **kwargs): | ||||
| 
 | ||||
|     query = update(articles) \ | ||||
|         .where(articles.c.title == literal(title)) \ | ||||
|         .values(**kwargs) | ||||
|     dbc.execute(query) | ||||
| @ -0,0 +1,10 @@ | ||||
| from sqlalchemy import Table, Column, Integer, String, MetaData | ||||
| 
 | ||||
| metadata = MetaData() | ||||
| 
 | ||||
| articles = Table ( | ||||
|     'article', metadata, | ||||
|     Column('id', Integer, primary_key = True), | ||||
|     Column('content', String, nullable = False), | ||||
|     Column('title', String, nullable = False) | ||||
| ) | ||||
| @ -0,0 +1,48 @@ | ||||
| /* http://meyerweb.com/eric/tools/css/reset/  | ||||
|    v2.0 | 20110126 | ||||
|    License: none (public domain) | ||||
| */ | ||||
| 
 | ||||
| html, body, div, span, applet, object, iframe, | ||||
| h1, h2, h3, h4, h5, h6, p, blockquote, pre, | ||||
| a, abbr, acronym, address, big, cite, code, | ||||
| del, dfn, em, img, ins, kbd, q, s, samp, | ||||
| small, strike, strong, sub, sup, tt, var, | ||||
| b, u, i, center, | ||||
| dl, dt, dd, ol, ul, li, | ||||
| fieldset, form, label, legend, | ||||
| table, caption, tbody, tfoot, thead, tr, th, td, | ||||
| article, aside, canvas, details, embed,  | ||||
| figure, figcaption, footer, header, hgroup,  | ||||
| menu, nav, output, ruby, section, summary, | ||||
| time, mark, audio, video { | ||||
| 	margin: 0; | ||||
| 	padding: 0; | ||||
| 	border: 0; | ||||
| 	font-size: 100%; | ||||
| 	font: inherit; | ||||
| 	vertical-align: baseline; | ||||
| } | ||||
| /* HTML5 display-role reset for older browsers */ | ||||
| article, aside, details, figcaption, figure,  | ||||
| footer, header, hgroup, menu, nav, section { | ||||
| 	display: block; | ||||
| } | ||||
| body { | ||||
| 	line-height: 1; | ||||
| } | ||||
| ol, ul { | ||||
| 	list-style: none; | ||||
| } | ||||
| blockquote, q { | ||||
| 	quotes: none; | ||||
| } | ||||
| blockquote:before, blockquote:after, | ||||
| q:before, q:after { | ||||
| 	content: ''; | ||||
| 	content: none; | ||||
| } | ||||
| table { | ||||
| 	border-collapse: collapse; | ||||
| 	border-spacing: 0; | ||||
| } | ||||
| @ -0,0 +1,101 @@ | ||||
| * { | ||||
|     padding:0; | ||||
|     margin:0; | ||||
|     font-family: sans-serif; | ||||
|     /*background-color: #171321;*/ | ||||
|     font-size: 1em; | ||||
|     color: #d2c1e5; | ||||
|     font-family: Ubuntu; | ||||
| } | ||||
| html {padding:0; margin:0; height:100%;} | ||||
| body { | ||||
|     padding:0; margin:0; height:100%;  | ||||
|     background-image: linear-gradient(#060114, #0F0233); | ||||
|     background-repeat: no-repeat; | ||||
|     background-attachment: fixed; | ||||
| } | ||||
| footer { | ||||
| } | ||||
| a, a:link, a:visited, a:hover, a:active, b, p, ul, ol, li, h1, h2, h3, h4, h5, img, i, hr, pre, code { | ||||
|     background-color: transparent;  | ||||
| } | ||||
| a, a:link, a:visited, a:hover, a:active { | ||||
|     text-decoration: none; | ||||
| } | ||||
| a:link, a:visited { | ||||
|     color: #A68BA7; | ||||
| } | ||||
| a:hover, a:active { | ||||
|     color: #FCECC9;  | ||||
| } | ||||
| p { | ||||
|     text-indent: 2em; | ||||
| } | ||||
| ul, ol { | ||||
|     list-style-type: disc; | ||||
|     list-style-position: inside; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| li { | ||||
|     padding-left: 2em; | ||||
| } | ||||
| h1 { | ||||
|     font-size: 1.5em; | ||||
| } | ||||
| h1 a, h1 a:visited, h1 a:hover, h1 a:active { | ||||
| } | ||||
| h2 a:hover, h2 a:active { | ||||
| } | ||||
| h2 { | ||||
|     font-size: 1.4em; | ||||
| } | ||||
| h3 { | ||||
|     font-size: 1.3em; | ||||
| } | ||||
| h4 { | ||||
|     font-size: 1.2em; | ||||
| } | ||||
| h4 { | ||||
|     font-size: 1.1em; | ||||
| } | ||||
| p { | ||||
|     line-height: 2em; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| pre, code { | ||||
|     font-size: 0.9em; | ||||
|     margin-top: 1.5em; | ||||
|     margin-bottom: 1.5em; | ||||
|     padding-left: 0.5em; | ||||
|     padding-right: 0.5em; | ||||
|     padding-bottom: 0.1em; | ||||
| } | ||||
| img { | ||||
|     margin-top: 1.5em; | ||||
|     margin-bottom: 1.5em; | ||||
| } | ||||
| 
 | ||||
| input, textarea { | ||||
|     padding: .5em; | ||||
|     border: 2px solid #3b3748; | ||||
| } | ||||
| .container { | ||||
|     margin: 2em; | ||||
| } | ||||
| .title { | ||||
|     margin: 0em; | ||||
| } | ||||
| .edit { | ||||
|     font-size: .8em; | ||||
|     color: #3b3748; | ||||
|     margin: 0em; | ||||
| } | ||||
| .edit a { | ||||
|     color: #3b3748; | ||||
| } | ||||
| .content { | ||||
|     width: 80%; | ||||
|     max-width: 75ch; | ||||
|     margin: auto; | ||||
|     margin-top: 1em; | ||||
| } | ||||
											
												Binary file not shown.
											
										
									
								| After Width: | Height: | Size: 1.4 KiB | 
| @ -0,0 +1,7 @@ | ||||
| <head> | ||||
| 	<title>LazyWiki - {{title}}</title> | ||||
|     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1"> | ||||
| <!--    <link rel="stylesheet" type="text/css" href="/static/css/reset.css">--> | ||||
|     <link rel="stylesheet" type="text/css" href="/static/css/view.css"> | ||||
| </head> | ||||
| @ -0,0 +1,31 @@ | ||||
| <!doctype html> | ||||
| 
 | ||||
| <html> | ||||
| 
 | ||||
| 	% include('head.tpl', title = article['title']) | ||||
| 
 | ||||
| 	<body> | ||||
| 
 | ||||
| 	<div class = 'container'> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<h1>{{article['title']}}</h1> | ||||
|             <div class = 'edit'> | ||||
|             <a href=https://www.blessfrey.me/>-return to blessfrey-</a> • <a href=https://wiki.blessfrey.me/view/Main_Page>-return to wiki main page-</a> | ||||
|             </div> | ||||
|             <hr> | ||||
| 		</div> | ||||
| 
 | ||||
| 		<div class = 'row'> | ||||
| 			<div class = 'col'> | ||||
|                 <div class = 'content'> | ||||
| 				{{!article['content']}} | ||||
|                 </div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 
 | ||||
| 	</div> | ||||
| 
 | ||||
| 	</body> | ||||
| 
 | ||||
| </html> | ||||
| @ -0,0 +1,54 @@ | ||||
| from . import db | ||||
| import bottle | ||||
| from urllib.parse import unquote, quote | ||||
| import os | ||||
| import pathlib | ||||
| 
 | ||||
| static_dir = os.path.join(os.path.expanduser('~'), 'lazy_wiki') | ||||
| bottle.TEMPLATE_PATH = [os.path.join(os.path.dirname(__file__), 'views')] | ||||
| app = bottle.Bottle() | ||||
| 
 | ||||
| # Serve CSS | ||||
| @app.get('/static/css/<filename:path>') | ||||
| def serve_css(filename): | ||||
|     return bottle.static_file(filename, root=pathlib.Path(__file__).parent / 'static/css') | ||||
| 
 | ||||
| @app.get('/favicon.ico', method='GET') | ||||
| def get_favicon(): | ||||
|     return bottle.static_file('favicon.ico', root=pathlib.Path(__file__).parent / 'static/img') | ||||
| 
 | ||||
| # Error Page | ||||
| @app.error(404) | ||||
| def error404(error): | ||||
|     return "unfortunately, a 404 error. the page you're searching for doesn't exist. (or is it just in hiding?) try another page! <a href=https://www.blessfrey.me/>-return to blessfrey-</a> • <a href=https://wiki.blessfrey.me/view/Main_Page> " | ||||
| @app.error(500) | ||||
| def error500(error): | ||||
|     return "unfortunately, a 500 error. something is wrong with the page you're trying to find, if it exists at all. try another page! <a href=https://www.blessfrey.me/>-return to blessfrey-</a> • <a href=https://wiki.blessfrey.me/view/Main_Page>" | ||||
| @app.error(502) | ||||
| def error502(error): | ||||
|     return "unfortunately, a 502 error. this was likely due to website maintenance. usually it'll be back up before you finish reading this, but otherwise, I'll notice something's wrong soon! <a href=https://www.blessfrey.me/>-return to blessfrey-</a> • <a href=https://wiki.blessfrey.me/view/Main_Page></a>" | ||||
| 
 | ||||
| @app.get('/') | ||||
| def home(): | ||||
|     return get_article(Main_Page) | ||||
| @app.get('/Home') | ||||
| def home1(): | ||||
|     return get_article(Main_Page) | ||||
| @app.get('/Index') | ||||
| def home2(): | ||||
|     return get_article(Main_Page) | ||||
| @app.get('/Main') | ||||
| def home3(): | ||||
|     return get_article(Main_Page) | ||||
| @app.get('/Main_Page') | ||||
| def home4(): | ||||
|     return get_article(Main_Page) | ||||
| 
 | ||||
| @app.get('/view/<keyword>') | ||||
| def get_article(keyword): | ||||
|     ''' | ||||
|     Get an article. | ||||
|     ''' | ||||
| 
 | ||||
|     article = db.select_formatted_article(unquote(keyword)) | ||||
|     return bottle.template('view', article = article) | ||||
| @ -0,0 +1,29 @@ | ||||
| #!/usr/bin/env python | ||||
| 
 | ||||
| from setuptools import setup, find_packages | ||||
| 
 | ||||
| setup ( | ||||
|     name = 'Lazy Wiki', | ||||
|     version = '0.0.3', | ||||
|     packages = find_packages(), | ||||
|     include_package_data = True, | ||||
|     entry_points = { | ||||
|         'console_scripts' : [ | ||||
|             'lazywiki=lazy_wiki.__main__:main' | ||||
|         ] | ||||
|     }, | ||||
|     install_requires = [ | ||||
|         'sqlalchemy==1.4.42', | ||||
|         'bottle', | ||||
|         'markdown' | ||||
|     ], | ||||
|     data_files = [ | ||||
|         ('/', | ||||
|             [ | ||||
|                 'lazy_wiki/static/css/view.css', | ||||
|                 'lazy_wiki/static/css/reset.css', | ||||
|                 'lazy_wiki/static/img/favicon.ico' | ||||
|             ] | ||||
|         ) | ||||
|     ] | ||||
| ) | ||||
					Loading…
					
					
				
		Reference in New Issue