From 533ae8e2fab7acb1a0153f5d5e1af788d1cd4118 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 00:18:49 -0700 Subject: [PATCH 01/36] Restructure files for handling multiple games --- lexicon/readme.txt | 1 + out/article/Example_page.html | 49 ------------- out/article/Phantom_page.html | 40 ----------- out/contents/index.html | 75 -------------------- out/formatting/index.html | 58 --------------- out/index.html | 9 --- out/rules/index.html | 53 -------------- out/session/index.html | 35 --------- out/statistics/index.html | 61 ---------------- raw/example-page.txt | 15 ---- lexipython.py => src/lexipython.py | 0 {resources => src/resources}/contents.html | 0 {resources => src/resources}/entry-page.html | 0 {resources => src/resources}/formatting.html | 0 lexicon.cfg => src/resources/lexicon.cfg | 0 {resources => src/resources}/lexicon.css | 0 {resources => src/resources}/redirect.html | 0 {resources => src/resources}/rules.html | 0 18 files changed, 1 insertion(+), 395 deletions(-) create mode 100644 lexicon/readme.txt delete mode 100644 out/article/Example_page.html delete mode 100644 out/article/Phantom_page.html delete mode 100644 out/contents/index.html delete mode 100644 out/formatting/index.html delete mode 100644 out/index.html delete mode 100644 out/rules/index.html delete mode 100644 out/session/index.html delete mode 100644 out/statistics/index.html delete mode 100644 raw/example-page.txt rename lexipython.py => src/lexipython.py (100%) rename {resources => src/resources}/contents.html (100%) rename {resources => src/resources}/entry-page.html (100%) rename {resources => src/resources}/formatting.html (100%) rename lexicon.cfg => src/resources/lexicon.cfg (100%) rename {resources => src/resources}/lexicon.css (100%) rename {resources => src/resources}/redirect.html (100%) rename {resources => src/resources}/rules.html (100%) diff --git a/lexicon/readme.txt b/lexicon/readme.txt new file mode 100644 index 0000000..df62f29 --- /dev/null +++ b/lexicon/readme.txt @@ -0,0 +1 @@ +This directory contains the directories for all of your Lexicon games. diff --git a/out/article/Example_page.html b/out/article/Example_page.html deleted file mode 100644 index 575b4d5..0000000 --- a/out/article/Example_page.html +++ /dev/null @@ -1,49 +0,0 @@ - - -Example page | Lexicon 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. You can also cite a phantom page with just the title.

-

Dr. X. Amplepage

-
-
-

Next →

-

 

-

Citations: Phantom page

-

Cited by: --

-
- - \ No newline at end of file diff --git a/out/article/Phantom_page.html b/out/article/Phantom_page.html deleted file mode 100644 index 91530aa..0000000 --- a/out/article/Phantom_page.html +++ /dev/null @@ -1,40 +0,0 @@ - - -Phantom page | Lexicon Title - - - - - -
-

Phantom page

-

This entry hasn't been written yet.

-
-

← Previous

-

Citations: --

-

Cited by: Example page

-
- - \ No newline at end of file diff --git a/out/contents/index.html b/out/contents/index.html deleted file mode 100644 index e8121da..0000000 --- a/out/contents/index.html +++ /dev/null @@ -1,75 +0,0 @@ - - -Index of Lexicon Title | Lexicon Title - - - - - -
-

Index of Lexicon Title

-

There are 2 entries, 1 written and 1 phantom.

- - -
- -
- -
- - \ No newline at end of file diff --git a/out/formatting/index.html b/out/formatting/index.html deleted file mode 100644 index d20e31e..0000000 --- a/out/formatting/index.html +++ /dev/null @@ -1,58 +0,0 @@ - - -Formatting | Lexicon Title - - - - - -
-

Formatting

-

Lexipython provides support for a limited amount of Markdown-esque formatting.

-
-# 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
-
-

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 Author field is, except that it must be the same across all articles you write.

-

Two line breaks begins a new paragraph. A single line break does nothing, unless the line is neded by a double backslash (\\).

-

Text bounded by ** will be bolded: **bold** produces bold. Text bounded by // will be italicized: //italics// produces italics.

-

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 Example page. 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 this text. You must be precise in the entry title you cite to. 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.

-

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.

-
- - \ No newline at end of file diff --git a/out/index.html b/out/index.html deleted file mode 100644 index b676732..0000000 --- a/out/index.html +++ /dev/null @@ -1,9 +0,0 @@ - - -Lexicon Title - - - -

Redirecting to Lexicon Title...

- - \ No newline at end of file diff --git a/out/rules/index.html b/out/rules/index.html deleted file mode 100644 index 48b2004..0000000 --- a/out/rules/index.html +++ /dev/null @@ -1,53 +0,0 @@ - - -Rules | Lexicon Title - - - - - -
-

Rules

-
    -
  1. At the beginning of the game, you will be provided with a topic statement that sets the tone for the game. Use it for inspiration and a stepping-stone into shaping the world of the Lexicon.
  2. -
  3. Each round, you will be assigned an index, a grouping of letters. Your entry must alphabetize under that index.
      -
    1. 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 phantom. If there are no open slots in your index, you must write the article for a phantom in that index.
    2. -
    3. "The" and "A" aren't counted in indexing.
  4. -
  5. Once you've picked an article title, write your article on that subject.
      -
    1. 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.
    2. -
    3. 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 contradict what has been previously established as fact.
    4. -
    5. Aim for around 200-300 words.
  6. -
  7. Your article must cite other articles in the Lexicon. Sometimes these citations will be to phantoms, articles that have not been written yet.
      -
    1. On the first turn, your article must cite exactly two phantom articles.
    2. -
    3. On subsequent turns, your article must cite exactly two phantom articles, either already-cited phantoms or new ones. Your article must also cite at least one written article.
    4. -
    5. On the penultimate turn, you must cite exactly one phantom article and at least two written articles.
    6. -
    7. On the final turn, you must cite at least three written articles.
    8. -
    9. You may not cite an entry you wrote. You may cite phantoms you have cited before.
    10. -
    11. Once you cite a phantom, you cannot choose to write it if you write an article for that index later.
  8. - -

    Ersatz Scrivener. 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.

    -
- - \ No newline at end of file diff --git a/out/session/index.html b/out/session/index.html deleted file mode 100644 index 87556ed..0000000 --- a/out/session/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - -Lexicon Title | Lexicon Title - - - - - -
-

Lexicon Title

-

Put session information here, like the index grouping and turn count, where to send completed entries, index assignments, turn schedule, and so on.

- - \ No newline at end of file diff --git a/out/statistics/index.html b/out/statistics/index.html deleted file mode 100644 index 148e62f..0000000 --- a/out/statistics/index.html +++ /dev/null @@ -1,61 +0,0 @@ - - -Statistics | Lexicon Title - - - - - -
-

Statistics

-
-

Top 10 pages by page rank:
-1 – Example page
-2 – Phantom page

-
-
-

Most citations made from:
-2 – Example page
-0 – Phantom page

-
-
-

Most citations made to:
-1 – Phantom page

-
-
-

Author total page rank:
-Authorname – 0.5

-
-
-

Citations made by author
-Authorname – 1

-
-
-

Citations made to author
-Authorname – 0

