# Standard library imports import os # For reading directories import re # For parsing lex content # Application imports import utils from article import LexiconArticle from statistics import LexiconStatistics class LexiconPage: """ An abstraction layer around formatting a Lexicon page skeleton with kwargs so that kwargs that are constant across pages aren't repeated. """ def __init__(self, skeleton=None, page=None): self.kwargs = {} self.skeleton = skeleton if page is not None: self.skeleton = page.skeleton self.kwargs = dict(page.kwargs) def add_kwargs(self, **kwargs): self.kwargs.update(kwargs) def format(self, **kwargs): total_kwargs = {**self.kwargs, **kwargs} return self.skeleton.format(**total_kwargs) def article_matches_index(index_type, pattern, article): if index_type == "char": return utils.titlesort(article.title)[0].upper() in pattern.upper() if index_type == "prefix": return article.title.startswith(pattern) if index_type == "etc": return True raise ValueError("Unknown index type: '{}'".format(index_type)) def build_contents_page(config, page, articles): """ Builds the full HTML of the contents page. """ content = "
There are {0} entries in this lexicon.
\n".format(len(articles)) else: content += "There are {0} entries, {1} written and {2} phantom.
\n".format( len(articles), len(articles) - phantom_count, phantom_count) # Prepare article links link_by_title = {article.title : "{0}".format( article.title, article.title_filesafe, " class=\"phantom\"" if article.player is None else "") for article in articles} # Determine index order indices = config['INDEX_LIST'].split("\n") index_by_pri = {} index_list_order = [] for index in indices: match = re.match(r"([^[:]+)(\[([-\d]+)\])?:(.+)", index) index_type = match.group(1) pattern = match.group(4) try: pri_s = match.group(3) pri = int(pri_s) if pri_s else 0 except: raise TypeError("Could not parse index pri '{}' in '{}'".format(pri_s, index)) if pri not in index_by_pri: index_by_pri[pri] = [] index_by_pri[pri].append((index_type, pattern)) index_list_order.append(pattern) # Assign articles to indices articles_by_index = {pattern: [] for pattern in index_list_order} titlesort_order = sorted( articles, key=lambda a: utils.titlesort(a.title)) for article in titlesort_order: # Find the first index that matches matched = False for pri, indices in sorted(index_by_pri.items(), reverse=True): for index_type, pattern in indices: # Try to match the index if article_matches_index(index_type, pattern, article): articles_by_index[pattern].append(article) matched = True # Break out once a match is found if matched: break if matched: break if not matched: raise KeyError("No index matched article '{}'".format(article.title)) # Write index order div content += utils.load_resource("contents.html") content += "Editor | \n' if config['SEARCHABLE_FILE']: content += 'Compiled | \n'.format(config['SEARCHABLE_FILE'].strip()) content += '
{}
".format(cite_list) content += cite_block # Addendums for addendum in article.addendums: # Addendum content format_map = { "c"+str(c.id) : c.format("{text}{id}") for c in addendum.citations } article_content = addendum.content.format(**format_map) content += article_content # Addendum citations cite_list = "{}
".format(cite_list) content += cite_block content += "" return content def latex_from_markdown(raw_content): content = "" headers = raw_content.split('\n', 3) player_header, turn_header, title_header, content_raw = headers if not turn_header.startswith("# Turn:"): print("Turn header missing or corrupted") return None turn = int(turn_header[7:].strip()) if not title_header.startswith("# Title:"): print("Title header missing or corrupted") return None title = utils.titlecase(title_header[8:]) #content += "\\label{{{}}}\n".format(title) #content += "\\section*{{{}}}\n\n".format(title) # Parse content paras = re.split("\n\n+", content_raw.strip()) for para in paras: # Escape things para = re.sub("—", "---", para) para = re.sub("&", "\\&", para) para = re.sub(r"\"(?=\w)", "``", para) para = re.sub(r"(?<=\w)\"", "''", para) # Replace bold and italic marks with commands para = re.sub(r"//([^/]+)//", r"\\textit{\1}", para) para = re.sub(r"\*\*([^*]+)\*\*", r"\\textbf{\1}", para) # Footnotify citations 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))) # Stitch the title into a footnote para = (para[:link_match.start(0)] + cite_text + "\\footnote{" + cite_title + ", p. \\pageref{" + str(hash(cite_title)) + "}" + "}" + para[link_match.end(0):]) link_match = re.search(r"\[\[(([^|\[\]]+)\|)?([^|\[\]]+)\]\]", para) # Convert signature to right-aligned if para[:1] == '~': para = "\\begin{flushright}\n" + para[1:] + "\n\\end{flushright}\n\n" else: para = para + "\n\n" content += para return title, turn, content def latex_from_directory(directory): articles = {} for filename in os.listdir(directory): path = os.path.join(directory, filename) # Read only .txt files if filename[-4:] == ".txt": with open(path, "r", encoding="utf8") as src_file: raw = src_file.read() title, turn, latex = latex_from_markdown(raw) if title not in articles: articles[title] = {} articles[title][turn] = latex # Write the preamble content = "\\documentclass[12pt,a4paper,twocolumn,twoside]{article}\n"\ "\\usepackage[perpage]{footmisc}\n"\ "\\begin{document}\n"\ "\n" for title in sorted(articles.keys(), key=lambda t: utils.titlesort(t)): under_title = articles[title] turns = sorted(under_title.keys()) latex = under_title[turns[0]] # Section header content += "\\label{{{}}}\n".format(hash(title)) content += "\\section*{{{}}}\n\n".format(title) # Section content #format_map = { # "c"+str(c.id) : c.format("\\footnote{{{target}}}") # for c in article.citations #} #article_content = article.content.format(**format_map) content += latex # Addendums for turn in turns[1:]: #content += "\\vspace{6pt}\n\\hrule\n\\vspace{6pt}\n\n" content += "\\begin{center}\n$\\ast$~$\\ast$~$\\ast$\n\\end{center}\n\n" latex = under_title[turn] #format_map = { # "c"+str(c.id) : c.format("\\footnote{{{target}}}") # for c in addendum.citations #} #article_content = addendum.content.format(**format_map) content += latex content += "\\end{document}" content = re.sub(r"\"(?=\w)", "``", content) content = re.sub(r"(?<=\w)\"", "''", content) return content def parse_sort_type(sort): if sort in "?byindex": return "?byindex" if sort in "?byturn": return "?byturn" if sort in "?byplayer": return "?byplayer" return "" 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) page_skeleton = utils.load_resource("page-skeleton.html") page = LexiconPage(skeleton=page_skeleton) page.add_kwargs( lexicon=config["LEXICON_TITLE"], logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], sort=parse_sort_type(config["DEFAULT_SORT"])) # Parse the written articles articles = LexiconArticle.parse_from_directory(os.path.join(lex_path, "src")) # 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.interlink(articles), key=lambda a: (a.turn, utils.titlesort(a.title))) 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", newline='') as f: f.write(utils.load_resource("redirect.html").format( lexicon=config["LEXICON_TITLE"], sort=parse_sort_type(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", newline='') as f: content = article.build_default_content() article_html = page.format( title = article.title, content = content) 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", newline='') as f: f.write(build_contents_page(config, page, articles)) print(" Wrote Contents") with open(pathto("rules", "index.html"), "w", encoding="utf-8", newline='') as f: f.write(build_rules_page(page)) print(" Wrote Rules") with open(pathto("formatting", "index.html"), "w", encoding="utf-8", newline='') as f: f.write(build_formatting_page(page)) print(" Wrote Formatting") with open(pathto("session", "index.html"), "w", encoding="utf-8", newline='') as f: f.write(build_session_page(page, config)) print(" Wrote Session") with open(pathto("statistics", "index.html"), "w", encoding="utf-8", newline='') as f: f.write(build_statistics_page(config, page, articles)) print(" Wrote Statistics") # Write auxiliary pages if "SEARCHABLE_FILE" in config and config["SEARCHABLE_FILE"]: with open(pathto(config["SEARCHABLE_FILE"]), "w", encoding="utf-8", newline='') as f: f.write(build_compiled_page(articles, config)) print(" Wrote compiled page to " + config["SEARCHABLE_FILE"]) with open(pathto("editor.html"), "w", encoding="utf-8", newline='') as f: editor = utils.load_resource("editor.html") writtenArticles = "" phantomArticles = "" for article in articles: citedby = {'"' + citer.player + '"' for citer in article.citedby} if article.player is None: phantomArticles += "{{title: \"{0}\", citedby: [{1}]}},".format( article.title.replace("\"", "\\\""), ",".join(sorted(citedby))) else: writtenArticles += "{{title: \"{0}\", author: \"{1.player}\"}},".format( article.title.replace("\"", "\\\""), article) nextTurn = 0 if articles: nextTurn = max([article.turn for article in articles if article.player is not None]) + 1 editor = editor.replace("//writtenArticles", writtenArticles) editor = editor.replace("//phantomArticles", phantomArticles) editor = editor.replace("TURNNUMBER", str(nextTurn)) f.write(editor) # Check that authors aren't citing themselves print("Running citation checks...") for parent in articles: for article in [parent] + parent.addendums: for citation in article.citations: if article.player == citation.article.player: print(" {2}: {0} cites {1}".format(article.title, citation.target, article.player)) print()