yoastseo
Version:
Yoast client-side content analysis
394 lines (369 loc) • 19.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = stem;
var _yoastseo = require("yoastseo");
var _stemHelpers = require("./stemHelpers");
const {
flattenSortLength,
buildFormRule,
createRulesFromArrays
} = _yoastseo.languageProcessing;
/**
* MIT License
*
* Adapted from: Copyright (c) 2013 Adinda Praditya
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the \"Software\"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
*/
/**
* Checks if a single syllable word has one of the suffixes/suffix combination.
*
* @param {string} word The word to check.
* @param {Array} suffixesArray The array of suffixes
* @returns {boolean} Whether the word ends in one of the suffixes or not.
*/
const checkSingleSyllableWordSuffix = function (word, suffixesArray) {
for (const suffix of suffixesArray) {
if (word.match(suffix)) {
return true;
}
}
};
/**
* Stems the prefix of the single syllable words, i.e. di-/penge-/menge-
*
* @param {string} word The word to check.
* @param {Object} morphologyData The Indonesian morphology data file.
*
* @returns {string} The stemmed word.
*/
const stemSingleSyllableWordsPrefixes = function (word, morphologyData) {
// If the word gets prefix di-, stem the prefix here. E.g. dicekkanlah -> cekkanlah, dibomi -> bomi
if (word.startsWith("di") && (0, _stemHelpers.checkBeginningsList)(word, 2, morphologyData.stemming.singleSyllableWords)) {
return word.substring(2, word.length);
}
/*
* If the word gets prefix menge-/penge- and is followed by one of the words in the list, stem the prefix here.
* E.g. pengeboman -> boman
*/
if (/^[mp]enge/i.test(word) && (0, _stemHelpers.checkBeginningsList)(word, 5, morphologyData.stemming.singleSyllableWords)) {
return word.substring(5, word.length);
}
return word;
};
/**
* Stems Indonesian single syllable words. This function concerns single syllable words
* with this possible word format [di/penge/menge] + single syllable word + [kan/an/i] + [ku/mu/nya] + [kah/lah/pun], with [] being optional.
* E.g. dipel -> pel, dipelkan -> pel, dipelkanlah -> pel, pelkan -> pel, pelmulah -> pel, pengeboman -> bom, mengesahkan -> sah
*
*
* @param {string} word The word to check.
* @param {Object} morphologyData The Indonesian morphology data file.
*
* @returns {string} The stemmed word.
*/
const stemSingleSyllableWords = function (word, morphologyData) {
const singleSyllableWords = morphologyData.stemming.singleSyllableWords;
const suffixCombination = morphologyData.stemming.singleSyllableWordsSuffixes;
const inputWord = word;
// If the word starts with prefix di-/penge-/menge-, stem the prefix here. E.g. pengeboman -> boman, dipelkan -> pelkan
word = stemSingleSyllableWordsPrefixes(word, morphologyData);
// Check if a word starts with one of the words in the list, has maximum 3 syllables, and ends in one of the single syllable suffixes
if (singleSyllableWords.some(shortWord => word.startsWith(shortWord)) && (0, _stemHelpers.calculateTotalNumberOfSyllables)(word) <= 3 && checkSingleSyllableWordSuffix(word, suffixCombination)) {
// If the word gets a particle suffix, stem the particle. E.g. cekkanlah -> cekkan, pelkan -> pel, vasmulah -> vasmu
word = (0, _stemHelpers.removeEnding)(word, morphologyData.stemming.regexRules.removeParticle, morphologyData.stemming.doNotStemWords.doNotStemParticle, morphologyData);
// If the word gets a possessive pronoun suffix, stem the possessive pronoun. E.g. vasmu -> vas
word = (0, _stemHelpers.removeEnding)(word, morphologyData.stemming.regexRules.removePronoun, morphologyData.stemming.doNotStemWords.doNotStemPronounSuffix, morphologyData);
// If the word ends in -kan/-an/-i suffix, stem the suffix. E.g. cekkan -> cek, bomi -> bom
const wordWithoutDerivationalSuffix = (0, _stemHelpers.removeEnding)(word, morphologyData.stemming.regexRules.removeSuffixes, morphologyData.stemming.doNotStemWords.doNotStemSuffix, morphologyData);
if (singleSyllableWords.includes(wordWithoutDerivationalSuffix)) {
word = wordWithoutDerivationalSuffix;
}
}
/*
* We only want to stem single syllable words here.
* Thus, if the output word has more than one syllable, we don't stem the input word at all.
*/
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 1 || word.length === 1) {
word = inputWord;
}
return word;
};
/**
* Tries stemming prefixes ke- and ter-. Ke- is always stemmed, and ter- only if it is a prefix and not part of the stem.
* Also if the stem of the word begins with r-, only te- is stemmed, not ter-.
*
* @param {Object} morphologyData The Indonesian morphology data file.
* @param {string} word The word to check.
*
* @returns {string|null} The stem or null if the word did not start with ter/keter.
*/
const tryStemmingKeAndTer = function (morphologyData, word) {
const terException = morphologyData.stemming.doNotStemWords.doNotStemPrefix.doNotStemFirstOrderPrefix.doNotStemTer;
// If prefix -ter is preceded by prefix -ke, remove it first.
if (word.startsWith("keter")) {
word = word.substring(2, word.length);
}
if (word.startsWith("ter")) {
// If word is on an exception list of words where -ter should not be stemmed, do not stem -ter and return the word.
if (terException.some(wordWithTer => word.startsWith(wordWithTer))) {
return word;
}
// If word (without prefixes) is on the list of words beginning with -r, remove only -te instead of -ter.
if ((0, _stemHelpers.checkBeginningsList)(word, 3, morphologyData.stemming.beginningModification.rBeginning)) {
return word.replace(/^ter/i, "r");
}
// Otherwise, remove -ter.
return word.substring(3, word.length);
}
};
/**
* Checks whether a word has a first order prefix and whether it is on an exception list of words which require a stem mofification
* after removing the prefix. Returns the stem if the prefix was found and the word was matched on an exception list.
*
*
* @param {string} word The word to check.
* @param {Object} morphologyData The Indonesian morphology data file.
*
* @returns {string|null} The stem or null if a prefix was not found, or was found but the word was not on the exception list.
*/
const checkFirstOrderPrefixExceptions = function (word, morphologyData) {
const beginningModification = morphologyData.stemming.beginningModification;
// If a word starts with "men" or "pen" and is present in the nBeginning exception list, the prefix should be replaced with "n".
if (/^[mp]en/i.test(word)) {
if ((0, _stemHelpers.checkBeginningsList)(word, 3, beginningModification.nBeginning)) {
return word.replace(/^[mp]en/i, "n");
}
}
if (/^[mp]eng/i.test(word) && (0, _stemHelpers.checkBeginningsList)(word, 4, beginningModification.kBeginning)) {
return word.replace(/^[mp]eng/i, "k");
}
if (/^[mp]em/i.test(word)) {
if ((0, _stemHelpers.checkBeginningsList)(word, 3, beginningModification.pBeginning)) {
return word.replace(/^(mem|pem)/i, "p");
} else if ((0, _stemHelpers.checkBeginningsList)(word, 3, beginningModification.mBeginning)) {
return word.replace(/^(mem|pem)/i, "m");
}
}
// Stem prefix ke- if found. Stem te(r)- unless the word was found on the exception list of words with stem beginning in -ter.
const wordAfterKeTerCheck = tryStemmingKeAndTer(morphologyData, word);
if (wordAfterKeTerCheck) {
return wordAfterKeTerCheck;
}
};
/**
* Stems the first-order prefix of a word based on regexRules. If the word is found in an exception list, implements a stem modification.
*
* @param {string} word The word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string} The stemmed word.
*/
const removeFirstOrderPrefix = function (word, morphologyData) {
// Checks whether the word has a first order prefix and requires a stem modification.
const firstOrderPrefixException = checkFirstOrderPrefixExceptions(word, morphologyData);
if (firstOrderPrefixException) {
return firstOrderPrefixException;
}
const regex = createRulesFromArrays(morphologyData.stemming.regexRules.removeFirstOrderPrefixes);
const withRemovedFirstOrderPrefix = buildFormRule(word, regex);
return withRemovedFirstOrderPrefix || word;
};
/**
* Stems the second-order prefix of a word based on regexRules. If the word is found in an exception list, implements a stem modification.
*
* @param {string} word The word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string} The stemmed word.
*/
const removeSecondOrderPrefix = function (word, morphologyData) {
// If a word starts with "ber" or "per" and is present in the rBeginning exception list, the prefix should be replaced with "r".
if ((word.startsWith("ber") || word.startsWith("per")) && (0, _stemHelpers.checkBeginningsList)(word, 3, morphologyData.stemming.beginningModification.rBeginning)) {
return word.replace(/^(ber|per)/i, "r");
}
// If a word starts with a first order prefix followed by peng- and is in the kBeginning exception list, then peng- should be replaced with k.
if (/^peng/i.test(word) && (0, _stemHelpers.checkBeginningsList)(word, 4, morphologyData.stemming.beginningModification.kBeginning)) {
return word.replace(/^peng/i, "k");
}
const regex = createRulesFromArrays(morphologyData.stemming.regexRules.removeSecondOrderPrefixes);
const withRemovedSecondOrderPrefix = buildFormRule(word, regex);
return withRemovedSecondOrderPrefix || word;
};
/**
* Stems derivational affixes of Indonesian words.
*
* @param {string} word The word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string} The stemmed word.
*/
const stemDerivational = function (word, morphologyData) {
let wordLength = word.length;
const removeSuffixRules = morphologyData.stemming.regexRules.removeSuffixes;
const removeSuffixExceptions = morphologyData.stemming.doNotStemWords.doNotStemSuffix;
const doNotStemFirstOrderPrefix = flattenSortLength(morphologyData.stemming.doNotStemWords.doNotStemPrefix.doNotStemFirstOrderPrefix);
const doNotStemSecondOrderPrefix = flattenSortLength(morphologyData.stemming.doNotStemWords.doNotStemPrefix.doNotStemSecondOrderPrefix);
// If a word is in the list of words with a beginning that looks like a valid suffix, do not stem the suffix
if (!doNotStemFirstOrderPrefix.some(wordWithPrefixLookAlike => word.startsWith(wordWithPrefixLookAlike))) {
/*
* If the word has more than 2 syllables and starts with one of first order prefixes (i.e. meng-, meny-, men-, mem-, me-,
* peng-, peny-, pen-, pem-, di-, ter-, ke- ), the prefix will be stemmed here. e.g. penyebaran -> sebaran, diperlebarkan -> perlebarkan
*/
word = removeFirstOrderPrefix(word, morphologyData);
}
if (wordLength === word.length) {
if (!doNotStemSecondOrderPrefix.some(wordWithPrefixLookAlike => word.startsWith(wordWithPrefixLookAlike))) {
/*
* If the word does not start with one of the first order prefixes but starts with one of the second order prefixes,
* the prefix will be stemmed here, e.g., peranakan -> anakan
*/
word = removeSecondOrderPrefix(word, morphologyData);
}
// If the word has more than 2 syllables and ends in either -kan, -an, or -i suffixes, the suffix will be deleted here, e.g., anakan -> anak
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 2) {
word = (0, _stemHelpers.removeEnding)(word, removeSuffixRules, removeSuffixExceptions, morphologyData);
}
} else {
// If the word previously had a first order prefix, assign wordLength to the length of the word after prefix deletion.
wordLength = word.length;
/*
* If the word after first order prefix deletion is bigger than 2 and ends in either -kan, -an, or -i suffixes,
* the suffix will be stemmed here. e.g. penyebaran - sebar.
*/
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 2) {
word = (0, _stemHelpers.removeEnding)(word, removeSuffixRules, removeSuffixExceptions, morphologyData);
}
/*
* If the word previously had a suffix, we check further if the word after first order prefix and suffix deletion has more than 2 syllables.
* If it does have more than 2 syllables and starts with one of the second order prefixes (i.e. ber-, be-, per-, pe-), the prefix will
* be stemmed here unless the word is in the exception list of words with a beginning that looks like a second order prefix.
*/
if (wordLength !== word.length && !doNotStemSecondOrderPrefix.includes(word)) {
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 2) {
word = removeSecondOrderPrefix(word, morphologyData);
}
}
}
return word;
};
/**
* Stems Indonesian singular words.
*
* @param {string} word The singular word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string} The stem of an Indonesian singular word.
*/
const stemSingular = function (word, morphologyData) {
const singleSyllableWords = stemSingleSyllableWords(word, morphologyData);
// Stem the single syllable words
word = singleSyllableWords;
const doNotStemParticle = morphologyData.stemming.doNotStemWords.doNotStemParticle;
const doNotStemPronoun = morphologyData.stemming.doNotStemWords.doNotStemPronounSuffix;
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) <= 2) {
return word;
}
// Check if a word after its derivational affixes stemmed exists in the exception list.
const firstDerivationalStem = stemDerivational(word, morphologyData);
if (doNotStemParticle.includes(firstDerivationalStem) || doNotStemPronoun.includes(firstDerivationalStem)) {
// If it does exist in the exception list, the ending that looks like a particle or a pronoun suffix should not be stemmed.
return firstDerivationalStem;
}
/*
* If the word has more than 2 syllables and ends in of the particle endings (i.e. -kah, -lah, -pun), stem the particle here.
* e.g. bajumulah -> bajumu, bawalah -> bawa
*/
word = (0, _stemHelpers.removeEnding)(word, morphologyData.stemming.regexRules.removeParticle, doNotStemParticle, morphologyData);
// If the word (still) has more than 2 syllables and ends in of the possessive pronoun endings (i.e. -ku, -mu, -nya), stem the ending here.
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 2) {
// E.g. bajumu -> baju
word = (0, _stemHelpers.removeEnding)(word, morphologyData.stemming.regexRules.removePronoun, doNotStemPronoun, morphologyData);
}
// If the word (still) has more than 2 syllables and has derivational affixes, the affix(es) will be stemmed here.
if ((0, _stemHelpers.calculateTotalNumberOfSyllables)(word) > 2) {
word = stemDerivational(word, morphologyData);
}
return word;
};
/**
* Stems Indonesian plural words.
*
* @param {string} word The plural word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string|null} The stem of an Indonesian plural word or null if no plural was detected.
*/
const stemPlural = function (word, morphologyData) {
const hyphenIndex = word.indexOf("-");
// If there is no hyphen in the word, it can't be a reduplicated plural.
if (hyphenIndex === -1) {
return null;
}
const splitWord = word.split("-");
if (splitWord.length === 2) {
let firstPart = splitWord[0];
let secondPart = splitWord[1];
firstPart = stemSingular(firstPart, morphologyData);
secondPart = stemSingular(secondPart, morphologyData);
/*
* To compare the first and second part and see whether it's actually a reduplication:
* Trim the beginning of the word since it might be variable due to stem changes caused by prefixes.
* For example, in "meniru-nirukan" the singular stemmer will correctly stem the first "niru" to "tiru" because
* of the prefix "me". Since the second part of the word is stemmed individually, there is no "me" and hence
* "niru" remains "niru". To still be able to link these two forms to each other,
* we compare the two parts of the word after stripping the variable first or first and second letter.
*
*/
const firstPartBeginningTrimmed = firstPart.substring(1);
const secondPartBeginningTrimmed = secondPart.startsWith("ng") || secondPart.startsWith("ny") ? secondPart.substring(2) : secondPart.substring(1);
if (firstPartBeginningTrimmed === secondPartBeginningTrimmed) {
const nonPlurals = morphologyData.stemming.nonPluralReduplications;
// Check non-plural reduplication.
if (nonPlurals.includes(firstPart) && nonPlurals.includes(secondPart)) {
/*
* In words such as "mengira-ngira" prefix "me" causes a modification on both words (k->ng). This will
* be correctly stemmed for the first word, but not the second. Therefore, the correct base form
* "kira-kira" is created based on a reduplication of the correctly stemmed first part, "kira".
*/
return firstPart + "-" + firstPart;
}
// Return the stemmed singular form of a reduplicated plural.
return firstPart;
}
}
return null;
};
/**
* Stems Indonesian words
*
* @param {string} word The word to stem.
* @param {Object} morphologyData The object that contains regex-based rules and exception lists for Indonesian stemming.
*
* @returns {string} The stem of an Indonesian word.
*/
function stem(word, morphologyData) {
// Check words that shouldn't receive any stemming.
if (morphologyData.stemming.shouldNotBeStemmed.includes(word)) {
return word;
}
const stemmedPlural = stemPlural(word, morphologyData);
if (stemmedPlural) {
return stemmedPlural;
}
word = stemSingular(word, morphologyData);
return word;
}
//# sourceMappingURL=stem.js.map