-
-
- - \ No newline at end of file diff --git a/raw/example-page.txt b/raw/example-page.txt deleted file mode 100644 index 092d315..0000000 --- a/raw/example-page.txt +++ /dev/null @@ -1,15 +0,0 @@ -# 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 \ No newline at end of file diff --git a/lexipython.py b/src/lexipython.py similarity index 100% rename from lexipython.py rename to src/lexipython.py diff --git a/resources/contents.html b/src/resources/contents.html similarity index 100% rename from resources/contents.html rename to src/resources/contents.html diff --git a/resources/entry-page.html b/src/resources/entry-page.html similarity index 100% rename from resources/entry-page.html rename to src/resources/entry-page.html diff --git a/resources/formatting.html b/src/resources/formatting.html similarity index 100% rename from resources/formatting.html rename to src/resources/formatting.html diff --git a/lexicon.cfg b/src/resources/lexicon.cfg similarity index 100% rename from lexicon.cfg rename to src/resources/lexicon.cfg diff --git a/resources/lexicon.css b/src/resources/lexicon.css similarity index 100% rename from resources/lexicon.css rename to src/resources/lexicon.css diff --git a/resources/redirect.html b/src/resources/redirect.html similarity index 100% rename from resources/redirect.html rename to src/resources/redirect.html diff --git a/resources/rules.html b/src/resources/rules.html similarity index 100% rename from resources/rules.html rename to src/resources/rules.html From ec1608b15463227b9461b968ae8972d951d90914 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 02:41:07 -0700 Subject: [PATCH 02/36] Add main control script, rename engine code for clarity --- lexipython.py | 106 +++++++++++++++++++++++++++++++ src/{lexipython.py => parser.py} | 0 2 files changed, 106 insertions(+) create mode 100644 lexipython.py rename src/{lexipython.py => parser.py} (100%) diff --git a/lexipython.py b/lexipython.py new file mode 100644 index 0000000..88e05c0 --- /dev/null +++ b/lexipython.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +# Check for the right Python version +import sys +if sys.version_info[0] < 3: + raise Exception("Lexipython requires Python 3") + +import argparse +import os + +def is_lexicon(name): + """ + Checks whether the given name is an extant Lexicon game. + Inputs: + name The Lexicon name to check. + Output: + If the given name is an extant Lexicon, returns a tuple (True, status) + where status is a string with the status of the named Lexicon. + If the given name is not an extant Lexicon, returns a tuple + (False, errormsg) where errormsg is a string detailing the error. + """ + if not os.path.isdir(os.path.join("lexicon", name)): + return (False, "There is no Lexicon named '{}'.".format(name)) + # TODO: Verify the folder is a Lexicon + #return (False, "'{}' is not a Lexicon game, or it may be corrupted.".format(name)) + return (True, "A Lexicon") + +def overview_all(): + """ + Prints the names and statuses of all extant Lexicons, + or a short help message if none have been created yet. + """ + # Scan the directory + lexicon_names = [] + with os.scandir("lexicon") as lexicons: + for entry in lexicons: + check = is_lexicon(entry.name) + if check[0]: + lexicon_names.append((entry.name, check[1])) + # Print the results + if len(lexicon_names) > 0: + l = max([len(name[0]) for name in lexicon_names]) + 4 + print("Lexicons:") + for name, status in sorted(lexicon_names): + print(" {}{}{}".format(name, " " * (l - len(name)), status)) + else: + print("There are no Lexicons yet. Create one with:\n\n"\ + " lexipython.py [name] init") + +def overview_one(name): + """ + Prints the status and summary information for the Lexicon with the + given name. + """ + # Verify the name + check = is_lexicon(name) + if not check[0]: + print("Error: " + check[1]) + +def run_command(name, command): + """ + Runs the specified command on the specified Lexicon. + """ + # Verify name + check = is_lexicon(name) + if not check[0]: + print("Error: " + check[1]) + return + # Dispatch commands + if command == "init": + return + elif command == "build": + return + elif command == "run": + return + else: + print("Error: '{}' is not a valid command.".format(command)) + return + +def main(): + parser = argparse.ArgumentParser( + description="Lexipython is a Python application for playing the Lexicon RPG.", + epilog="Run lexipython.py without arguments to list the extant Lexicons.\n\n"\ + "Available commands:\n\n"\ + " init Create a Lexicon with the provided name\n"\ + " 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: + run_command(args.name, args.command) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/lexipython.py b/src/parser.py similarity index 100% rename from src/lexipython.py rename to src/parser.py From e1b2d7b96d10a87357867305c2c2214a1a2d3759 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 11:23:13 -0700 Subject: [PATCH 03/36] Add command execution framework --- lexipython.py | 75 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/lexipython.py b/lexipython.py index 88e05c0..160de7a 100644 --- a/lexipython.py +++ b/lexipython.py @@ -10,17 +10,14 @@ import os def is_lexicon(name): """ - Checks whether the given name is an extant Lexicon game. + Checks whether the given folder is a Lexicon game. Inputs: - name The Lexicon name to check. + name The Lexicon name to check. Assumed to be an existing folder. Output: - If the given name is an extant Lexicon, returns a tuple (True, status) - where status is a string with the status of the named Lexicon. - If the given name is not an extant Lexicon, returns a tuple + If the given name is a Lexicon game, returns a tuple (True, status) + where status is a string with the Lexicon's status. Otherwise, returns (False, errormsg) where errormsg is a string detailing the error. """ - if not os.path.isdir(os.path.join("lexicon", name)): - return (False, "There is no Lexicon named '{}'.".format(name)) # TODO: Verify the folder is a Lexicon #return (False, "'{}' is not a Lexicon game, or it may be corrupted.".format(name)) return (True, "A Lexicon") @@ -34,9 +31,10 @@ def overview_all(): lexicon_names = [] with os.scandir("lexicon") as lexicons: for entry in lexicons: - check = is_lexicon(entry.name) - if check[0]: - lexicon_names.append((entry.name, check[1])) + if entry.is_dir(): + check = is_lexicon(entry.name) + if check[0]: + lexicon_names.append((entry.name, check[1])) # Print the results if len(lexicon_names) > 0: l = max([len(name[0]) for name in lexicon_names]) + 4 @@ -53,30 +51,73 @@ def overview_one(name): given name. """ # Verify the name + if not os.path.isdir(os.path.join("lexicon", name)): + print("Error: There is no Lexicon named '{}'.".format(name)) + return check = is_lexicon(name) if not check[0]: print("Error: " + check[1]) + return + # Print status and summary + # TODO + print("Lexicon {} exists, status {}".format(name, check[1])) def run_command(name, command): """ - Runs the specified command on the specified Lexicon. + Runs a command on a Lexicon. """ - # Verify name - check = is_lexicon(name) - if not check[0]: - print("Error: " + check[1]) - return - # Dispatch commands 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 + check = is_lexicon(name) + if not check[0]: + print("Error: " + check[1]) + 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 + check = is_lexicon(name) + if not check[0]: + print("Error: " + check[1]) + return + # Run a server managing the Lexicon + command_run(name) return else: print("Error: '{}' is not a valid command.".format(command)) return +def command_init(name): + """ + Sets up a Lexicon game with the given name. + """ + pass + +def command_build(name): + """ + Rebuilds the browsable pages of a Lexicon. + """ + pass + +def command_run(name): + """ + Runs as a server managing a Lexicon. + """ + pass + def main(): parser = argparse.ArgumentParser( description="Lexipython is a Python application for playing the Lexicon RPG.", From 0de4670e502aa92efee42c430920119ce972f47c Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 13:04:35 -0700 Subject: [PATCH 04/36] Implement init command --- lexipython.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/lexipython.py b/lexipython.py index 160de7a..9872d5f 100644 --- a/lexipython.py +++ b/lexipython.py @@ -7,6 +7,7 @@ if sys.version_info[0] < 3: import argparse import os +import re def is_lexicon(name): """ @@ -104,7 +105,30 @@ def command_init(name): """ Sets up a Lexicon game with the given name. """ - pass + # Create the folder structure + lex_path = os.path.join("lexicon", name) + os.mkdir(lex_path) + os.mkdir(os.path.join(lex_path, "src")) + os.mkdir(os.path.join(lex_path, "article")) + os.mkdir(os.path.join(lex_path, "contents")) + os.mkdir(os.path.join(lex_path, "formatting")) + os.mkdir(os.path.join(lex_path, "rules")) + os.mkdir(os.path.join(lex_path, "session")) + os.mkdir(os.path.join(lex_path, "statistics")) + # Open the default config file + config = None + with open(os.path.join("src", "resources", "lexicon.cfg")) as def_cfg: + config = def_cfg.read() + # Edit the name field + config = re.sub("Lexicon Title", "Lexicon {}".format(name), config) + # Create the Lexicon's config file + with open(os.path.join(lex_path, "lexicon.cfg"), "w") as config_file: + config_file.write(config) + # Create an example page + with open(os.path.join("src", "resources", "example-page.txt")) as srcfile: + with open(os.path.join(lex_path, "src", "example-page.txt"), "w") as destfile: + destfile.write(srcfile.read()) + print("Created Lexicon {}".format(name)) def command_build(name): """ From 1e71ff0061d363e8ffc0454f0841aad39d0ac67d Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 14:26:14 -0700 Subject: [PATCH 05/36] Add example page to resources --- src/resources/example-page.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/resources/example-page.txt diff --git a/src/resources/example-page.txt b/src/resources/example-page.txt new file mode 100644 index 0000000..c00028d --- /dev/null +++ b/src/resources/example-page.txt @@ -0,0 +1,15 @@ +# Author: AN +# 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 \ No newline at end of file From deeaeafb39adf2016c8a64edb815fc54ead5be48 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 14:27:44 -0700 Subject: [PATCH 06/36] Move utility functions to utils.py --- src/parser.py | 60 ----------------------------------------- src/utils.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 60 deletions(-) create mode 100644 src/utils.py diff --git a/src/parser.py b/src/parser.py index be9644e..d81bd65 100644 --- a/src/parser.py +++ b/src/parser.py @@ -8,31 +8,6 @@ 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 urllib import parse - -# Short utility functions for handling titles - -def titlecase(s): - """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 @@ -227,41 +202,6 @@ def populate(lexicon_articles): 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): diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..a0a328b --- /dev/null +++ b/src/utils.py @@ -0,0 +1,74 @@ +import os +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 titlestrip(s): + """ + Strips articles 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 + +# 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="utf8") 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 \ No newline at end of file From 82106e3deb54f5fb960498572e8e02c15d412379 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 14:31:12 -0700 Subject: [PATCH 07/36] Improve Lexicon status checking --- lexipython.py | 73 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/lexipython.py b/lexipython.py index 9872d5f..a1b7c0c 100644 --- a/lexipython.py +++ b/lexipython.py @@ -8,6 +8,8 @@ if sys.version_info[0] < 3: import argparse import os import re +import json +import src.utils as utils def is_lexicon(name): """ @@ -15,13 +17,25 @@ def is_lexicon(name): Inputs: name The Lexicon name to check. Assumed to be an existing folder. Output: - If the given name is a Lexicon game, returns a tuple (True, status) - where status is a string with the Lexicon's status. Otherwise, returns - (False, errormsg) where errormsg is a string detailing the error. + Returns a tuple (result, msg, status), where result is True if the + given name is a Lexicon game and False otherwise, msg is the Lexicon's + status or an error message, and status is the status dictionary of the + Lexicon or None. """ - # TODO: Verify the folder is a Lexicon - #return (False, "'{}' is not a Lexicon game, or it may be corrupted.".format(name)) - return (True, "A Lexicon") + if not os.path.isfile(os.path.join("lexicon", name, "lexicon.cfg")): + 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")): + return (True, "status missing", None) + with open(os.path.join("lexicon", name, "status")) as statusfile: + raw = statusfile.read() + if len(raw) == 0: + return (True, "unbuilt", {}) + try: + status = json.loads(raw) + except: + return (True, "status corrupted", None) + return (True, "ye", status) # TODO + return (False, "Error checking Lexicon status", None) def overview_all(): """ @@ -33,15 +47,15 @@ def overview_all(): with os.scandir("lexicon") as lexicons: for entry in lexicons: if entry.is_dir(): - check = is_lexicon(entry.name) - if check[0]: - lexicon_names.append((entry.name, check[1])) + result, msg, status = is_lexicon(entry.name) + if result: + lexicon_names.append((entry.name, msg)) # Print the results if len(lexicon_names) > 0: - l = max([len(name[0]) for name in lexicon_names]) + 4 + l = max([len(name) for name, msg in lexicon_names]) + 4 print("Lexicons:") - for name, status in sorted(lexicon_names): - print(" {}{}{}".format(name, " " * (l - len(name)), status)) + for name, msg in sorted(lexicon_names): + print(" {}{}{}".format(name, " " * (l - len(name)), msg)) else: print("There are no Lexicons yet. Create one with:\n\n"\ " lexipython.py [name] init") @@ -55,13 +69,14 @@ def overview_one(name): if not os.path.isdir(os.path.join("lexicon", name)): print("Error: There is no Lexicon named '{}'.".format(name)) return - check = is_lexicon(name) - if not check[0]: - print("Error: " + check[1]) + result, msg, status = is_lexicon(name) + if not result: + print("Error: " + msg) return # Print status and summary + print(msg) + print(status) # TODO - print("Lexicon {} exists, status {}".format(name, check[1])) def run_command(name, command): """ @@ -79,9 +94,9 @@ def run_command(name, command): if not os.path.exists(os.path.join("lexicon", name)): print("Error: There is no Lexicon named '{}'.".format(name)) return - check = is_lexicon(name) - if not check[0]: - print("Error: " + check[1]) + result, msg, status = is_lexicon(name) + if not result: + print("Error: " + msg) return # Build the Lexicon command_build(name) @@ -90,9 +105,9 @@ def run_command(name, command): if not os.path.exists(os.path.join("lexicon", name)): print("Error: There is no Lexicon named '{}'.".format(name)) return - check = is_lexicon(name) - if not check[0]: - print("Error: " + check[1]) + result, msg, status = is_lexicon(name) + if not result: + print("Error: " + msg) return # Run a server managing the Lexicon command_run(name) @@ -116,18 +131,20 @@ def command_init(name): os.mkdir(os.path.join(lex_path, "session")) os.mkdir(os.path.join(lex_path, "statistics")) # Open the default config file - config = None - with open(os.path.join("src", "resources", "lexicon.cfg")) as def_cfg: - config = def_cfg.read() + config = utils.load_resource("lexicon.cfg") + #with open(os.path.join("src", "resources", "lexicon.cfg")) as def_cfg: + # config = def_cfg.read() # Edit the name field config = re.sub("Lexicon Title", "Lexicon {}".format(name), config) # Create the Lexicon's config file with open(os.path.join(lex_path, "lexicon.cfg"), "w") as config_file: config_file.write(config) # Create an example page - with open(os.path.join("src", "resources", "example-page.txt")) as srcfile: - with open(os.path.join(lex_path, "src", "example-page.txt"), "w") as destfile: - destfile.write(srcfile.read()) + #with open(os.path.join("src", "resources", "example-page.txt")) as srcfile: + with open(os.path.join(lex_path, "src", "example-page.txt"), "w") as destfile: + 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)) def command_build(name): From 28afbd5e9baa87e12155c451335078ff619d7e09 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 14:37:31 -0700 Subject: [PATCH 08/36] Add Lexicon directories to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 7bbc71c..d1f8edc 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ ENV/ # mypy .mypy_cache/ + +# Ignore directories in lexicon/ +lexicon/*/ \ No newline at end of file From 1ac3d4c7ca2dc02119388f92b22a774b3a4447a6 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 15:46:52 -0700 Subject: [PATCH 09/36] Remove leftover code --- lexipython.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/lexipython.py b/lexipython.py index a1b7c0c..0f65315 100644 --- a/lexipython.py +++ b/lexipython.py @@ -132,15 +132,12 @@ def command_init(name): os.mkdir(os.path.join(lex_path, "statistics")) # Open the default config file config = utils.load_resource("lexicon.cfg") - #with open(os.path.join("src", "resources", "lexicon.cfg")) as def_cfg: - # config = def_cfg.read() # Edit the name field config = re.sub("Lexicon Title", "Lexicon {}".format(name), config) # Create the Lexicon's config file with open(os.path.join(lex_path, "lexicon.cfg"), "w") as config_file: config_file.write(config) # Create an example page - #with open(os.path.join("src", "resources", "example-page.txt")) as srcfile: with open(os.path.join(lex_path, "src", "example-page.txt"), "w") as destfile: destfile.write(utils.load_resource("example-page.txt")) # Create an empty status file From f3914ca1cd97b8a71dc7ca39d1fa2b9eb0f744a9 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Tue, 29 May 2018 15:49:01 -0700 Subject: [PATCH 10/36] Add missing import --- src/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils.py b/src/utils.py index a0a328b..aa35d70 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,4 +1,5 @@ import os +import re from urllib import parse # Short utility functions for handling titles From 65acbe9ab5ad673cb17f5d9872348193f386eb59 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 1 Jul 2018 13:06:43 -0700 Subject: [PATCH 11/36] Rename file for build functions --- src/{parser.py => build.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{parser.py => build.py} (100%) diff --git a/src/parser.py b/src/build.py similarity index 100% rename from src/parser.py rename to src/build.py From f05664fc1c3816e54edb1641709a9e364d9b43d5 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 1 Jul 2018 13:12:57 -0700 Subject: [PATCH 12/36] Refactor article and build code --- lexipython.py | 70 ++++++++++- src/article.py | 195 +++++++++++++++++++++++++++++++ src/build.py | 306 +++---------------------------------------------- 3 files changed, 280 insertions(+), 291 deletions(-) create mode 100644 src/article.py diff --git a/lexipython.py b/lexipython.py index 0f65315..c41551c 100644 --- a/lexipython.py +++ b/lexipython.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -# Check for the right Python version import sys if sys.version_info[0] < 3: raise Exception("Lexipython requires Python 3") @@ -9,6 +8,8 @@ import argparse import os import re import json +from src.article import LexiconArticle +import src.build as build import src.utils as utils def is_lexicon(name): @@ -143,12 +144,77 @@ def command_init(name): # Create an empty status file open(os.path.join(lex_path, "status"), "w").close() print("Created Lexicon {}".format(name)) + # Done initializing + return def command_build(name): """ Rebuilds the browsable pages of a Lexicon. """ - pass + # Load the Lexicon's peripherals + config = utils.load_config(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("lexicon", name, "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] + articles = sorted( + LexiconArticle.populate(articles), + key=lambda a: utils.titlestrip(a.title)) + #phantom_titles = [article.title for article in articles if article.title not in written_titles] + lex_path = os.path.join("lexicon", name) + 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"])) + + # 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="utf8") as f: + content = article.build_default_content() + citeblock = article.build_default_citeblock( + None if idx == 0 else articles[idx - 1].title, + None if idx == l-1 else articles[idx + 1].title) + article_html = entry_skeleton.format( + title = article.title, + lexicon = config["LEXICON_TITLE"], + css = css, + logo = config["LOGO_FILENAME"], + prompt = config["PROMPT"], + 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="utf8") as f: + f.write(build.build_contents_page(articles, config)) + print(" Wrote Contents") + with open(pathto("rules", "index.html"), "w", encoding="utf8") as f: + f.write(build.build_rules_page(config)) + print(" Wrote Rules") + with open(pathto("formatting", "index.html"), "w", encoding="utf8") as f: + f.write(build.build_formatting_page(config)) + print(" Wrote Formatting") + with open(pathto("session", "index.html"), "w", encoding="utf8") as f: + f.write(build.build_session_page(config)) + print(" Wrote Session") + with open(pathto("statistics", "index.html"), "w", encoding="utf8") as f: + f.write(build.build_statistics_page(articles, config)) + print(" Wrote Statistics") def command_run(name): """ diff --git a/src/article.py b/src/article.py new file mode 100644 index 0000000..c809054 --- /dev/null +++ b/src/article.py @@ -0,0 +1,195 @@ +import os +import sys +import re +import src.utils as utils + +class LexiconArticle: + """ + A Lexicon article and its metadata. + + Members: + author string: the author 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, author, turn, title, content, citations): + """ + Creates a LexiconArticle object with the given parameters. + """ + self.author = author + 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 + 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 or corrupted") + return None + author = author_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("<", "<", para) + para = re.sub(">", ">", para) + # Replace bold and italic marks with tags + para = re.sub(r"//([^/]+)//", r"\1", para) + para = re.sub(r"\*\*([^*]+)\*\*", r"\1", para) + # Replace \\LF with
LF + para = re.sub(r"\\\\\n", "
\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(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 = "

" + para[1:] + "

\n" + else: + para = "

" + para + "

\n" + content += para + return LexiconArticle(author, 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, "

This entry hasn't been written yet.

", {}) + # 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 build_default_content(self): + """ + Formats citations into the article content as normal HTML links + and returns the result. + """ + format_map = { + format_id: "{0}".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_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 = "
\n" + # Prev/next links + if next_target is not None: + citeblock += "

Next →

\n".format(utils.titleescape(next_target)) + if prev_target is not None: + citeblock += "

← Previous

\n".format(utils.titleescape(prev_target)) + elif next_target is not None: + citeblock += "

 

\n" + # Citations + cites_links = [ + "{0}".format( + title, utils.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 += "

Citations: {}

\n".format(cites_str) + # Citedby + citedby_links = [ + "{0}".format( + title, utils.titleescape(title)) + for title in self.citedby] + citedby_str = " | ".join(citedby_links) + if len(citedby_str) < 1: citedby_str = "--" + citeblock += "

Cited by: {}

\n
\n".format(citedby_str) + return citeblock diff --git a/src/build.py b/src/build.py index d81bd65..7c783d6 100644 --- a/src/build.py +++ b/src/build.py @@ -1,7 +1,3 @@ -############################### -## Lexipython Lexicon engine ## -############################### - import sys # For argv and stderr import os # For reading directories import re # For parsing lex content @@ -9,200 +5,7 @@ import io # For writing pages out as UTF-8 import networkx # For pagerank analytics from collections import defaultdict # For rank inversion in statistics -# Main article class - -class LexiconArticle: - """ - A Lexicon article and its metadata. - - Members: - author string: the author 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 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(). - """ - - def __init__(self, author, turn, title, content, citations): - """ - Creates a LexiconArticle object with the given parameters. - """ - self.author = author - self.turn = turn - self.title = title - 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: - 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") - return None - title = 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("<", "<", para) - para = re.sub(">", ">", para) - # Replace bold and italic marks with tags - para = re.sub(r"//([^/]+)//", r"\1", para) - para = re.sub(r"\*\*([^*]+)\*\*", r"\1", para) - # Replace \\LF with
LF - para = re.sub(r"\\\\\n", "
\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 = 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 = "

" + para[1:] + "

\n" - else: - para = "

" + para + "

\n" - content += para - return LexiconArticle(author, turn, title, content, citations) - - def build_page_content(self): - """ - Formats citations into the article content as normal HTML links - and returns the result. - """ - format_map = { - format_id: "{0}".format( - cite_tuple[0], 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_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 = "
\n" - # Prev/next links - if next_target is not None: - citeblock += "

Next →

\n".format(titleescape(next_target)) - if prev_target is not None: - citeblock += "

← Previous

\n".format(titleescape(prev_target)) - elif next_target is not None: - citeblock += "

 

\n" - # Citations - cites_links = [ - "{0}".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 += "

Citations: {}

\n".format(cites_str) - # Citedby - citedby_links = [ - "{0}".format( - title, titleescape(title)) - for title in self.citedby] - citedby_str = " | ".join(citedby_links) - if len(citedby_str) < 1: citedby_str = "--" - citeblock += "

Cited by: {}

\n
\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, "

This entry hasn't been written yet.

", {}) - # 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()) - -# Build functions +import src.utils as utils def build_contents_page(articles, config): """ @@ -222,15 +25,15 @@ def build_contents_page(articles, config): "" 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 += utils.load_resource("contents.html") content += "
\n
    \n" indices = config["INDEX_LIST"].split("\n") - alphabetical_order = sorted(articles, key=lambda a: a.title) + alphabetical_order = sorted(articles, key=lambda a: utils.titlecase(a.title)) check_off = list(alphabetical_order) for index_str in indices: content += "

    {0}

    \n".format(index_str) for article in alphabetical_order: - if (titlestrip(article.title)[0].upper() in index_str): + if (utils.titlestrip(article.title)[0].upper() in index_str): check_off.remove(article) content += "
  • " content += link_by_title[article.title] @@ -245,7 +48,7 @@ def build_contents_page(articles, config): # Write the articles in turn order content += "
    \n
      \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)) + turn_order = sorted(articles, key=lambda a: (a.turn, utils.titlecase(a.title))) check_off = list(turn_order) for turn_num in range(1, latest_turn + 1): content += "

      Turn {0}

      \n".format(turn_num) @@ -263,8 +66,8 @@ def build_contents_page(articles, config): content += "\n" content += "
    \n
    \n" # Fill in the page skeleton - entry_skeleton = load_resource("entry-page.html") - css = load_resource("lexicon.css") + 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"], @@ -278,10 +81,10 @@ def build_rules_page(config): """ Builds the full HTML of the rules page. """ - content = load_resource("rules.html") + content = utils.load_resource("rules.html") # Fill in the entry skeleton - entry_skeleton = load_resource("entry-page.html") - css = load_resource("lexicon.css") + entry_skeleton = utils.load_resource("entry-page.html") + css = utils.load_resource("lexicon.css") return entry_skeleton.format( title="Rules", lexicon=config["LEXICON_TITLE"], @@ -295,10 +98,10 @@ def build_formatting_page(config): """ Builds the full HTML of the formatting page. """ - content = load_resource("formatting.html") + content = utils.load_resource("formatting.html") # Fill in the entry skeleton - entry_skeleton = load_resource("entry-page.html") - css = load_resource("lexicon.css") + entry_skeleton = utils.load_resource("entry-page.html") + css = utils.load_resource("lexicon.css") return entry_skeleton.format( title="Formatting", lexicon=config["LEXICON_TITLE"], @@ -313,8 +116,8 @@ 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") + 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"], @@ -401,8 +204,8 @@ def build_statistics_page(articles, config): content += "
