(pronouns) add support for empty and unset morphs in slash notation

This commit is contained in:
Valentyne Stigloher 2024-01-13 02:00:14 +01:00
parent 48992c53d6
commit 6309daa78c
7 changed files with 77 additions and 4 deletions

View File

@ -59,7 +59,7 @@ const buildPronounFromTemplate = (key, template) => {
}; };
const buildPronounFromSlashes = (config, path) => { const buildPronounFromSlashes = (config, path) => {
const chunks = path.split(/(?<!`)\//).filter((s) => !!s); const chunks = path.split(/(?<!`)\//);
let plural = false; let plural = false;
let pluralHonorific = false; let pluralHonorific = false;
let description = ''; let description = '';
@ -76,10 +76,16 @@ const buildPronounFromSlashes = (config, path) => {
description = unescapeControlSymbols(chunk.substring(descriptionModifierPrefix.length)); description = unescapeControlSymbols(chunk.substring(descriptionModifierPrefix.length));
} }
} }
} else {
if (chunk === '~') {
morphemeChunks.push(null);
} else if (chunk === ' ') {
morphemeChunks.push('');
} else { } else {
morphemeChunks.push(unescapeControlSymbols(chunk)); morphemeChunks.push(unescapeControlSymbols(chunk));
} }
} }
}
if (description.length > Pronoun.DESCRIPTION_MAXLENGTH) { if (description.length > Pronoun.DESCRIPTION_MAXLENGTH) {
return null; return null;
} }

View File

