(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 chunks = path.split(/(?<!`)\//).filter((s) => !!s);
const chunks = path.split(/(?<!`)\//);
let plural = false;
let pluralHonorific = false;
let description = '';
@ -77,7 +77,13 @@ const buildPronounFromSlashes = (config, path) => {
}
}
} else {
morphemeChunks.push(unescapeControlSymbols(chunk));
if (chunk === '~') {
morphemeChunks.push(null);
} else if (chunk === ' ') {
morphemeChunks.push('');
} else {
morphemeChunks.push(unescapeControlSymbols(chunk));
}
}
}
if (description.length > Pronoun.DESCRIPTION_MAXLENGTH) {

View File

@ -478,7 +478,16 @@ export class Pronoun {
} else {
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]) {
chunks.push(':plural');
@ -490,7 +499,8 @@ export class Pronoun {
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) {

View File

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

View File

@ -162,6 +162,21 @@ describe('when configured that slashes contain all morphemes', () => {
actual.toBeDefined();
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', () => {
expect(buildPronoun(pronouns, 'ae/aer/aer/aerself')).toBeUndefined();
});

View File

@ -56,6 +56,18 @@ describe('formatting a pronoun with slashes', () => {
expect(generatedPronouns.sSlashHe.toStringSlashes())
.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', () => {
expect(generatedPronouns.aerPlural.toStringSlashes())
.toEqual('ae/aer/aer/aers/aerselves/:plural');

View File

@ -164,6 +164,23 @@ const aerWithEmptyPossessivePronoun = new Pronoun(
'__generator__',
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(
'ae/aer',
'',
@ -207,6 +224,7 @@ export const generated = {
aerWithDescription,
aerPluralWithDescription,
aerWithEmptyPossessivePronoun,
aerWithEmptyReflexive,
aerWithUnsetPossessivePronoun,
sSlashHe,
};

View File

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