跳到主要内容

搜索高亮HightlightMark

阅读需 8 分钟
MongoRolls

需求

根据关键词对内容进行检索,将完全匹配或者部分匹配度字体进行高亮显示

提示

搜索的内容中文或者英文

实现思路

对关键词进行拆分,根据需求这里是后端处理后返回,

e g: 特征角 --> 特征,特征角,特征

返回的字符数组,我们肯定是根据长度从长到短去进行正则替换

不过要对传入的string进行过滤,不如HTML实体,Latex公式,HTML标签等

最好封装成一个stringMark函数后续可以多次使用

拆分

stringMark(text = '', wordRegex: any, opt: any = {}) {
if (!wordRegex) return text;

// 初始化配置对象
opt = { ...this.config, ...opt };
///拆分正则
// this.splitRegex /(<[^>]+>|\$\$.+?\$\$|\$.+?\$|&[a-zA-Z]{2,8};)/gim
const splitRegex = opt.splitRegex || this.splitRegex;

const strArr = text.split(splitRegex);
const newArr: string[] = [];

strArr.forEach((str) => {
if (!str) return;
// 检查str是否是过滤项
if (includesFilterRegex(str)) {
newArr.push(str);
} else {
str = str.replace(
createWordRegex(wordRegex),
`<${opt.tag} class="${opt.className}">$&</${opt.tag}>`
);
newArr.push(str);
}
});
const newText = newArr.join('');
return newText;
}

最终代码实现

limiters: [
',',
'.',
'-',
'!',
'"',
'\'',
'(',
')',
'%',
';',
'*',
'+',
'?',
'^',
'\\',
'|'
],
// 拆分正则
splitRegex: /(<[^>]+>|\$\$.+?\$\$|\$.+?\$|&[a-zA-Z]{2,8};)/gim,

// 检查是否包含过滤项
includesFilterRegex(str: string) {
splitRegex.lastIndex = 0; // 清除上次匹配位置的缓存
return splitRegex.test(str);
}
// 是否是英文文本
enRegex: /^[\\x00-\\xff]+$/gi,
enRegexTest(str: string) {
this.enRegex.lastIndex = 0; // 清除上次匹配位置的缓存
return this.enRegex.test(str);
},

// 排序
sortByLength(arry: any[]) {
return arry.sort((a, b) =>
// 对相同长度的元素排序a-z
// eslint-disable-next-line no-nested-ternary
(a.length === b.length ? a > b ? 1 : -1 : b.length - a.length));
},
// 处理转义字符串
escapeStr(str: string) {
// eslint-disable-next-line no-useless-escape
return str.trim().replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
},
/*
创建一个正则表达式字符串,使指定的字符串与定义的精度相匹配。正如正则表达式“exactly”可以是一个开头包含一个空格的组,所有正则表达式都将创建两个组。第一组* 可以忽略(可能包含所述的空格),第二组包含实际匹配的内容
*/
createAccuracyRegExp(str: string) {
if (!str) return str;
const joinStr = this.limiters.reduce(
// eslint-disable-next-line no-return-assign
(prev, next) => prev += `|${escapeStr(next)}`,
''
);
return `(^|\\s${joinStr})(${str})(?=$|\\s${joinStr})`;
},

// 创建匹配的分词正则
createWordRegex(keyWords: any[]) {
let wordRegex: any = '';
// 处理keyWords
if (typeof keyWords === 'string') {
keyWords = [keyWords];
} else if (!Array.isArray(keyWords) || !keyWords.length) {
return wordRegex;
}
// 按字符长度倒序,解决搜索关键词有包含关系
// 长度相同按字典序号
keyWords = sortByLength(keyWords);

const enList: any[] = []; // 英文
const zhList: any[] = []; // 中文
keyWords.forEach((text) => {
if (this.mathRegexTest(text)) return;
text = this.escapeStr(text); // 转译特殊字符
if (this.enRegexTest(text)) {
enList.push(text);
} else {
zhList.push(text);
}
});

if (zhList.length) {
wordRegex = zhList.join('|');
}
if (enList.length) {
wordRegex += `${wordRegex ? '|' : ''}${this.createAccuracyRegExp(
enList.join('|')
)}`;
}
if (wordRegex) {
wordRegex = new RegExp(wordRegex, 'gim');
}
return wordRegex;
},