@ -478,7 +478,16 @@ export class Pronoun {
} else { } else {
chunks = Object.values(this.morphemes); chunks = Object.values(this.morphemes);
} }
chunks = chunks.map(escapeControlSymbols); chunks = chunks.map((chunk) => {
if (chunk === null) {
return '~';
} else if (chunk === '') {
// use an extra space because double slashes get replaced by a single one during a request
return ' ';
} else {
return escapeControlSymbols(chunk);
}
});
if (this.plural[0]) { if (this.plural[0]) {
chunks.push(':plural'); chunks.push(':plural');
@ -490,7 +499,8 @@ export class Pronoun {
chunks.push(`:description=${escapeControlSymbols(this.description)}`); chunks.push(`:description=${escapeControlSymbols(this.description)}`);
} }
return chunks.join('/'); // encode a trailing space so that it does not get removed during a request
return chunks.join('/').replace(/ $/, encodeURI(' '));
} }
static from(data) { static from(data) {

View File

@ -269,12 +269,18 @@ const escapeChars = {
export const escapeHtml = (text) => text.replace(/[&<>"]/g, (tag) => escapeChars[tag] || tag); export const escapeHtml = (text) => text.replace(/[&<>"]/g, (tag) => escapeChars[tag] || tag);
export const escapeControlSymbols = (text) => { export const escapeControlSymbols = (text) => {
if (text === null) {
return null;
}
// a backtick is used because browsers replace backslashes // a backtick is used because browsers replace backslashes
// with forward slashes if they are not url-encoded // with forward slashes if they are not url-encoded
return text.replaceAll(/[`&/|,]/g, '`$&'); return text.replaceAll(/[`&/|,]/g, '`$&');
}; };
export const unescapeControlSymbols = (text) => { export const unescapeControlSymbols = (text) => {
if (text === null) {
return null;
}
return text.replaceAll(/`(.)/g, '$1'); return text.replaceAll(/`(.)/g, '$1');
}; };

View File

@ -162,6 +162,21 @@ describe('when configured that slashes contain all morphemes', () => {
actual.toBeDefined(); actual.toBeDefined();
actual.toEqual(generatedPronouns.aerPluralWithDescription); actual.toEqual(generatedPronouns.aerPluralWithDescription);
}); });
test('builds generated pronoun with some morphemes empty', () => {
const actual = expect(buildPronoun(pronouns, 'ae/aer/aer/ /aerself'));
actual.toBeDefined();
actual.toEqual(generatedPronouns.aerWithEmptyPossessivePronoun);
});
test('builds generated pronoun with morpheme at end empty', () => {
const actual = expect(buildPronoun(pronouns, 'ae/aer/aer/aers/'));
actual.toBeDefined();
actual.toEqual(generatedPronouns.aerWithEmptyReflexive);
});
test('builds generated pronoun with some morphemes unset', () => {
const actual = expect(buildPronoun(pronouns, 'ae/aer/aer/~/aerself'));
actual.toBeDefined();
actual.toEqual(generatedPronouns.aerWithUnsetPossessivePronoun);
});
test('builds nothing if morphemes are missing', () => { test('builds nothing if morphemes are missing', () => {
expect(buildPronoun(pronouns, 'ae/aer/aer/aerself')).toBeUndefined(); expect(buildPronoun(pronouns, 'ae/aer/aer/aerself')).toBeUndefined();
}); });

View File

@ -56,6 +56,18 @@ describe('formatting a pronoun with slashes', () => {
expect(generatedPronouns.sSlashHe.toStringSlashes()) expect(generatedPronouns.sSlashHe.toStringSlashes())
.toEqual('s`/he/hir/hir/hirs/hirself'); .toEqual('s`/he/hir/hir/hirs/hirself');
}); });
test('empty morphemes receive space as placeholder', () => {
expect(generatedPronouns.aerWithEmptyPossessivePronoun.toStringSlashes())
.toEqual('ae/aer/aer/ /aerself');
});
test('empty morphemes at end receive url-encoded space as placeholder', () => {
expect(generatedPronouns.aerWithEmptyReflexive.toStringSlashes())
.toEqual('ae/aer/aer/aers/%20');
});
test('unset morphemes receive tilde as placeholder', () => {
expect(generatedPronouns.aerWithUnsetPossessivePronoun.toStringSlashes())
.toEqual('ae/aer/aer/~/aerself');
});
test('adds plural modifier if necessary', () => { test('adds plural modifier if necessary', () => {
expect(generatedPronouns.aerPlural.toStringSlashes()) expect(generatedPronouns.aerPlural.toStringSlashes())
.toEqual('ae/aer/aer/aers/aerselves/:plural'); .toEqual('ae/aer/aer/aers/aerselves/:plural');

View File

@ -164,6 +164,23 @@ const aerWithEmptyPossessivePronoun = new Pronoun(
'__generator__', '__generator__',
false, false,
); );
const aerWithEmptyReflexive = new Pronoun(
'ae/aer',
'',
false,
{
pronoun_subject: 'ae',
pronoun_object: 'aer',
possessive_determiner: 'aer',
possessive_pronoun: 'aers',
reflexive: '',
},
[false],
[false],
[],
'__generator__',
false,
);
const aerWithUnsetPossessivePronoun = new Pronoun( const aerWithUnsetPossessivePronoun = new Pronoun(
'ae/aer', 'ae/aer',
'', '',
@ -207,6 +224,7 @@ export const generated = {
aerWithDescription, aerWithDescription,
aerPluralWithDescription, aerPluralWithDescription,
aerWithEmptyPossessivePronoun, aerWithEmptyPossessivePronoun,
aerWithEmptyReflexive,
aerWithUnsetPossessivePronoun, aerWithUnsetPossessivePronoun,
sSlashHe, sSlashHe,
}; };

View File

@ -36,12 +36,18 @@ const controlSymbols = [
]; ];
describe('when escaping control symbols', () => { describe('when escaping control symbols', () => {
test('safely handles null', () => {
expect(escapeControlSymbols(null)).toBeNull();
});
test.each(controlSymbols)('$description get escaped with `', ({ unescaped, escaped }) => { test.each(controlSymbols)('$description get escaped with `', ({ unescaped, escaped }) => {
expect(escapeControlSymbols(unescaped)).toBe(escaped); expect(escapeControlSymbols(unescaped)).toBe(escaped);
}); });
}); });
describe('when unescaping control symbols', () => { describe('when unescaping control symbols', () => {
test('safely handles null', () => {
expect(unescapeControlSymbols(null)).toBeNull();
});
test.each(controlSymbols)('$description get unescaped', ({ unescaped, escaped }) => { test.each(controlSymbols)('$description get unescaped', ({ unescaped, escaped }) => {
expect(unescapeControlSymbols(escaped)).toBe(unescaped); expect(unescapeControlSymbols(escaped)).toBe(unescaped);
}); });