Merge pull request #1 from Jaculabilis/server-app

Merge server-app branch
This commit is contained in:
Tim Van Baak 2018-08-20 01:27:46 -07:00 committed by GitHub
commit 8d0b0c7c79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 934 additions and 971 deletions

3
.gitignore vendored
View File

@ -99,3 +99,6 @@ ENV/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# Ignore directories in lexicon/
lexicon/*/

1
lexicon/readme.txt Normal file
View File

@ -0,0 +1 @@
This directory contains the directories for all of your Lexicon games.

720
lexipython.py Normal file → Executable file
View File

@ -1,576 +1,188 @@
############################### #!/usr/bin/env python3
## Lexipython Lexicon engine ##
###############################
import sys # For argv and stderr import sys
import os # For reading directories if sys.version_info[0] < 3:
import re # For parsing lex content raise Exception("Lexipython requires Python 3")
import io # For writing pages out as UTF-8
import networkx # For pagerank analytics
from collections import defaultdict # For rank inversion in statistics
from urllib import parse
# Short utility functions for handling titles import argparse
import os
import re
import json
from src.article import LexiconArticle
from src import build
from src import utils
def titlecase(s): def is_lexicon(name):
"""Enforces capitalization of titles."""
s = s.strip()
return s[:1].capitalize() + s[1:]
def titleescape(s):
"""Makes an article title filename-safe."""
s = s.strip()
s = re.sub(r"\s+", '_', s) # Replace whitespace with _
s = parse.quote(s) # Encode all other characters
s = re.sub(r"%", "", s) # Strip encoding %s
if len(s) > 64: # If the result is unreasonably long,
s = hex(abs(hash(s)))[2:] # Replace it with a hex hash
return s
def titlestrip(s):
"""Strips certain prefixes for title sorting."""
if s.startswith("The "): return s[4:]
if s.startswith("An "): return s[3:]
if s.startswith("A "): return s[2:]
return s
# Main article class
class LexiconArticle:
""" """
A Lexicon article and its metadata. Checks whether the given folder is a Lexicon game.
Inputs:
Members: name The Lexicon name to check. Assumed to be an existing folder.
author string: the author of the article Output:
turn integer: the turn the article was written for Returns a tuple (result, msg, status), where result is True if the
title string: the article title given name is a Lexicon game and False otherwise, msg is the Lexicon's
title_filesafe string: the title, escaped, used for filenames status or an error message, and status is the status dictionary of the
content string: the HTML content, with citations replaced by format hooks Lexicon or None.
citations dict from format hook string to tuple of link alias and link target title
wcites list: titles of written articles cited
pcites list: titles of phantom articles cited
citedby list: titles of articles that cite this
The last three are filled in by populate().
""" """
if not os.path.isfile(os.path.join("lexicon", name, "lexicon.cfg")):
def __init__(self, author, turn, title, content, citations): return (False, "'{}' is not a Lexicon game, or its config file may be missing.".format(name), None)
""" if not os.path.isfile(os.path.join("lexicon", name, "status")):
Creates a LexiconArticle object with the given parameters. return (True, "status missing", None)
""" with open(os.path.join("lexicon", name, "status")) as statusfile:
self.author = author raw = statusfile.read()
self.turn = turn if len(raw) == 0:
self.title = title return (True, "unbuilt", {})
self.title_filesafe = titleescape(title)
self.content = content
self.citations = citations
self.wcites = set()
self.pcites = set()
self.citedby = set()
@staticmethod
def from_file_raw(raw_content):
"""
Parses the contents of a Lexipython source file into a LexiconArticle
object. If the source file is malformed, returns None.
"""
headers = raw_content.split('\n', 3)
if len(headers) != 4:
print("Header read error")
return None
author_header, turn_header, title_header, content_raw = headers
# Validate and sanitize the author header
if not author_header.startswith("# Author:"):
print("Author header missing")
return None
author = author_header[9:].strip()
# Validate and sanitize the turn header
if not turn_header.startswith("# Turn:"):
print("Turn header missing")
return None
turn = None
try: try:
turn = int(turn_header[7:].strip()) status = json.loads(raw)
except: except:
print("Turn header error") return (True, "status corrupted", None)
return None return (True, "ye", status) # TODO
# Validate and sanitize the title header return (False, "Error checking Lexicon status", None)
if not title_header.startswith("# Title:"):
print("Title header missing") def overview_all():
return None """
title = titlecase(title_header[8:]) Prints the names and statuses of all extant Lexicons,
# Parse the content and extract citations or a short help message if none have been created yet.
paras = re.split("\n\n+", content_raw.strip()) """
content = "" # Scan the directory
citations = {} lexicon_names = []
format_id = 1 with os.scandir("lexicon") as lexicons:
if not paras: for entry in lexicons:
print("No content") if entry.is_dir():
for para in paras: result, msg, status = is_lexicon(entry.name)
# Escape angle brackets if result:
para = re.sub("<", "&lt;", para) lexicon_names.append((entry.name, msg))
para = re.sub(">", "&gt;", para) # Print the results
# Replace bold and italic marks with tags if len(lexicon_names) > 0:
para = re.sub(r"//([^/]+)//", r"<i>\1</i>", para) l = max([len(name) for name, msg in lexicon_names]) + 4
para = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", para) print("Lexicons:")
# Replace \\LF with <br>LF for name, msg in sorted(lexicon_names):
para = re.sub(r"\\\\\n", "<br>\n", para) print(" {}{}{}".format(name, " " * (l - len(name)), msg))
# Abstract citations into the citation record
link_match = re.search(r"\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]", para)
while link_match:
# Identify the citation text and cited article
cite_text = link_match.group(2) if link_match.group(2) else link_match.group(3)
cite_title = titlecase(link_match.group(3))
# Record the citation
citations["c"+str(format_id)] = (cite_text, cite_title)
# Stitch the format id in place of the citation
para = para[:link_match.start(0)] + "{c"+str(format_id)+"}" + para[link_match.end(0):]
format_id += 1 # Increment to the next format citation
link_match = re.search(r"\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]", para)
# Convert signature to right-aligned
if para[:1] == '~':
para = "<hr><span class=\"signature\"><p>" + para[1:] + "</p></span>\n"
else: else:
para = "<p>" + para + "</p>\n" print("There are no Lexicons yet. Create one with:\n\n"\
content += para " lexipython.py [name] init\n")
return LexiconArticle(author, turn, title, content, citations)
def build_page_content(self): def overview_one(name):
""" """
Formats citations into the article content as normal HTML links Prints the status and summary information for the Lexicon with the
and returns the result. given name.
""" """
format_map = { # Verify the name
format_id: "<a href=\"{1}.html\"{2}>{0}</a>".format( if not os.path.isdir(os.path.join("lexicon", name)):
cite_tuple[0], titleescape(cite_tuple[1]), print("Error: There is no Lexicon named '{}'.".format(name))
"" if cite_tuple[1] in self.wcites else " class=\"phantom\"")
for format_id, cite_tuple in self.citations.items()
}
return self.content.format(**format_map)
def build_page_citeblock(self, prev_target, next_target):
"""
Builds the citeblock content HTML for use in regular article pages.
For each defined target, links the target page as Previous or Next.
"""
citeblock = "<div class=\"content citeblock\">\n"
# Prev/next links
if next_target is not None:
citeblock += "<p style=\"float:right\"><a href=\"{}.html\">Next &#8594;</a></p>\n".format(titleescape(next_target))
if prev_target is not None:
citeblock += "<p><a href=\"{}.html\">&#8592; Previous</a></p>\n".format(titleescape(prev_target))
elif next_target is not None:
citeblock += "<p>&nbsp;</p>\n"
# Citations
cites_links = [
"<a href=\"{1}.html\"{2}>{0}</a>".format(
title, titleescape(title),
"" if title in self.wcites else " class=\"phantom\"")
for title in sorted(self.wcites | self.pcites)]
cites_str = " | ".join(cites_links)
if len(cites_str) < 1: cites_str = "--"
citeblock += "<p>Citations: {}</p>\n".format(cites_str)
# Citedby
citedby_links = [
"<a href=\"{1}.html\">{0}</a>".format(
title, titleescape(title))
for title in self.citedby]
citedby_str = " | ".join(citedby_links)
if len(citedby_str) < 1: citedby_str = "--"
citeblock += "<p>Cited by: {}</p>\n</div>\n".format(citedby_str)
return citeblock
# Parsing functions for source intake
def parse_from_directory(directory):
"""
Reads and parses each source file in the given directory.
Input: directory, the path to the folder to read
Output: a list of parsed articles
"""
articles = []
print("Reading source files from", directory)
for filename in os.listdir(directory):
path = directory + filename
# Read only .txt files
if filename[-4:] == ".txt":
print(" Parsing", filename)
with open(path, "r", encoding="utf8") as src_file:
raw = src_file.read()
article = LexiconArticle.from_file_raw(raw)
if article is None:
print(" ERROR")
else:
print(" success:", article.title)
articles.append(article)
return articles
def populate(lexicon_articles):
"""
Given a list of lexicon articles, fills out citation information
for each article and creates phantom pages for missing articles.
"""
article_by_title = {article.title : article for article in lexicon_articles}
# Determine all articles that exist or should exist
extant_titles = set([citation[1] for article in lexicon_articles for citation in article.citations])
# Interlink all citations
for article in lexicon_articles:
for cite_tuple in article.citations.values():
target = cite_tuple[1]
# Create article objects for phantom citations
if target not in article_by_title:
article_by_title[target] = LexiconArticle(None, sys.maxsize, target, "<p><i>This entry hasn't been written yet.</i></p>", {})
# Interlink citations
if article_by_title[target].author is None:
article.pcites.add(target)
else:
article.wcites.add(target)
article_by_title[target].citedby.add(article.title)
return list(article_by_title.values())
def load_resource(filename, cache={}):
"""Loads files from the resources directory with caching."""
if filename not in cache:
cache[filename] = open("resources/" + filename, "r", encoding="utf8").read()
return cache[filename]
def load_config():
"""Loads values from the config file."""
config = {}
with open("lexicon.cfg", "r", encoding="utf8") as f:
line = f.readline()
while line:
# Skim lines until a value definition begins
conf_match = re.match(">>>([^>]+)>>>\s+", line)
if not conf_match:
line = f.readline()
continue
# Accumulate the conf value until the value ends
conf = conf_match.group(1)
conf_value = ""
line = f.readline()
conf_match = re.match("<<<{0}<<<\s+".format(conf), line)
while line and not conf_match:
conf_value += line
line = f.readline()
conf_match = re.match("<<<{0}<<<\s+".format(conf), line)
if not line:
raise SystemExit("Reached EOF while reading config value {}".format(conf))
config[conf] = conf_value.strip()
# Check that all necessary values were configured
for config_value in ['LEXICON_TITLE', 'PROMPT', 'SESSION_PAGE', "INDEX_LIST"]:
if config_value not in config:
raise SystemExit("Error: {} not set in lexipython.cfg".format(config_value))
return config
# Build functions
def build_contents_page(articles, config):
"""
Builds the full HTML of the contents page.
"""
content = ""
# Article counts
phantom_count = len([article for article in articles if article.author is None])
if phantom_count == 0:
content = "<p>There are <b>{0}</b> entries in this lexicon.</p>\n".format(len(articles))
else:
content = "<p>There are <b>{0}</b> entries, <b>{1}</b> written and <b>{2}</b> phantom.</p>\n".format(
len(articles), len(articles) - phantom_count, phantom_count)
# Prepare article links
link_by_title = {article.title : "<a href=\"../article/{1}.html\"{2}>{0}</a>".format(
article.title, article.title_filesafe,
"" if article.author is not None else " class=\"phantom\"")
for article in articles}
# Write the articles in alphabetical order
content += load_resource("contents.html")
content += "<div id=\"index-order\" style=\"display:block\">\n<ul>\n"
indices = config["INDEX_LIST"].split("\n")
alphabetical_order = sorted(articles, key=lambda a: a.title)
check_off = list(alphabetical_order)
for index_str in indices:
content += "<h3>{0}</h3>\n".format(index_str)
for article in alphabetical_order:
if (titlestrip(article.title)[0].upper() in index_str):
check_off.remove(article)
content += "<li>"
content += link_by_title[article.title]
content += "</li>\n"
if len(check_off) > 0:
content += "<h3>&c.</h3>\n".format(index_str)
for article in check_off:
content += "<li>"
content += link_by_title[article.title]
content += "</li>\n"
content += "</ul>\n</div>\n"
# Write the articles in turn order
content += "<div id=\"turn-order\" style=\"display:none\">\n<ul>\n"
latest_turn = max([article.turn for article in articles if article.author is not None])
turn_order = sorted(articles, key=lambda a: (a.turn, a.title))
check_off = list(turn_order)
for turn_num in range(1, latest_turn + 1):
content += "<h3>Turn {0}</h3>\n".format(turn_num)
for article in turn_order:
if article.turn == turn_num:
check_off.remove(article)
content += "<li>"
content += link_by_title[article.title]
content += "</li>\n"
if len(check_off) > 0:
content += "<h3>Unwritten</h3>\n"
for article in check_off:
content += "<li>"
content += link_by_title[article.title]
content += "</li>\n"
content += "</ul>\n</div>\n"
# Fill in the page skeleton
entry_skeleton = load_resource("entry-page.html")
css = load_resource("lexicon.css")
return entry_skeleton.format(
title="Index of " + config["LEXICON_TITLE"],
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
content=content,
citeblock="")
def build_rules_page(config):
"""
Builds the full HTML of the rules page.
"""
content = load_resource("rules.html")
# Fill in the entry skeleton
entry_skeleton = load_resource("entry-page.html")
css = load_resource("lexicon.css")
return entry_skeleton.format(
title="Rules",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
content=content,
citeblock="")
def build_formatting_page(config):
"""
Builds the full HTML of the formatting page.
"""
content = load_resource("formatting.html")
# Fill in the entry skeleton
entry_skeleton = load_resource("entry-page.html")
css = load_resource("lexicon.css")
return entry_skeleton.format(
title="Formatting",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
content=content,
citeblock="")
def build_session_page(config):
"""
Builds the full HTML of the session page.
"""
# Fill in the entry skeleton
entry_skeleton = load_resource("entry-page.html")
css = load_resource("lexicon.css")
return entry_skeleton.format(
title=config["LEXICON_TITLE"],
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
content=config["SESSION_PAGE"],
citeblock="")
def build_statistics_page(articles, config):
"""
Builds the full HTML of the statistics page.
"""
content = ""
cite_map = {article.title : [cite_tuple[1] for cite_tuple in article.citations.values()] for article in articles}
# Pages by pagerank
content += "<div class=\"moveable\">\n"
content += "<p><u>Top 10 pages by page rank:</u><br>\n"
G = networkx.Graph()
for citer, citeds in cite_map.items():
for cited in citeds:
G.add_edge(citer, cited)
ranks = networkx.pagerank(G)
sranks = sorted(ranks.items(), key=lambda x: x[1], reverse=True)
ranking = list(enumerate(map(lambda x: x[0], sranks)))
content += "<br>\n".join(map(lambda x: "{0} &ndash; {1}".format(x[0]+1, x[1]), ranking[:10]))
content += "</p>\n"
content += "</div>\n"
# Top numebr of citations made
content += "<div class=\"moveable\">\n"
content += "<p><u>Most citations made from:</u><br>\n"
citation_tally = [(kv[0], len(kv[1])) for kv in cite_map.items()]
citation_count = defaultdict(list)
for title, count in citation_tally: citation_count[count].append(title)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], "; ".join(kv[1])),
sorted(citation_count.items(), reverse=True)[:3]))
content += "</p>\n"
content += "</div>\n"
# Top number of times cited
content += "<div class=\"moveable\">\n"
content += "<p><u>Most citations made to:</u><br>\n"
all_cited = set([title for cites in cite_map.values() for title in cites])
cited_by_map = { cited: [citer for citer in cite_map.keys() if cited in cite_map[citer]] for cited in all_cited }
cited_tally = [(kv[0], len(kv[1])) for kv in cited_by_map.items()]
cited_count = defaultdict(list)
for title, count in cited_tally: cited_count[count].append(title)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], "; ".join(kv[1])),
sorted(cited_count.items(), reverse=True)[:3]))
content += "</p>\n"
content += "</div>\n"
# Author pageranks
content += "<div class=\"moveable\">\n"
content += "<p><u>Author total page rank:</u><br>\n"
authors = sorted(set([article.author for article in articles if article.author is not None]))
articles_by = {author : [a for a in articles if a.author == author] for author in authors}
author_rank = {author : sum(map(lambda a: ranks[a.title], articles)) for author, articles in articles_by.items()}
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], round(kv[1], 3)),
sorted(author_rank.items(), key=lambda t:-t[1])))
content += "</p>\n"
content += "</div>\n"
# Author citations made
content += "<div class=\"moveable\">\n"
content += "<p><u>Citations made by author</u><br>\n"
author_cite_count = {author : sum(map(lambda a:len(a.wcites | a.pcites), articles)) for author, articles in articles_by.items()}
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], kv[1]),
sorted(author_cite_count.items(), key=lambda t:-t[1])))
content += "</p>\n"
content += "</div>\n"
# Author cited count
content += "<div class=\"moveable\">\n"
content += "<p><u>Citations made to author</u><br>\n"
cited_times = {author : 0 for author in authors}
for article in articles:
if article.author is not None:
cited_times[article.author] += len(article.citedby)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], kv[1]),
sorted(cited_times.items(), key=lambda t:-t[1])))
content += "</p>\n"
content += "</div>\n"
# Fill in the entry skeleton
entry_skeleton = load_resource("entry-page.html")
css = load_resource("lexicon.css")
return entry_skeleton.format(
title="Statistics",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
content=content,
citeblock="")
def build_graphviz_file(cite_map):
"""
Builds a citation graph in dot format for Graphviz.
"""
result = []
result.append("digraph G {\n")
# Node labeling
written_entries = list(cite_map.keys())
phantom_entries = set([title for cites in cite_map.values() for title in cites if title not in written_entries])
node_labels = [title[:20] for title in written_entries + list(phantom_entries)]
node_names = [hash(i) for i in node_labels]
for i in range(len(node_labels)):
result.append("{} [label=\"{}\"];\n".format(node_names[i], node_labels[i]))
# Edges
for citer in written_entries:
for cited in cite_map[citer]:
result.append("{}->{};\n".format(hash(citer[:20]), hash(cited[:20])))
# Return result
result.append("overlap=false;\n}\n")
return "".join(result)#"…"
# Summative functions
def command_build(argv):
if len(argv) >= 3 and (argv[2] != "partial" and argv[2] != "full"):
print("unknown build type: " + argv[2])
return return
# Load content result, msg, status = is_lexicon(name)
config = load_config() if not result:
entry_skeleton = load_resource("entry-page.html") print("Error: " + msg)
css = load_resource("lexicon.css") return
articles = [article for article in parse_from_directory("raw/") if article is not None] # Print status and summary
written_titles = [article.title for article in articles] print(msg)
articles = sorted(populate(articles), key=lambda a: (a.turn, a.title)) print(status)
#print(articles[13].title_filesafe) # TODO
#return
phantom_titles = [article.title for article in articles if article.title not in written_titles]
# Write the redirect page def run_command(name, command):
print("Writing redirect page...") """
with open("out/index.html", "w", encoding="utf8") as f: Runs a command on a Lexicon.
f.write(load_resource("redirect.html").format(lexicon=config["LEXICON_TITLE"])) """
if command == "init":
# Check that the folder isn't already there
if os.path.exists(os.path.join("lexicon", name)):
print("Error: Can't create '{}', it already exists.".format(name))
return
# Create the Lexicon
command_init(name)
return
elif command == "build":
if not os.path.exists(os.path.join("lexicon", name)):
print("Error: There is no Lexicon named '{}'.".format(name))
return
result, msg, status = is_lexicon(name)
if not result:
print("Error: " + msg)
return
# Build the Lexicon
command_build(name)
return
elif command == "run":
if not os.path.exists(os.path.join("lexicon", name)):
print("Error: There is no Lexicon named '{}'.".format(name))
return
result, msg, status = is_lexicon(name)
if not result:
print("Error: " + msg)
return
# Run a server managing the Lexicon
command_run(name)
return
else:
print("Error: '{}' is not a valid command.".format(command))
return
# Write the article pages def command_init(name):
print("Deleting old article pages...") """
for filename in os.listdir("out/article/"): Sets up a Lexicon game with the given name.
if filename[-5:] == ".html": """
os.remove("out/article/" + filename) # Create the folder structure
print("Writing article pages...") lex_path = os.path.join("lexicon", name)
l = len(articles) os.mkdir(lex_path)
for idx in range(l): os.mkdir(os.path.join(lex_path, "src"))
article = articles[idx] os.mkdir(os.path.join(lex_path, "article"))
with open("out/article/" + article.title_filesafe + ".html", "w", encoding="utf8") as f: os.mkdir(os.path.join(lex_path, "contents"))
content = article.build_page_content() os.mkdir(os.path.join(lex_path, "formatting"))
citeblock = article.build_page_citeblock( os.mkdir(os.path.join(lex_path, "rules"))
None if idx == 0 else articles[idx - 1].title, os.mkdir(os.path.join(lex_path, "session"))
None if idx == l-1 else articles[idx + 1].title) os.mkdir(os.path.join(lex_path, "statistics"))
article_html = entry_skeleton.format( # Open the default config file
title = article.title, config = utils.load_resource("lexicon.cfg")
lexicon = config["LEXICON_TITLE"], # Edit the name field
css = css, config = re.sub("Lexicon Title", "Lexicon {}".format(name), config)
logo = config["LOGO_FILENAME"], # Create the Lexicon's config file
prompt = config["PROMPT"], with open(os.path.join(lex_path, "lexicon.cfg"), "w") as config_file:
content = content, config_file.write(config)
citeblock = citeblock) # Create an example page
f.write(article_html) with open(os.path.join(lex_path, "src", "example-page.txt"), "w") as destfile:
print(" Wrote " + article.title) destfile.write(utils.load_resource("example-page.txt"))
# Create an empty status file
open(os.path.join(lex_path, "status"), "w").close()
print("Created Lexicon {}".format(name))
# Done initializing
return
# Write default pages def command_build(name):
print("Writing default pages...") """
with open("out/contents/index.html", "w", encoding="utf8") as f: Rebuilds the browsable pages of a Lexicon.
f.write(build_contents_page(articles, config)) """
print(" Wrote Contents") build.build_all("lexicon", name)
with open("out/rules/index.html", "w", encoding="utf8") as f:
f.write(build_rules_page(config))
print(" Wrote Rules")
with open("out/formatting/index.html", "w", encoding="utf8") as f:
f.write(build_formatting_page(config))
print(" Wrote Formatting")
with open("out/session/index.html", "w", encoding="utf8") as f:
f.write(build_session_page(config))
print(" Wrote Session")
with open("out/statistics/index.html", "w", encoding="utf8") as f:
f.write(build_statistics_page(articles, config))
print(" Wrote Statistics")
# Write auxiliary files def command_run(name):
# TODO: write graphviz file """
# TODO: write compiled lexicon page Runs as a server managing a Lexicon.
"""
print("Not implemented")
def main(): def main():
if len(sys.argv) < 2: parser = argparse.ArgumentParser(
print("Available commands:") description="Lexipython is a Python application for playing the Lexicon RPG.",
print(" - build [partial] : Build the lexicon and generate phantom stubs for all unwritten articles.") epilog="Run lexipython.py without arguments to list the extant Lexicons.\n\n"\
print(" - build full : Build the lexicon and generate Ersatz pages for all unwritten articles.") "Available commands:\n\n"\
elif sys.argv[1] == "build": " init Create a Lexicon with the provided name\n"\
command_build(sys.argv) " build Build the Lexicon, then exit\n"\
" run Launch a persistent server managing the Lexicon\n",
formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument("name", help="The name of the Lexicon to operate on",
nargs="?", default=None)
parser.add_argument("command", help="The operation to perform on the Lexicon",
nargs="?", default=None)
args = parser.parse_args()
# If no Lexicon as specified
if args.name is None:
overview_all()
# If no command was specified
elif args.command is None:
overview_one(args.name)
# A command was specified
else: else:
print("Unknown command: " + sys.argv[1]) run_command(args.name, args.command)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -1,49 +0,0 @@
<html>
<head>
<title>Example page | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Example page</h1>
<p>This is an example page.
Some words are <i>italicized</i>,
and some words are <b>bolded</b>.
All of these sentences are part of the same paragraph.</p>
<p>This is a new paragraph.<br>
Unlike the last paragraph, this line will be after a line break within the paragraph.</p>
<p>This is an <a href="Phantom_page.html" class="phantom">example citation</a>. You can also cite a <a href="Phantom_page.html" class="phantom">phantom page</a> with just the title.</p>
<hr><span class="signature"><p>Dr. X. Amplepage</p></span>
</div>
<div class="content citeblock">
<p style="float:right"><a href="Phantom_page.html">Next &#8594;</a></p>
<p>&nbsp;</p>
<p>Citations: <a href="Phantom_page.html" class="phantom">Phantom page</a></p>
<p>Cited by: --</p>
</div>
</body>
</html>

View File

@ -1,40 +0,0 @@
<html>
<head>
<title>Phantom page | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Phantom page</h1>
<p><i>This entry hasn't been written yet.</i></p></div>
<div class="content citeblock">
<p><a href="Example_page.html">&#8592; Previous</a></p>
<p>Citations: --</p>
<p>Cited by: <a href="Example_page.html">Example page</a></p>
</div>
</body>
</html>

View File

@ -1,75 +0,0 @@
<html>
<head>
<title>Index of Lexicon Title | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Index of Lexicon Title</h1>
<p>There are <b>2</b> entries, <b>1</b> written and <b>1</b> phantom.</p>
<script type="text/javascript">
contentsToggle = function() {
var b = document.getElementById("toggle-button")
var i = document.getElementById("index-order");
var t = document.getElementById("turn-order");
if (t.style.display == "none") {
i.style.display = "none"
t.style.display = "block"
b.innerText = "Switch to index order"
} else {
i.style.display = "block"
t.style.display = "none"
b.innerText = "switch to turn order"
}
}
</script>
<button id="toggle-button" onClick="javascript:contentsToggle()">Switch to turn order</button>
<div id="index-order" style="display:block">
<ul>
<h3>ABC</h3>
<h3>DEF</h3>
<li><a href="../article/Example_page.html">Example page</a></li>
<h3>GHI</h3>
<h3>JKL</h3>
<h3>MNO</h3>
<h3>PQRS</h3>
<li><a href="../article/Phantom_page.html" class="phantom">Phantom page</a></li>
<h3>TUV</h3>
<h3>WXYZ</h3>
</ul>
</div>
<div id="turn-order" style="display:none">
<ul>
<h3>Turn 1</h3>
<li><a href="../article/Example_page.html">Example page</a></li>
<h3>Unwritten</h3>
<li><a href="../article/Phantom_page.html" class="phantom">Phantom page</a></li>
</ul>
</div>
</div>
</body>
</html>

View File

@ -1,58 +0,0 @@
<html>
<head>
<title>Formatting | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Formatting</h1>
<p>Lexipython provides support for a limited amount of Markdown-esque formatting.</p>
<pre style="background:#eeeeee">
# Author: Authorname
# Turn: 1
# Title: Example page
This is an example page.
Some words are //italicized//,
and some words are **bolded**.
All of these sentences are part of the same paragraph.
This is a new paragraph.\\
Unlike the last paragraph, this line will be after a line break within the paragraph.
This is an [[example citation|Phantom page]]. You can also cite a [[phantom page]] with just the title.
~Dr. X. Amplepage
</pre>
<p>Each turn, fill out the header with your author information, the current turn, and the title of your entry. It doesn't really matter what the <i>Author</i> field is, except that it must be the same across all articles you write.</p>
<p>Two line breaks begins a new paragraph. A single line break does nothing, unless the line is neded by a double backslash (\\).</p>
<p>Text bounded by ** will be bolded: **bold** produces <b>bold</b>. Text bounded by // will be italicized: //italics// produces <i>italics</i>.</p>
<p>To cite another Lexicon entry, use double brackets. Text in double brackets will cite and link to the entry of the same name: [[Example page]] produces <a href="Example_page.html" class="phantom">Example page</a>. Text in double brackets split with a | will alias the link as the left text and link to the entry with the name of the right text: [[this text|Example page]] produces <a href="Example_page.html" class="phantom">this text</a>. <b>You must be precise in the entry title you cite to.</b> Citations to "Example" vs. "The Example" will point to different entries and create different phantoms, and your GM will probably have to clean up after you.</p>
<p>Beginning a paragraph with ~ will right-align it and place a horizontal line above it. Use this for signing your entry with your scholar's name.</p>
</div>
</body>
</html>

View File

@ -1,9 +0,0 @@
<html>
<head>
<title>Lexicon Title</title>
<meta http-equiv="refresh" content="0; url=contents/" />
</head>
<body>
<p>Redirecting to <a href="contents/">Lexicon Title</a>...</p>
</body>
</html>

View File

@ -1,53 +0,0 @@
<html>
<head>
<title>Rules | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Rules</h1>
<ol>
<li>At the beginning of the game, you will be provided with a <i>topic statement</i> that sets the tone for the game. Use it for inspiration and a stepping-stone into shaping the world of the Lexicon.</li>
<li>Each round, you will be assigned an <i>index</i>, a grouping of letters. Your entry must alphabetize under that index.<ol>
<li>Each index has a number of open slots equal to the number of players, which are taken up by article titles when an article is written in that index or a citation is made to an unwritten article, or <i>phantom</i>. If there are no open slots in your index, you must write the article for a phantom in that index.</li>
<li>"The" and "A" aren't counted in indexing.</li></ol></li>
<li>Once you've picked an article title, write your article on that subject.<ol>
<li>There are no hard and fast rules about style. Try to sound like an encyclopedia entry or the overview section at the top of a wiki article.</li>
<li>You must respect and not contradict any factual content of any posted articles. You may introduce new facts that place things in a new light, provide alternative interpretations, or flesh out unexplained details in unexpected ways; but you must not <i>contradict</i> what has been previously established as fact.</li>
<li>Aim for around 200-300 words.</li></ol></li>
<li>Your article must cite other articles in the Lexicon. Sometimes these citations will be to phantoms, articles that have not been written yet.<ol>
<li>On the first turn, your article must cite <i>exactly two</i> phantom articles.</li>
<li>On subsequent turns, your article must cite <i>exactly two</i> phantom articles, either already-cited phantoms or new ones. Your article must also cite <i>at least one</i> written article.</li>
<li>On the penultimate turn, you must cite <i>exactly one</i> phantom article and <i>at least two</i> written articles.</li>
<li>On the final turn, you must cite <i>at least three</i> written articles.</li>
<li>You may not cite an entry you wrote. You may cite phantoms you have cited before.</li>
<li>Once you cite a phantom, you cannot choose to write it if you write an article for that index later.</li></ol></li>
<p><b>Ersatz Scrivener.</b> In the course of the game, it may come to pass that a scholar is assigned an index in which no slots are available, because this scholar has already cited all the phantoms in previous articles. When this happens, the player instead writes their article as Ersatz Scrivener, radical skeptic. Ersatz does not believe in the existence of whatever he is writing about, no matter how obvious it seems to others or how central it is in the developing history of the world. All references, testimony, etc. with regard to its existence are tragic delusion at best or malicious lies at worst. Unlike the other scholars, Ersatz does not treat the research of his peers as fact, because he does not believe he has peers. Players writing articles as Ersatz are encouraged to name and shame the work of the misguided amateurs collaborating with him.</p>
</div>
</body>
</html>

View File

@ -1,35 +0,0 @@
<html>
<head>
<title>Lexicon Title | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Lexicon Title</h1>
<p>Put session information here, like the index grouping and turn count, where to send completed entries, index assignments, turn schedule, and so on.</p></div>
</body>
</html>

View File

@ -1,61 +0,0 @@
<html>
<head>
<title>Statistics | Lexicon Title</title>
<!--<link rel="shortcut icon" href="favicon.png" />-->
<style>
body { background-color: #eeeeee; margin: 10px; }
div#header { background-color: #ffffff; margin: 10px 0; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
img#logo { float:left; margin:8px; max-width: 140px; }
div#header p { margin:10px; }
div.content { margin-top: 10px; background-color: #ffffff; padding: 10px; box-shadow: 2px 2px 10px #888888; overflow: hidden; }
a.phantom { color: #cc2200; }
div.citeblock a.phantom { font-style: italic; }
span.signature { text-align: right; }
div.moveable { float: left; margin: 8px; }
div.moveable p { margin: 0px; }
</style>
</head>
<body>
<div id="header">
<img id="logo" src="../logo.png">
<p><span style="font-size:1.5em;">Lexicon Title</span></p>
<p>
<a href="../contents">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash;
<a href="../statistics/">Statistics</a>
</p>
<p><i>Prompt goes here</i></p>
</div>
<div class="content">
<h1>Statistics</h1>
<div class="moveable">
<p><u>Top 10 pages by page rank:</u><br>
1 &ndash; Example page<br>
2 &ndash; Phantom page</p>
</div>
<div class="moveable">
<p><u>Most citations made from:</u><br>
2 &ndash; Example page<br>
0 &ndash; Phantom page</p>
</div>
<div class="moveable">
<p><u>Most citations made to:</u><br>
1 &ndash; Phantom page</p>
</div>
<div class="moveable">
<p><u>Author total page rank:</u><br>
Authorname &ndash; 0.5</p>
</div>
<div class="moveable">
<p><u>Citations made by author</u><br>
Authorname &ndash; 1</p>
</div>
<div class="moveable">
<p><u>Citations made to author</u><br>
Authorname &ndash; 0</p>
</div>
</div>
</body>
</html>

View File

@ -1,21 +0,0 @@
<script type="text/javascript">
contentsToggle = function() {
var b = document.getElementById("toggle-button")
var i = document.getElementById("index-order");
var t = document.getElementById("turn-order");
if (t.style.display == "none") {
i.style.display = "none"
t.style.display = "block"
b.innerText = "Switch to index order"
} else {
i.style.display = "block"
t.style.display = "none"
b.innerText = "Switch to turn order"
}
}
window.onload = function(){
if (location.search.search("byturn") > 0)
contentsToggle();
}
</script>
<button id="toggle-button" onClick="javascript:contentsToggle()">Switch to turn order</button>

View File

@ -1,9 +0,0 @@
<html>
<head>
<title>{lexicon}</title>
<meta http-equiv="refresh" content="0; url=contents/" />
</head>
<body>
<p>Redirecting to <a href="contents/">{lexicon}</a>...</p>
</body>
</html>

201
src/article.py Normal file
View File

@ -0,0 +1,201 @@
import os
import sys
import re
import src.utils as utils
class LexiconArticle:
"""
A Lexicon article and its metadata.
Members:
player string: the player of the article
turn integer: the turn the article was written for
title string: the article title
title_filesafe string: the title, escaped, used for filenames
content string: the HTML content, with citations replaced by format hooks
citations dict mapping format hook string to tuple of link alias and link target title
wcites list: titles of written articles cited
pcites list: titles of phantom articles cited
citedby list: titles of articles that cite this
The last three are filled in by populate().
"""
def __init__(self, player, turn, title, content, citations):
"""
Creates a LexiconArticle object with the given parameters.
"""
self.player = player
self.turn = turn
self.title = title
self.title_filesafe = utils.titleescape(title)
self.content = content
self.citations = citations
self.wcites = set()
self.pcites = set()
self.citedby = set()
@staticmethod
def from_file_raw(raw_content):
"""
Parses the contents of a Lexipython source file into a LexiconArticle
object. If the source file is malformed, returns None.
"""
headers = raw_content.split('\n', 3)
if len(headers) != 4:
print("Header read error")
return None
player_header, turn_header, title_header, content_raw = headers
# Validate and sanitize the player header
if not player_header.startswith("# Player:"):
print("Player header missing or corrupted")
return None
player = player_header[9:].strip()
# Validate and sanitize the turn header
if not turn_header.startswith("# Turn:"):
print("Turn header missing or corrupted")
return None
turn = None
try:
turn = int(turn_header[7:].strip())
except:
print("Turn header error")
return None
# Validate and sanitize the title header
if not title_header.startswith("# Title:"):
print("Title header missing or corrupted")
return None
title = utils.titlecase(title_header[8:])
# Parse the content and extract citations
paras = re.split("\n\n+", content_raw.strip())
content = ""
citations = {}
format_id = 1
if not paras:
print("No content")
for para in paras:
# Escape angle brackets
para = re.sub("<", "&lt;", para)
para = re.sub(">", "&gt;", para)
# Replace bold and italic marks with tags
para = re.sub(r"//([^/]+)//", r"<i>\1</i>", para)
para = re.sub(r"\*\*([^*]+)\*\*", r"<b>\1</b>", para)
# Replace \\LF with <br>LF
para = re.sub(r"\\\\\n", "<br>\n", para)
# Abstract citations into the citation record
link_match = re.search(r"\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]", para)
while link_match:
# Identify the citation text and cited article
cite_text = link_match.group(2) if link_match.group(2) else link_match.group(3)
cite_title = utils.titlecase(re.sub(r"\s+", " ", link_match.group(3)))
# Record the citation
citations["c"+str(format_id)] = (cite_text, cite_title)
# Stitch the format id in place of the citation
para = para[:link_match.start(0)] + "{c"+str(format_id)+"}" + para[link_match.end(0):]
format_id += 1 # Increment to the next format citation
link_match = re.search(r"\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]", para)
# Convert signature to right-aligned
if para[:1] == '~':
para = "<hr><span class=\"signature\"><p>" + para[1:] + "</p></span>\n"
else:
para = "<p>" + para + "</p>\n"
content += para
return LexiconArticle(player, turn, title, content, citations)
@staticmethod
def parse_from_directory(directory):
"""
Reads and parses each source file in the given directory.
Input: directory, the path to the folder to read
Output: a list of parsed articles
"""
articles = []
print("Reading source files from", directory)
for filename in os.listdir(directory):
path = os.path.join(directory, filename)
# Read only .txt files
if filename[-4:] == ".txt":
print(" Parsing", filename)
with open(path, "r", encoding="utf8") as src_file:
raw = src_file.read()
article = LexiconArticle.from_file_raw(raw)
if article is None:
print(" ERROR")
else:
print(" success:", article.title)
articles.append(article)
return articles
@staticmethod
def populate(lexicon_articles):
"""
Given a list of lexicon articles, fills out citation information
for each article and creates phantom pages for missing articles.
"""
article_by_title = {article.title : article for article in lexicon_articles}
# Determine all articles that exist or should exist
extant_titles = set([citation[1] for article in lexicon_articles for citation in article.citations])
# Interlink all citations
for article in lexicon_articles:
for cite_tuple in article.citations.values():
target = cite_tuple[1]
# Create article objects for phantom citations
if target not in article_by_title:
article_by_title[target] = LexiconArticle(None, sys.maxsize, target, "<p><i>This entry hasn't been written yet.</i></p>", {})
# Interlink citations
if article_by_title[target].player is None:
article.pcites.add(target)
else:
article.wcites.add(target)
article_by_title[target].citedby.add(article.title)
return list(article_by_title.values())
def build_default_content(self):
"""
Formats citations into the article content as normal HTML links
and returns the result.
"""
format_map = {
format_id: "<a href=\"{1}.html\"{2}>{0}</a>".format(
cite_tuple[0], utils.titleescape(cite_tuple[1]),
"" if cite_tuple[1] in self.wcites else " class=\"phantom\"")
for format_id, cite_tuple in self.citations.items()
}
return self.content.format(**format_map)
def build_default_citeblock(self, prev_article, next_article):
"""
Builds the citeblock content HTML for use in regular article pages.
For each defined target, links the target page as Previous or Next.
"""
citeblock = "<div class=\"content citeblock\">\n"
# Prev/next links
if next_article is not None:
citeblock += "<p style=\"float:right\"><a href=\"{}.html\"{}>Next &#8594;</a></p>\n".format(
next_article.title_filesafe, " class=\"phantom\"" if next_article.player is None else "")
if prev_article is not None:
citeblock += "<p><a href=\"{}.html\"{}>&#8592; Previous</a></p>\n".format(
prev_article.title_filesafe, " class=\"phantom\"" if prev_article.player is None else "")
if next_article is None and prev_article is None:
citeblock += "<p>&nbsp;</p>\n"
# Citations
cites_links = [
"<a href=\"{1}.html\"{2}>{0}</a>".format(
title, utils.titleescape(title),
"" if title in self.wcites else " class=\"phantom\"")
for title in sorted(
self.wcites | self.pcites,
key=lambda t: utils.titlesort(t))]
cites_str = " | ".join(cites_links)
if len(cites_str) < 1: cites_str = "&mdash;"
citeblock += "<p>Citations: {}</p>\n".format(cites_str)
# Citedby
citedby_links = [
"<a href=\"{1}.html\">{0}</a>".format(
title, utils.titleescape(title))
for title in sorted(
self.citedby,
key=lambda t: utils.titlesort(t))]
citedby_str = " | ".join(citedby_links)
if len(citedby_str) < 1: citedby_str = "&mdash;"
citeblock += "<p>Cited by: {}</p>\n</div>\n".format(citedby_str)
return citeblock

425
src/build.py Normal file
View File

@ -0,0 +1,425 @@
import sys # For argv and stderr
import os # For reading directories
import re # For parsing lex content
import io # For writing pages out as UTF-8
import networkx # For pagerank analytics
from collections import defaultdict # For rank inversion in statistics
from src import utils
from src.article import LexiconArticle
def build_contents_page(articles, config):
"""
Builds the full HTML of the contents page.
"""
content = ""
# Head the contents page with counts of written and phantom articles
phantom_count = len([article for article in articles if article.player is None])
if phantom_count == 0:
content = "<p>There are <b>{0}</b> entries in this lexicon.</p>\n".format(len(articles))
else:
content = "<p>There are <b>{0}</b> entries, <b>{1}</b> written and <b>{2}</b> phantom.</p>\n".format(
len(articles), len(articles) - phantom_count, phantom_count)
# Prepare article links
link_by_title = {article.title : "<a href=\"../article/{1}.html\"{2}>{0}</a>".format(
article.title, article.title_filesafe,
" class=\"phantom\"" if article.player is None else "")
for article in articles}
# Write the articles in alphabetical order
content += utils.load_resource("contents.html")
content += "<div id=\"index-order\" style=\"display:none\">\n<ul>\n"
indices = config["INDEX_LIST"].split("\n")
alphabetical_order = sorted(
articles,
key=lambda a: utils.titlesort(a.title))
check_off = list(alphabetical_order)
for index_str in indices:
content += "<h3>{0}</h3>\n".format(index_str)
for article in alphabetical_order:
if (utils.titlesort(article.title)[0].upper() in index_str):
check_off.remove(article)
content += "<li>{}</li>\n".format(link_by_title[article.title])
if len(check_off) > 0:
content += "<h3>&c.</h3>\n"
for article in check_off:
content += "<li>{}</li>\n".format(link_by_title[article.title])
content += "</ul>\n</div>\n"
# Write the articles in turn order
content += "<div id=\"turn-order\" style=\"display:none\">\n<ul>\n"
latest_turn = max([article.turn for article in articles if article.player is not None])
turn_order = sorted(
articles,
key=lambda a: (a.turn, utils.titlesort(a.title)))
check_off = list(turn_order)
for turn_num in range(1, latest_turn + 1):
content += "<h3>Turn {0}</h3>\n".format(turn_num)
for article in turn_order:
if article.turn == turn_num:
check_off.remove(article)
content += "<li>{}</li>\n".format(link_by_title[article.title])
if len(check_off) > 0:
content += "<h3>Unwritten</h3>\n"
for article in check_off:
content += "<li>{}</li>\n".format(link_by_title[article.title])
content += "</ul>\n</div>\n"
# Fill in the page skeleton
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
return entry_skeleton.format(
title="Index of " + config["LEXICON_TITLE"],
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
sort=config["DEFAULT_SORT"],
content=content,
citeblock="")
def build_rules_page(config):
"""
Builds the full HTML of the rules page.
"""
content = utils.load_resource("rules.html")
# Fill in the entry skeleton
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
return entry_skeleton.format(
title="Rules",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
sort=config["DEFAULT_SORT"],
content=content,
citeblock="")
def build_formatting_page(config):
"""
Builds the full HTML of the formatting page.
"""
content = utils.load_resource("formatting.html")
# Fill in the entry skeleton
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
return entry_skeleton.format(
title="Formatting",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
sort=config["DEFAULT_SORT"],
content=content,
citeblock="")
def build_session_page(config):
"""
Builds the full HTML of the session page.
"""
# Fill in the entry skeleton
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
return entry_skeleton.format(
title=config["LEXICON_TITLE"],
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
sort=config["DEFAULT_SORT"],
content=config["SESSION_PAGE"],
citeblock="")
def build_statistics_page(articles, config):
"""
Builds the full HTML of the statistics page.
"""
content = ""
cite_map = {
article.title : [
cite_tuple[1]
for cite_tuple in article.citations.values()]
for article in articles}
# Pages by pagerank
content += "<div class=\"moveable\">\n"
content += "<p><u>Top 10 pages by page rank:</u><br>\n"
G = networkx.Graph()
for citer, citeds in cite_map.items():
for cited in citeds:
G.add_edge(citer, cited)
ranks = networkx.pagerank(G)
sranks = sorted(ranks.items(), key=lambda x: x[1], reverse=True)
ranking = list(enumerate(map(lambda x: x[0], sranks)))
content += "<br>\n".join(map(lambda x: "{0} &ndash; {1}".format(x[0]+1, x[1]), ranking[:10]))
content += "</p>\n"
content += "</div>\n"
# Top number of citations made
content += "<div class=\"moveable\">\n"
content += "<p><u>Most citations made from:</u><br>\n"
citation_tally = [(kv[0], len(kv[1])) for kv in cite_map.items()]
citation_count = defaultdict(list)
for title, count in citation_tally: citation_count[count].append(title)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(
kv[0],
"; ".join(sorted(
kv[1],
key=lambda t: utils.titlesort(t)))),
sorted(citation_count.items(), reverse=True)[:3]))
content += "</p>\n"
content += "</div>\n"
# Top number of times cited
content += "<div class=\"moveable\">\n"
content += "<p><u>Most citations made to:</u><br>\n"
all_cited = set([title for cites in cite_map.values() for title in cites])
cited_by_map = {
cited: [
citer
for citer in cite_map.keys()
if cited in cite_map[citer]]
for cited in all_cited }
cited_tally = [(kv[0], len(kv[1])) for kv in cited_by_map.items()]
cited_count = defaultdict(list)
for title, count in cited_tally: cited_count[count].append(title)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], "; ".join(sorted(kv[1]))),
sorted(cited_count.items(), reverse=True)[:3]))
content += "</p>\n"
content += "</div>\n"
# Top article length, roughly by words
content += "<div class=\"moveable\">\n"
content += "<p><u>Longest article:</u><br>\n"
article_length = {}
for article in articles:
format_map = {
format_id: cite_tuple[0]
for format_id, cite_tuple in article.citations.items()
}
plain_content = article.content.format(**format_map)
words = len(plain_content.split())
article_length[article.title] = words
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[1], kv[0]),
sorted(article_length.items(), reverse=True, key=lambda t: t[1])[:3]))
content += "</p>\n"
content += "</div>\n"
# Player pageranks
content += "<div class=\"moveable\">\n"
content += "<p><u>Player total page rank:</u><br>\n"
players = sorted(set([article.player for article in articles if article.player is not None]))
articles_by = {
player : [
a
for a in articles
if a.player == player]
for player in players}
player_rank = {
player : sum(map(lambda a: ranks[a.title] if a.title in ranks else 0, articles))
for player, articles in articles_by.items()}
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], round(kv[1], 3)),
sorted(player_rank.items(), key=lambda t:t[1], reverse=True)))
content += "</p>\n"
content += "</div>\n"
# Player citations made
content += "<div class=\"moveable\">\n"
content += "<p><u>Citations made by player</u><br>\n"
player_cite_count = {
player : sum(map(lambda a:len(a.wcites | a.pcites), articles))
for player, articles in articles_by.items()}
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], kv[1]),
sorted(player_cite_count.items(), key=lambda t:t[1], reverse=True)))
content += "</p>\n"
content += "</div>\n"
# Player cited count
content += "<div class=\"moveable\">\n"
content += "<p><u>Citations made to player</u><br>\n"
cited_times = {player : 0 for player in players}
for article in articles:
if article.player is not None:
cited_times[article.player] += len(article.citedby)
content += "<br>\n".join(map(
lambda kv: "{0} &ndash; {1}".format(kv[0], kv[1]),
sorted(cited_times.items(), key=lambda t:t[1], reverse=True)))
content += "</p>\n"
content += "</div>\n"
# Fill in the entry skeleton
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
return entry_skeleton.format(
title="Statistics",
lexicon=config["LEXICON_TITLE"],
css=css,
logo=config["LOGO_FILENAME"],
prompt=config["PROMPT"],
sort=config["DEFAULT_SORT"],
content=content,
citeblock="")
def build_graphviz_file(cite_map):
"""
Builds a citation graph in dot format for Graphviz.
"""
result = []
result.append("digraph G {\n")
# Node labeling
written_entries = list(cite_map.keys())
phantom_entries = set([title for cites in cite_map.values() for title in cites if title not in written_entries])
node_labels = [title[:20] for title in written_entries + list(phantom_entries)]
node_names = [hash(i) for i in node_labels]
for i in range(len(node_labels)):
result.append("{} [label=\"{}\"];\n".format(node_names[i], node_labels[i]))
# Edges
for citer in written_entries:
for cited in cite_map[citer]:
result.append("{}->{};\n".format(hash(citer[:20]), hash(cited[:20])))
# Return result
result.append("overlap=false;\n}\n")
return "".join(result)#"…"
def build_compiled_page(articles, config):
"""
Builds a page compiling all articles in the Lexicon.
"""
# Sort by turn and title
turn_order = sorted(
articles,
key=lambda a: (a.turn, utils.titlesort(a.title)))
# Build the content of each article
css = utils.load_resource("lexicon.css")
css += "\n"\
"body { background: #ffffff; }\n"\
"sup { vertical-align: top; font-size: 0.6em; }\n"
content = "<html>\n"\
"<head>\n"\
"<title>{lexicon}</title>\n"\
"<style>\n"\
"{css}\n"\
"</style>\n"\
"<body>\n"\
"<h1>{lexicon}</h1>".format(
lexicon=config["LEXICON_TITLE"],
css=css)
for article in turn_order:
# Stitch in superscripts for citations
format_map = {
format_id: "{}<sup>{}</sup>".format(cite_tuple[0], format_id[1:])
for format_id, cite_tuple in article.citations.items()
}
article_body = article.content.format(**format_map)
# Stitch a page-break-avoid div around the header and first paragraph
article_body = article_body.replace("</p>", "</p></div>", 1)
# Append the citation block
cite_list = "<br>\n".join(
"{}. {}\n".format(format_id[1:], cite_tuple[1])
for format_id, cite_tuple in sorted(
article.citations.items(),
key=lambda t:int(t[0][1:])))
cite_block = "" if article.player is None else ""\
"<p><i>Citations:</i><br>\n"\
"{}\n</p>".format(cite_list)
article_block = "<div style=\"page-break-inside:avoid;\">\n"\
"<h2>{}</h2>\n"\
"{}\n"\
"{}\n".format(article.title, article_body, cite_block)
content += article_block
content += "</body></html>"
return content
def build_all(path_prefix, lexicon_name):
"""
Builds all browsable articles and pages in the Lexicon.
"""
lex_path = os.path.join(path_prefix, lexicon_name)
# Load the Lexicon's peripherals
config = utils.load_config(lexicon_name)
entry_skeleton = utils.load_resource("entry-page.html")
css = utils.load_resource("lexicon.css")
# Parse the written articles
articles = LexiconArticle.parse_from_directory(os.path.join(lex_path, "src"))
# At this point, the articles haven't been cross-populated,
# so we can derive the written titles from this list
#written_titles = [article.title for article in articles]
# Once they've been populated, the articles list has the titles of all articles
# Sort this by turn before title so prev/next links run in turn order
articles = sorted(
LexiconArticle.populate(articles),
key=lambda a: (a.turn, utils.titlesort(a.title)))
#phantom_titles = [article.title for article in articles if article.title not in written_titles]
def pathto(*els):
return os.path.join(lex_path, *els)
# Write the redirect page
print("Writing redirect page...")
with open(pathto("index.html"), "w", encoding="utf8") as f:
f.write(utils.load_resource("redirect.html").format(lexicon=config["LEXICON_TITLE"], sort=config["DEFAULT_SORT"]))
# Write the article pages
print("Deleting old article pages...")
for filename in os.listdir(pathto("article")):
if filename[-5:] == ".html":
os.remove(pathto("article", filename))
print("Writing article pages...")
l = len(articles)
for idx in range(l):
article = articles[idx]
with open(pathto("article", article.title_filesafe + ".html"), "w", encoding="utf-8") as f:
content = article.build_default_content()
citeblock = article.build_default_citeblock(
None if idx == 0 else articles[idx - 1],
None if idx == l-1 else articles[idx + 1])
article_html = entry_skeleton.format(
title = article.title,
lexicon = config["LEXICON_TITLE"],
css = css,
logo = config["LOGO_FILENAME"],
prompt = config["PROMPT"],
sort = config["DEFAULT_SORT"],
content = content,
citeblock = citeblock)
f.write(article_html)
print(" Wrote " + article.title)
# Write default pages
print("Writing default pages...")
with open(pathto("contents", "index.html"), "w", encoding="utf-8") as f:
f.write(build_contents_page(articles, config))
print(" Wrote Contents")
with open(pathto("rules", "index.html"), "w", encoding="utf-8") as f:
f.write(build_rules_page(config))
print(" Wrote Rules")
with open(pathto("formatting", "index.html"), "w", encoding="utf-8") as f:
f.write(build_formatting_page(config))
print(" Wrote Formatting")
with open(pathto("session", "index.html"), "w", encoding="utf-8") as f:
f.write(build_session_page(config))
print(" Wrote Session")
with open(pathto("statistics", "index.html"), "w", encoding="utf-8") as f:
f.write(build_statistics_page(articles, config))
print(" Wrote Statistics")
# Write auxiliary pages
if "PRINTABLE_FILE" in config and config["PRINTABLE_FILE"]:
with open(pathto(config["PRINTABLE_FILE"]), "w", encoding="utf-8") as f:
f.write(build_compiled_page(articles, config))
print(" Wrote compiled page to " + config["PRINTABLE_FILE"])
# Check that authors aren't citing themselves
print("Running citation checks...")
article_by_title = {article.title : article for article in articles}
for article in articles:
for _, tup in article.citations.items():
cited = article_by_title[tup[1]]
if article.player == cited.player:
print(" {2}: {0} cites {1}".format(article.title, cited.title, cited.player))
print()

View File

@ -0,0 +1,29 @@
<script type="text/javascript">
contentsToggle = function() {
var b = document.getElementById("toggle-button");
var i = document.getElementById("index-order");
var t = document.getElementById("turn-order");
if (t.style.display == "none") {
i.style.display = "none";
t.style.display = "block";
b.innerText = "Switch to index order";
} else {
i.style.display = "block";
t.style.display = "none";
b.innerText = "Switch to turn order";
}
}
window.onload = function(){
if (location.search.search("byturn") > 0)
{
document.getElementById("turn-order").style.display = "block";
document.getElementById("toggle-button").innerText = "Switch to index order";
}
if (location.search.search("byindex") > 0)
{
document.getElementById("index-order").style.display = "block";
document.getElementById("toggle-button").innerText = "Switch to turn order";
}
}
</script>
<button id="toggle-button" onClick="javascript:contentsToggle()">Switch to turn order</button>

View File

@ -2,6 +2,7 @@
<head> <head>
<title>{title} | {lexicon}</title> <title>{title} | {lexicon}</title>
<!--<link rel="shortcut icon" href="favicon.png" />--> <!--<link rel="shortcut icon" href="favicon.png" />-->
<meta charset="utf-8"/>
<style> <style>
{css} {css}
</style> </style>
@ -11,7 +12,7 @@
<img id="logo" src="../{logo}"> <img id="logo" src="../{logo}">
<p><span style="font-size:1.5em;">{lexicon}</span></p> <p><span style="font-size:1.5em;">{lexicon}</span></p>
<p> <p>
<a href="../contents">Contents</a> &mdash; <a href="../contents/{sort}">Contents</a> &mdash;
<a href="../rules/">Rules</a> &mdash; <a href="../rules/">Rules</a> &mdash;
<a href="../formatting/">Formatting</a> &mdash; <a href="../formatting/">Formatting</a> &mdash;
<a href="../session/">Session</a> &mdash; <a href="../session/">Session</a> &mdash;

View File

@ -1,4 +1,4 @@
# Author: Authorname # Player: PN
# Turn: 1 # Turn: 1
# Title: Example page # Title: Example page

View File

@ -1,6 +1,6 @@
<p>Lexipython provides support for a limited amount of Markdown-esque formatting.</p> <p>Lexipython provides support for a limited amount of Markdown-esque formatting.</p>
<pre style="background:#eeeeee"> <pre style="background:#eeeeee">
# Author: Authorname # Player: PN
# Turn: 1 # Turn: 1
# Title: Example page # Title: Example page
@ -16,8 +16,8 @@ This is an [[example citation|Phantom page]]. You can also cite a [[phantom page
~Dr. X. Amplepage ~Dr. X. Amplepage
</pre> </pre>
<p>Each turn, fill out the header with your author information, the current turn, and the title of your entry. It doesn't really matter what the <i>Author</i> field is, except that it must be the same across all articles you write.</p> <p>Each turn, fill out the header with your player information, the current turn, and the title of your entry. The <i>Player</i> field can be anything as long as it's the same for all articles you write (even when they're by different characters). Using your initials is recommended.</p>
<p>Two line breaks begins a new paragraph. A single line break does nothing, unless the line is neded by a double backslash (\\).</p> <p>Two line breaks begins a new paragraph. A single line break does nothing, unless the line is ended by a double backslash (\\).</p>
<p>Text bounded by ** will be bolded: **bold** produces <b>bold</b>. Text bounded by // will be italicized: //italics// produces <i>italics</i>.</p> <p>Text bounded by ** will be bolded: **bold** produces <b>bold</b>. Text bounded by // will be italicized: //italics// produces <i>italics</i>.</p>
<p>To cite another Lexicon entry, use double brackets. Text in double brackets will cite and link to the entry of the same name: [[Example page]] produces <a href="Example_page.html" class="phantom">Example page</a>. Text in double brackets split with a | will alias the link as the left text and link to the entry with the name of the right text: [[this text|Example page]] produces <a href="Example_page.html" class="phantom">this text</a>. <b>You must be precise in the entry title you cite to.</b> Citations to "Example" vs. "The Example" will point to different entries and create different phantoms, and your GM will probably have to clean up after you.</p> <p>To cite another Lexicon entry, use double brackets. Text in double brackets will cite and link to the entry of the same name: [[Example page]] produces <a href="Example_page.html" class="phantom">Example page</a>. Text in double brackets split with a | will alias the link as the left text and link to the entry with the name of the right text: [[this text|Example page]] produces <a href="Example_page.html" class="phantom">this text</a>. <b>You must be precise in the entry title you cite to.</b> Citations to "Example" vs. "The Example" will point to different entries and create different phantoms, and your GM will probably have to clean up after you.</p>
<p>Beginning a paragraph with ~ will right-align it and place a horizontal line above it. Use this for signing your entry with your scholar's name.</p> <p>Beginning a paragraph with ~ will right-align it and place a horizontal line above it. Use this for signing your entry with your scholar's name.</p>

View File

@ -43,3 +43,19 @@ PQRS
TUV TUV
WXYZ WXYZ
<<<INDEX_LIST<<< <<<INDEX_LIST<<<
# The default sorting to use on the contents page.
# Allowed values are "?byturn" and "?byindex".
>>>DEFAULT_SORT>>>
?byturn
<<<DEFAULT_SORT<<<
# Graphviz file name. If present, the graph of page citations will be written
# in the dot file format.
>>>GRAPHVIZ_FILE>>>
<<<GRAPHVIZ_FILE<<<
# Print version file name. If present, the lexicon will be compiled and written
# into a single print-ready HTML file.
>>>PRINTABLE_FILE>>>
<<<PRINTABLE_FILE<<<

View File

@ -0,0 +1,9 @@
<html>
<head>
<title>{lexicon}</title>
<meta http-equiv="refresh" content="0; url=contents/{sort}" />
</head>
<body>
<p>Redirecting to <a href="contents/{sort}">{lexicon}</a>...</p>
</body>
</html>

76
src/utils.py Normal file
View File

@ -0,0 +1,76 @@
import os
import re
from urllib import parse
# Short utility functions for handling titles
def titlecase(s):
"""
Capitalizes the first word.
"""
s = s.strip()
return s[:1].capitalize() + s[1:]
def titleescape(s):
"""
Makes an article title filename-safe.
"""
s = s.strip()
s = re.sub(r"\s+", '_', s) # Replace whitespace with _
s = parse.quote(s) # Encode all other characters
s = re.sub(r"%", "", s) # Strip encoding %s
if len(s) > 64: # If the result is unreasonably long,
s = hex(abs(hash(s)))[2:] # Replace it with a hex hash
return s
def titlesort(s):
"""
Reduces titles down for sorting.
"""
s = s.lower()
if s.startswith("the "): return s[4:]
if s.startswith("an "): return s[3:]
if s.startswith("a "): return s[2:]
return s
# Load functions
def load_resource(filename, cache={}):
"""Loads files from the resources directory with caching."""
if filename not in cache:
with open(os.path.join("src", "resources", filename), "r", encoding="utf-8") as f:
cache[filename] = f.read()
return cache[filename]
def load_config(name):
"""
Loads values from a Lexicon's config file.
"""
config = {}
with open(os.path.join("lexicon", name, "lexicon.cfg"), "r", encoding="utf8") as f:
line = f.readline()
while line:
# Skim lines until a value definition begins
conf_match = re.match(">>>([^>]+)>>>\s+", line)
if not conf_match:
line = f.readline()
continue
# Accumulate the conf value until the value ends
conf = conf_match.group(1)
conf_value = ""
line = f.readline()
conf_match = re.match("<<<{0}<<<\s+".format(conf), line)
while line and not conf_match:
conf_value += line
line = f.readline()
conf_match = re.match("<<<{0}<<<\s+".format(conf), line)
if not line:
# TODO Not this
raise SystemExit("Reached EOF while reading config value {}".format(conf))
config[conf] = conf_value.strip()
# Check that all necessary values were configured
for config_value in ['LEXICON_TITLE', 'PROMPT', 'SESSION_PAGE', "INDEX_LIST"]:
if config_value not in config:
# TODO Not this either
raise SystemExit("Error: {} not set in lexipython.cfg".format(config_value))
return config