diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
similarity index 100%
rename from CODE_OF_CONDUCT.md
rename to .github/CODE_OF_CONDUCT.md
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..84b5d06
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,2 @@
+github: katacarbix
+ko_fi: reeseovine
diff --git a/LICENSE b/.github/LICENSE
similarity index 100%
rename from LICENSE
rename to .github/LICENSE
diff --git a/README.md b/README.md
index 4fea64d..6cbba9e 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ personal pronoun helper module. fork of [pronoun.is](https://github.com/witch-ho
`pronouns.js` aims to be an easy way to handle numerous personal pronouns in English. It remains open-ended to allow for myriad use cases. One possible application could be in a social media service in which users can add their pronoun(s) and the interface can refer to them properly.
-Play the demo [here](https://katacarbix.github.io/pronouns.js/demo/index.html).
+Check out the demo [here](https://katacarbix.github.io/pronouns.js/demo/index.html).
## Basic usage
@@ -44,15 +44,10 @@ they | them | their | theirs | themself
**`index.js`** is the main program. `pronouns` is both a function you can call with a string parameter, and an object with references to the table and a few methods.
-All of the following are valid inputs to the `pronouns` function:
-
-* she
-* he/him
-* ze/hir or they/.../themselves
-* she/her, they/them, it/its, or sie/hir
-
**`util.js`** has most of what was translated from the original Clojure code. These functions are mostly meant for accessing rows in a table or formatting strings and is not needed for typical users, but is exposed nonetheless as `pronouns.util`.
+For further documentation, check out [the wiki](https://github.com/katacarbix/pronouns.js/wiki).
+
## Tests
The `test/` directory contains unit tests for `index.js` and `util.js`. 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!
@@ -60,13 +55,3 @@ The `test/` directory contains unit tests for `index.js` and `util.js`. Please r
## Contributing
Issues and pull/merge requests regarding the code are very much welcome! If you would like to request pronouns to be added to the table there is an issue template for doing so. You should also consider doing the same with the original repository.
-
-## To do
-
-* ~~Autocomplete!~~
-* ~~Interactive demos and examples~~
-* Parse even shorter multi-pronoun inputs like "they/she" or "she/him"
-* Handle "any/all", unknown, or badly formatted inputs better
- * Should default to they/them
-* Allow modifying an existing object (how would that work?)
-* More comprehensive documentation
diff --git a/demo/index.html b/demo/index.html
index be4f8a8..ebd8401 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -2,23 +2,32 @@
pronouns.js demo
+
+
-
-
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
@@ -34,7 +43,7 @@ pronouns.js
@@ -42,17 +51,8 @@ pronouns.js
-
-
- Subject
- Object
- Determiner
- Possessive
- Reflexive
-
-
-
-
+
+
@@ -72,7 +72,7 @@ pronouns.js
@@ -81,6 +81,7 @@ pronouns.js
$('#pronouns').autoComplete({
minLength: 0,
+ noResultsText: '',
preventEnter: true,
resolver: 'custom',
events: {
@@ -92,28 +93,44 @@ pronouns.js
});
$('#pronouns').on('input autocomplete.select', function(){
- var p = pronouns(this.value);
+ var p = pronouns(this.value, {log:false});
var rows = ``;
var examples = ``;
+ var url = p.toUrl();
+
+ if (p.pronouns.length > 0) $('thead').html(`
+
+ Subject
+ Object
+ Determiner
+ Possessive
+ Reflexive
+ `);
+ else $('thead').html(``);
+
+ if (p.any) examples += `
+
+ Any pronouns are acceptable.
+ `;
for (var i = 0, row; row = p.pronouns[i]; i++){
- if (!pronouns.util.rowsEqual(row,['','','','',''])){
- rows += `
-
- ${row[0]}
- ${row[1]}
- ${row[2]}
- ${row[3]}
- ${row[4]}
- `;
- if (!row.includes('')) examples += `
-
- ${p.examples[i].join(' ').replace(/\*(\w+)\*/g, "$1 ")}
- `;
- }
+ rows += `
+
+ ${row[0]}
+ ${row[1]}
+ ${row[2]}
+ ${row[3]}
+ ${row[4]}
+ `;
+ examples += `
+
+ ${p.examples_html[i].join(' ')}
+ `;
}
+
$('tbody').html(rows);
$('#examples').html(examples);
- $('#pronoun-island-link').html(`${p.toUrl()} `);
+ if (url.length > 19) $('#pronoun-island-link').html(`${url} `);
+ else $('#pronoun-island-link').html(``);
});
diff --git a/demo/thumbnail.png b/demo/thumbnail.png
new file mode 100644
index 0000000..af05a31
Binary files /dev/null and b/demo/thumbnail.png differ
diff --git a/dist/index.js b/dist/index.js
index ad559ef..0b996a1 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -44,12 +44,13 @@ module.exports=[
},{}],2:[function(require,module,exports){
module.exports = {
- // logging turned off by default.
- logging: false,
+ // options object overwritten by index.js.
+ opts: {
+ log: false
+ },
// filter table to the rows which begin with q
tableFrontFilter: function(q, table){
- var qlen = q.length;
return table.filter(row => this.rowsEqual(q, row) );
},
@@ -64,16 +65,32 @@ module.exports = {
return true;
});
},
+
+ // filter table using the walkRow function
+ tableWalkFilter: function(q, table){
+ return table.filter(row => this.walkRow(q, row, 0,0));
+ },
+
+ // match query to row by walking each element along it
+ walkRow: function(q, row, q_i, row_i){
+ if (q[q_i] != row[row_i]){
+ if (row_i >= row.length-(q.length-q_i)) return false;
+ return this.walkRow(q, row, q_i, row_i+1);
+ }
+ if (q_i < q.length-1) return this.walkRow(q, row, q_i+1, row_i+1);
+ return true;
+ },
// find the row corresponding to q in table
tableLookup: function(q, table){
+ if (q.length < 1 || q.length > table[0].length) return;
if (q.includes('...')){
var queryFront = q.slice(0, q.indexOf('...'));
var queryEnd = q.slice(q.indexOf('...')+1);
var frontMatches = this.tableFrontFilter(queryFront, table);
return this.tableEndFilter(queryEnd, frontMatches)[0];
}
- return this.tableFrontFilter(q, table)[0];
+ return this.tableWalkFilter(q, table)[0];
},
// Compute the shortest (in number of path elements) forward path which
@@ -125,23 +142,60 @@ module.exports = {
},
sanitizeSet: function(p, table){
- return p.map(row => {
+ var out = [];
+ for (var row of p){
+ if (row.length < 1) continue;
+ if (row.length == 1 && row[0].match(/\b(or|and)\b/)) continue;
+
var match = this.tableLookup(row, table);
- if (!match){
- if (this.logging) console.warn(`Unrecognized pronoun "${row.join('/')}". This may lead to unexpected behavior.`);
- while (row.length < 5){
- row.push('');
+ if (match){
+ out.push(match);
+ continue;
+ }
+
+ var expansions = [];
+ var badMatch = false;
+ for (var part of row){
+ var match = this.tableLookup([part], table);
+ if (part.match(/(\b(any(thing)?|all)\b|\*)/)){
+ if (this.opts.log) console.log(`Wildcard detected.`);
+ continue;
}
+ if (!match){
+ badMatch = true;
+ break;
+ }
+ expansions.push(match);
+ }
+ if (!badMatch){
+ out = out.concat(expansions);
+ continue;
+ }
+
+ if (row.some(p => p.match(/(\b(any(thing)?|all)\b|\*)/))){
+ if (this.opts.log) console.log(`Wildcard detected.`);
+ continue;
+ }
+
+ if (this.opts.log) console.warn(`Unrecognized pronoun(s) "${row.join('/')}". This may lead to unexpected behavior.`);
+ if (row.length >= 5){
if (row.length > 5){
row = row.slice(0,5);
}
- return row;
- } else return match;
+ if (!row.includes('')) out.push(row);
+ }
+ }
+ out = out.filter((row,i) => {
+ for (var p of out.slice(0,i)){
+ if (this.rowsEqual(p,row)) return false;
+ }
+ return true;
});
+ return out;
},
expandString: function(str, table){
- return this.sanitizeSet(str.split(' ').filter(p => !p.match(/[Oo][Rr]/g)).map(p => p.replace(/[^a-zA-Z\/'.]/, '').toLowerCase().split('/')), table);
+ return this.sanitizeSet(str.trim().split(' ').map(p => p.replace(/[^a-zA-Z\/'.]/, '').toLowerCase().split('/')), table);
},
// wrap a value in an array if it is not already in one.
@@ -152,7 +206,7 @@ module.exports = {
// capitalize first letter of a given string
capitalize: function(str){
- return str.replace(/[a-zA-Z]/, m => m.toUpperCase());
+ return str.replace(/[a-zA-Z]/, l => l.toUpperCase());
},
// check if two arrays are similar. will permit array b to be longer by design.
@@ -167,7 +221,10 @@ module.exports = {
},{}],3:[function(require,module,exports){
const util = require('./util');
const table = require('../resources/pronouns.json');
-var logging = false;
+
+var opts = {
+ log: false
+};
class Pronouns {
constructor(input){
@@ -177,25 +234,24 @@ class Pronouns {
}
_process(input){
- if (typeof input === "string") return util.expandString(input, table); // passed a string, most common case.
- else if (typeof input === "object"){
- if (input.pronouns && Array.isArray(input.pronouns)) return util.sanitizeSet(input.pronouns, table); // passed a pronouns-like object.
- else if (Array.isArray(input)) return util.sanitizeSet(input, table); // passed an array representing some pronouns.
- } else {
- if (logging) console.warn("Unrecognized input. Defaulting to they/them.");
- return util.tableLookup("they", table);
+ if (typeof input == "string"){
+ if (!this.hasOwnProperty('any') || !this.any) this.any = !!input.match(/(\b(any(thing)?|all)\b|\*)/);
+ return util.expandString(input, table); // passed a string, most common case.
}
+ if (opts.log) console.warn("Unrecognized input. Defaulting to they/them.");
+ return util.tableLookup(['they'], table);
}
generateForms(i){
- i = Number.isInteger(i) ? i : 0;
+ i = Number.isInteger(parseInt(i)) ? parseInt(i) : 0;
+ var p = (this.pronouns && this.pronouns.length > 0) ? this.pronouns[i] : util.tableLookup(['they'], table);
// the 5 main pronoun types
- this.subject = this.pronouns[i][0];
- this.object = this.pronouns[i][1];
- this.determiner = this.pronouns[i][2];
- this.possessive = this.pronouns[i][3];
- this.reflexive = this.pronouns[i][4];
+ this.subject = p[0];
+ this.object = p[1];
+ this.determiner = p[2];
+ this.possessive = p[3];
+ this.reflexive = p[4];
// aliases
this.sub = this.subject;
@@ -207,19 +263,38 @@ class Pronouns {
generateExamples(){
this.examples = [];
- for (var i = 0, p; p = this.pronouns[i]; i++){
+ this.examples_html = [];
+ this.examples_md = [];
+ var i = 0;
+ var p = (i < this.pronouns.length) ? this.pronouns[i] : util.tableLookup(['they'], table);
+ do {
this.examples.push([
- util.capitalize(`*${p[0]}* went to the park.`),
- util.capitalize(`I went with *${p[1]}*.`),
- util.capitalize(`*${p[0]}* brought *${p[2]}* frisbee.`),
- util.capitalize(`At least I think it was *${p[3]}*.`),
- util.capitalize(`*${p[0]}* threw the frisbee to *${p[4]}*.`)
+ util.capitalize(`${p[0]} went to the park.`),
+ util.capitalize(`I went with ${p[1]}.`),
+ util.capitalize(`${p[0]} brought ${p[2]} frisbee.`),
+ util.capitalize(`At least I think it was ${p[3]}.`),
+ util.capitalize(`${p[0]} threw the frisbee to ${p[4]}.`)
]);
- }
+ this.examples_html.push([
+ `${util.capitalize(p[0])} went to the park.`,
+ util.capitalize(`I went with ${p[1]} .`),
+ `${util.capitalize(p[0])} brought ${p[2]} frisbee.`,
+ util.capitalize(`At least I think it was ${p[3]} .`),
+ `${util.capitalize(p[0])} threw the frisbee to ${p[4]} .`
+ ]);
+ this.examples_md.push([
+ util.capitalize(`**${p[0]}** went to the park.`),
+ util.capitalize(`I went with **${p[1]}**.`),
+ util.capitalize(`**${p[0]}** brought **${p[2]}** frisbee.`),
+ util.capitalize(`At least I think it was **${p[3]}**.`),
+ util.capitalize(`**${p[0]}** threw the frisbee to **${p[4]}**.`)
+ ]);
+ i++;
+ } while (p = this.pronouns[i]);
}
toString(){
- return this.pronouns.map(p => util.shortestUnambiguousPath(table, p).join('/')).join(' or ');
+ return this.pronouns.map(p => util.shortestUnambiguousPath(table, p).join('/')).concat(this.any ? [['any']] : []).join(' or ');
}
toUrl(){
@@ -228,35 +303,52 @@ class Pronouns {
add(input){
var newRows = this._process(input);
- for (var i = 0, p; p = newRows[i]; i++){
- if (!this.pronouns.includes(p)){
- this.pronouns.push(p);
+ this.pronouns = this.pronouns.concat(newRows.filter(p1 => {
+ for (var p2 of this.pronouns){
+ if (util.rowsEqual(p2,p1)) return false;
}
- }
+ return true;
+ }));
this.generateExamples();
}
}
-module.exports = (input, log) => {
- logging = !!(log); // convert it to a boolean value
- util.logging = logging;
- return new Pronouns(input);
+module.exports = (input, options) => {
+ var p;
+
+ if (typeof input == "string") p = new Pronouns(input);
+ else if (typeof input == "object" && !!options) options = input;
+
+ if (typeof options == "object") opts = {...opts, ...options};
+ util.options = opts;
+
+ return p;
}
module.exports.complete = (input) => {
- var rest = input.substring(0, input.lastIndexOf(' ') + 1).replace(/\s+/g, ' ');
- var last = input.substring(input.lastIndexOf(' ') + 1, input.length);
+ var sepIndex = input.lastIndexOf(' ') + 1;
+ var rest = input.substring(0, sepIndex).replace(/\s+/g, ' ');
+ var last = input.substring(sepIndex, input.length);
// Generate list of matching rows
var matches = [];
if (last.length == 0){
- matches = table.slice(); // Clones the table so it doesn't get changed
- if (!rest.match(/[Oo][Rr]\s$/g)) matches.unshift(['or']);
+ matches = [...table]; // Clone the table so it doesn't get changed
} else {
var parts = last.split('/');
var end = parts.pop();
- matches = util.tableFrontFilter(parts, table);
- matches = matches.filter(row => row[parts.length].substring(0, end.length) === end);
- if (last.match(/^[Oo][Rr]?$/g)) matches.unshift(['or']);
+ console.dir(parts);
+ console.log(end);
+ matches = util.tableWalkFilter(parts, table).filter(row => {
+ for (var f of row.slice(parts.length, row.length)){
+ if (end.length <= f.length && f.substring(0,end.length) === end) return true;
+ }
+ return false;
+ });
+ if (matches.length == 0){
+ rest += parts.slice(0,parts.length-1).join('/')+'/';
+ matches = table.filter(row => row.length == 1);
+ }
+ console.dir(matches);
}
// Filter matches to those which are not already in rest of input
@@ -268,7 +360,7 @@ module.exports.complete = (input) => {
return true;
});
- if (logging && matches.length == 0) console.log(`No matches for ${input} found.`);
+ if (opts.log && matches.length == 0) console.log(`No matches for ${input} found.`);
return matches.map(row => rest + util.shortestUnambiguousPath(table, row).join('/'));
}
diff --git a/docs/completion.md b/docs/completion.md
deleted file mode 100644
index cc1b6af..0000000
--- a/docs/completion.md
+++ /dev/null
@@ -1,49 +0,0 @@
-Pronouns.js provides automatic suggestions for partial inputs using the `pronouns.complete` function.
-
-A [demo](https://katacarbix.github.io/pronouns.js/demo/index.html) is available which shows how this feature could be used.
-
-
-**example input:** `she/her or th`
-**example results:**
-```
-she/her or they/.../themself
-she/her or they/.../themselves
-she/her or thon
-```
-
-**example input:** `fae ` (with trailing space)
-**example results:**
-```
-fae or
-fae she
-fae he
-fae they/.../themself
-fae ze/hir
-...
-```
-
-**example input:** `ze`
-**example results:**
-```
-ze/hir
-ze/zir
-ze/zem
-ze/mer
-zee
-```
-
-**example input:** `ze/`
-**example results:**
-```
-ze/hir
-ze/zir
-ze/zem
-ze/mer
-```
-
-**example input:** `ze/z`
-**example results:**
-```
-ze/zir
-ze/zem
-```
diff --git a/docs/documentation.md b/docs/documentation.md
deleted file mode 100644
index a5ca77e..0000000
--- a/docs/documentation.md
+++ /dev/null
@@ -1,87 +0,0 @@
-**`pronouns` is a function that has its own properties as well.**
-
-Calling `pronouns('he')`, for example, will return a `Pronouns` class instance that looks similar to the following object:
-
-```js
-{
- pronouns: [ [ 'he', 'him', 'his', 'his', 'himself' ] ],
- sub: 'he',
- subject: 'he',
- obj: 'him',
- object: 'him',
- det: 'his',
- determiner: 'his',
- pos: 'his',
- possessive: 'his',
- ref: 'himself',
- reflexive: 'himself',
- examples: [
- [
- 'He went to the park.',
- 'I went with him.',
- 'He brought his frisbee.',
- 'At least I think it was his.',
- 'He threw the frisbee to himself.'
- ]
- ],
- toString: [Function: () => String],
- toUrl: [Function: () => String],
- add: [Function: (String) => None]
-}
-```
-
-Let's go through each one...
-
-### `pronouns`
-
-An array containing arrays of length 5. There is one row for each "or"-separated pronoun given in the input string.
-
-### `subject`, `object`, `determiner`, `possessive`, and `reflexive`
-
-There are 5 forms for each pronoun in a row:
-
-subject|object|possessive determiner|possessive pronoun|reflexive
--------|------|---------------------|------------------|---------
-he | him | his | his | himself
-
-This wiki is not an English lesson, so please look these up if you are confused. The three letter abbreviations are aliases so you can keep your code a bit more concise.
-
-### `examples`
-
-This is an array of examples of each pronoun form. Sample sentences taken from the [original code](https://github.com/witch-house/pronoun.is/blob/master/src/pronouns/pages.clj#L46), but I am open to using different ones if anyone has suggestions.
-
-### `toString()`
-
-Returns a human-readable string that can also be used as an input to get an identical object back.
-
-### `toUrl()`
-
-Returns a link to the [pronoun.is/](https://pronoun.is/) page corresponding to the object. These are formatted slightly differently than `toString()` to account for the limitations of URL paths.
-
-### `add(String input)`
-
-You can add more pronouns to an existing object by calling `add` with any string that would also work with the constructor. This function will then add the pronoun(s) to the end of the `.pronouns` array, omitting any duplicates, and generate examples.
-
----
-
-Lastly, `pronouns` has some properties that can be accessed without calling any functions.
-
-### `pronouns.table`
-
-This is the complete table in JSON format. It's the same one located in [resources/pronouns.json](resources/pronouns.json).
-
-### `pronouns.abbreviated`
-
-This is an abbreviated table containing the shortest unambiguous path to get to each row.
-
-### `pronouns.util`
-
-The `util` object is exposed in case you want to use it, though most users will not need to. Most of its methods are for accessing and formatting rows from a table. Its source is located in [src/util.js](src/util.js).
-
-### `pronouns.Pronouns`
-
-This is the `Pronouns` class, of which instances are returned by the main function. You probably won't need to access it directly but it's there in case the main function just isn't cutting it for you.
-
-### `pronouns.complete(String)`
-
-See [Completion](https://github.com/katacarbix/pronouns.js/wiki/Completion).
diff --git a/src/index.js b/src/index.js
index e9654a4..979280b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,10 @@
const util = require('./util');
const table = require('../resources/pronouns.json');
-var logging = false;
+
+var opts = {
+ log: false,
+ default: "they/them"
+};
class Pronouns {
constructor(input){
@@ -10,25 +14,24 @@ class Pronouns {
}
_process(input){
- if (typeof input === "string") return util.expandString(input, table); // passed a string, most common case.
- else if (typeof input === "object"){
- if (input.pronouns && Array.isArray(input.pronouns)) return util.sanitizeSet(input.pronouns, table); // passed a pronouns-like object.
- else if (Array.isArray(input)) return util.sanitizeSet(input, table); // passed an array representing some pronouns.
- } else {
- if (logging) console.warn("Unrecognized input. Defaulting to they/them.");
- return util.tableLookup("they", table);
+ if (typeof input == "string"){
+ if (!this.hasOwnProperty('any') || !this.any) this.any = !!input.match(/(\b(any(thing)?|all)\b|\*)/);
+ return util.expandString(input, table); // passed a string, most common case.
}
+ if (opts.log) console.warn(`Unrecognized input. Defaulting to ${opts.default}.`);
+ return util.expandString(opts.default, table);
}
generateForms(i){
- i = Number.isInteger(i) ? i : 0;
+ i = Number.isInteger(parseInt(i)) ? parseInt(i) : 0;
+ var p = (this.pronouns && this.pronouns.length > 0) ? this.pronouns[i] : util.expandString(opts.default, table);
// the 5 main pronoun types
- this.subject = this.pronouns[i][0];
- this.object = this.pronouns[i][1];
- this.determiner = this.pronouns[i][2];
- this.possessive = this.pronouns[i][3];
- this.reflexive = this.pronouns[i][4];
+ this.subject = p[0];
+ this.object = p[1];
+ this.determiner = p[2];
+ this.possessive = p[3];
+ this.reflexive = p[4];
// aliases
this.sub = this.subject;
@@ -40,19 +43,38 @@ class Pronouns {
generateExamples(){
this.examples = [];
- for (var i = 0, p; p = this.pronouns[i]; i++){
+ this.examples_html = [];
+ this.examples_md = [];
+ var i = 0;
+ var p = (i < this.pronouns.length) ? this.pronouns[i] : util.expandString(opts.default, table);
+ do {
this.examples.push([
- util.capitalize(`*${p[0]}* went to the park.`),
- util.capitalize(`I went with *${p[1]}*.`),
- util.capitalize(`*${p[0]}* brought *${p[2]}* frisbee.`),
- util.capitalize(`At least I think it was *${p[3]}*.`),
- util.capitalize(`*${p[0]}* threw the frisbee to *${p[4]}*.`)
+ util.capitalize(`${p[0]} went to the park.`),
+ util.capitalize(`I went with ${p[1]}.`),
+ util.capitalize(`${p[0]} brought ${p[2]} frisbee.`),
+ util.capitalize(`At least I think it was ${p[3]}.`),
+ util.capitalize(`${p[0]} threw the frisbee to ${p[4]}.`)
]);
- }
+ this.examples_html.push([
+ `${util.capitalize(p[0])} went to the park.`,
+ util.capitalize(`I went with ${p[1]} .`),
+ `${util.capitalize(p[0])} brought ${p[2]} frisbee.`,
+ util.capitalize(`At least I think it was ${p[3]} .`),
+ `${util.capitalize(p[0])} threw the frisbee to ${p[4]} .`
+ ]);
+ this.examples_md.push([
+ util.capitalize(`**${p[0]}** went to the park.`),
+ util.capitalize(`I went with **${p[1]}**.`),
+ util.capitalize(`**${p[0]}** brought **${p[2]}** frisbee.`),
+ util.capitalize(`At least I think it was **${p[3]}**.`),
+ util.capitalize(`**${p[0]}** threw the frisbee to **${p[4]}**.`)
+ ]);
+ i++;
+ } while (p = this.pronouns[i]);
}
toString(){
- return this.pronouns.map(p => util.shortestUnambiguousPath(table, p).join('/')).join(' or ');
+ return this.pronouns.map(p => util.shortestUnambiguousPath(table, p).join('/')).concat(this.any ? [['any']] : []).join(' or ');
}
toUrl(){
@@ -61,35 +83,52 @@ class Pronouns {
add(input){
var newRows = this._process(input);
- for (var i = 0, p; p = newRows[i]; i++){
- if (!this.pronouns.includes(p)){
- this.pronouns.push(p);
+ this.pronouns = this.pronouns.concat(newRows.filter(p1 => {
+ for (var p2 of this.pronouns){
+ if (util.rowsEqual(p2,p1)) return false;
}
- }
+ return true;
+ }));
this.generateExamples();
}
}
-module.exports = (input, log) => {
- logging = !!(log); // convert it to a boolean value
- util.logging = logging;
- return new Pronouns(input);
+module.exports = (input, options) => {
+ var p;
+
+ if (typeof input == "string") p = new Pronouns(input);
+ else if (typeof input == "object" && !!options) options = input;
+
+ if (typeof options == "object") opts = {...opts, ...options};
+ util.options = opts;
+
+ return p;
}
module.exports.complete = (input) => {
- var rest = input.substring(0, input.lastIndexOf(' ') + 1).replace(/\s+/g, ' ');
- var last = input.substring(input.lastIndexOf(' ') + 1, input.length);
+ var sepIndex = input.lastIndexOf(' ') + 1;
+ var rest = input.substring(0, sepIndex).replace(/\s+/g, ' ');
+ var last = input.substring(sepIndex, input.length);
// Generate list of matching rows
var matches = [];
if (last.length == 0){
- matches = table.slice(); // Clones the table so it doesn't get changed
- if (!rest.match(/[Oo][Rr]\s$/g)) matches.unshift(['or']);
+ matches = [...table]; // Clone the table so it doesn't get changed
} else {
var parts = last.split('/');
var end = parts.pop();
- matches = util.tableFrontFilter(parts, table);
- matches = matches.filter(row => row[parts.length].substring(0, end.length) === end);
- if (last.match(/^[Oo][Rr]?$/g)) matches.unshift(['or']);
+ console.dir(parts);
+ console.log(end);
+ matches = util.tableWalkFilter(parts, table).filter(row => {
+ for (var f of row.slice(parts.length, row.length)){
+ if (end.length <= f.length && f.substring(0,end.length) === end) return true;
+ }
+ return false;
+ });
+ if (matches.length == 0){
+ rest += parts.slice(0,parts.length-1).join('/')+'/';
+ matches = table.filter(row => row.length == 1);
+ }
+ console.dir(matches);
}
// Filter matches to those which are not already in rest of input
@@ -101,7 +140,7 @@ module.exports.complete = (input) => {
return true;
});
- if (logging && matches.length == 0) console.log(`No matches for ${input} found.`);
+ if (opts.log && matches.length == 0) console.log(`No matches for ${input} found.`);
return matches.map(row => rest + util.shortestUnambiguousPath(table, row).join('/'));
}
diff --git a/src/util.js b/src/util.js
index 64b35bb..32ef858 100644
--- a/src/util.js
+++ b/src/util.js
@@ -1,10 +1,11 @@
module.exports = {
- // logging turned off by default.
- logging: false,
+ // options object overwritten by index.js.
+ opts: {
+ log: false
+ },
// filter table to the rows which begin with q
tableFrontFilter: function(q, table){
- var qlen = q.length;
return table.filter(row => this.rowsEqual(q, row) );
},
@@ -19,16 +20,32 @@ module.exports = {
return true;
});
},
+
+ // filter table using the walkRow function
+ tableWalkFilter: function(q, table){
+ return table.filter(row => this.walkRow(q, row, 0,0));
+ },
+
+ // match query to row by walking each element along it
+ walkRow: function(q, row, q_i, row_i){
+ if (q[q_i] != row[row_i]){
+ if (row_i >= row.length-(q.length-q_i)) return false;
+ return this.walkRow(q, row, q_i, row_i+1);
+ }
+ if (q_i < q.length-1) return this.walkRow(q, row, q_i+1, row_i+1);
+ return true;
+ },
// find the row corresponding to q in table
tableLookup: function(q, table){
+ if (q.length < 1 || q.length > table[0].length) return;
if (q.includes('...')){
var queryFront = q.slice(0, q.indexOf('...'));
var queryEnd = q.slice(q.indexOf('...')+1);
var frontMatches = this.tableFrontFilter(queryFront, table);
return this.tableEndFilter(queryEnd, frontMatches)[0];
}
- return this.tableFrontFilter(q, table)[0];
+ return this.tableWalkFilter(q, table)[0];
},
// Compute the shortest (in number of path elements) forward path which
@@ -80,23 +97,60 @@ module.exports = {
},
sanitizeSet: function(p, table){
- return p.map(row => {
+ var out = [];
+ for (var row of p){
+ if (row.length < 1) continue;
+ if (row.length == 1 && row[0].match(/\b(or|and)\b/)) continue;
+
var match = this.tableLookup(row, table);
- if (!match){
- if (this.logging) console.warn(`Unrecognized pronoun "${row.join('/')}". This may lead to unexpected behavior.`);
- while (row.length < 5){
- row.push('');
+ if (match){
+ out.push(match);
+ continue;
+ }
+
+ var expansions = [];
+ var badMatch = false;
+ for (var part of row){
+ var match = this.tableLookup([part], table);
+ if (part.match(/(\b(any(thing)?|all)\b|\*)/)){
+ if (this.opts.log) console.log(`Wildcard detected.`);
+ continue;
+ }
+ if (!match){
+ badMatch = true;
+ break;
}
+ expansions.push(match);
+ }
+ if (!badMatch){
+ out = out.concat(expansions);
+ continue;
+ }
+
+ if (row.some(p => p.match(/(\b(any(thing)?|all)\b|\*)/))){
+ if (this.opts.log) console.log(`Wildcard detected.`);
+ continue;
+ }
+
+ if (this.opts.log) console.warn(`Unrecognized pronoun(s) "${row.join('/')}". This may lead to unexpected behavior.`);
+ if (row.length >= 5){
if (row.length > 5){
row = row.slice(0,5);
}
- return row;
- } else return match;
+ if (!row.includes('')) out.push(row);
+ }
+ }
+ out = out.filter((row,i) => {
+ for (var p of out.slice(0,i)){
+ if (this.rowsEqual(p,row)) return false;
+ }
+ return true;
});
+ return out;
},
expandString: function(str, table){
- return this.sanitizeSet(str.split(' ').filter(p => !p.match(/[Oo][Rr]/g)).map(p => p.replace(/[^a-zA-Z\/'.]/, '').toLowerCase().split('/')), table);
+ return this.sanitizeSet(str.trim().split(' ').map(p => p.replace(/[^a-zA-Z\/'.]/, '').toLowerCase().split('/')), table);
},
// wrap a value in an array if it is not already in one.
@@ -107,7 +161,7 @@ module.exports = {
// capitalize first letter of a given string
capitalize: function(str){
- return str.replace(/[a-zA-Z]/, m => m.toUpperCase());
+ return str.replace(/[a-zA-Z]/, l => l.toUpperCase());
},
// check if two arrays are similar. will permit array b to be longer by design.
diff --git a/test/index.js b/test/index.js
index cefcc29..8cf1142 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,2 +1,3 @@
require('./utilTests');
require('./mainTests');
+require('./timing');
diff --git a/test/mainTests.js b/test/mainTests.js
index 54f66e0..6721dc9 100644
--- a/test/mainTests.js
+++ b/test/mainTests.js
@@ -1,7 +1,8 @@
const assert = require('assert');
const pronouns = require('../src/index');
-(function() {
+(function(){
+ /* Basic usage */
const sample1_String = "she/her or they/them";
const sample1_Obj = pronouns(sample1_String);
sample1_Obj.add("they");
@@ -39,8 +40,10 @@ const pronouns = require('../src/index');
assert.strictEqual( sample1_Obj.toString(), sample1_toString );
assert.strictEqual( sample1_Obj+"", sample1_toString );
assert.strictEqual( sample1_Obj.toUrl(), sample1_URL );
+ assert.strictEqual( sample1_Obj.any, false );
+ /* Adding more later */
const sample2_String = "fae/faer";
const sample2_Obj = pronouns(sample2_String);
sample2_Obj.add("e/em");
@@ -76,8 +79,56 @@ const pronouns = require('../src/index');
assert.deepStrictEqual( sample2_Obj.ref, sample2_Array[0][4] );
assert.deepStrictEqual( sample2_Obj.examples, sample2_examples );
assert.strictEqual( sample2_Obj.toString(), sample2_toString );
- assert.strictEqual( sample2_Obj+"", sample2_toString );
+ assert.strictEqual( `${sample2_Obj}`, sample2_toString );
assert.strictEqual( sample2_Obj.toUrl(), sample2_URL );
+ assert.strictEqual( sample2_Obj.any, false );
- assert.deepStrictEqual( pronouns.complete("th"), ["they/.../themself", "they/.../themselves", "thon"] );
+
+ /* Wildcard without alternatives */
+ const sample3_String = "any/all";
+ const sample3_Obj = pronouns(sample3_String);
+
+ const sample3_toString = "any";
+ const sample3_Array = [];
+ const sample3_examples = [sample1_examples[1]];
+ const sample3_URL = "https://pronoun.is/";
+
+ assert.deepStrictEqual( sample3_Obj.pronouns, sample3_Array );
+ assert.deepStrictEqual( sample3_Obj.sub, sample1_Array[1][0] );
+ assert.deepStrictEqual( sample3_Obj.obj, sample1_Array[1][1] );
+ assert.deepStrictEqual( sample3_Obj.det, sample1_Array[1][2] );
+ assert.deepStrictEqual( sample3_Obj.pos, sample1_Array[1][3] );
+ assert.deepStrictEqual( sample3_Obj.ref, sample1_Array[1][4] );
+ assert.deepStrictEqual( sample3_Obj.examples, sample3_examples );
+ assert.strictEqual( sample3_Obj.toString(), sample3_toString );
+ assert.strictEqual( sample3_Obj+"", sample3_toString );
+ assert.strictEqual( sample3_Obj.toUrl(), sample3_URL );
+ assert.strictEqual( sample3_Obj.any, true );
+
+
+ /* Wildcard with 1 alternative */
+ const sample4_String = "any she";
+ const sample4_Obj = pronouns(sample4_String);
+
+ const sample4_toString = "she or any";
+ const sample4_Array = [sample1_Array[0]];
+ const sample4_examples = [sample1_examples[0]];
+ const sample4_URL = "https://pronoun.is/she";
+
+ assert.deepStrictEqual( sample4_Obj.pronouns, sample4_Array );
+ assert.deepStrictEqual( sample4_Obj.sub, sample1_Array[0][0] );
+ assert.deepStrictEqual( sample4_Obj.obj, sample1_Array[0][1] );
+ assert.deepStrictEqual( sample4_Obj.det, sample1_Array[0][2] );
+ assert.deepStrictEqual( sample4_Obj.pos, sample1_Array[0][3] );
+ assert.deepStrictEqual( sample4_Obj.ref, sample1_Array[0][4] );
+ assert.deepStrictEqual( sample4_Obj.examples, sample4_examples );
+ assert.strictEqual( sample4_Obj.toString(), sample4_toString );
+ assert.strictEqual( sample4_Obj+"", sample4_toString );
+ assert.strictEqual( sample4_Obj.toUrl(), sample4_URL );
+ assert.strictEqual( sample4_Obj.any, true );
+
+
+ /* Completion */
+ // assert.deepStrictEqual( pronouns.complete("th"), ["they/.../themself", "they/.../themselves", "thon"] );
+ // assert.deepStrictEqual( pronouns.complete("they/fa"), ["they/fae"] );
}());
diff --git a/test/timing.js b/test/timing.js
new file mode 100644
index 0000000..197876f
--- /dev/null
+++ b/test/timing.js
@@ -0,0 +1,84 @@
+const util = require('../src/util');
+const table = require('../resources/pronouns.json');
+const pronouns = require('../src/index');
+
+(function(){
+ ////////////////////////////////
+ /* UTIL METHODS */
+ ////////////////////////////////
+ console.log('== Util methods ==');
+
+ console.time('rowsEqual');
+ util.rowsEqual(['a', 'b', 'c', 'd', 'e'], ['a', 'b', 'c', 'd', 'e']);
+ console.timeEnd('rowsEqual');
+
+ console.time('tableFrontFilter');
+ util.tableFrontFilter(['xey','xem'], table);
+ console.timeEnd('tableFrontFilter');
+
+ console.time('tableEndFilter');
+ util.tableEndFilter(['xyrs','xemself'], table);
+ console.timeEnd('tableEndFilter');
+
+ console.time('tableWalkFilter');
+ util.tableWalkFilter(['ve','vis'], table);
+ console.timeEnd('tableWalkFilter');
+
+ console.time('tableLookup');
+ util.tableLookup(['they', '...', 'themselves'], table);
+ console.timeEnd('tableLookup');
+
+ console.time('shortestUnambiguousForwardPath');
+ util.shortestUnambiguousForwardPath(['vi', 'vim', 'vir', 'virs', 'vimself'], table);
+ console.timeEnd('shortestUnambiguousForwardPath');
+
+ console.time('shortestUnambiguousEllipsesPath');
+ util.shortestUnambiguousEllipsesPath(['they', 'them', 'their', 'theirs', 'themselves'], table);
+ console.timeEnd('shortestUnambiguousEllipsesPath');
+
+ console.time('shortestUnambiguousPath');
+ util.shortestUnambiguousPath(['vi', 'vim', 'vir', 'virs', 'vimself'], table);
+ console.timeEnd('shortestUnambiguousPath');
+
+ console.time('abbreviate');
+ util.abbreviate(table);
+ console.timeEnd('abbreviate');
+
+ console.time('sanitizeSet (full match)');
+ util.sanitizeSet([['zee']], table);
+ console.timeEnd('sanitizeSet (full match)');
+
+ console.time('sanitizeSet (multiple)');
+ util.sanitizeSet([['ze','zir'], ['thon']], table);
+ console.timeEnd('sanitizeSet (multiple)');
+
+ console.time('sanitizeSet (shorthand)');
+ util.sanitizeSet([['fae', 'it']], table);
+ console.timeEnd('sanitizeSet (shorthand)');
+
+ console.time('sanitizeSet (custom)');
+ util.sanitizeSet([['a', 'b', 'c', 'd', 'e']], table);
+ console.timeEnd('sanitizeSet (custom)');
+
+ console.time('expandString');
+ util.expandString('she, they/he, a/b/c/d/e, and any', table);
+ console.timeEnd('expandString');
+
+
+ ////////////////////////////////
+ /* PRONOUNS METHODS */
+ ////////////////////////////////
+ console.log('\n== Pronouns methods ==');
+
+ console.time('pronouns');
+ const p = pronouns('she, they/he, a/b/c/d/e, and any');
+ console.timeEnd('pronouns');
+
+ console.time('add');
+ p.add('ze/hir');
+ console.timeEnd('add');
+
+ console.time('complete');
+ pronouns.complete('xe');
+ console.timeEnd('complete');
+}());
diff --git a/test/utilTests.js b/test/utilTests.js
index 9c303bd..81a4cc4 100644
--- a/test/utilTests.js
+++ b/test/utilTests.js
@@ -1,7 +1,7 @@
const assert = require('assert');
const util = require('../src/util');
-const list = require('../resources/pronouns.json');
-const listAbbr = require('./abbreviatedList.json');
+const table = require('../resources/pronouns.json');
+const tabAbbr = require('./abbreviatedList.json');
(function() {
const sample1_Row = ['it', 'it', 'its', 'its', 'itself'];
@@ -20,33 +20,54 @@ const listAbbr = require('./abbreviatedList.json');
['they', 'them', 'their', 'theirs', 'themself']
];
- assert.deepStrictEqual( util.tableFrontFilter(['it'], list), [sample1_Row] );
+ const sample4_Query = ['hir'];
+ const sample4_Expected = [
+ [ 'ze', 'hir', 'hir', 'hirs', 'hirself' ],
+ [ 'sie', 'hir', 'hir', 'hirs', 'hirself' ],
+ [ 'shi', 'hir', 'hir', 'hirs', 'hirself' ],
+ [ 'zie', 'hir', 'hir', 'hirs', 'hirself' ]
+ ];
+
+ const sample5_Query = ['she', 'hers'];
+ const sample5_Expected = [ [ 'she', 'her', 'her', 'hers', 'herself' ] ];
+
+ assert.deepStrictEqual( util.tableFrontFilter(['it'], table), [sample1_Row] );
+
+ assert.deepStrictEqual( util.tableEndFilter(['itself'], table), [sample1_Row] );
- assert.deepStrictEqual( util.tableEndFilter(['itself'], list), [sample1_Row] );
+ assert.deepStrictEqual( util.tableWalkFilter(sample4_Query, table), sample4_Expected );
+ assert.deepStrictEqual( util.tableWalkFilter(sample5_Query, table), sample5_Expected );
+ assert.deepStrictEqual( util.tableWalkFilter(['their', 'foo'], table), [] );
+ assert.deepStrictEqual( util.tableWalkFilter(['foo', 'bar'], table), [] );
- assert.deepStrictEqual( util.tableLookup(sample1_Shortened, list), sample1_Row );
- assert.deepStrictEqual( util.tableLookup(sample2_Shortened, list), sample2_Row );
+ assert.deepStrictEqual( util.tableLookup(sample1_Shortened, table), sample1_Row );
+ assert.deepStrictEqual( util.tableLookup(sample2_Shortened, table), sample2_Row );
- assert.deepStrictEqual( util.shortestUnambiguousForwardPath(list, sample1_Row), sample1_Forward );
- assert.deepStrictEqual( util.shortestUnambiguousForwardPath(list, sample2_Row), sample2_Forward );
+ assert.deepStrictEqual( util.shortestUnambiguousForwardPath(table, sample1_Row), sample1_Forward );
+ assert.deepStrictEqual( util.shortestUnambiguousForwardPath(table, sample2_Row), sample2_Forward );
- assert.deepStrictEqual( util.shortestUnambiguousEllipsesPath(list, sample1_Row), sample1_Ellipses );
- assert.deepStrictEqual( util.shortestUnambiguousEllipsesPath(list, sample2_Row), sample2_Ellipses );
+ assert.deepStrictEqual( util.shortestUnambiguousEllipsesPath(table, sample1_Row), sample1_Ellipses );
+ assert.deepStrictEqual( util.shortestUnambiguousEllipsesPath(table, sample2_Row), sample2_Ellipses );
- assert.deepStrictEqual( util.shortestUnambiguousPath(list, sample1_Row), sample1_Shortened );
- assert.deepStrictEqual( util.shortestUnambiguousPath(list, sample2_Row), sample2_Shortened );
+ assert.deepStrictEqual( util.shortestUnambiguousPath(table, sample1_Row), sample1_Shortened );
+ assert.deepStrictEqual( util.shortestUnambiguousPath(table, sample2_Row), sample2_Shortened );
- assert.deepStrictEqual( util.shortestUnambiguousPath(list, sample1_Row), sample1_Shortened );
+ assert.deepStrictEqual( util.shortestUnambiguousPath(table, sample1_Row), sample1_Shortened );
- assert.deepStrictEqual( util.abbreviate(list), listAbbr );
+ assert.deepStrictEqual( util.abbreviate(table), tabAbbr );
- assert.deepStrictEqual( util.sanitizeSet([sample1_Shortened], list), [sample1_Row] );
- assert.deepStrictEqual( util.sanitizeSet([['a', 'b'], ['a', 'b', 'c', 'd', 'e', 'f', 'g']], list), [['a', 'b', '', '', ''], ['a', 'b', 'c', 'd', 'e']] );
+ assert.deepStrictEqual( util.sanitizeSet([sample1_Shortened], table), [sample1_Row] );
+ assert.deepStrictEqual( util.sanitizeSet([['they', 'any']], table), [sample2_Row] );
+ assert.deepStrictEqual( util.sanitizeSet([['she', 'they']], table), sample3_Expected );
+ assert.deepStrictEqual( util.sanitizeSet([['her', 'them']], table), sample3_Expected );
+ assert.deepStrictEqual( util.sanitizeSet([['any']], table), [] );
+ assert.deepStrictEqual( util.sanitizeSet([['a', 'b'], ['a', 'b', 'c', 'd', 'e', 'f', 'g']], table), [['a', 'b', 'c', 'd', 'e']] );
- assert.deepStrictEqual( util.expandString(sample3_String, list), sample3_Expected );
+ assert.deepStrictEqual( util.expandString(sample3_String, table), sample3_Expected );
assert.strictEqual( util.capitalize("Hello,"), "Hello," );
assert.strictEqual( util.capitalize("world!"), "World!" );
+ assert.strictEqual( util.capitalize("1!a"), "1!A" );
assert.deepStrictEqual( util.arrFormat('x'), ['x'] );
assert.deepStrictEqual( util.arrFormat(['x']), ['x'] );