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