about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.circleci/config.yml42
-rw-r--r--.github/CONTRIBUTING.md5
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md39
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/ISSUE_TEMPLATE/new-pronouns.md18
-rw-r--r--.github/PULL_REQUEST_TEMPLATE.md8
-rw-r--r--.gitignore1
-rw-r--r--CODE_OF_CONDUCT.md76
-rw-r--r--COPYING518
-rw-r--r--README.md166
-rw-r--r--project.clj33
-rwxr-xr-xquickstart.sh5
-rw-r--r--resources/pronouns.css32
-rw-r--r--resources/pronouns.tab38
-rw-r--r--src/pronouns/config.clj29
-rw-r--r--src/pronouns/pages.clj242
-rw-r--r--src/pronouns/util.clj120
-rw-r--r--src/pronouns/web.clj73
-rw-r--r--test/pronouns/pages_test.clj23
-rw-r--r--test/pronouns/resource_test.clj12
-rw-r--r--test/pronouns/util_test.clj32
21 files changed, 1344 insertions, 188 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000..f3e8a34
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,42 @@
+# Clojure CircleCI 2.0 configuration file
+#
+# Check https://circleci.com/docs/2.0/language-clojure/ for more details
+#
+version: 2
+jobs:
+  build:
+    docker:
+      # specify the version you desire here
+      - image: circleci/clojure:lein-2.7.1
+      
+      # Specify service dependencies here if necessary
+      # CircleCI maintains a library of pre-built images
+      # documented at https://circleci.com/docs/2.0/circleci-images/
+      # - image: circleci/postgres:9.4
+
+    working_directory: ~/repo
+
+    environment:
+      LEIN_ROOT: "true"
+      # Customize the JVM maximum heap limit
+      JVM_OPTS: -Xmx3200m
+    
+    steps:
+      - checkout
+
+      # Download and cache dependencies
+      - restore_cache:
+          keys:
+          - v1-dependencies-{{ checksum "project.clj" }}
+          # fallback to using the latest cache if no exact match is found
+          - v1-dependencies-
+
+      - run: lein deps
+
+      - save_cache:
+          paths:
+            - ~/.m2
+          key: v1-dependencies-{{ checksum "project.clj" }}
+        
+      # run tests!
+      - run: lein test
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..6480088
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,5 @@
+Please check out the following resources:
+
+  * [Info For Developers](../README.md#for-developers) 
+  * [FAQ](../README.md#FAQ) 
+  * [Code of Conduct](../CODE_OF_CONDUCT.md)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..d5c163d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,39 @@
+---
+name: Bug report
+about: Create a report to help us improve
+labels: 
+
+---
+*please search the issues for duplicates and read the [FAQ](/witch-house/pronoun.is/blob/master/README.md#FAQ) 
+before submitting a new issue :)*
+
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..3bae905
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+labels: 
+
+---
+*please search the issues for duplicates and read the [FAQ](/witch-house/pronoun.is/blob/master/README.md#FAQ) 
+before submitting a new issue :)*
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/new-pronouns.md b/.github/ISSUE_TEMPLATE/new-pronouns.md
new file mode 100644
index 0000000..8093d3b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/new-pronouns.md
@@ -0,0 +1,18 @@
+---
+name: New pronouns
+about: For requesting a new set of pronouns to pronoun.is
+labels: 
+
+---
+
+### Link to fully filled-out pronoun.is link of your pronouns
+
+https://pronoun.is/a/b/c/d/e
+
+### External link showing real-world usage of these pronouns
+
+https://example.com/whatever
+
+### Should this pronoun set be included according to the [Philosophy on pronoun inclusion](/witch-house/pronoun.is/blob/master/README.md#philosophy-on-pronoun-inclusion)?
+
+yes/no
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..0fa69b2
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,8 @@
+*Thanks for contributing! Please check the following things before submitting your PR :)*
+
+  - [ ] PR message includes a link to the relevant issue(s) ๐Ÿ’โ€โ™€๏ธ๐Ÿ–‡๏ธ๐Ÿงพ
+  - [ ] Commit messages are well-formatted
+  (please follow [this guide](https://chris.beams.io/posts/git-commit/)) ๐Ÿ“๐Ÿ“
+  - [ ] New features have test coverage ๐Ÿ“‹โ˜‘๏ธโ˜‘๏ธโ˜‘๏ธ
+  - [ ] The app boots and pronoun pages are served correctly 
+  (try `lein ring server`) ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿš€
diff --git a/.gitignore b/.gitignore
index fffd8d2..91c4c9a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ pom.xml
 /checkouts
 /.env
 /target
+.nrepl-port
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..6ed8000
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,76 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project maintainer, @morganastra. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..84592c6
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,518 @@
+                       GNU AFFERO GENERAL PUBLIC LICENSE
+
+                          Version 3, 19 November 2007
+
+Copyright ยฉ 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is
+permitted to copy and distribute verbatim copies of this license document, but
+changing it is not allowed. Preamble
+
+The GNU Affero General Public License is a free, copyleft license for software
+and other kinds of works, specifically designed to ensure cooperation with the
+community in the case of network server software.
+
+The licenses for most software and other practical works are designed to take
+away your freedom to share and change the works. By contrast, our General Public
+Licenses are intended to guarantee your freedom to share and change all versions
+of a program--to make sure it remains free software for all its users.
+
+When we speak of free software, we are referring to freedom, not price. Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for them if you wish), that you
+receive source code or can get it if you want it, that you can change the
+software or use pieces of it in new free programs, and that you know you can do
+these things.
+
+Developers that use our General Public Licenses protect your rights with two
+steps: (1) assert copyright on the software, and (2) offer you this License
+which gives you legal permission to copy, distribute and/or modify the software.
+
+A secondary benefit of defending all users' freedom is that improvements made in
+alternate versions of the program, if they receive widespread use, become
+available for other developers to incorporate. Many developers of free software
+are heartened and encouraged by the resulting cooperation. However, in the case
+of software used on network servers, this result may fail to come about. The GNU
+General Public License permits making a modified version and letting the public
+access it on a server without ever releasing its source code to the public.
+
+The GNU Affero General Public License is designed specifically to ensure that,
+in such cases, the modified source code becomes available to the community. It
+requires the operator of a network server to provide the source code of the
+modified version running there to the users of that server. Therefore, public
+use of a modified version, on a publicly accessible server, gives the public
+access to the source code of the modified version.
+
+An older license, called the Affero General Public License and published by
+Affero, was designed to accomplish similar goals. This is a different license,
+not a version of the Affero GPL, but Affero has released a new version of the
+Affero GPL which permits relicensing under this license.
+
+The precise terms and conditions for copying, distribution and modification
+follow. TERMS AND CONDITIONS 0. Definitions.
+
+"This License" refers to version 3 of the GNU Affero General Public License.
+
+"Copyright" also means copyright-like laws that apply to other kinds of works,
+such as semiconductor masks.
+
+"The Program" refers to any copyrightable work licensed under this License. Each
+licensee is addressed as "you". "Licensees" and "recipients" may be individuals
+or organizations.
+
+To "modify" a work means to copy from or adapt all or part of the work in a
+fashion requiring copyright permission, other than the making of an exact copy.
+The resulting work is called a "modified version" of the earlier work or a work
+"based on" the earlier work.
+
+A "covered work" means either the unmodified Program or a work based on the
+Program.
+
+To "propagate" a work means to do anything with it that, without permission,
+would make you directly or secondarily liable for infringement under applicable
+copyright law, except executing it on a computer or modifying a private copy.
+Propagation includes copying, distribution (with or without modification),
+making available to the public, and in some countries other activities as well.
+
+To "convey" a work means any kind of propagation that enables other parties to
+make or receive copies. Mere interaction with a user through a computer network,
+with no transfer of a copy, is not conveying.
+
+An interactive user interface displays "Appropriate Legal Notices" to the extent
+that it includes a convenient and prominently visible feature that (1) displays
+an appropriate copyright notice, and (2) tells the user that there is no
+warranty for the work (except to the extent that warranties are provided), that
+licensees may convey the work under this License, and how to view a copy of this
+License. If the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion. 1. Source Code.
+
+The "source code" for a work means the preferred form of the work for making
+modifications to it. "Object code" means any non-source form of a work.
+
+A "Standard Interface" means an interface that either is an official standard
+defined by a recognized standards body, or, in the case of interfaces specified
+for a particular programming language, one that is widely used among developers
+working in that language.
+
+The "System Libraries" of an executable work include anything, other than the
+work as a whole, that (a) is included in the normal form of packaging a Major
+Component, but which is not part of that Major Component, and (b) serves only to
+enable use of the work with that Major Component, or to implement a Standard
+Interface for which an implementation is available to the public in source code
+form. A "Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system (if any) on
+which the executable work runs, or a compiler used to produce the work, or an
+object code interpreter used to run it.
+
+The "Corresponding Source" for a work in object code form means all the source
+code needed to generate, install, and (for an executable work) run the object
+code and to modify the work, including scripts to control those activities.
+However, it does not include the work's System Libraries, or general-purpose
+tools or generally available free programs which are used unmodified in
+performing those activities but which are not part of the work. For example,
+Corresponding Source includes interface definition files associated with source
+files for the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require, such as by
+intimate data communication or control flow between those subprograms and other
+parts of the work.
+
+The Corresponding Source need not include anything that users can regenerate
+automatically from other parts of the Corresponding Source.
+
+The Corresponding Source for a work in source code form is that same work. 2.
+Basic Permissions.
+
+All rights granted under this License are granted for the term of copyright on
+the Program, and are irrevocable provided the stated conditions are met. This
+License explicitly affirms your unlimited permission to run the unmodified
+Program. The output from running a covered work is covered by this License only
+if the output, given its content, constitutes a covered work. This License
+acknowledges your rights of fair use or other equivalent, as provided by
+copyright law.
+
+You may make, run and propagate covered works that you do not convey, without
+conditions so long as your license otherwise remains in force. You may convey
+covered works to others for the sole purpose of having them make modifications
+exclusively for you, or provide you with facilities for running those works,
+provided that you comply with the terms of this License in conveying all
+material for which you do not control copyright. Those thus making or running
+the covered works for you must do so exclusively on your behalf, under your
+direction and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+Conveying under any other circumstances is permitted solely under the conditions
+stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3.
+Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+No covered work shall be deemed part of an effective technological measure under
+any applicable law fulfilling obligations under article 11 of the WIPO copyright
+treaty adopted on 20 December 1996, or similar laws prohibiting or restricting
+circumvention of such measures.
+
+When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention is
+effected by exercising rights under this License with respect to the covered
+work, and you disclaim any intention to limit operation or modification of the
+work as a means of enforcing, against the work's users, your or third parties'
+legal rights to forbid circumvention of technological measures. 4. Conveying
+Verbatim Copies.
+
+You may convey verbatim copies of the Program's source code as you receive it,
+in any medium, provided that you conspicuously and appropriately publish on each
+copy an appropriate copyright notice; keep intact all notices stating that this
+License and any non-permissive terms added in accord with section 7 apply to the
+code; keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+You may charge any price or no price for each copy that you convey, and you may
+offer support or warranty protection for a fee. 5. Conveying Modified Source
+Versions.
+
+You may convey a work based on the Program, or the modifications to produce it
+from the Program, in the form of source code under the terms of section 4,
+provided that you also meet all of these conditions:
+
+a) The work must carry prominent notices stating that you modified it, and
+giving a relevant date. b) The work must carry prominent notices stating that it
+is released under this License and any conditions added under section 7. This
+requirement modifies the requirement in section 4 to "keep intact all notices".
+c) You must license the entire work, as a whole, under this License to anyone
+who comes into possession of a copy. This License will therefore apply, along
+with any applicable section 7 additional terms, to the whole of the work, and
+all its parts, regardless of how they are packaged. This License gives no
+permission to license the work in any other way, but it does not invalidate such
+permission if you have separately received it. d) If the work has interactive
+user interfaces, each must display Appropriate Legal Notices; however, if the
+Program has interactive interfaces that do not display Appropriate Legal
+Notices, your work need not make them do so.
+
+A compilation of a covered work with other separate and independent works, which
+are not by their nature extensions of the covered work, and which are not
+combined with it such as to form a larger program, in or on a volume of a
+storage or distribution medium, is called an "aggregate" if the compilation and
+its resulting copyright are not used to limit the access or legal rights of the
+compilation's users beyond what the individual works permit. Inclusion of a
+covered work in an aggregate does not cause this License to apply to the other
+parts of the aggregate. 6. Conveying Non-Source Forms.
+
+You may convey a covered work in object code form under the terms of sections 4
+and 5, provided that you also convey the machine-readable Corresponding Source
+under the terms of this License, in one of these ways:
+
+a) Convey the object code in, or embodied in, a physical product (including a
+physical distribution medium), accompanied by the Corresponding Source fixed on
+a durable physical medium customarily used for software interchange. b) Convey
+the object code in, or embodied in, a physical product (including a physical
+distribution medium), accompanied by a written offer, valid for at least three
+years and valid for as long as you offer spare parts or customer support for
+that product model, to give anyone who possesses the object code either (1) a
+copy of the Corresponding Source for all the software in the product that is
+covered by this License, on a durable physical medium customarily used for
+software interchange, for a price no more than your reasonable cost of
+physically performing this conveying of source, or (2) access to copy the
+Corresponding Source from a network server at no charge. c) Convey individual
+copies of the object code with a copy of the written offer to provide the
+Corresponding Source. This alternative is allowed only occasionally and
+noncommercially, and only if you received the object code with such an offer, in
+accord with subsection 6b. d) Convey the object code by offering access from a
+designated place (gratis or for a charge), and offer equivalent access to the
+Corresponding Source in the same way through the same place at no further
+charge. You need not require recipients to copy the Corresponding Source along
+with the object code. If the place to copy the object code is a network server,
+the Corresponding Source may be on a different server (operated by you or a
+third party) that supports equivalent copying facilities, provided you maintain
+clear directions next to the object code saying where to find the Corresponding
+Source. Regardless of what server hosts the Corresponding Source, you remain
+obligated to ensure that it is available for as long as needed to satisfy these
+requirements. e) Convey the object code using peer-to-peer transmission,
+provided you inform other peers where the object code and Corresponding Source
+of the work are being offered to the general public at no charge under
+subsection 6d.
+
+A separable portion of the object code, whose source code is excluded from the
+Corresponding Source as a System Library, need not be included in conveying the
+object code work.
+
+A "User Product" is either (1) a "consumer product", which means any tangible
+personal property which is normally used for personal, family, or household
+purposes, or (2) anything designed or sold for incorporation into a dwelling. In
+determining whether a product is a consumer product, doubtful cases shall be
+resolved in favor of coverage. For a particular product received by a particular
+user, "normally used" refers to a typical or common use of that class of
+product, regardless of the status of the particular user or of the way in which
+the particular user actually uses, or expects or is expected to use, the
+product. A product is a consumer product regardless of whether the product has
+substantial commercial, industrial or non-consumer uses, unless such uses
+represent the only significant mode of use of the product.
+
+"Installation Information" for a User Product means any methods, procedures,
+authorization keys, or other information required to install and execute
+modified versions of a covered work in that User Product from a modified version
+of its Corresponding Source. The information must suffice to ensure that the
+continued functioning of the modified object code is in no case prevented or
+interfered with solely because modification has been made.
+
+If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as part of a
+transaction in which the right of possession and use of the User Product is
+transferred to the recipient in perpetuity or for a fixed term (regardless of
+how the transaction is characterized), the Corresponding Source conveyed under
+this section must be accompanied by the Installation Information. But this
+requirement does not apply if neither you nor any third party retains the
+ability to install modified object code on the User Product (for example, the
+work has been installed in ROM).
+
+The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates for a
+work that has been modified or installed by the recipient, or for the User
+Product in which it has been modified or installed. Access to a network may be
+denied when the modification itself materially and adversely affects the
+operation of the network or violates the rules and protocols for communication
+across the network.
+
+Corresponding Source conveyed, and Installation Information provided, in accord
+with this section must be in a format that is publicly documented (and with an
+implementation available to the public in source code form), and must require no
+special password or key for unpacking, reading or copying. 7. Additional Terms.
+
+"Additional permissions" are terms that supplement the terms of this License by
+making exceptions from one or more of its conditions. Additional permissions
+that are applicable to the entire Program shall be treated as though they were
+included in this License, to the extent that they are valid under applicable
+law. If additional permissions apply only to part of the Program, that part may
+be used separately under those permissions, but the entire Program remains
+governed by this License without regard to the additional permissions.
+
+When you convey a copy of a covered work, you may at your option remove any
+additional permissions from that copy, or from any part of it. (Additional
+permissions may be written to require their own removal in certain cases when
+you modify the work.) You may place additional permissions on material, added by
+you to a covered work, for which you have or can give appropriate copyright
+permission.
+
+Notwithstanding any other provision of this License, for material you add to a
+covered work, you may (if authorized by the copyright holders of that material)
+supplement the terms of this License with terms:
+
+a) Disclaiming warranty or limiting liability differently from the terms of
+sections 15 and 16 of this License; or b) Requiring preservation of specified
+reasonable legal notices or author attributions in that material or in the
+Appropriate Legal Notices displayed by works containing it; or c) Prohibiting
+misrepresentation of the origin of that material, or requiring that modified
+versions of such material be marked in reasonable ways as different from the
+original version; or d) Limiting the use for publicity purposes of names of
+licensors or authors of the material; or e) Declining to grant rights under
+trademark law for use of some trade names, trademarks, or service marks; or f)
+Requiring indemnification of licensors and authors of that material by anyone
+who conveys the material (or modified versions of it) with contractual
+assumptions of liability to the recipient, for any liability that these
+contractual assumptions directly impose on those licensors and authors.
+
+All other non-permissive additional terms are considered "further restrictions"
+within the meaning of section 10. If the Program as you received it, or any part
+of it, contains a notice stating that it is governed by this License along with
+a term that is a further restriction, you may remove that term. If a license
+document contains a further restriction but permits relicensing or conveying
+under this License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does not survive
+such relicensing or conveying.
+
+If you add terms to a covered work in accord with this section, you must place,
+in the relevant source files, a statement of the additional terms that apply to
+those files, or a notice indicating where to find the applicable terms.
+
+Additional terms, permissive or non-permissive, may be stated in the form of a
+separately written license, or stated as exceptions; the above requirements
+apply either way. 8. Termination.
+
+You may not propagate or modify a covered work except as expressly provided
+under this License. Any attempt otherwise to propagate or modify it is void, and
+will automatically terminate your rights under this License (including any
+patent licenses granted under the third paragraph of section 11).
+
+However, if you cease all violation of this License, then your license from a
+particular copyright holder is reinstated (a) provisionally, unless and until
+the copyright holder explicitly and finally terminates your license, and (b)
+permanently, if the copyright holder fails to notify you of the violation by
+some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated
+permanently if the copyright holder notifies you of the violation by some
+reasonable means, this is the first time you have received notice of violation
+of this License (for any work) from that copyright holder, and you cure the
+violation prior to 30 days after your receipt of the notice.
+
+Termination of your rights under this section does not terminate the licenses of
+parties who have received copies or rights from you under this License. If your
+rights have been terminated and not permanently reinstated, you do not qualify
+to receive new licenses for the same material under section 10. 9. Acceptance
+Not Required for Having Copies.
+
+You are not required to accept this License in order to receive or run a copy of
+the Program. Ancillary propagation of a covered work occurring solely as a
+consequence of using peer-to-peer transmission to receive a copy likewise does
+not require acceptance. However, nothing other than this License grants you
+permission to propagate or modify any covered work. These actions infringe
+copyright if you do not accept this License. Therefore, by modifying or
+propagating a covered work, you indicate your acceptance of this License to do
+so. 10. Automatic Licensing of Downstream Recipients.
+
+Each time you convey a covered work, the recipient automatically receives a
+license from the original licensors, to run, modify and propagate that work,
+subject to this License. You are not responsible for enforcing compliance by
+third parties with this License.
+
+An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered work results
+from an entity transaction, each party to that transaction who receives a copy
+of the work also receives whatever licenses to the work the party's predecessor
+in interest had or could give under the previous paragraph, plus a right to
+possession of the Corresponding Source of the work from the predecessor in
+interest, if the predecessor has it or can get it with reasonable efforts.
+
+You may not impose any further restrictions on the exercise of the rights
+granted or affirmed under this License. For example, you may not impose a
+license fee, royalty, or other charge for exercise of rights granted under this
+License, and you may not initiate litigation (including a cross-claim or
+counterclaim in a lawsuit) alleging that any patent claim is infringed by
+making, using, selling, offering for sale, or importing the Program or any
+portion of it. 11. Patents.
+
+A "contributor" is a copyright holder who authorizes use under this License of
+the Program or a work on which the Program is based. The work thus licensed is
+called the contributor's "contributor version".
+
+A contributor's "essential patent claims" are all patent claims owned or
+controlled by the contributor, whether already acquired or hereafter acquired,
+that would be infringed by some manner, permitted by this License, of making,
+using, or selling its contributor version, but do not include claims that would
+be infringed only as a consequence of further modification of the contributor
+version. For purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of this License.
+
+Each contributor grants you a non-exclusive, worldwide, royalty-free patent
+license under the contributor's essential patent claims, to make, use, sell,
+offer for sale, import and otherwise run, modify and propagate the contents of
+its contributor version.
+
+In the following three paragraphs, a "patent license" is any express agreement
+or commitment, however denominated, not to enforce a patent (such as an express
+permission to practice a patent or covenant not to sue for patent infringement).
+To "grant" such a patent license to a party means to make such an agreement or
+commitment not to enforce a patent against the party.
+
+If you convey a covered work, knowingly relying on a patent license, and the
+Corresponding Source of the work is not available for anyone to copy, free of
+charge and under the terms of this License, through a publicly available network
+server or other readily accessible means, then you must either (1) cause the
+Corresponding Source to be so available, or (2) arrange to deprive yourself of
+the benefit of the patent license for this particular work, or (3) arrange, in a
+manner consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have actual
+knowledge that, but for the patent license, your conveying the covered work in a
+country, or your recipient's use of the covered work in a country, would
+infringe one or more identifiable patents in that country that you have reason
+to believe are valid.
+
+If, pursuant to or in connection with a single transaction or arrangement, you
+convey, or propagate by procuring conveyance of, a covered work, and grant a
+patent license to some of the parties receiving the covered work authorizing
+them to use, propagate, modify or convey a specific copy of the covered work,
+then the patent license you grant is automatically extended to all recipients of
+the covered work and works based on it.
+
+A patent license is "discriminatory" if it does not include within the scope of
+its coverage, prohibits the exercise of, or is conditioned on the non-exercise
+of one or more of the rights that are specifically granted under this License.
+You may not convey a covered work if you are a party to an arrangement with a
+third party that is in the business of distributing software, under which you
+make payment to the third party based on the extent of your activity of
+conveying the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory patent
+license (a) in connection with copies of the covered work conveyed by you (or
+copies made from those copies), or (b) primarily for and in connection with
+specific products or compilations that contain the covered work, unless you
+entered into that arrangement, or that patent license was granted, prior to 28
+March 2007.
+
+Nothing in this License shall be construed as excluding or limiting any implied
+license or other defenses to infringement that may otherwise be available to you
+under applicable patent law. 12. No Surrender of Others' Freedom.
+
+If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not excuse
+you from the conditions of this License. If you cannot convey a covered work so
+as to satisfy simultaneously your obligations under this License and any other
+pertinent obligations, then as a consequence you may not convey it at all. For
+example, if you agree to terms that obligate you to collect a royalty for
+further conveying from those to whom you convey the Program, the only way you
+could satisfy both those terms and this License would be to refrain entirely
+from conveying the Program. 13. Remote Network Interaction; Use with the GNU
+General Public License.
+
+Notwithstanding any other provision of this License, if you modify the Program,
+your modified version must prominently offer all users interacting with it
+remotely through a computer network (if your version supports such interaction)
+an opportunity to receive the Corresponding Source of your version by providing
+access to the Corresponding Source from a network server at no charge, through
+some standard or customary means of facilitating copying of software. This
+Corresponding Source shall include the Corresponding Source for any work covered
+by version 3 of the GNU General Public License that is incorporated pursuant to
+the following paragraph.
+
+Notwithstanding any other provision of this License, you have permission to link
+or combine any covered work with a work licensed under version 3 of the GNU
+General Public License into a single combined work, and to convey the resulting
+work. The terms of this License will continue to apply to the part which is the
+covered work, but the work with which it is combined will remain governed by
+version 3 of the GNU General Public License. 14. Revised Versions of this
+License.
+
+The Free Software Foundation may publish revised and/or new versions of the GNU
+Affero General Public License from time to time. Such new versions will be
+similar in spirit to the present version, but may differ in detail to address
+new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program specifies
+that a certain numbered version of the GNU Affero General Public License "or any
+later version" applies to it, you have the option of following the terms and
+conditions either of that numbered version or of any later version published by
+the Free Software Foundation. If the Program does not specify a version number
+of the GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+If the Program specifies that a proxy can decide which future versions of the
+GNU Affero General Public License can be used, that proxy's public statement of
+acceptance of a version permanently authorizes you to choose that version for
+the Program.
+
+Later license versions may give you additional or different permissions.
+However, no additional obligations are imposed on any author or copyright holder
+as a result of your choosing to follow a later version. 15. Disclaimer of
+Warranty.
+
+THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER
+PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
+EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE
+QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+16. Limitation of Liability.
+
+IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
+COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS
+PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL,
+INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
+THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE
+PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY
+HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of
+Sections 15 and 16.
+
+If the disclaimer of warranty and limitation of liability provided above cannot
+be given local legal effect according to their terms, reviewing courts shall
+apply local law that most closely approximates an absolute waiver of all civil
+liability in connection with the Program, unless a warranty or assumption of
+liability accompanies a copy of the Program in return for a fee.
+
+                          END OF TERMS AND CONDITIONS
\ No newline at end of file
diff --git a/README.md b/README.md
index d061e39..faedb55 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,39 @@
-# [pronoun.is](http://pronoun.is) is a www site for showing people how to use pronouns
+# pronoun.is
+
+[pronoun.is](https://pronoun.is) is a website for personal pronoun usage examples
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [pronoun.is](#pronounis)
+    - [For users](#for-users)
+    - [For developers](#for-developers)
+        - [The database](#the-database)
+        - [The code](#the-code)
+        - [Tests](#tests)
+        - [Running the app in a dev environment](#running-the-app-in-a-dev-environment)
+        - [The git repo](#the-git-repo)
+    - [Philosophy on pronoun inclusion](#philosophy-on-pronoun-inclusion)
+    - [FAQ](#faq)
+        - [Can we translate pronoun.is into another language?](#can-we-translate-pronounis-into-another-language)
+        - [Can we change the example sentences?](#can-we-change-the-example-sentences)
+        - [Can we add pronunciation guides?](#can-we-add-pronunciation-guides)
+        - [Can we add pluralization support (i.e. themself vs themselves)](#can-we-add-pluralization-support-ie-themself-vs-themselves)
+    - [License](#license)
+
+<!-- markdown-toc end -->
+
 
 ## For users
 
 You can use any pronouns you like simply by filling them into the
-url path. For example, http://pronoun.is/ze/zir/zir/zirs/zirself
+url path. For example, https://pronoun.is/ze/zir/zir/zirs/zirself
 
 That's pretty unwieldy! Fortunately you can also give it only the
-first pronoun or two: http://pronoun.is/she/her or http://pronoun.is/they
+first pronoun or two: https://pronoun.is/she/her or https://pronoun.is/they
 
 Automatically filling in the rest from only one or two forms only
-works for pronouns in the [database](resources/pronouns.tab). If the
+works for pronouns in the [database][pronoun-database]. If the
 pronouns you or a friend uses aren't supported, please let us know and
 we'll add them. Alternatively you could add them yourself and submit a
 pull request (see the next section for details)
@@ -18,35 +42,137 @@ pull request (see the next section for details)
 
 ### The database
 
-The pronouns "database" is a tab-delimited file with fields and
-example values as follows:
+The pronouns "database" is a tab-delimited file located in [resources/pronouns.tab][pronoun-database] with fields and example values as follows:
 
 subject|object|possessive-determiner|possessive-pronoun|reflexive
 -------|------|---------------------|------------------|---------
 they   | them | their               | theirs           | themselves
 
-If you edit it with a text editor, make sure your editor inputs real
-tab characters in that file (a thing your editor might normally be
-configured not to do!) In Emacs, you can input real tabs by doing
-Ctrl+q <tab>
+The top 6 pronouns are displayed on the front page. Please don't edit these
+without talking to me, they've been hand-curated based on usage frequency.
+Below the top 6, the remaining pronouns are sorted in alphabetical order by
+subject and then in roughly frequency order for sets that have the same subject
+pronoun. If you're adding a set that shares the same object pronoun as other
+set(s) already in the database, please insert it immediately below those ones.
+
+If you edit the database with a text editor, make sure your editor inputs real
+tab characters in that file (a thing your editor might normally be configured 
+not to do!) In Emacs, you can input real tabs by doing Ctrl+q <tab>. 
+In Vi you can use Ctrl+v <tab>.
+
+[pronoun-database]: resources/pronouns.tab
+
+### The code
+
+The top-level logic for running the server lives in [`pronouns.web`](src/pronouns/web.clj)
+
+Page rendering markup is in [`pronouns.pages`](src/pronouns/pages.clj), it uses
+[hiccup](https://github.com/weavejester/hiccup) for rendering HTML from Clojure
+datastructures.
+
+[`pronouns.config`](src/pronouns/config.clj) is currently used only for loading
+the [pronouns database][pronoun-database]
+
+The unfortunately-named [`pronouns.util`](src/pronouns/util.clj) includes both
+actual utility functions used elsewhere in the code, but also what you might
+think of as "controllers" if you're used to the MVC model of web design - code
+that does the computations necessary for the `pages` (analogous to "views")
+to render themselves. We should probably break up `util` into (at least) two
+namespaces and be a little more deliberate about where everything currently
+in that namespace should live!
+
+### Tests
+
+Run the suite with `lein test`
+
+Test coverage is not great but getting better. Please run the tests and
+confirm that everything passes before merging changes, and please include
+tests with any new logic you introduce in a PR!
+
+Goals for the future include setting up automated CI to run the tests for
+us on every PR branch
 
 ### Running the app in a dev environment
 
-You can launch the app on your own computer by running the following
-commands:
+First, install [leiningen](https://leiningen.org/). Then you can launch the app
+on your own computer by running the following command:
 
 ```
-$ lein uberjar
-$ java -cp target/pronouns-standalone.jar clojure.main -m pronouns.web
+$ lein ring server
 ```
 
-Then browse to localhost:5000
+This will launch a server running the app and open your default web browser to 
+the index page. The server will automatically reload files as you edit them -
+with the unfortunate exception of `pronouns.tab`, which is loaded as a resource
+and requires an app restart to reload.
 
-You can also just run quickstart.sh which will do all of these things for you. Do it like this to get an example running on port 666
+### The git repo
 
-```
-$ ./quickstart.sh 666
-```
+For most of this project's history we had separate `master` and `develop`
+branches but that's proven to be more trouble than it's worth. Going
+forward we'll be doing all development in feature branches off of `master`,
+and PRs should be issued against `master`.
+
+Please follow [this guide](https://chris.beams.io/posts/git-commit/) 
+for writing good commit messages :)
+
+## Philosophy on pronoun inclusion
+
+Pronoun.is aims foremost and exclusively to be a useful resource for people to communicate the personal pronoun they use for themselves.
+
+It is possible to use these example sentences to demonstrate the usage of words that are not personal pronouns, or even cleverly insert an [entire story](https://pronoun.is/she/or%20they,%20those%20ships%20who%20were%20docked%20and%20still%20equipped%20with%20ancillaries,%20arranged%20to%20share%20the%20duty%20of%20monitoring%20our%20guest%20as%20it%20fit%20into%20their%20routines;%20that%20was%20the%20agreement,%20despite%20it%20being%20less%20convenient%20for%20me%20to%20participate%20at%20all,%20on%20the%20grounds%20that%20certain%20visitors%20might%20prefer%20a%20constant%20individual%20companion%20to%20what%20might%20seem,%20depending%20on%20their%20past%20experiences,%20to%20be%20undue%20attention%20from%20every%20soldier%20they%20passed.%20As%20usual,%20then,%20I%20took%20the%20first%20shift/the%20one%20possession%20of%20hers%20that%20Station%20Security%20hadn't%20confiscated,%20a/knowingly%20left%20with%20her.%20What%20a%20Presger%20frisbee%20might%20do%20or%20even%20look%20like%20I%20couldn't%20say.%20She%20hadn't%20seemed%20the%20sort%20to%20have%20alien%20technology,%20but,%20then%20again,%20neither%20had%20I/another%20unremarkable%20stranger,%20quite%20a%20ways%20down%20the%20concourse,%20who%20caught%20it%20with%20a%20degree%20of%20coordination%20that%20most%20would%20have%20overlooked.%20It%20did%20not%20escape%20my%20notice,%20however.%20"Cousin,"%20I%20said,%20enough%20to%20convey%20-%20unless%20our%20visitor%20were%20quite%20ignorant,%20but,%20of%20course,%20at%20this%20point%20I%20was%20certain%20that%20she%20couldn't%20be%20-%20both%20that%20I%20knew%20what%20she%20was%20not%20and%20that%20I%20was%20giving%20her%20the%20benefit%20of%20the%20doubt%20as%20to%20what,%20or%20who,%20she%20was)! However, as a policy we will not include such entries in the database.
+
+
+## FAQ
+
+### Can we translate pronoun.is into another language?
+
+I'd love to see pronoun.is-like apps exist in the world for other languages.
+I'll answer any questions you need answered if you decide to fork the code to
+create a similar site for a different language, and I'll also entertain the
+idea of patching the pronoun.is codebase to support rending pages in different
+languages as long as it doesn't make the code substantially more complicated.
+
+Even if we add I18N support to the code, other language versions of the site
+should have their own domains names (ideally with clever TLD puns like 
+pronoun.is has for english!)
+
+(This was discussed in issues #13, #14, and #66)
+
+### Can we change the example sentences?
+
+Probably not, sorry, I really like these ones! Maybe in the future we can add
+multiple different example sentence sets, though.
+
+### Can we add pronunciation guides?
+
+The short answer is, this is very hard! I'd love to have them if someone can
+come up with a design for how to do this that'd work (including the logistics
+of hosting media files and stuff).
+
+(This was discussed in issues #43 and #80)
+
+### Can we add pluralization support (i.e. themself vs themselves)
+
+All of the ways I can think of to do this are pretty ugly (both visually
+and in the code) so I'm pretty reluctant to do it. That's not an absolute no,
+but any proposed solution would need to keep the pronouns database simple
+enough that non-programmers can edit it and keep the URLs short, pretty, and
+readable to humans as a sentence if you take all the punctuation out.
 
 ## License
-Copyright ยฉ 2014-2015 Morgan Astra <m@morganastra.me>
+Copyright ยฉ 2014-2018 Morgan Astra <m@morganastra.me>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <https://www.gnu.org/licenses/>
+
diff --git a/project.clj b/project.clj
index f2b2382..30a5011 100644
--- a/project.clj
+++ b/project.clj
@@ -1,20 +1,21 @@
-(defproject pronouns "1.0.0-SNAPSHOT"
-  :description "FIXME: write description"
-  :url "http://pronouns.herokuapp.com"
-  :license {:name "FIXME: choose"
-            :url "http://example.com/FIXME"}
-  :dependencies [[org.clojure/clojure "1.6.0"]
-                 [compojure "1.1.8"]
-                 [ring/ring-jetty-adapter "1.2.2"]
-                 [ring.middleware.logger "0.5.0"]
-                 [ring/ring-devel "1.2.2"]
-                 [ring-basic-authentication "1.0.5"]
-                 [environ "0.5.0"]
+(defproject witch-house/pronouns "1.12.0-SNAPSHOT"
+  :description "Pronoun.is is a website for personal pronoun usage examples"
+  :url "https://pronoun.is"
+  :license "GNU Affero General Public License 3.0"
+  :dependencies [[compojure "1.6.1"]
+                 [environ "1.1.0"]
                  [hiccup "1.0.5"]
-                 [com.cemerick/drawbridge "0.0.6"]
-                 [org.clojure/data.json "0.2.6"]]
+                 [lambdaisland/ring.middleware.logger "0.5.1"]
+                 [org.clojure/clojure "1.9.0"]
+                 [org.clojure/data.json "0.2.6"]
+                 [ring/ring-devel "1.7.1"]
+                 [ring/ring-jetty-adapter "1.7.1"]]
   :min-lein-version "2.0.0"
-  :plugins [[environ/environ.lein "0.2.1"]]
+  :plugins [[environ/environ.lein "0.2.1"]
+            [lein-ring "0.9.7"]]
   :hooks [environ.leiningen.hooks]
   :uberjar-name "pronouns-standalone.jar"
-  :profiles {:production {:env {:production true}}})
+  ;; FIXME morgan.astra <2018-11-14 Wed>
+  ;; Is this production profile used for anything?
+  :profiles {:production {:env {:production true}}}
+  :ring {:handler pronouns.web/app})
diff --git a/quickstart.sh b/quickstart.sh
deleted file mode 100755
index 8969778..0000000
--- a/quickstart.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/sh
-
-export PORT=6666
-lein uberjar
-java -cp target/pronouns-standalone.jar clojure.main -m pronouns.web
diff --git a/resources/pronouns.css b/resources/pronouns.css
index 1285cee..2d101e0 100644
--- a/resources/pronouns.css
+++ b/resources/pronouns.css
@@ -1,34 +1,30 @@
 body {
-	font-family: Futura, sans-serif;
-	background-color: #F6CEFC;
+    font-family: Futura, sans-serif;
+    background-color: #F6CEFC;
+}
+
+footer {
+    font-size: 0.8em;
+}
+
+.section {
+    margin: 8px;
+    padding: 4px 6px 4px 6px;
+    border: 4px solid #EEEEEE;
+    border-radius: 7px;
 }
 
 .examples {
-	margin: 8px;
-	padding: 4px 6px 4px 6px;
-	border: 4px solid #eeeeee;
 }
 
 .title {
-	margin: 8px;
-	padding: 4px 6px 4px 6px;
-	border: 4px solid #eeeeee;
 }
 
-.about {
-	margin: 8px;
-	padding: 4px 6px 4px 6px;
-	border: 4px solid #eeeeee;
+.usage {
 }
 
 .contact {
-	margin: 8px;
-	padding: 4px 6px 4px 6px;
-	border: 4px solid #eeeeee;
 }
 
 .table {
-	margin: 8px;
-	padding: 4px 6px 4px 6px;
-	border: 4px solid #eeeeee;
 }
diff --git a/resources/pronouns.tab b/resources/pronouns.tab
index e19c6e7..7f788b2 100644
--- a/resources/pronouns.tab
+++ b/resources/pronouns.tab
@@ -1,26 +1,40 @@
-ze	hir	hir	hirs	hirself
-ze	zir	zir	zirs	zirself
 she	her	her	hers	herself
 he	him	his	his	himself
 they	them	their	theirs	themselves
+ze	hir	hir	hirs	hirself
+ze	zir	zir	zirs	zirself
 xey	xem	xyr	xyrs	xemself
-sie	hir	hir	hirs	hirself
-it	it	its	its	itself
-ey	em	eir	eirs	eirself
+ae	aer	aer	aers	aerself
 e	em	eir	eirs	emself
+ey	em	eir	eirs	eirself
+fae	faer	faer	faers	faerself
+fey	fem	feir	feirs	feirself
 hu	hum	hus	hus	humself
+it	it	its	its	itself
+jee	jem	jeir	jeirs	jemself
+kit	kit	kits	kits	kitself
+ne	nem	nir	nirs	nemself
 peh	pehm	peh's	peh's	pehself
 per	per	per	pers	perself
+sie	hir	hir	hirs	hirself
+se	sim	ser	sers	serself
+shi	hir	hir	hirs	hirself
+si	hyr	hyr	hyrs	hyrself
+they	them	their	theirs	themself
 thon	thon	thons	thons	thonself
-jee	jem	jeir	jeirs	jemself
 ve	ver	vis	vis	verself
+ve	vem	vir	virs	vemself
+vi	ver	ver	vers	verself
+vi	vim	vir	virs	vimself
+vi	vim	vim	vims	vimself
+xie	xer	xer	xers	xerself
 xe	xem	xyr	xyrs	xemself
-zie	zir	zir	zirs	zirself
+xey	xem	xeir	xeirs	xemself
+yo	yo	yos	yos	yosself
 ze	zem	zes	zes	zirself
-zie	zem	zes	zes	zirself
 ze	mer	zer	zers	zemself
-se	sim	ser	sers	serself
-zme	zmyr	zmyr	zmyrs	zmyrself
-ve	vem	vir	virs	vemself
 zee	zed	zeta	zetas	zedself
-fae	faer	faer	faers	faerself
+zie	zir	zir	zirs	zirself
+zie	zem	zes	zes	zirself
+zie	hir	hir	hirs	hirself
+zme	zmyr	zmyr	zmyrs	zmyrself
diff --git a/src/pronouns/config.clj b/src/pronouns/config.clj
new file mode 100644
index 0000000..e35ac7e
--- /dev/null
+++ b/src/pronouns/config.clj
@@ -0,0 +1,29 @@
+;; pronoun.is - a website for pronoun usage examples
+;; Copyright (C) 2014 - 2018 Morgan Astra
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>
+
+(ns pronouns.config
+  (:require [pronouns.util :as u]))
+
+(def pronouns-table
+  (atom (u/slurp-tabfile "resources/pronouns.tab")))
+
+(defn replace-value [old new] new)
+
+(defn reload-table! []
+  (swap! pronouns-table
+         replace-value
+         (u/slurp-tabfile "resources/pronouns.tab")))
+
diff --git a/src/pronouns/pages.clj b/src/pronouns/pages.clj
index 8aed689..e629329 100644
--- a/src/pronouns/pages.clj
+++ b/src/pronouns/pages.clj
@@ -1,10 +1,42 @@
+;; pronoun.is - a website for pronoun usage examples
+;; Copyright (C) 2014 - 2018 Morgan Astra
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>
+
 (ns pronouns.pages
-  (:require [clojure.string :as s]
-            [clojure.data.json :as json]
+  (:require [clojure.data.json :as json]
+            [clojure.string :as s]
+            [pronouns.config :refer [pronouns-table]]
             [pronouns.util :as u]
             [hiccup.core :refer :all]
+            [hiccup.element :as e]
             [hiccup.util :refer [escape-html]]))
 
+(defn prose-comma-list
+  [items]
+  (let [c (count items)]
+    (cond
+      (<= c 1) (or (first items) "")
+      (= c 2) (s/join " and " items)
+      :else (str (s/join ", " (butlast items)) ", and " (last items)))))
+
+(defn href
+  [url text]
+  [:a {:href url} text])
+
+;; FIXME morgan.astra <2018-11-14 Wed>
+;; use a div for this instead of a plain bold tag
 (defn wrap-pronoun
   [pronoun]
   [:b pronoun])
@@ -29,7 +61,9 @@
 
 (defn possessive-pronoun-example
   [possessive-pronoun]
-  (render-sentence "At least I think it was " (wrap-pronoun possessive-pronoun) "."))
+  (render-sentence "At least I think it was "
+                   (wrap-pronoun possessive-pronoun)
+                   "."))
 
 (defn reflexive-example
   [subject reflexive]
@@ -38,73 +72,92 @@
                    (wrap-pronoun reflexive)
                    "."))
 
-(defn title-block [title]
-  [:div {:class "title"}
-   [:h1 title]])
+(defn header-block [header]
+  [:div {:class "section title"}
+   (href "/" [:h1 header])])
 
 (defn examples-block
   [subject object possessive-determiner possessive-pronoun reflexive]
-  [:div {:class "examples"}
-   [:p [:h2 "Here are some usage examples for my pronouns:"]]
-   (subject-example subject)
-   (object-example object)
-   (posessive-determiner-example subject possessive-determiner)
-   (possessive-pronoun-example possessive-pronoun)
-   (reflexive-example subject reflexive)])
-
-(defn about-block []
-  [:div {:class "about"}
+  (let [sub-obj (s/join "/" [subject object])
+        header-str (str "Here are some example sentences using my "
+                        sub-obj
+                        " pronouns:")]
+    [:div {:class "section examples"}
+     [:h2 header-str]
+     [:p (subject-example subject)
+         (object-example object)
+         (posessive-determiner-example subject possessive-determiner)
+         (possessive-pronoun-example possessive-pronoun)
+         (reflexive-example subject reflexive)]]))
+
+(defn usage-block []
+  [:div {:class "section usage"}
    [:p "Full usage: "
-    [:tt "http://pronoun.is/subject-pronoun/object-pronoun/possessive-determiner/possessive-pronoun/reflexive"]
-    " displays examples of your pronouns."]
-   [:p "This is a bit unwieldy. If we have a good guess we'll let you use just the first one or two."]])
+    ;; FIXME morgan.astra <2018-11-14 Wed>
+    ;; This looks really ugly in the browser
+       [:tt "https://pronoun.is/subject-pronoun/object-pronoun/possessive-determiner/possessive-pronoun/reflexive"]
+       " displays examples of your pronouns."]
+   [:p "This is a bit unwieldy. If we have a good guess we'll let you use"
+       " just the first one or two."]])
 
 (defn contact-block []
-  (let [twitter-name (fn [handle] [:a {:href (str "https://www.twitter.com/" handle)} (str "@" handle)])]
-  [:div {:class "contact"}
-   [:p
-    "Written by "
-    (twitter-name "morganastra")
-    ", whose "
-    [:a {:href "http://pronoun.is/ze/zir"} "pronoun.is/ze/zir"]
-    ". "
-   "Visit the project on " [:a {:href "https://github.com/witch-house/pronoun.is"} "github!"]]]))
-
+  (let [twitter-name (fn [handle] (href (str "https://www.twitter.com/" handle)
+                                       (str "@" handle)))]
+    [:div {:class "section contact"}
+     [:p "Written by "
+         (twitter-name "morganastra")
+         ", whose "
+         (href "https://pronoun.is/she" "pronoun.is/she")]
+     [:p "pronoun.is is free software under the "
+         (href "https://www.gnu.org/licenses/agpl.html" "AGPLv3")
+         "! visit the project on "
+         (href "https://github.com/witch-house/pronoun.is" "github")]
+     [:p "<3"]]))
+
+(defn footer-block []
+  [:footer (usage-block) (contact-block)])
 
 (defn format-pronoun-examples
-  [subject object possessive-determiner possessive-pronoun reflexive]
-  (let [title "Pronoun Island: English Language Examples"]
-  (html
-   [:html
-    [:head
-     [:title title]
-     [:meta {:name "viewport" :content "width=device-width"}]
-     [:link {:rel "stylesheet" :href "/pronouns.css"}]]
-    [:body
-     (title-block title)
-     (examples-block subject object possessive-determiner possessive-pronoun reflexive)
-     (about-block)
-     (contact-block)]])))
-
-
-(defn format-pronoun-json [& pronouns]
-  (json/write-str pronouns))
-
+  [pronoun-declensions]
+  (let [sub-objs (map #(s/join "/" (take 2 %)) pronoun-declensions)
+        title (str "Pronoun Island: " (prose-comma-list sub-objs) " examples")
+        examples (map #(apply examples-block %) pronoun-declensions)]
+    (html
+     [:html
+      [:head
+       [:title title]
+       [:meta {:name "viewport" :content "width=device-width"}]
+       [:meta {:name "description" :content (u/strip-markup examples)}]
+       [:meta {:name "twitter:card" :content "summary"}]
+       [:meta {:name "twitter:title" :content title}]
+       [:meta {:name "twitter:description" :content (u/strip-markup examples)}]
+       [:link {:rel "stylesheet" :href "/pronouns.css"}]]
+      [:body
+       (header-block title)
+       examples
+       (footer-block)]])))
 
-(defn parse-pronouns-with-lookup [pronouns-string pronouns-table]
+(defn table-lookup* [pronouns-string]
   (let [inputs (s/split pronouns-string #"/")
         n (count inputs)]
     (if (>= n 5)
       (take 5 inputs)
-      (u/table-lookup inputs pronouns-table))))
+      (u/table-lookup inputs @pronouns-table))))
+
+(defn lookup-pronouns
+  "Given a seq of pronoun sets, look up each set in the pronouns table"
+  [pronoun-sets]
+  (->> pronoun-sets
+       (map (comp table-lookup* escape-html))
+       (filter some?)))
 
 (defn make-link [path]
   (let [link (str "/" path)
         label path]
-    [:li [:a {:href link} label]]))
+    [:li (href link label)]))
 
-(defn front [pronouns-table]
-  (let [abbreviations (u/abbreviate pronouns-table)
+(defn front []
+  (let [abbreviations (take 6 (u/abbreviate @pronouns-table))
         links (map make-link abbreviations)
         title "Pronoun Island"]
     (html
@@ -114,29 +167,72 @@
        [:meta {:name "viewport" :content "width=device-width"}]
        [:link {:rel "stylesheet" :href "/pronouns.css"}]]
       [:body
-       (title-block title)
-       [:div {:class "table"}
-       [:p "pronoun.is is a www site for showing people how to use pronouns in English."]
-       [:p "here are some pronouns the site knows about:"]
-       [:ul links]]]
-      (contact-block)])))
-
-(defn not-found []
-  (str "We couldn't find those pronouns in our database, please ask us to "
-       "add them, or issue a pull request at "
-       "https://github.com/witch-house/pronoun.is/blob/master/resources/pronouns.tab"))
+       (header-block title)
+       [:div {:class "section table"}
+        [:p "pronoun.is is a website for personal pronoun usage examples"]
+        [:p "here are some pronouns the site knows about:"]
+        [:ul links]
+        [:p [:small (href "all-pronouns" "see all pronouns in the database")]]]]
+      (footer-block)])))
+
+(defn all-pronouns []
+  (let [abbreviations (u/abbreviate @pronouns-table)
+        links (map make-link abbreviations)
+        title "Pronoun Island"]
+    (html
+     [:html
+      [:head
+       [:title title]
+       [:meta {:name "viewport" :content "width=device-width"}]
+       [:link {:rel "stylesheet" :href "/pronouns.css"}]]
+      [:body
+       (header-block title)
+       [:div {:class "section table"}
+        [:p "All pronouns the site knows about:"]
+        [:ul links]]]
+      (footer-block)])))
+
+(defn not-found [path]
+  (let [title "Pronoun Island: English Language Examples"
+        or-re #"/[oO][rR]/"]
+    (html
+     [:html
+      [:head
+       [:title title]
+       [:meta {:name "viewport" :content "width=device-width"}]
+       [:link {:rel "stylesheet" :href "/pronouns.css"}]]
+      [:body
+       (header-block title)
+       [:div {:class "section examples"}
+        [:p [:h2 "We couldn't find those pronouns in our database :("]
+         "If you think we should have them, please reach out!"]
+        (when (re-find or-re path)
+          (let [alts (s/split path or-re)
+                new-path (str "/" (s/join "/:OR/" alts))]
+            [:div
+             "Did you mean: "
+             (href new-path
+                   (str "pronoun.is"
+                        new-path))]))]
+       (footer-block)]])))
+
+(defn pronouns [params format-fn not-found-fn]
+  (let [path (params :*)
+        param-alts (u/vec-coerce (or (params "or") []))
+        path-alts (s/split path #"/:[oO][rR]/")
+        pronouns (lookup-pronouns (concat path-alts param-alts))]
+    (if (seq pronouns)
+      (format-fn pronouns)
+      (not-found-fn path))))
+
+(defn format-pronoun-json [& pronouns]
+  (json/write-str pronouns))
 
 (defn not-found-json []
   (json/write-str {:error "Not found"}))
 
-(defn pronouns-page [path pronouns-table format-pronouns not-found]
-  (let [pronouns (parse-pronouns-with-lookup (escape-html path) pronouns-table)]
-    (if pronouns
-      (apply format-pronouns pronouns)
-      (not-found))))
-
-(defn pronouns [path pronouns-table]
-  (pronouns-page path pronouns-table format-pronoun-examples not-found))
-
 (defn pronouns-json [path pronouns-table]
-  (pronouns-page path pronouns-table format-pronoun-json not-found-json))
+  (pronouns params format-pronoun-json not-found-json))
+
+(defn pronouns-html [path pronouns-table]
+  (pronouns params format-pronoun-examples not-found))
diff --git a/src/pronouns/util.clj b/src/pronouns/util.clj
index 4d4f928..7469dcf 100644
--- a/src/pronouns/util.clj
+++ b/src/pronouns/util.clj
@@ -1,31 +1,109 @@
+;; pronoun.is - a website for pronoun usage examples
+;; Copyright (C) 2014 - 2018 Morgan Astra
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>
+
 (ns pronouns.util
   (:require [clojure.string :as s]))
 
-(defn slurp-tabfile [path]
+(defn slurp-tabfile
+  "Read a tabfile from a filesystem <path> as a table"
+  [path]
   (let [lines (s/split (slurp path) #"\n")]
     (map #(s/split % #"\t") lines)))
 
+(defn table-front-filter
+  "filter a <table> to the rows which begin with <query-key>"
+  [query-key table]
+  (let [arity (count query-key)]
+    (filter #(= query-key (take arity %)) table)))
+
+(defn table-end-filter
+  "filter a <table> to the rows which end with <query-key>"
+  [query-key table]
+  (let [table-arity (count (first table))
+        query-arity (count query-key)]
+    (filter #(= query-key (drop (- table-arity query-arity) %)) table)))
+
 (defn table-lookup
+  "find the row corresponding to <query-key> in <table>"
   [query-key table]
-  (let [arity (count query-key)
-        filtered-table (filter #(= query-key (take arity %)) table)]
-    (first filtered-table)))
-
-(defn tabfile-lookup
-  [query-key tabfile]
-  (table-lookup query-key (slurp-tabfile tabfile)))
-
-(defn minimum-unambiguous-path
-  ([pronouns-table sections] (minimum-unambiguous-path pronouns-table sections 1))
-  ([pronouns-table sections number-of-sections]
-    (let [sections-subset (take number-of-sections sections)
-          results (filter #(= (take number-of-sections %) sections-subset) pronouns-table)]
-      (case (count results)
-        0 nil
-        1 (clojure.string/join "/" sections-subset)
-        (recur pronouns-table sections (+ number-of-sections 1))))))
+  (if (some #(= "..." %) query-key)
+    (let [[query-front query-end-] (split-with #(not= "..." %) query-key)
+          query-end (drop 1 query-end-)
+          front-matches (table-front-filter query-front table)]
+      (first (table-end-filter query-end front-matches)))
+    (first (table-front-filter query-key table))))
+
+(defn shortest-unambiguous-forward-path
+  "Compute the shortest (in number of path elements) forward path which
+  unambiguously refers to a specific <row> in a <table>. The behavior of
+  this function is undefined if given a <row> that is not in the <table>.
+
+  See also: shortest-unambiguous-path"
+  [table row]
+  (loop [n 1]
+    (let [row-front (take n row)]
+      (if (>= 1 (count (table-front-filter row-front table)))
+        row-front
+        (recur (inc n))))))
+
+(defn shortest-unambiguous-ellipses-path
+  "Compute the shortest (in number of path elements) ellipses path which
+  unambiguously refers to a specific <row> in a <table>. The behavior of
+  this function is undefined if given a <row> that is not in the <table>.
+
+  See also: shortest-unambiguous-path"
+  [table row]
+  (let [row-end (last row)
+        filtered-table (table-end-filter [row-end] table)]
+    (loop [n 1]
+      (let [row-front (take n row)]
+        (if (>= 1 (count (table-front-filter row-front filtered-table)))
+          (concat row-front ["..." row-end])
+          (recur (inc n)))))))
+
+(defn shortest-unambiguous-path
+  "Compute the shortest (in number of path elements) path which unambiguously
+  refers to a specific <row> in a <table>. The behavior of this function is
+  undefined if given a <row> that is not in the <table>.
+
+  A path can either be a 'forward path', in which it specifies the row with
+  elements from the front (e.g. ze/zir), or an 'ellipses path', which elides
+  unnecessary elements from the middle (e.g. they/.../themselves). If the
+  shortest forward and ellipses paths are the same length, prefer the forward
+  path"
+  [table row]
+  (let [forward-path (shortest-unambiguous-forward-path table row)
+        ellipses-path (shortest-unambiguous-ellipses-path table row)]
+    (s/join "/"
+            (if (> (count forward-path) (count ellipses-path))
+              ellipses-path
+              forward-path))))
 
 (defn abbreviate
-  "given a list of pronoun rows, return a list of minimum unabiguous paths"
-  [pronouns-table]
-  (map (partial minimum-unambiguous-path pronouns-table) pronouns-table))
+  "return the list of minimum unabiguous paths from a <table>"
+  [table]
+  (map (partial shortest-unambiguous-path table) table))
+
+(defn vec-coerce
+  "wrap a value <x> in a vector if it is not already in one. note that if
+  <x> is already in a sequence for which vector? is false, this will add
+  another layer of nesting."
+  [x]
+  (if (vector? x) x [x]))
+
+(defn strip-markup [form]
+  (s/join " " (filter string? (flatten form))))
+
diff --git a/src/pronouns/web.clj b/src/pronouns/web.clj
index 5af441f..6321198 100644
--- a/src/pronouns/web.clj
+++ b/src/pronouns/web.clj
@@ -1,64 +1,91 @@
+;; pronoun.is - a website for pronoun usage examples
+;; Copyright (C) 2014 - 2018 Morgan Astra
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU Affero General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU Affero General Public License for more details.
+
+;; You should have received a copy of the GNU Affero General Public License
+;; along with this program.  If not, see <https://www.gnu.org/licenses/>
+
 (ns pronouns.web
   (:require [compojure.core :refer [defroutes GET PUT POST DELETE ANY]]
             [compojure.handler :refer [site]]
             [compojure.route :as route]
-            [clojure.java.io :as io]
             [clojure.string :as s]
+            [clojure.java.io :as io]
+            [ring.adapter.jetty :as jetty]
             [ring.middleware.logger :as logger]
             [ring.middleware.stacktrace :as trace]
-            [ring.middleware.session :as session]
-            [ring.middleware.session.cookie :as cookie]
-            [ring.adapter.jetty :as jetty]
+            [ring.middleware.params :as params]
+            [ring.middleware.resource :refer [wrap-resource]]
+            [ring.middleware.content-type :refer [wrap-content-type]]
+            [ring.middleware.not-modified :refer [wrap-not-modified]]
             [environ.core :refer [env]]
             [pronouns.util :as u]
             [pronouns.pages :as pages]))
 
-(def config {:default-server-port 5000
-             :pronoun-table-path "resources/pronouns.tab"})
-(def pronouns-table (u/slurp-tabfile (:pronoun-table-path config)))
-
 (defroutes app-routes
   (GET "/" []
        {:status 200
         :headers {"Content-Type" "text/html"}
-        :body (pages/front pronouns-table)})
+        :body (pages/front)})
+
+  (GET "/all-pronouns" []
+       {:status 200
+        :headers {"Content-Type" "text/html"}
+        :body (pages/all-pronouns)})
 
-  (GET "/pronouns.css" {params :params}
+  (GET "/pronouns.css" []
      {:status 200
      :headers {"Content-Type" "text/css"}
      :body (slurp (io/resource "pronouns.css"))})
 
   (GET "/*" {params :params headers :headers}
-       (if (= "application/json" (.toLowerCase (get headers "accept")))
+       (if (= "application/json" (s/lower-case (get headers "accept")))
          {:status 200
           :headers {"Content-Type" "application/json"}
-          :body (pages/pronouns-json (:* params) pronouns-table)}
+          :body (pages/pronouns-json params pronouns-table)}
          {:status 200
           :headers {"Content-Type" "text/html"}
-          :body (pages/pronouns (:* params) pronouns-table)}))
+          :body (pages/pronouns params pronouns-table)}))
 
   (ANY "*" []
        (route/not-found (slurp (io/resource "404.html")))))
 
+(defn wrap-gnu-natalie-nguyen [handler]
+  (fn [req]
+    (when-let [resp (handler req)]
+      (assoc-in resp [:headers "X-Clacks-Overhead"] "GNU Natalie Nguyen"))))
+
 (defn wrap-error-page [handler]
   (fn [req]
     (try (handler req)
          (catch Exception e
-           {:status 500
-            :headers {"Content-Type" "text/html"}
-            :body (slurp (io/resource "500.html"))}))))
+           (binding [*out* *err*]
+             {:status 500
+              :headers {"Content-Type" "text/html"}
+              :body (slurp (io/resource "500.html"))})))))
 
 (def app
   (-> app-routes
+      ;; FIXME morgan.astra <2018-11-14 Wed>
+      ;; use this resource or delete it
+      ;; (wrap-resource "images")
+      wrap-content-type
+      wrap-not-modified
       logger/wrap-with-logger
       wrap-error-page
-      trace/wrap-stacktrace))
+      wrap-gnu-natalie-nguyen
+      trace/wrap-stacktrace
+      params/wrap-params))
 
 (defn -main []
-  (let [port (Integer. (:port env
-                              (:default-server-port config)))]
+  (let [port (Integer. (:port env))]
     (jetty/run-jetty app {:port port})))
-
-;; For interactive development:
-;; (.stop server)
-;; (def server (-main))
diff --git a/test/pronouns/pages_test.clj b/test/pronouns/pages_test.clj
new file mode 100644
index 0000000..7563be1
--- /dev/null
+++ b/test/pronouns/pages_test.clj
@@ -0,0 +1,23 @@
+(ns pronouns.pages-test
+  (:require [pronouns.pages :as pages]
+            [clojure.test :refer [deftest testing is are]]))
+
+(deftest prose-comma-list
+  (testing "`prose-comma-list` turns a list of strings into a prose list"
+    (are [v s] (= (pages/prose-comma-list v) s)
+      ["foo" "bar" "baz" "bobble"] "foo, bar, baz, and bobble"
+      ["foo" "bar" "baz"]          "foo, bar, and baz"
+      ["foo" "bar"]                "foo and bar"
+      ["foo"]                      "foo"
+      []                           "")))
+
+(deftest lookup-pronouns
+  (are [pronoun-strs pronouns]
+      (= (pages/lookup-pronouns pronoun-strs)
+         pronouns)
+    ["she/her"]           '(["she" "her" "her" "hers" "herself"])
+    ["she" "they"]        '(["she" "her" "her" "hers" "herself"]
+                            ["they" "them" "their" "theirs" "themselves"])
+    ["she/her" "foo/bar"] '(["she" "her" "her" "hers" "herself"])
+    ["foo/bar"]           '()
+    ["a/b/c/d/e"]         '(("a" "b" "c" "d" "e"))))
diff --git a/test/pronouns/resource_test.clj b/test/pronouns/resource_test.clj
new file mode 100644
index 0000000..38ee4e3
--- /dev/null
+++ b/test/pronouns/resource_test.clj
@@ -0,0 +1,12 @@
+(ns pronouns.resource-test
+  (:require [pronouns.util :as util]
+            [clojure.test :refer [deftest testing is]]))
+
+(deftest valid-pronouns-table
+  (let [table (util/slurp-tabfile "resources/pronouns.tab")]
+    (is table "pronouns.tab exists and is non-empty")
+    (doseq [row table]
+      (is (= (count row) 5)
+          "row has five elements")
+      (is (re-matches #".*sel(f|ves)$" (last row))
+          "final element is reflexive"))))
diff --git a/test/pronouns/util_test.clj b/test/pronouns/util_test.clj
new file mode 100644
index 0000000..3aecc2a
--- /dev/null
+++ b/test/pronouns/util_test.clj
@@ -0,0 +1,32 @@
+(ns pronouns.util-test
+  (:require [pronouns.util :as util]
+            [clojure.test :refer [deftest testing is are]]))
+
+(def test-table [["ze" "hir" "hir" "hirs" "hirself"]
+                 ["ze" "zir" "zir" "zirs" "zirself"]
+                 ["she" "her" "her" "hers" "herself"]
+                 ["he" "him" "his" "his" "himself"]
+                 ["they" "them" "their" "theirs" "themselves"]
+                 ["they" "them" "their" "theirs" "themself"]])
+
+(deftest table-filters
+  (testing "table-front-filter"
+    (are [arg return] (= (util/table-front-filter arg test-table) return)
+      ["she"] [["she" "her" "her" "hers" "herself"]]
+      ["ze"] [["ze" "hir" "hir" "hirs" "hirself"]
+              ["ze" "zir" "zir" "zirs" "zirself"]]
+      ["ze" "zir"] [["ze" "zir" "zir" "zirs" "zirself"]]))
+
+  (testing "table-end-filter"
+    (are [arg return] (= (util/table-end-filter arg test-table) return)
+      ["themself"] [["they" "them" "their" "theirs" "themself"]]
+      ["themselves"] [["they" "them" "their" "theirs" "themselves"]])))
+
+(deftest table-lookup
+  (are [arg return] (= (util/table-lookup arg test-table) return)
+    ["she"] ["she" "her" "her" "hers" "herself"]
+    ["ze"] ["ze" "hir" "hir" "hirs" "hirself"]
+    ["ze" "zir"] ["ze" "zir" "zir" "zirs" "zirself"]
+    ["they"] ["they" "them" "their" "theirs" "themselves"]
+    ["they" "..." "themself"] ["they" "them" "their" "theirs" "themself"]))
+