UNPKG

pdfmark.js

Version:

Highlight keywords in pdfjs svg output. Based on mark.js.

372 lines (358 loc) 14.2 kB
/** * Creates regular expressions based on specified settings * @example * new RegExpCreator({caseSensitive: true, diacritics: false}).create('lorem'); * // => /()(lorem)/gm */ class RegExpCreator { /** * @typedef RegExpCreator~accuracyObj * @type {object.<string>} * @property {string} value - An accuracy string value * @property {string[]} limiters - A custom array of limiters. For example * <code>["-", ","]</code> */ /** * @typedef RegExpCreator~accuracy * @type {string} * @property {"partially"|"complementary"|"exactly"|RegExpCreator~accuracyObj} * [accuracy="partially"] - Either one of the following string values: * <ul> * <li><i>partially</i>: When searching for "lor" only "lor" inside * "lorem" will be marked</li> * <li><i>complementary</i>: When searching for "lor" the whole word * "lorem" will be marked</li> * <li><i>exactly</i>: When searching for "lor" only those exact words * will be marked. In this example nothing inside "lorem". This value * is equivalent to the previous option <i>wordBoundary</i></li> * </ul> * Or an object containing two properties: * <ul> * <li><i>value</i>: One of the above named string values</li> * <li><i>limiters</i>: A custom array of string limiters for accuracy * "exactly" or "complementary"</li> * </ul> */ /** * @typedef RegExpCreator~wildcards * @type {string} * @property {"disabled"|"enabled"|"withSpaces"} * [wildcards="disabled"] - Set to any of the following string values: * <ul> * <li><i>disabled</i>: Disable wildcard usage</li> * <li><i>enabled</i>: When searching for "lor?m", the "?" will match zero * or one non-space character (e.g. "lorm", "loram", "lor3m", etc). When * searching for "lor*m", the "*" will match zero or more non-space * characters (e.g. "lorm", "loram", "lor123m", etc).</li> * <li><i>withSpaces</i>: When searching for "lor?m", the "?" will * match zero or one space or non-space character (e.g. "lor m", "loram", * etc). When searching for "lor*m", the "*" will match zero or more space * or non-space characters (e.g. "lorm", "lore et dolor ipsum", "lor: m", * etc).</li> * </ul> */ /** * @typedef RegExpCreator~ignorePunctuation * @type {string[]} * @property {string} The strings in this setting will contain punctuation * marks that will be ignored: * <ul> * <li>These punctuation marks can be between any characters, e.g. setting * this option to <code>["'"]</code> would match "Worlds", "World's" and * "Wo'rlds"</li> * <li>One or more apostrophes between the letters would still produce a * match (e.g. "W'o''r'l'd's").</li> * <li>A typical setting for this option could be as follows: * <pre>ignorePunctuation: ":;.,-–—‒_(){}[]!'\"+=".split(""),</pre> This * setting includes common punctuation as well as a minus, en-dash, * em-dash and figure-dash * ({@link https://en.wikipedia.org/wiki/Dash#Figure_dash ref}), as well * as an underscore.</li> * </ul> */ /** * @typedef RegExpCreator~options * @type {object.<string>} * @property {boolean} [diacritics=true] - If diacritic characters should be * matched. ({@link https://en.wikipedia.org/wiki/Diacritic Diacritics}) * @property {object} [synonyms] - An object with synonyms. The key will be * a synonym for the value and the value for the key * @property {RegExpCreator~accuracy} [accuracy] * @property {boolean} [caseSensitive=false] - Whether to search case * sensitive * @property {boolean} [ignoreJoiners=false] - Whether to ignore word * joiners inside of key words. These include soft-hyphens, zero-width * space, zero-width non-joiners and zero-width joiners. * @property {RegExpCreator~ignorePunctuation} [ignorePunctuation] * @property {RegExpCreator~wildcards} [wildcards] */ /** * @param {RegExpCreator~options} [options] - Optional options object */ constructor(options) { this.opt = Object.assign({}, { 'diacritics': true, 'synonyms': {}, 'accuracy': 'partially', 'caseSensitive': false, 'ignoreJoiners': false, 'ignorePunctuation': [], 'wildcards': 'disabled' }, options); } /** * Creates a regular expression to match the specified search term considering * the available option settings * @param {string} str - The search term to be used * @return {RegExp} */ create(str) { if (this.opt.wildcards !== 'disabled') { str = this.setupWildcardsRegExp(str); } str = this.escapeStr(str); if (Object.keys(this.opt.synonyms).length) { str = this.createSynonymsRegExp(str); } if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { str = this.setupIgnoreJoinersRegExp(str); } if (this.opt.diacritics) { str = this.createDiacriticsRegExp(str); } str = this.createMergedBlanksRegExp(str); if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { str = this.createJoinersRegExp(str); } if (this.opt.wildcards !== 'disabled') { str = this.createWildcardsRegExp(str); } str = this.createAccuracyRegExp(str); return new RegExp(str, `gm${this.opt.caseSensitive ? '' : 'i'}`); } /** * Escapes a string for usage within a regular expression * @param {string} str - The string to escape * @return {string} */ escapeStr(str) { // eslint-disable-next-line no-useless-escape return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'); } /** * Creates a regular expression string to match the defined synonyms * @param {string} str - The search term to be used * @return {string} */ createSynonymsRegExp(str) { const syn = this.opt.synonyms, sens = this.opt.caseSensitive ? '' : 'i', // add replacement character placeholder before and after the // synonym group joinerPlaceholder = this.opt.ignoreJoiners || this.opt.ignorePunctuation.length ? '\u0000' : ''; for (let index in syn) { if (syn.hasOwnProperty(index)) { const value = syn[index], k1 = this.opt.wildcards !== 'disabled' ? this.setupWildcardsRegExp(index) : this.escapeStr(index), k2 = this.opt.wildcards !== 'disabled' ? this.setupWildcardsRegExp(value) : this.escapeStr(value); if (k1 !== '' && k2 !== '') { str = str.replace( new RegExp( `(${this.escapeStr(k1)}|${this.escapeStr(k2)})`, `gm${sens}` ), joinerPlaceholder + `(${this.processSynonyms(k1)}|` + `${this.processSynonyms(k2)})` + joinerPlaceholder ); } } } return str; } /** * Setup synonyms to work with ignoreJoiners and or ignorePunctuation * @param {string} str - synonym key or value to process * @return {string} - processed synonym string */ processSynonyms(str) { if (this.opt.ignoreJoiners || this.opt.ignorePunctuation.length) { str = this.setupIgnoreJoinersRegExp(str); } return str; } /** * Sets up the regular expression string to allow later insertion of wildcard * regular expression matches * @param {string} str - The search term to be used * @return {string} */ setupWildcardsRegExp(str) { // replace single character wildcard with unicode 0001 str = str.replace(/(?:\\)*\?/g, val => { return val.charAt(0) === '\\' ? '?' : '\u0001'; }); // replace multiple character wildcard with unicode 0002 return str.replace(/(?:\\)*\*/g, val => { return val.charAt(0) === '\\' ? '*' : '\u0002'; }); } /** * Sets up the regular expression string to allow later insertion of wildcard * regular expression matches * @param {string} str - The search term to be used * @return {string} */ createWildcardsRegExp(str) { // default to "enable" (i.e. to not include spaces) // "withSpaces" uses `[\\S\\s]` instead of `.` because the latter // does not match new line characters let spaces = this.opt.wildcards === 'withSpaces'; return str // replace unicode 0001 with a RegExp class to match any single // character, or any single non-whitespace character depending // on the setting .replace(/\u0001/g, spaces ? '[\\S\\s]?' : '\\S?') // replace unicode 0002 with a RegExp class to match zero or // more characters, or zero or more non-whitespace characters // depending on the setting .replace(/\u0002/g, spaces ? '[\\S\\s]*?' : '\\S*'); } /** * Sets up the regular expression string to allow later insertion of * designated characters (soft hyphens & zero width characters) * @param {string} str - The search term to be used * @return {string} */ setupIgnoreJoinersRegExp(str) { // adding a "null" unicode character as it will not be modified by the // other "create" regular expression functions return str.replace(/[^(|)\\]/g, (val, indx, original) => { // don't add a null after an opening "(", around a "|" or before // a closing "(", or between an escapement (e.g. \+) let nextChar = original.charAt(indx + 1); if (/[(|)\\]/.test(nextChar) || nextChar === '') { return val; } else { return val + '\u0000'; } }); } /** * Creates a regular expression string to allow ignoring of designated * characters (soft hyphens, zero width characters & punctuation) based on the * specified option values of <code>ignorePunctuation</code> and * <code>ignoreJoiners</code> * @param {string} str - The search term to be used * @return {string} */ createJoinersRegExp(str) { let joiner = []; const ignorePunctuation = this.opt.ignorePunctuation; if (Array.isArray(ignorePunctuation) && ignorePunctuation.length) { joiner.push(this.escapeStr(ignorePunctuation.join(''))); } if (this.opt.ignoreJoiners) { // u+00ad = soft hyphen // u+200b = zero-width space // u+200c = zero-width non-joiner // u+200d = zero-width joiner joiner.push('\\u00ad\\u200b\\u200c\\u200d'); } return joiner.length ? str.split(/\u0000+/).join(`[${joiner.join('')}]*`) : str; } /** * Creates a regular expression string to match diacritics * @param {string} str - The search term to be used * @return {string} */ createDiacriticsRegExp(str) { const sens = this.opt.caseSensitive ? '' : 'i', dct = this.opt.caseSensitive ? [ 'aàáảãạăằắẳẵặâầấẩẫậäåāą', 'AÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ', 'cçćč', 'CÇĆČ', 'dđď', 'DĐĎ', 'eèéẻẽẹêềếểễệëěēę', 'EÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ', 'iìíỉĩịîïī', 'IÌÍỈĨỊÎÏĪ', 'lł', 'LŁ', 'nñňń', 'NÑŇŃ', 'oòóỏõọôồốổỗộơởỡớờợöøō', 'OÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ', 'rř', 'RŘ', 'sšśșş', 'SŠŚȘŞ', 'tťțţ', 'TŤȚŢ', 'uùúủũụưừứửữựûüůū', 'UÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ', 'yýỳỷỹỵÿ', 'YÝỲỶỸỴŸ', 'zžżź', 'ZŽŻŹ' ] : [ 'aàáảãạăằắẳẵặâầấẩẫậäåāąAÀÁẢÃẠĂẰẮẲẴẶÂẦẤẨẪẬÄÅĀĄ', 'cçćčCÇĆČ', 'dđďDĐĎ', 'eèéẻẽẹêềếểễệëěēęEÈÉẺẼẸÊỀẾỂỄỆËĚĒĘ', 'iìíỉĩịîïīIÌÍỈĨỊÎÏĪ', 'lłLŁ', 'nñňńNÑŇŃ', 'oòóỏõọôồốổỗộơởỡớờợöøōOÒÓỎÕỌÔỒỐỔỖỘƠỞỠỚỜỢÖØŌ', 'rřRŘ', 'sšśșşSŠŚȘŞ', 'tťțţTŤȚŢ', 'uùúủũụưừứửữựûüůūUÙÚỦŨỤƯỪỨỬỮỰÛÜŮŪ', 'yýỳỷỹỵÿYÝỲỶỸỴŸ', 'zžżźZŽŻŹ' ]; let handled = []; str.split('').forEach(ch => { dct.every(dct => { // Check if the character is inside a diacritics list if (dct.indexOf(ch) !== -1) { // Check if the related diacritics list was not // handled yet if (handled.indexOf(dct) > -1) { return false; } // Make sure that the character OR any other // character in the diacritics list will be matched str = str.replace( new RegExp(`[${dct}]`, `gm${sens}`), `[${dct}]` ); handled.push(dct); } return true; }); }); return str; } /** * Creates a regular expression string that merges whitespaces characters * including subsequent ones into a single pattern, one or multiple * whitespaces * @param {string} str - The search term to be used * @return {string} */ createMergedBlanksRegExp(str) { return str.replace(/[\s]+/gmi, '[\\s]+'); } /** * Creates a regular expression string to match the specified string with the * defined accuracy. As in the regular expression of "exactly" can be a group * containing a blank at the beginning, all regular expressions will be * created with two groups. The first group can be ignored (may contain * the said blank), the second contains the actual match * @param {string} str - The searm term to be used * @return {string} */ createAccuracyRegExp(str) { const chars = '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~¡¿'; let acc = this.opt.accuracy, val = typeof acc === 'string' ? acc : acc.value, ls = typeof acc === 'string' ? [] : acc.limiters, lsJoin = ''; ls.forEach(limiter => { lsJoin += `|${this.escapeStr(limiter)}`; }); switch (val) { case 'partially': default: return `()(${str})`; case 'complementary': lsJoin = '\\s' + (lsJoin ? lsJoin : this.escapeStr(chars)); return `()([^${lsJoin}]*${str}[^${lsJoin}]*)`; case 'exactly': return `(^|\\s${lsJoin})(${str})(?=$|\\s${lsJoin})`; } } } export default RegExpCreator;