\n" # Fill in the entry skeleton - entry_skeleton = load_resource("entry-page.html") - css = load_resource("lexicon.css") + entry_skeleton = utils.load_resource("entry-page.html") + css = utils.load_resource("lexicon.css") return entry_skeleton.format( title="Statistics", lexicon=config["LEXICON_TITLE"], @@ -435,82 +238,7 @@ def build_graphviz_file(cite_map): # 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 - # Load content - config = load_config() - entry_skeleton = load_resource("entry-page.html") - css = load_resource("lexicon.css") - articles = [article for article in parse_from_directory("raw/") if article is not None] - written_titles = [article.title for article in articles] - articles = sorted(populate(articles), key=lambda a: (a.turn, a.title)) - #print(articles[13].title_filesafe) - #return - phantom_titles = [article.title for article in articles if article.title not in written_titles] - - # Write the redirect page - print("Writing redirect page...") - with open("out/index.html", "w", encoding="utf8") as f: - f.write(load_resource("redirect.html").format(lexicon=config["LEXICON_TITLE"])) - - # Write the article pages - print("Deleting old article pages...") - for filename in os.listdir("out/article/"): - if filename[-5:] == ".html": - os.remove("out/article/" + filename) - print("Writing article pages...") - l = len(articles) - for idx in range(l): - article = articles[idx] - with open("out/article/" + article.title_filesafe + ".html", "w", encoding="utf8") as f: - content = article.build_page_content() - citeblock = article.build_page_citeblock( - None if idx == 0 else articles[idx - 1].title, - None if idx == l-1 else articles[idx + 1].title) - article_html = entry_skeleton.format( - title = article.title, - lexicon = config["LEXICON_TITLE"], - css = css, - logo = config["LOGO_FILENAME"], - prompt = config["PROMPT"], - content = content, - citeblock = citeblock) - f.write(article_html) - print(" Wrote " + article.title) - - # Write default pages - print("Writing default pages...") - with open("out/contents/index.html", "w", encoding="utf8") as f: - f.write(build_contents_page(articles, config)) - print(" Wrote Contents") - 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 # TODO: write graphviz file # TODO: write compiled lexicon page - -def main(): - if len(sys.argv) < 2: - print("Available commands:") - print(" - build [partial] : Build the lexicon and generate phantom stubs for all unwritten articles.") - print(" - build full : Build the lexicon and generate Ersatz pages for all unwritten articles.") - elif sys.argv[1] == "build": - command_build(sys.argv) - else: - print("Unknown command: " + sys.argv[1]) - -if __name__ == "__main__": - main() From 8fdba48fe71896ce44434df640daa98007fbdddf Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 1 Jul 2018 13:14:10 -0700 Subject: [PATCH 13/36] Fix Contents link in entry page --- src/resources/entry-page.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/entry-page.html b/src/resources/entry-page.html index 59619d3..db2e393 100644 --- a/src/resources/entry-page.html +++ b/src/resources/entry-page.html @@ -11,7 +11,7 @@