// 实现搜索字符串高亮
stringMark(text = '', wordRegex: any, opt: any = {}) {
if (!wordRegex) return text;

// 初始化配置对象
opt = { ...this.config, ...opt };
///拆分正则
// this.splitRegex /(<[^>]+>|\$\$.+?\$\$|\$.+?\$|&[a-zA-Z]{2,8};)/gim
const splitRegex = opt.splitRegex || this.splitRegex;

const strArr = text.split(splitRegex);
const newArr: string[] = [];

strArr.forEach((str) => {
if (!str) return;
// 检查str是否是过滤项
if (includesFilterRegex(str)) {
newArr.push(str);
} else {
str = str.replace(
createWordRegex(wordRegex),
`<${opt.tag} class="${opt.className}">$&</${opt.tag}>`
);
newArr.push(str);
}
});
const newText = newArr.join('');
return newText;
},
/* eslint-disable max-len */
const HighlightMark = {
// 默认配置
config: {
tag: 'mark', // html标签
className: 'highlight__mark', // html高亮类名
accuracy: 'exactly' // 匹配的程度,partially:部分匹配,exactly: 全部匹配,英文单词有边界
},
// 虚拟dom节点,用于转译特殊字符
virtualDom: document.createElement('div'),
// 创建虚拟dom节点
createVirtualDom() {
if (this.virtualDom) return;
const fragment = document.createDocumentFragment();
fragment.appendChild(this.virtualDom);
},
limiters: [
',',
'.',
'-',
'!',
'"',
'\'',
'(',
')',
'%',
';',
'*',
'+',
'?',
'^',
'\\',
'|'
],
// 拆分正则
splitRegex: /(<[^>]+>|\$\$.+?\$\$|\$.+?\$|&[a-zA-Z]{2,8};)/gim,
mathRegex: /(\$\$.+?\$\$|\$.+?\$)/gim,
mathRegexTest(str: string) {
this.mathRegex.lastIndex = 0; // 清除上次匹配位置的缓存
return this.mathRegex.test(str);
},
// 是否是英文文本
enRegex: /^[\\x00-\\xff]+$/gi,
enRegexTest(str: string) {
this.enRegex.lastIndex = 0; // 清除上次匹配位置的缓存
return this.enRegex.test(str);
},
// 特殊符号
chars: '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~¡¿',
// 创建特殊符号匹配正则
createCharsRegex() {
const joinStr = this.chars
.split('')
// eslint-disable-next-line no-return-assign
.reduce((prev, next) => prev += `${prev ? '|' : ''}\\${next}`, '');
return new RegExp(`[${joinStr}]`, 'g');
},
// 需要过滤的元素匹配的正则列表
filterRegexList: [
/^<[^>]+>$/i,
/^\$\$.+?\$\$$/,
/^\$.+?\$$/,
/^&[a-zA-Z]{2,8};$/
],
// 是否存在过滤列表中
includesFilterRegex(str: string) {
// return this.filterRegexList.some(regex => regex.test(str))
this.splitRegex.lastIndex = 0; // 清除上次匹配位置的缓存
return this.splitRegex.test(str);
},
// 替换空格
createMergedBlanksRegExp(str: string) {
return str.replace(/[\s]+/gim, '$&');
},
/**
* 转义字符串以便在正则表达式中使用
* @param {string} str - The string to escape
* @return {string}
*/
escapeStr(str: string) {
// eslint-disable-next-line no-useless-escape
return str.trim().replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
},
/**
* 从最长的项到最短的项进行排序
* @param {array} arry - The array to sort
* @return {array}
*/
sortByLength(arry: any[]) {
return arry.sort((a, b) =>
// 对相同长度的元素排序a-z
// eslint-disable-next-line no-nested-ternary
(a.length === b.length ? a > b ? 1 : -1 : b.length - a.length));
},
/**
* 创建一个正则表达式字符串,使指定的字符串与定义的精度相匹配。正如正则表达式“exactly”可以是一个开头包含一个空格的组,所有正则表达式都将创建两个组。第一组* 可以忽略(可能包含所述的空格),第二组包含实际匹配的内容
* @param {string} str - 字符串
* @param {string} accuracy - 正则匹配的模式,主要用于英文单词的边界
* @return {string}
*/
createAccuracyRegExp(str: string) {
if (!str) return str;
const joinStr = this.limiters.reduce(
// eslint-disable-next-line no-return-assign
(prev, next) => prev += `|${this.escapeStr(next)}`,
''
);
return `(^|\\s${joinStr})(${str})(?=$|\\s${joinStr})`;
},
// 创建匹配的分词正则
createWordRegex(keyWords: any[]) {
let wordRegex: any = '';
// 处理keyWords
if (typeof keyWords === 'string') {
keyWords = [keyWords];
} else if (!Array.isArray(keyWords) || !keyWords.length) {
return wordRegex;
}
// 按字符长度倒序,解决搜索关键词有包含关系
keyWords = this.sortByLength(keyWords);

const enList: any[] = []; // 英文
const zhList: any[] = []; // 中文
keyWords.forEach((text) => {
if (this.mathRegexTest(text)) return;
text = this.escapeStr(text); // 转译特殊字符
if (this.enRegexTest(text)) {
enList.push(text);
} else {
zhList.push(text);
}
});

if (zhList.length) {
wordRegex = zhList.join('|');
}
if (enList.length) {
wordRegex += `${wordRegex ? '|' : ''}${this.createAccuracyRegExp(
enList.join('|')
)}`;
}
if (wordRegex) {
wordRegex = new RegExp(wordRegex, 'gim');
}
return wordRegex;
},
/**
* 字符串匹配标红
* @param {string} text 需要处理的文本
* @param {string|string[]} keyWords // 需要处理的分词
* @param {object} opt // 配置对象
*/
stringMark(text = '', wordRegex: any, opt: any = {}) {
if (!wordRegex) return text;

// 初始化配置对象
opt = { ...this.config, ...opt };
const splitRegex = opt.splitRegex || this.splitRegex;

const strArr = text.split(splitRegex);
const newArr: string[] = [];

strArr.forEach((str) => {
if (!str) return;
if (this.includesFilterRegex(str)) {
newArr.push(str);
} else {
str = str.replace(
this.createWordRegex(wordRegex),
`<${opt.tag} class="${opt.className}">$&</${opt.tag}>`
);
newArr.push(str);
}
});
const newText = newArr.join('');
return newText;
},
/**
* 试题数据字符串正则匹配替换
* @param {string} text 需要处理的文本
* @param {regex} wordRegex // 需要处理的分词正则
* @return {string}
*/
questionMark(text = '', wordRegex: any) {
if (!wordRegex || !text) return text;
this.virtualDom.innerHTML = text;
text = this.virtualDom.innerHTML;
const strArr = text.split(this.splitRegex);
// 调试
// console.log('start: ', strArr, text)
const newArr: string[] = [];
const opt = this.config;
strArr.forEach((str) => {
if (!str) return;
if (this.includesFilterRegex(str)) {
newArr.push(str);
} else {
// console.log('start---string>>>>', str)
str = str.replace(
wordRegex,
`<${opt.tag} class="${opt.className}">$&</${opt.tag}>`
);
// console.log('end---string>>>>', str)
newArr.push(str);
}
});
const newText = newArr.join('');
// 调试
// console.log('end: ', newText, newArr)
return newText;
},
// 试题数据根据attrsKey进行标红处理
transfromQuestion(options: any = {}) {
const { quesition, wordRegex, attrsKey } = options;
attrsKey.forEach((key: string) => {
const keyValue = quesition[key];
if (!keyValue) return;
// 不同的字段,对应的数据类型不一致
switch (typeof keyValue) {
case 'object':
if (Array.isArray(keyValue)) {
keyValue.forEach((item, index) => {
// answer 二维数组
if (Array.isArray(item)) {
// 判断题不需要处理answer对应的数据,会影响试题SDK渲染
if (key === 'answer' && quesition.logicQuesTypeId !== '5') {
keyValue[index] = item.map((text) =>
this.questionMark(text, wordRegex));
} else if (key === 'answerOptionList') {
// answerOptionList 选项
item.forEach((obj) => {
obj.content = this.questionMark(obj.content, wordRegex);
});
}
} else if (typeof item === 'object') {
// optionAnalysisList 选项解析
item.analysis = (item.analysis || []).map(
(text: string | undefined) =>
this.questionMark(text, wordRegex)
);
} else if (typeof item === 'string') {
keyValue[index] = this.questionMark(item, wordRegex);
}
});
} else if (typeof keyValue === 'string') {
quesition[key] = this.questionMark(keyValue, wordRegex);
}
break;
default:
quesition[key] = this.questionMark(quesition[key], wordRegex);
break;
}
});
const { childList } = quesition;
if (Array.isArray(childList) && childList.length) {
quesition.childList = this.forEachQuestion({
...options,
queList: childList
});
}
},
// 遍历试题数据列表
forEachQuestion(options: any = {}) {
const { queList = [], wordRegex, attrsKey = [] } = options;
queList.forEach((que: any) => {
this.transfromQuestion({
quesition: que,
wordRegex,
attrsKey
});
});
return queList;
},
mark(options: any = {}) {
const { queList = [], attrsKey = [], keyWords } = options;

const wordRegex = this.createWordRegex(keyWords);

// if (!queList.length || !wordRegex instanceof RegExp) return queList;
if (!queList.length) return queList;

// 如果没有需要处理的字段,则设置题干
if (!attrsKey.length) {
attrsKey.push(
'content',
'answerOptionList',
'answer',
'analysis',
'optionAnalysisList',
'stem', // 编程试题题目描述
'title', // 编程试题标题
'teacherRemark' // 教法备注
);
}

this.createVirtualDom();
return this.forEachQuestion({
queList,
attrsKey,
wordRegex
});
},
// 设置默认配置
setConfig(config = {}) {
this.config = { ...this.config, ...config };
},
// 初始化样式
initStyle() {
const styleDom = document.getElementById('highlight__mark');
if (styleDom) return;
const style = document.createElement('style');
style.type = 'text/css';
style.id = 'highlight__mark';
style.innerHTML =
'.highlight__mark {color: #eb381c;background: inherit;font-style: normal;font-weight: normal;}';
document.head.appendChild(style);
}
};
export default HighlightMark;

// const text =
// '已知非零向量才$$ overrightarrow _lbrc_ a _rbrc_ $$$$ overrightarrow _lbrc_ b _rbrc_ $$满足|$$| overrightarrow _lbrc_ a _rbrc_ | _eq_ 2| overrightarrow _lbrc_ b _rbrc_ |'

// console.log(HighlightMark.mathRegexTest(text))

扩展

纯css实现HightlightMark效果

Loading Comments...