| @@ -1,23 +1,53 @@ | ||
| 1 | +/** | |
| 2 | + * External dependencies. | |
| 3 | + */ | |
| 1 | 4 | import { v4 as uuidv4 } from 'uuid'; |
| 2 | 5 | |
| 3 | 6 | /** |
| 4 | - * Generate universally unique identifier. | |
| 7 | + * WordPress dependencies. | |
| 8 | + */ | |
| 9 | +import { __ } from '@wordpress/i18n'; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * @typedef Synonym | |
| 13 | + * @property {string} value The synonym value. | |
| 14 | + * @property {boolean} primary Whether the synonym is a primary term. | |
| 5 | 15 | * |
| 6 | - * @returns {string} A universally unique identifier | |
| 16 | + * @typedef Rule | |
| 17 | + * @property {string} id Rule ID. | |
| 18 | + * @property {Synonym[]} synonyms Rule synonyms. | |
| 19 | + * @property {boolean} valid Whether the rule is valid. | |
| 7 | 20 | */ |
| 8 | -const uuid = () => { | |
| 9 | - return uuidv4(); | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * Determine whether a synonym is a primary term. | |
| 24 | + * | |
| 25 | + * @param {Synonym} synonym Synonym. | |
| 26 | + * @returns {boolean} | |
| 27 | + */ | |
| 28 | +const isPrimary = (synonym) => { | |
| 29 | + return synonym.primary; | |
| 10 | 30 | }; |
| 11 | 31 | |
| 12 | 32 | /** |
| 13 | - * Map entry | |
| 33 | + * Determine whether a synonym is not a primary term. | |
| 14 | 34 | * |
| 15 | - * @param {Array} synonyms Array of synonyms. | |
| 16 | - * @param {string} id The id, default generated by the application. | |
| 17 | - * @returns {object} Map entry | |
| 35 | + * @param {Synonym} synonym Synonym. | |
| 36 | + * @returns {boolean} | |
| 18 | 37 | */ |
| 19 | -const mapEntry = (synonyms = [], id = '') => { | |
| 38 | +const isNotPrimary = (synonym) => { | |
| 39 | + return !synonym.primary; | |
| 40 | +}; | |
| 41 | + | |
| 42 | +/** | |
| 43 | + * Get a rule object for a list of synonyms, | |
| 44 | + * | |
| 45 | + * @param {Synonym[]} synonyms Array of synonyms. | |
| 46 | + * @param {string} id Rule ID. | |
| 47 | + * @returns {Rule} Map entry | |
| 48 | + */ | |
| 49 | +const getRule = (synonyms = [], id = '') => { | |
| 20 | 50 | return { |
| 21 | 51 | id: id.length ? id : uuidv4(), |
| 22 | 52 | synonyms, |
| 23 | 53 | valid: true, |
| @@ -24,107 +54,218 @@ | ||
| 24 | 54 | }; |
| 25 | 55 | }; |
| 26 | 56 | |
| 27 | 57 | /** |
| 58 | + * Get a rule in Solr format. | |
| 59 | + * | |
| 60 | + * @param {Rule} rule Rule set. | |
| 61 | + * @param {Synonym[]} rule.synonyms Rule synonyms. | |
| 62 | + * @returns {string} | |
| 63 | + */ | |
| 64 | +const getSolr = ({ synonyms }) => { | |
| 65 | + const terms = synonyms.filter(isPrimary); | |
| 66 | + const replacements = synonyms.filter(isNotPrimary); | |
| 67 | + | |
| 68 | + const sides = [terms, replacements] | |
| 69 | + .map((side) => | |
| 70 | + side | |
| 71 | + .map((synonym) => synonym.value.trim()) | |
| 72 | + .filter((synonym) => !!synonym) | |
| 73 | + .join(', '), | |
| 74 | + ) | |
| 75 | + .filter((side) => side); | |
| 76 | + | |
| 77 | + return sides.join(' => '); | |
| 78 | +}; | |
| 79 | + | |
| 80 | +/** | |
| 81 | + * Get synonyms from a Solr line. | |
| 82 | + * | |
| 83 | + * @param {string} line Solr line. | |
| 84 | + * @returns {Synonym[]} | |
| 85 | + */ | |
| 86 | +const getSynonyms = (line) => { | |
| 87 | + const parts = line.split('=>').map((p, i, a) => { | |
| 88 | + const part = p | |
| 89 | + .split(',') | |
| 90 | + .map((v) => v.trim()) | |
| 91 | + .filter((v) => v); | |
| 92 | + | |
| 93 | + return part | |
| 94 | + .filter((v, i) => part.indexOf(v) === i) | |
| 95 | + .filter((v) => v) | |
| 96 | + .map((v) => ({ | |
| 97 | + label: v, | |
| 98 | + value: v, | |
| 99 | + primary: a.length === 2 && i === 0, | |
| 100 | + })); | |
| 101 | + }); | |
| 102 | + | |
| 103 | + return parts.flat(); | |
| 104 | +}; | |
| 105 | + | |
| 106 | +/** | |
| 107 | + * Determine whether a rule describes hyponyms. | |
| 108 | + * | |
| 109 | + * Hyponyms are rules where there is a single primary term and where the | |
| 110 | + * primary term is also included as a replacement. | |
| 111 | + * | |
| 112 | + * @param {Rule} rule Rule set. | |
| 113 | + * @param {Synonym[]} rule.synonyms Rule synonyms. | |
| 114 | + * @returns {boolean} | |
| 115 | + */ | |
| 116 | +const isHyponyms = (rule) => { | |
| 117 | + const hypernyms = rule.synonyms.filter(isPrimary); | |
| 118 | + | |
| 119 | + return ( | |
| 120 | + hypernyms.length === 1 && | |
| 121 | + rule.synonyms.filter(isNotPrimary).some((s) => hypernyms.some((h) => h.value === s.value)) | |
| 122 | + ); | |
| 123 | +}; | |
| 124 | + | |
| 125 | +/** | |
| 126 | + * Validate a new set of hyponyms. | |
| 127 | + * | |
| 128 | + * Hyponyms are valid if there is only one primary term and at least one | |
| 129 | + * replacement that is not also the primary term. | |
| 130 | + * | |
| 131 | + * This function is used before the hypernym is automatically injected as a | |
| 132 | + * hypernym, so make sure to use `isHyponyms` first to verify that the hypernym | |
| 133 | + * is included as a hyponym. | |
| 134 | + * | |
| 135 | + * @param {Array} synonyms Synonyms. | |
| 136 | + * @returns {boolean} | |
| 137 | + */ | |
| 138 | +const isHyponymsValid = (synonyms) => { | |
| 139 | + const hypernyms = synonyms.filter(isPrimary); | |
| 140 | + const hyponyms = synonyms | |
| 141 | + .filter(isNotPrimary) | |
| 142 | + .filter((s) => !hypernyms.some((h) => h.value === s.value)); | |
| 143 | + | |
| 144 | + return hypernyms.length === 1 && hyponyms.length > 0; | |
| 145 | +}; | |
| 146 | + | |
| 147 | +/** | |
| 148 | + * Determine whether a rule describes replacements. | |
| 149 | + * | |
| 150 | + * Replacements are rules where there are terms and replacements that do not | |
| 151 | + * otherwise describe hyponyms. | |
| 152 | + * | |
| 153 | + * @param {Rule} rule Rule set. | |
| 154 | + * @param {Synonym[]} rule.synonyms Rule synonyms. | |
| 155 | + * @returns {boolean} | |
| 156 | + */ | |
| 157 | +const isReplacements = ({ synonyms }) => { | |
| 158 | + return !isHyponyms({ synonyms }) && synonyms.some(isPrimary); | |
| 159 | +}; | |
| 160 | + | |
| 161 | +/** | |
| 162 | + * Validate a new set of replacements. | |
| 163 | + * | |
| 164 | + * Replacements are valid if there is at least one term and one replacement. | |
| 165 | + * | |
| 166 | + * @param {Array} synonyms Synonyms. | |
| 167 | + * @returns {boolean} | |
| 168 | + */ | |
| 169 | +const isReplacementsValid = (synonyms) => { | |
| 170 | + return !isHyponyms({ synonyms }) && synonyms.some(isPrimary) && synonyms.some(isNotPrimary); | |
| 171 | +}; | |
| 172 | + | |
| 173 | +/** | |
| 174 | + * Is a list of synonyms a synonyms rule set. | |
| 175 | + * | |
| 176 | + * | |
| 177 | + * @param {Rule} rule Rule set. | |
| 178 | + * @param {Synonym[]} rule.synonyms Rule synonyms. | |
| 179 | + * @returns {boolean} | |
| 180 | + */ | |
| 181 | +const isSynonyms = ({ synonyms }) => { | |
| 182 | + return synonyms.every(isNotPrimary); | |
| 183 | +}; | |
| 184 | + | |
| 185 | +/** | |
| 186 | + * Is a synonyms rule set valid. | |
| 187 | + * | |
| 188 | + * @param {Array} synonyms Rule synonyms. | |
| 189 | + * @returns {boolean} | |
| 190 | + */ | |
| 191 | +const isSynonymsValid = (synonyms) => { | |
| 192 | + return synonyms.length > 1 && synonyms.every(isNotPrimary); | |
| 193 | +}; | |
| 194 | + | |
| 195 | +/** | |
| 28 | 196 | * Reduce state to Solr spec. |
| 29 | 197 | * |
| 30 | - * @param {object} state Current state. | |
| 31 | - * @param {object[]} state.sets Array of synonym sets. | |
| 32 | - * @param {object[]} state.alternatives Array of alternative sets. | |
| 198 | + * @param {Rule[]} rules Array of rule sets. | |
| 33 | 199 | * @returns {string} new state |
| 34 | 200 | */ |
| 35 | -const reduceStateToSolr = ({ sets, alternatives }) => { | |
| 36 | - const synonymsList = []; | |
| 201 | +const getSolrFromRules = (rules) => { | |
| 202 | + const synonyms = rules.filter(isSynonyms).map(getSolr); | |
| 203 | + const hyponyms = rules.filter(isHyponyms).map(getSolr); | |
| 204 | + const replacements = rules.filter(isReplacements).map(getSolr); | |
| 37 | 205 | |
| 38 | - // Handle sets. | |
| 39 | - synonymsList.push('# Defined sets ( equivalent synonyms).'); | |
| 40 | - synonymsList.push(...sets.map(({ synonyms }) => synonyms.map(({ value }) => value).join(', '))); | |
| 206 | + const lines = [ | |
| 207 | + __('# Defined synonyms.', 'elasticpress'), | |
| 208 | + '', | |
| 209 | + ...synonyms, | |
| 210 | + '', | |
| 211 | + __('# Defined hyponyms.', 'elasticpress'), | |
| 212 | + '', | |
| 213 | + ...hyponyms, | |
| 214 | + '', | |
| 215 | + __('# Defined replacements.', 'elasticpress'), | |
| 216 | + '', | |
| 217 | + ...replacements, | |
| 218 | + '', | |
| 219 | + ]; | |
| 41 | 220 | |
| 42 | - // Handle alternatives. | |
| 43 | - synonymsList.push('\r'); | |
| 44 | - synonymsList.push('# Defined alternatives (explicit mappings).'); | |
| 45 | - synonymsList.push( | |
| 46 | - ...alternatives.map((alternative) => | |
| 47 | - alternative.synonyms.find((item) => item.primary && item.value.length) | |
| 48 | - ? alternative.synonyms | |
| 49 | - .find((item) => item.primary) | |
| 50 | - .value.concat(' => ') | |
| 51 | - .concat( | |
| 52 | - alternative.synonyms | |
| 53 | - .filter((i) => !i.primary) | |
| 54 | - .map(({ value }) => value) | |
| 55 | - .join(', '), | |
| 56 | - ) | |
| 57 | - : false, | |
| 58 | - ), | |
| 59 | - ); | |
| 60 | - | |
| 61 | - return synonymsList.filter(Boolean).join('\n'); | |
| 221 | + return lines.join('\n'); | |
| 62 | 222 | }; |
| 63 | 223 | |
| 64 | 224 | /** |
| 65 | 225 | * Reduce Solr text file to State. |
| 66 | 226 | * |
| 67 | - * @param {string} solr A string in the Solr parseable synonym format. | |
| 68 | - * @param {object} currentState The current sate. | |
| 69 | - * @returns {object} State | |
| 227 | + * @param {string} solr A string in the Solr parseable synonym format. | |
| 228 | + * @returns {Rule[]} State | |
| 70 | 229 | */ |
| 71 | -const reduceSolrToState = (solr, currentState) => { | |
| 72 | - /** | |
| 73 | - * Format token. | |
| 74 | - * | |
| 75 | - * @param {string} value The value. | |
| 76 | - * @param {boolean} primary Whether it's a primary. | |
| 77 | - * @returns {object} Formated token | |
| 78 | - */ | |
| 79 | - const formatToken = (value, primary = false) => { | |
| 80 | - return { | |
| 81 | - label: value, | |
| 82 | - value, | |
| 83 | - primary, | |
| 84 | - }; | |
| 85 | - }; | |
| 230 | +const getRulesFromSolr = (solr) => { | |
| 231 | + const rules = solr.split(/\r?\n/).reduce((rules, line) => { | |
| 232 | + if (line.indexOf('#') === 0) { | |
| 233 | + return rules; | |
| 234 | + } | |
| 86 | 235 | |
| 87 | - return { | |
| 88 | - ...currentState, | |
| 89 | - ...solr.split(/\r?\n/).reduce( | |
| 90 | - (newState, line) => { | |
| 91 | - if (line.indexOf('#') === 0 || !line.trim().length) { | |
| 92 | - return newState; | |
| 93 | - } | |
| 236 | + if (line.trim().length === 0) { | |
| 237 | + return rules; | |
| 238 | + } | |
| 94 | 239 | |
| 95 | - if (line.indexOf('=>') !== -1) { | |
| 96 | - const parts = line.split('=>'); | |
| 97 | - return { | |
| 98 | - ...newState, | |
| 99 | - alternatives: [ | |
| 100 | - ...newState.alternatives, | |
| 101 | - mapEntry([ | |
| 102 | - formatToken(parts[0].trim(), true), | |
| 103 | - ...parts[1] | |
| 104 | - .split(',') | |
| 105 | - .filter((v) => v.trim()) | |
| 106 | - .map((token) => formatToken(token.trim())), | |
| 107 | - ]), | |
| 108 | - ], | |
| 109 | - }; | |
| 110 | - } | |
| 240 | + const synonyms = getSynonyms(line); | |
| 241 | + const rule = getRule(synonyms); | |
| 111 | 242 | |
| 112 | - return { | |
| 113 | - ...newState, | |
| 114 | - sets: [ | |
| 115 | - ...newState.sets, | |
| 116 | - mapEntry([ | |
| 117 | - ...line | |
| 118 | - .split(',') | |
| 119 | - .filter((v) => v.trim()) | |
| 120 | - .map((token) => formatToken(token.trim())), | |
| 121 | - ]), | |
| 122 | - ], | |
| 123 | - }; | |
| 124 | - }, | |
| 125 | - { alternatives: [], sets: [] }, | |
| 126 | - ), | |
| 127 | - }; | |
| 243 | + rules.push(rule); | |
| 244 | + | |
| 245 | + return rules; | |
| 246 | + }, []); | |
| 247 | + | |
| 248 | + return rules; | |
| 128 | 249 | }; |
| 129 | 250 | |
| 130 | -export { reduceStateToSolr, reduceSolrToState, uuid, mapEntry }; | |
| 251 | +/** | |
| 252 | + * Generate universally unique identifier. | |
| 253 | + * | |
| 254 | + * @returns {string} A universally unique identifier | |
| 255 | + */ | |
| 256 | +const uuid = () => { | |
| 257 | + return uuidv4(); | |
| 258 | +}; | |
| 259 | + | |
| 260 | +export { | |
| 261 | + isHyponyms, | |
| 262 | + isHyponymsValid, | |
| 263 | + isReplacements, | |
| 264 | + isReplacementsValid, | |
| 265 | + isSynonyms, | |
| 266 | + isSynonymsValid, | |
| 267 | + getRule, | |
| 268 | + getRulesFromSolr, | |
| 269 | + getSolrFromRules, | |
| 270 | + uuid, | |
| 271 | +}; | |