{lexicon}

-Contents — +ContentsRulesFormattingSession — @@ -23,4 +23,4 @@

{title}

{content} {citeblock} - \ No newline at end of file + From eb93b4173924364c9d3b0a4b0f1106b5d0796910 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 1 Jul 2018 13:47:34 -0700 Subject: [PATCH 14/36] Fix some whitespace --- src/utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils.py b/src/utils.py index aa35d70..d97111d 100644 --- a/src/utils.py +++ b/src/utils.py @@ -16,11 +16,11 @@ 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 + 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): @@ -72,4 +72,4 @@ def load_config(name): if config_value not in config: # TODO Not this either raise SystemExit("Error: {} not set in lexipython.cfg".format(config_value)) - return config \ No newline at end of file + return config From e13febb079c66bbc099b7fa5fcbc708e310ad4fa Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 6 Jul 2018 21:59:09 -0700 Subject: [PATCH 15/36] Make minor formatting changes --- lexipython.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lexipython.py b/lexipython.py index c41551c..b6fc0ff 100644 --- a/lexipython.py +++ b/lexipython.py @@ -9,8 +9,8 @@ import os import re import json from src.article import LexiconArticle -import src.build as build -import src.utils as utils +from src import build +from src import utils def is_lexicon(name): """ @@ -248,4 +248,4 @@ def main(): run_command(args.name, args.command) if __name__ == "__main__": - main() \ No newline at end of file + main() From 4cdf2af13f6b6c323c0169c56db03567d7ddb465 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 12:43:04 -0700 Subject: [PATCH 16/36] Refactor 'author' to 'player' to remove ambiguity about player vs character --- src/article.py | 20 ++++++++-------- src/build.py | 42 ++++++++++++++++++---------------- src/resources/example-page.txt | 2 +- src/resources/formatting.html | 4 ++-- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/article.py b/src/article.py index c809054..8132f6e 100644 --- a/src/article.py +++ b/src/article.py @@ -8,7 +8,7 @@ class LexiconArticle: A Lexicon article and its metadata. Members: - author string: the author of the article + 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 @@ -20,11 +20,11 @@ class LexiconArticle: The last three are filled in by populate(). """ - def __init__(self, author, turn, title, content, citations): + def __init__(self, player, turn, title, content, citations): """ Creates a LexiconArticle object with the given parameters. """ - self.author = author + self.player = player self.turn = turn self.title = title self.title_filesafe = utils.titleescape(title) @@ -44,12 +44,12 @@ class LexiconArticle: 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 or corrupted") + 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 - author = author_header[9:].strip() + player = player_header[9:].strip() # Validate and sanitize the turn header if not turn_header.startswith("# Turn:"): print("Turn header missing or corrupted") @@ -99,7 +99,7 @@ class LexiconArticle: else: para = "

