@dcoffey/espells
Version:
Pure JS/TS spellchecker, using Hunspell dictionaries. Based on Spylls.
162 lines • 5.48 kB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import { CONSTANTS as C } from "../constants.js";
import { re, uppercase } from "../util.js";
// TODO: use homegrown metaphone implementation (maybe)
/**
* A table for metaphone transformations. These transformations may provide
* superior suggestions because they describe similar sounding (as in
* spoken) syllables, which will be used by the suggestion engine to find
* words that may not be spelled similarly but *sound* similar.
*
* It roughly uses the following syntax:
*
* ```text
* PHONE <number of entries>
* PHONE <pattern> <replacement>
* ```
*
* `replacement` is a simple string, with the special value `_`
* (underscore) meaning an empty string. `pattern` is complex, and
* currently isn't very well documented. Additionally, both Spylls and
* Espells do not fully implement Hunspell's more intricate details, such
* as rule prioritizing and concepts like "follow-up rules".
*
* Dictionaries that use this feature are unfortunately quite rare.
*/
export class PhonetTable {
constructor(table) {
var _a, _b;
Object.defineProperty(this, "rules", {
enumerable: true,
configurable: true,
writable: true,
value: {}
});
for (const [search, replacement] of table) {
const match = C.PHONET_RULE_REGEX.exec(search);
if (!match)
throw new SyntaxError(`Invalid PhonetTable pattern '${search}'`);
const [letters, optional, lookahead, flags, priority] = match;
let text = [...letters];
if (optional)
text.push(`[${optional}]`);
let regex;
if (lookahead) {
const la = lookahead.length;
regex = re `/${text.slice(0, -la).join("")}(?=${text.slice(-la).join("")})/`;
}
else {
regex = re `/${text.join("")}/`;
}
(_a = this.rules)[_b = search[0]] ?? (_a[_b] = []);
this.rules[search[0]].push(new PhonetTableRule(regex, replacement, flags?.includes("^"), flags?.includes("$"), Boolean(lookahead), priority ? parseInt(priority) : 5));
}
}
/**
* Returns the metaphone representation of a word.
*
* @param word - The word to transform.
*/
metaphone(word) {
word = uppercase(word);
let pos = 0;
let res = "";
while (pos < word.length) {
let match = false;
if (this.rules[word[pos]]) {
for (const rule of this.rules[word[pos]]) {
match = rule.match(word, pos);
if (match) {
res += rule.replacement;
pos += match.index + match[0].length - match.index;
}
}
}
if (!match)
pos++;
}
return res;
}
}
/**
* An individual phonetic table rule.
*
* @see {@link PhonetTable}
*/
class PhonetTableRule {
constructor(
/** The `RegExp` used for checking if this rule applies. */
search,
/** The string to represent a matched phoneme with. */
replacement,
/** If true, this rule only applies at the start of a word. */
start = false,
/** If true, this rule only applies at the end of a word. */
end = false,
/** Currently unusued in both Spylls and Espells. */
followup = true,
/** Currently unusued in both Spylls and Espells. */
priority = 5) {
Object.defineProperty(this, "search", {
enumerable: true,
configurable: true,
writable: true,
value: search
});
Object.defineProperty(this, "replacement", {
enumerable: true,
configurable: true,
writable: true,
value: replacement
});
Object.defineProperty(this, "start", {
enumerable: true,
configurable: true,
writable: true,
value: start
});
Object.defineProperty(this, "end", {
enumerable: true,
configurable: true,
writable: true,
value: end
});
Object.defineProperty(this, "followup", {
enumerable: true,
configurable: true,
writable: true,
value: followup
});
Object.defineProperty(this, "priority", {
enumerable: true,
configurable: true,
writable: true,
value: priority
});
}
/**
* Checks if a rule is matched by this rule, and if it is, returns the
* `RegExpExecArray` match, otherwise returning false.
*
* @param word - The word to check.
* @param pos - The position in the word to check.
*/
match(word, pos) {
if (this.start && pos > 0)
return false;
this.search.lastIndex = pos;
const match = this.search.exec(word);
if (match) {
if (this.end) {
return match[0].length !== word.length ? false : match;
}
else {
return match;
}
}
return false;
}
}
//# sourceMappingURL=phonet-table.js.map