" + para + "

\n" content += para - return LexiconArticle(author, turn, title, content, citations) + return LexiconArticle(player, turn, title, content, citations) @staticmethod def parse_from_directory(directory): @@ -142,7 +142,7 @@ class LexiconArticle: if target not in article_by_title: article_by_title[target] = LexiconArticle(None, sys.maxsize, target, "

This entry hasn't been written yet.

", {}) # Interlink citations - if article_by_title[target].author is None: + if article_by_title[target].player is None: article.pcites.add(target) else: article.wcites.add(target) diff --git a/src/build.py b/src/build.py index 7c783d6..476b6aa 100644 --- a/src/build.py +++ b/src/build.py @@ -13,7 +13,7 @@ def build_contents_page(articles, config): """ content = "" # Article counts - phantom_count = len([article for article in articles if article.author is None]) + phantom_count = len([article for article in articles if article.player is None]) if phantom_count == 0: content = "

There are {0} entries in this lexicon.

\n".format(len(articles)) else: @@ -22,7 +22,7 @@ def build_contents_page(articles, config): # Prepare article links link_by_title = {article.title : "{0}".format( article.title, article.title_filesafe, - "" if article.author is not None else " class=\"phantom\"") + "" if article.player is not None else " class=\"phantom\"") for article in articles} # Write the articles in alphabetical order content += utils.load_resource("contents.html") @@ -47,7 +47,7 @@ def build_contents_page(articles, config): content += "\n\n" # Write the articles in turn order content += "
\n
    \n" - latest_turn = max([article.turn for article in articles if article.author is not None]) + 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.titlecase(a.title))) check_off = list(turn_order) for turn_num in range(1, latest_turn + 1): @@ -146,7 +146,7 @@ def build_statistics_page(articles, config): content += "
    \n".join(map(lambda x: "{0} – {1}".format(x[0]+1, x[1]), ranking[:10])) content += "

    \n" content += "
\n" - # Top numebr of citations made + # Top number of citations made content += "
\n" content += "

Most citations made from:
\n" citation_tally = [(kv[0], len(kv[1])) for kv in cite_map.items()] @@ -170,36 +170,38 @@ def build_statistics_page(articles, config): sorted(cited_count.items(), reverse=True)[:3])) content += "

\n" content += "
\n" - # Author pageranks + # player pageranks content += "
\n" - content += "

Author total page rank:
\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 += "

Player total page rank:
\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], articles)) for player, articles in articles_by.items()} content += "
\n".join(map( lambda kv: "{0} – {1}".format(kv[0], round(kv[1], 3)), - sorted(author_rank.items(), key=lambda t:-t[1]))) + sorted(player_rank.items(), key=lambda t:t[1], reverse=True))) content += "

\n" content += "
\n" - # Author citations made + # Player citations made content += "
\n" - content += "

Citations made by author
\n" - author_cite_count = {author : sum(map(lambda a:len(a.wcites | a.pcites), articles)) for author, articles in articles_by.items()} + content += "

Citations made by player
\n" + player_cite_count = { + player : sum(map(lambda a:len(a.wcites | a.pcites), articles)) + for player, articles in articles_by.items()} content += "
\n".join(map( lambda kv: "{0} – {1}".format(kv[0], kv[1]), - sorted(author_cite_count.items(), key=lambda t:-t[1]))) + sorted(player_cite_count.items(), key=lambda t:t[1], reverse=True))) content += "

\n" content += "
\n" - # Author cited count + # player cited count content += "
\n" - content += "

Citations made to author
\n" - cited_times = {author : 0 for author in authors} + content += "

Citations made to player
\n" + cited_times = {player : 0 for player in players} for article in articles: - if article.author is not None: - cited_times[article.author] += len(article.citedby) + if article.player is not None: + cited_times[article.player] += len(article.citedby) content += "
\n".join(map( lambda kv: "{0} – {1}".format(kv[0], kv[1]), - sorted(cited_times.items(), key=lambda t:-t[1]))) + sorted(cited_times.items(), key=lambda t:t[1], reverse=True))) content += "

\n" content += "
\n" diff --git a/src/resources/example-page.txt b/src/resources/example-page.txt index c00028d..3f721ba 100644 --- a/src/resources/example-page.txt +++ b/src/resources/example-page.txt @@ -1,4 +1,4 @@ -# Author: AN +# Player: PN # Turn: 1 # Title: Example page diff --git a/src/resources/formatting.html b/src/resources/formatting.html index 9521423..ceaa19c 100644 --- a/src/resources/formatting.html +++ b/src/resources/formatting.html @@ -1,6 +1,6 @@

Lexipython provides support for a limited amount of Markdown-esque formatting.

-# Author: Authorname
+# Player: PN
 # Turn: 1
 # Title: Example page
 
@@ -16,7 +16,7 @@ This is an [[example citation|Phantom page]]. You can also cite a [[phantom page
 
 ~Dr. X. Amplepage
 
-

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 Author field is, except that it must be the same across all articles you write.

+

Each turn, fill out the header with your player information, the current turn, and the title of your entry. The Player 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.

Two line breaks begins a new paragraph. A single line break does nothing, unless the line is neded by a double backslash (\\).

Text bounded by ** will be bolded: **bold** produces bold. Text bounded by // will be italicized: //italics// produces italics.

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 Example page. 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 this text. You must be precise in the entry title you cite to. 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.

From 49917bdc17090f17de61e3b3030c2d1ff0f88505 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 12:58:55 -0700 Subject: [PATCH 17/36] Refactor build code to libraries --- lexipython.py | 69 ++-------------------------------------- src/build.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 73 deletions(-) mode change 100644 => 100755 lexipython.py diff --git a/lexipython.py b/lexipython.py old mode 100644 new mode 100755 index b6fc0ff..c01864d --- a/lexipython.py +++ b/lexipython.py @@ -59,7 +59,7 @@ def overview_all(): print(" {}{}{}".format(name, " " * (l - len(name)), msg)) else: print("There are no Lexicons yet. Create one with:\n\n"\ - " lexipython.py [name] init") + " lexipython.py [name] init\n") def overview_one(name): """ @@ -151,76 +151,13 @@ def command_build(name): """ Rebuilds the browsable pages of a Lexicon. """ - # Load the Lexicon's peripherals - config = utils.load_config(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("lexicon", name, "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] - articles = sorted( - LexiconArticle.populate(articles), - key=lambda a: utils.titlestrip(a.title)) - #phantom_titles = [article.title for article in articles if article.title not in written_titles] - lex_path = os.path.join("lexicon", name) - 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"])) - - # 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="utf8") as f: - content = article.build_default_content() - citeblock = article.build_default_citeblock( - None if idx == 0 else articles[idx - 1].title, - None if idx == l-1 else articles[idx + 1].title) - article_html = entry_skeleton.format( - title = article.title, - lexicon = config["LEXICON_TITLE"], - css = css, - logo = config["LOGO_FILENAME"], - prompt = config["PROMPT"], - 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="utf8") as f: - f.write(build.build_contents_page(articles, config)) - print(" Wrote Contents") - with open(pathto("rules", "index.html"), "w", encoding="utf8") as f: - f.write(build.build_rules_page(config)) - print(" Wrote Rules") - with open(pathto("formatting", "index.html"), "w", encoding="utf8") as f: - f.write(build.build_formatting_page(config)) - print(" Wrote Formatting") - with open(pathto("session", "index.html"), "w", encoding="utf8") as f: - f.write(build.build_session_page(config)) - print(" Wrote Session") - with open(pathto("statistics", "index.html"), "w", encoding="utf8") as f: - f.write(build.build_statistics_page(articles, config)) - print(" Wrote Statistics") + build.build_all("lexicon", name) def command_run(name): """ Runs as a server managing a Lexicon. """ - pass + print("Not implemented") def main(): parser = argparse.ArgumentParser( diff --git a/src/build.py b/src/build.py index 476b6aa..8e37e38 100644 --- a/src/build.py +++ b/src/build.py @@ -5,7 +5,8 @@ import io # For writing pages out as UTF-8 import networkx # For pagerank analytics from collections import defaultdict # For rank inversion in statistics -import src.utils as utils +from src import utils +from src.article import LexiconArticle def build_contents_page(articles, config): """ @@ -39,7 +40,7 @@ def build_contents_page(articles, config): content += link_by_title[article.title] content += "\n" if len(check_off) > 0: - content += "

&c.

\n".format(index_str) + content += "

&c.

\n" for article in check_off: content += "
  • " content += link_by_title[article.title] @@ -132,7 +133,11 @@ 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} + cite_map = { + article.title : [ + cite_tuple[1] + for cite_tuple in article.citations.values()] + for article in articles} # Pages by pagerank content += "
    \n" content += "

    Top 10 pages by page rank:
    \n" @@ -238,9 +243,78 @@ def build_graphviz_file(cite_map): result.append("overlap=false;\n}\n") return "".join(result)#"…" -# Summative functions +def build_compiled_page(articles, config): + """ + Builds a page compiling all articles in the Lexicon. + """ + pass +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 + articles = sorted( + LexiconArticle.populate(articles), + key=lambda a: utils.titlestrip(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 auxiliary files - # TODO: write graphviz file - # TODO: write compiled lexicon page + # 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"])) + + # 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="utf8") as f: + content = article.build_default_content() + citeblock = article.build_default_citeblock( + None if idx == 0 else articles[idx - 1].title, + None if idx == l-1 else articles[idx + 1].title) + article_html = entry_skeleton.format( + title = article.title, + lexicon = config["LEXICON_TITLE"], + css = css, + logo = config["LOGO_FILENAME"], + prompt = config["PROMPT"], + 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="utf8") as f: + f.write(build_contents_page(articles, config)) + print(" Wrote Contents") + with open(pathto("rules", "index.html"), "w", encoding="utf8") as f: + f.write(build_rules_page(config)) + print(" Wrote Rules") + with open(pathto("formatting", "index.html"), "w", encoding="utf8") as f: + f.write(build_formatting_page(config)) + print(" Wrote Formatting") + with open(pathto("session", "index.html"), "w", encoding="utf8") as f: + f.write(build_session_page(config)) + print(" Wrote Session") + with open(pathto("statistics", "index.html"), "w", encoding="utf8") as f: + f.write(build_statistics_page(articles, config)) + print(" Wrote Statistics") \ No newline at end of file From 1f897ecbb0704800531025795c2a1420741bb81b Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 13:31:17 -0700 Subject: [PATCH 18/36] Refactor citeblock code to take full article object --- src/article.py | 18 ++++++++++-------- src/build.py | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/article.py b/src/article.py index 8132f6e..0af2b06 100644 --- a/src/article.py +++ b/src/article.py @@ -162,18 +162,20 @@ class LexiconArticle: } return self.content.format(**format_map) - def build_default_citeblock(self, prev_target, next_target): + 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 = "

    \n" # Prev/next links - if next_target is not None: - citeblock += "

    Next →

    \n".format(utils.titleescape(next_target)) - if prev_target is not None: - citeblock += "

    ← Previous

    \n".format(utils.titleescape(prev_target)) - elif next_target is not None: + if next_article is not None: + citeblock += "

    Next →

    \n".format( + next_article.title_filesafe) + if prev_article is not None: + citeblock += "

    ← Previous

    \n".format( + prev_article.title_filesafe) + if next_article is None and prev_article is None: citeblock += "

     

    \n" # Citations cites_links = [ @@ -182,7 +184,7 @@ class LexiconArticle: "" 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 = "--" + if len(cites_str) < 1: cites_str = "—" citeblock += "

    Citations: {}

    \n".format(cites_str) # Citedby citedby_links = [ @@ -190,6 +192,6 @@ class LexiconArticle: title, utils.titleescape(title)) for title in self.citedby] citedby_str = " | ".join(citedby_links) - if len(citedby_str) < 1: citedby_str = "--" + if len(citedby_str) < 1: citedby_str = "—" citeblock += "

    Cited by: {}

    \n
    \n".format(citedby_str) return citeblock diff --git a/src/build.py b/src/build.py index 8e37e38..314a470 100644 --- a/src/build.py +++ b/src/build.py @@ -288,8 +288,8 @@ def build_all(path_prefix, lexicon_name): with open(pathto("article", article.title_filesafe + ".html"), "w", encoding="utf8") as f: content = article.build_default_content() citeblock = article.build_default_citeblock( - None if idx == 0 else articles[idx - 1].title, - None if idx == l-1 else articles[idx + 1].title) + 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"], From 86478e3d872d573669f50453c766649762653204 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 13:42:14 -0700 Subject: [PATCH 19/36] Phantom style prev/next links --- src/article.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/article.py b/src/article.py index 0af2b06..5e0adce 100644 --- a/src/article.py +++ b/src/article.py @@ -170,11 +170,11 @@ class LexiconArticle: citeblock = "
    \n" # Prev/next links if next_article is not None: - citeblock += "

    Next →

    \n".format( - next_article.title_filesafe) + citeblock += "

    Next →

    \n".format( + next_article.title_filesafe, " class=\"phantom\"" if next_article.player is None else "") if prev_article is not None: - citeblock += "

    ← Previous

    \n".format( - prev_article.title_filesafe) + citeblock += "

    ← Previous

    \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 += "

     

    \n" # Citations From e314b98eaa9a24cd062f8907293ed57cb8333129 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 14:21:09 -0700 Subject: [PATCH 20/36] Fix sorting in contents page --- src/build.py | 33 +++++++++++++++------------------ src/utils.py | 11 ++++++----- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/build.py b/src/build.py index 314a470..6b5ff43 100644 --- a/src/build.py +++ b/src/build.py @@ -13,7 +13,7 @@ def build_contents_page(articles, config): Builds the full HTML of the contents page. """ content = "" - # Article counts + # 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 = "

    There are {0} entries in this lexicon.

    \n".format(len(articles)) @@ -23,48 +23,44 @@ def build_contents_page(articles, config): # Prepare article links link_by_title = {article.title : "{0}".format( article.title, article.title_filesafe, - "" if article.player is not None else " class=\"phantom\"") + " 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 += "
    \n
      \n" indices = config["INDEX_LIST"].split("\n") - alphabetical_order = sorted(articles, key=lambda a: utils.titlecase(a.title)) + alphabetical_order = sorted( + articles, + key=lambda a: utils.titlesort(a.title)) check_off = list(alphabetical_order) for index_str in indices: content += "

      {0}

      \n".format(index_str) for article in alphabetical_order: - if (utils.titlestrip(article.title)[0].upper() in index_str): + if (utils.titlesort(article.title)[0].upper() in index_str): check_off.remove(article) - content += "
    • " - content += link_by_title[article.title] - content += "
    • \n" + content += "
    • {}
    • \n".format(link_by_title[article.title]) if len(check_off) > 0: content += "

      &c.

      \n" for article in check_off: - content += "
    • " - content += link_by_title[article.title] - content += "
    • \n" + content += "
    • {}
    • \n".format(link_by_title[article.title]) content += "
    \n
    \n" # Write the articles in turn order content += "
    \n
      \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.titlecase(a.title))) + 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 += "

      Turn {0}

      \n".format(turn_num) for article in turn_order: if article.turn == turn_num: check_off.remove(article) - content += "
    • " - content += link_by_title[article.title] - content += "
    • \n" + content += "
    • {}
    • \n".format(link_by_title[article.title]) if len(check_off) > 0: content += "

      Unwritten

      \n" for article in check_off: - content += "
    • " - content += link_by_title[article.title] - content += "
    • \n" + content += "
    • {}
    • \n".format(link_by_title[article.title]) content += "
    \n
    \n" # Fill in the page skeleton entry_skeleton = utils.load_resource("entry-page.html") @@ -264,9 +260,10 @@ def build_all(path_prefix, lexicon_name): # 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: utils.titlestrip(a.title)) + 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) diff --git a/src/utils.py b/src/utils.py index d97111d..af15187 100644 --- a/src/utils.py +++ b/src/utils.py @@ -23,13 +23,14 @@ def titleescape(s): s = hex(abs(hash(s)))[2:] # Replace it with a hex hash return s -def titlestrip(s): +def titlesort(s): """ - Strips articles for title sorting. + Reduces titles down for sorting. """ - if s.startswith("The "): return s[4:] - if s.startswith("An "): return s[3:] - if s.startswith("A "): return s[2:] + 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 From a459896238395f2011f6ae66cc9dddd3353c9b04 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 14:21:37 -0700 Subject: [PATCH 21/36] Add missing newline --- src/resources/example-page.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/example-page.txt b/src/resources/example-page.txt index 3f721ba..cb7d04b 100644 --- a/src/resources/example-page.txt +++ b/src/resources/example-page.txt @@ -12,4 +12,4 @@ Unlike the last paragraph, this line will be after a line break within the parag This is an [[example citation|Phantom page]]. You can also cite a [[phantom page]] with just the title. -~Dr. X. Amplepage \ No newline at end of file +~Dr. X. Amplepage From 2c59fbefc85f2cda4150e7381c039178120419e9 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 15:28:18 -0700 Subject: [PATCH 22/36] Implement compiled page --- src/build.py | 42 +++++++++++++++++++++++++++++++++++++-- src/resources/lexicon.cfg | 10 ++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/build.py b/src/build.py index 6b5ff43..c5ec9b3 100644 --- a/src/build.py +++ b/src/build.py @@ -243,7 +243,39 @@ def build_compiled_page(articles, config): """ Builds a page compiling all articles in the Lexicon. """ - pass + # 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" + content = "\n"\ + "\n"\ + "{lexicon}\n"\ + "\n"\ + "\n"\ + "

    {lexicon}

    ".format( + lexicon=config["LEXICON_TITLE"], + css=css) + for article in turn_order: + format_map = { + format_id: cite_tuple[0] # TODO + 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("

    ", "

    ", 1) + article_block = "
    \n"\ + "

    {}

    \n"\ + "{}\n".format(article.title, article_body) + content += article_block + content += "" + return content def build_all(path_prefix, lexicon_name): """ @@ -314,4 +346,10 @@ def build_all(path_prefix, lexicon_name): print(" Wrote Session") with open(pathto("statistics", "index.html"), "w", encoding="utf8") as f: f.write(build_statistics_page(articles, config)) - print(" Wrote Statistics") \ No newline at end of file + 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"]) \ No newline at end of file diff --git a/src/resources/lexicon.cfg b/src/resources/lexicon.cfg index 5b16ee7..8f0d826 100644 --- a/src/resources/lexicon.cfg +++ b/src/resources/lexicon.cfg @@ -43,3 +43,13 @@ PQRS TUV WXYZ <<>>GRAPHVIZ_FILE>>> +<<>>PRINTABLE_FILE>>> +<< Date: Sat, 7 Jul 2018 16:17:55 -0700 Subject: [PATCH 23/36] Add citations to compiled page --- src/build.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/build.py b/src/build.py index c5ec9b3..131016b 100644 --- a/src/build.py +++ b/src/build.py @@ -251,7 +251,8 @@ def build_compiled_page(articles, config): # Build the content of each article css = utils.load_resource("lexicon.css") css += "\n"\ - "body { background: #ffffff; }\n" + "body { background: #ffffff; }\n"\ + "sup { vertical-align: top; font-size: 0.6em; }\n" content = "\n"\ "\n"\ "{lexicon}\n"\ @@ -263,17 +264,29 @@ def build_compiled_page(articles, config): lexicon=config["LEXICON_TITLE"], css=css) for article in turn_order: + # Stitch in superscripts for citations format_map = { - format_id: cite_tuple[0] # TODO + format_id: "{}{}".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("

    ", "

    ", 1) + # Append the citation block + cite_list = "
    \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 ""\ + "

    Citations:
    \n"\ + "{}\n

    ".format(cite_list) article_block = "
    \n"\ "

    {}

    \n"\ - "{}\n".format(article.title, article_body) + "{}\n"\ + "{}\n".format(article.title, article_body, cite_block) content += article_block + content += "" return content From a8e533cde7b6f4e461b4f48191eb17b85b828300 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 17:02:10 -0700 Subject: [PATCH 24/36] Add article length to statistics --- src/build.py | 44 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/build.py b/src/build.py index 131016b..e260597 100644 --- a/src/build.py +++ b/src/build.py @@ -134,6 +134,7 @@ def build_statistics_page(articles, config): cite_tuple[1] for cite_tuple in article.citations.values()] for article in articles} + # Pages by pagerank content += "
    \n" content += "

    Top 10 pages by page rank:
    \n" @@ -147,6 +148,7 @@ def build_statistics_page(articles, config): content += "
    \n".join(map(lambda x: "{0} – {1}".format(x[0]+1, x[1]), ranking[:10])) content += "

    \n" content += "
    \n" + # Top number of citations made content += "
    \n" content += "

    Most citations made from:
    \n" @@ -158,11 +160,17 @@ def build_statistics_page(articles, config): sorted(citation_count.items(), reverse=True)[:3])) content += "

    \n" content += "
    \n" + # Top number of times cited content += "
    \n" content += "

    Most citations made to:
    \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_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) @@ -171,17 +179,42 @@ def build_statistics_page(articles, config): sorted(cited_count.items(), reverse=True)[:3])) content += "

    \n" content += "
    \n" - # player pageranks + + # Top article length, roughly by words + content += "
    \n" + content += "

    Longest article:
    \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 += "
    \n".join(map( + lambda kv: "{0} – {1}".format(kv[1], kv[0]), + sorted(article_length.items(), reverse=True, key=lambda t: t[1])[:3])) + content += "

    \n" + content += "
    \n" + + # Player pageranks content += "
    \n" content += "

    Player total page rank:
    \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} + 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], articles)) for player, articles in articles_by.items()} content += "
    \n".join(map( lambda kv: "{0} – {1}".format(kv[0], round(kv[1], 3)), sorted(player_rank.items(), key=lambda t:t[1], reverse=True))) content += "

    \n" content += "
    \n" + # Player citations made content += "
    \n" content += "

    Citations made by player
    \n" @@ -193,7 +226,8 @@ def build_statistics_page(articles, config): sorted(player_cite_count.items(), key=lambda t:t[1], reverse=True))) content += "

    \n" content += "
    \n" - # player cited count + + # Player cited count content += "
    \n" content += "

    Citations made to player
    \n" cited_times = {player : 0 for player in players} @@ -205,7 +239,7 @@ def build_statistics_page(articles, config): sorted(cited_times.items(), key=lambda t:t[1], reverse=True))) content += "

    \n" content += "
    \n" - + # Fill in the entry skeleton entry_skeleton = utils.load_resource("entry-page.html") css = utils.load_resource("lexicon.css") From 191da26c2c4b816208aeeeeca587c73be0882ef4 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sat, 7 Jul 2018 20:50:58 -0700 Subject: [PATCH 25/36] Fix typo in formatting page --- src/resources/formatting.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resources/formatting.html b/src/resources/formatting.html index ceaa19c..b2b3341 100644 --- a/src/resources/formatting.html +++ b/src/resources/formatting.html @@ -17,7 +17,7 @@ This is an [[example citation|Phantom page]]. You can also cite a [[phantom page ~Dr. X. Amplepage

    Each turn, fill out the header with your player information, the current turn, and the title of your entry. The Player 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.

    -

    Two line breaks begins a new paragraph. A single line break does nothing, unless the line is neded by a double backslash (\\).

    +

    Two line breaks begins a new paragraph. A single line break does nothing, unless the line is ended by a double backslash (\\).

    Text bounded by ** will be bolded: **bold** produces bold. Text bounded by // will be italicized: //italics// produces italics.

    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 Example page. 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 this text. You must be precise in the entry title you cite to. 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.

    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.

    From 0b2d3d4cc33f9c979afc6eac1e5118b4aee388d5 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 8 Jul 2018 00:37:33 -0700 Subject: [PATCH 26/36] Fix whitespace in docstring --- src/article.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/article.py b/src/article.py index 5e0adce..dbd5bea 100644 --- a/src/article.py +++ b/src/article.py @@ -8,15 +8,15 @@ 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 + 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(). """ From 7ffc2ba1e1b18930e333a501b3d87912ebe04046 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Sun, 8 Jul 2018 00:39:20 -0700 Subject: [PATCH 27/36] Fix bug with articles without a pagerank --- src/build.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index e260597..bf35602 100644 --- a/src/build.py +++ b/src/build.py @@ -208,7 +208,9 @@ def build_statistics_page(articles, config): for a in articles if a.player == player] for player in players} - player_rank = {player : sum(map(lambda a: ranks[a.title], articles)) for player, articles in articles_by.items()} + 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 += "
    \n".join(map( lambda kv: "{0} – {1}".format(kv[0], round(kv[1], 3)), sorted(player_rank.items(), key=lambda t:t[1], reverse=True))) From 16db91f5f1d8660f7cea9174825aa5c8fcb33cfd Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 3 Aug 2018 22:09:45 -0700 Subject: [PATCH 28/36] Add default sort option --- src/resources/lexicon.cfg | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/resources/lexicon.cfg b/src/resources/lexicon.cfg index 8f0d826..7185b72 100644 --- a/src/resources/lexicon.cfg +++ b/src/resources/lexicon.cfg @@ -44,6 +44,12 @@ TUV WXYZ <<>>DEFAULT_SORT>>> +?byturn +<<>>GRAPHVIZ_FILE>>> From f2e07cf471ef4c8a0d12ccc559f6b220bdbdfc15 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 3 Aug 2018 22:40:37 -0700 Subject: [PATCH 29/36] Implement sorting config --- src/build.py | 10 ++++++++-- src/resources/contents.html | 4 +++- src/resources/entry-page.html | 2 +- src/resources/redirect.html | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/build.py b/src/build.py index bf35602..f31ce1d 100644 --- a/src/build.py +++ b/src/build.py @@ -27,7 +27,7 @@ def build_contents_page(articles, config): for article in articles} # Write the articles in alphabetical order content += utils.load_resource("contents.html") - content += "
    \n
      \n" + content += "
      \n
        \n" indices = config["INDEX_LIST"].split("\n") alphabetical_order = sorted( articles, @@ -71,6 +71,7 @@ def build_contents_page(articles, config): css=css, logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], + sort=config["DEFAULT_SORT"], content=content, citeblock="") @@ -88,6 +89,7 @@ def build_rules_page(config): css=css, logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], + sort=config["DEFAULT_SORT"], content=content, citeblock="") @@ -105,6 +107,7 @@ def build_formatting_page(config): css=css, logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], + sort=config["DEFAULT_SORT"], content=content, citeblock="") @@ -121,6 +124,7 @@ def build_session_page(config): css=css, logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], + sort=config["DEFAULT_SORT"], content=config["SESSION_PAGE"], citeblock="") @@ -251,6 +255,7 @@ def build_statistics_page(articles, config): css=css, logo=config["LOGO_FILENAME"], prompt=config["PROMPT"], + sort=config["DEFAULT_SORT"], content=content, citeblock="") @@ -352,7 +357,7 @@ def build_all(path_prefix, lexicon_name): # 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"])) + 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...") @@ -374,6 +379,7 @@ def build_all(path_prefix, lexicon_name): css = css, logo = config["LOGO_FILENAME"], prompt = config["PROMPT"], + sort = config["DEFAULT_SORT"], content = content, citeblock = citeblock) f.write(article_html) diff --git a/src/resources/contents.html b/src/resources/contents.html index 42e3b19..a85b697 100644 --- a/src/resources/contents.html +++ b/src/resources/contents.html @@ -15,7 +15,9 @@ contentsToggle = function() { } window.onload = function(){ if (location.search.search("byturn") > 0) - contentsToggle(); + document.getElementById("turn-order").style.display = "block"; + if (location.search.search("byindex") > 0) + document.getElementById("index-order").style.display = "block"; } diff --git a/src/resources/entry-page.html b/src/resources/entry-page.html index db2e393..3b08063 100644 --- a/src/resources/entry-page.html +++ b/src/resources/entry-page.html @@ -11,7 +11,7 @@

        {lexicon}

        -Contents — +ContentsRulesFormattingSession — diff --git a/src/resources/redirect.html b/src/resources/redirect.html index e20197a..ba7ede1 100644 --- a/src/resources/redirect.html +++ b/src/resources/redirect.html @@ -1,9 +1,9 @@ {lexicon} - + -

        Redirecting to {lexicon}...

        +

        Redirecting to {lexicon}...

        \ No newline at end of file From c8cfbc6da8d2dc3e48d9fc4f67ac5498579c91a1 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 9 Aug 2018 09:10:00 -0700 Subject: [PATCH 30/36] Reduce whitespace inside citation targets --- src/article.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/article.py b/src/article.py index dbd5bea..9b2db7f 100644 --- a/src/article.py +++ b/src/article.py @@ -86,7 +86,7 @@ class LexiconArticle: 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(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 From 985f5be19f11ac9b56a7d70f4a8dc2900f927b6e Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 9 Aug 2018 09:10:47 -0700 Subject: [PATCH 31/36] Fix button text not changing with sort order preset --- src/resources/contents.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/resources/contents.html b/src/resources/contents.html index a85b697..3ff5158 100644 --- a/src/resources/contents.html +++ b/src/resources/contents.html @@ -1,23 +1,29 @@ From 5100ecc634c957966a1ad742375f485caaa57f56 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 9 Aug 2018 09:35:57 -0700 Subject: [PATCH 32/36] Sort cited-to results --- src/build.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index f31ce1d..ff22f48 100644 --- a/src/build.py +++ b/src/build.py @@ -179,7 +179,7 @@ def build_statistics_page(articles, config): cited_count = defaultdict(list) for title, count in cited_tally: cited_count[count].append(title) content += "
        \n".join(map( - lambda kv: "{0} – {1}".format(kv[0], "; ".join(kv[1])), + lambda kv: "{0} – {1}".format(kv[0], "; ".join(sorted(kv[1]))), sorted(cited_count.items(), reverse=True)[:3])) content += "

        \n" content += "
      \n" From f21213d67365a6f4b574e0d5e86f99ea62e075f7 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 9 Aug 2018 09:49:28 -0700 Subject: [PATCH 33/36] Fix reference in sort button code --- src/resources/contents.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resources/contents.html b/src/resources/contents.html index 3ff5158..a7c2543 100644 --- a/src/resources/contents.html +++ b/src/resources/contents.html @@ -17,12 +17,12 @@ window.onload = function(){ if (location.search.search("byturn") > 0) { document.getElementById("turn-order").style.display = "block"; - b.innerText = "Switch to index order"; + document.getElementById("toggle-button").innerText = "Switch to index order"; } if (location.search.search("byindex") > 0) { document.getElementById("index-order").style.display = "block"; - b.innerText = "Switch to turn order"; + document.getElementById("toggle-button").innerText = "Switch to turn order"; } } From 2e4f7f60a366f8b6cb6025b585c3fcb7e144d382 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 16 Aug 2018 00:02:52 -0700 Subject: [PATCH 34/36] Stabilize article sorting by title in citeblocks and statistics page --- src/article.py | 8 ++++++-- src/build.py | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/article.py b/src/article.py index 9b2db7f..a4aeffc 100644 --- a/src/article.py +++ b/src/article.py @@ -182,7 +182,9 @@ class LexiconArticle: "{0}".format( title, utils.titleescape(title), "" if title in self.wcites else " class=\"phantom\"") - for title in sorted(self.wcites | self.pcites)] + 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 = "—" citeblock += "

      Citations: {}

      \n".format(cites_str) @@ -190,7 +192,9 @@ class LexiconArticle: citedby_links = [ "{0}".format( title, utils.titleescape(title)) - for title in self.citedby] + for title in sorted( + self.citedby, + key=lambda t: utils.titlesort(t))] citedby_str = " | ".join(citedby_links) if len(citedby_str) < 1: citedby_str = "—" citeblock += "

      Cited by: {}

      \n
    \n".format(citedby_str) diff --git a/src/build.py b/src/build.py index ff22f48..d4fbed9 100644 --- a/src/build.py +++ b/src/build.py @@ -160,7 +160,11 @@ def build_statistics_page(articles, config): citation_count = defaultdict(list) for title, count in citation_tally: citation_count[count].append(title) content += "
    \n".join(map( - lambda kv: "{0} – {1}".format(kv[0], "; ".join(kv[1])), + lambda kv: "{0} – {1}".format( + kv[0], + "; ".join(sorted( + kv[1], + key=lambda t: utils.titlesort(t)))), sorted(citation_count.items(), reverse=True)[:3])) content += "

    \n" content += "
    \n" From 7cd588373450c78731b94946460da1963bad5c50 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Thu, 16 Aug 2018 17:03:16 -0700 Subject: [PATCH 35/36] Add self-citation sanity check --- src/build.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/build.py b/src/build.py index d4fbed9..4d9235a 100644 --- a/src/build.py +++ b/src/build.py @@ -411,4 +411,15 @@ def build_all(path_prefix, lexicon_name): 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"]) \ No newline at end of file + 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() From 637385a3b4536e469fcd635798f3765c4ba70bc1 Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Mon, 20 Aug 2018 01:25:06 -0700 Subject: [PATCH 36/36] Fix more encoding problems --- src/build.py | 12 ++++++------ src/resources/entry-page.html | 1 + src/utils.py | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/build.py b/src/build.py index 4d9235a..f49f8d6 100644 --- a/src/build.py +++ b/src/build.py @@ -372,7 +372,7 @@ def build_all(path_prefix, lexicon_name): l = len(articles) for idx in range(l): article = articles[idx] - with open(pathto("article", article.title_filesafe + ".html"), "w", encoding="utf8") as f: + 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], @@ -391,19 +391,19 @@ def build_all(path_prefix, lexicon_name): # Write default pages print("Writing default pages...") - with open(pathto("contents", "index.html"), "w", encoding="utf8") as f: + 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="utf8") as f: + 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="utf8") as f: + 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="utf8") as f: + 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="utf8") as f: + with open(pathto("statistics", "index.html"), "w", encoding="utf-8") as f: f.write(build_statistics_page(articles, config)) print(" Wrote Statistics") diff --git a/src/resources/entry-page.html b/src/resources/entry-page.html index 3b08063..cf65ece 100644 --- a/src/resources/entry-page.html +++ b/src/resources/entry-page.html @@ -2,6 +2,7 @@ {title} | {lexicon} + diff --git a/src/utils.py b/src/utils.py index af15187..ea47974 100644 --- a/src/utils.py +++ b/src/utils.py @@ -38,7 +38,7 @@ def titlesort(s): 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="utf8") as f: + with open(os.path.join("src", "resources", filename), "r", encoding="utf-8") as f: cache[filename] = f.read() return cache[filename]