coveo-search-ui
Version:
Coveo JavaScript Search Framework
321 lines (280 loc) • 9.6 kB
text/typescript
import { Utils } from './Utils';
import { IHighlight } from '../rest/Highlight';
import { Assert } from '../misc/Assert';
import * as _ from 'underscore';
import { $$ } from './Dom';
export interface IStringHole {
begin: number;
size: number;
replacementSize: number;
}
export class StringAndHoles {
value: string;
holes: IStringHole[];
static SHORTEN_END: string = '...';
static WORD_SHORTER: number = 10;
static replace(str: string, find: string, replace: string): StringAndHoles {
const strAndHoles = new StringAndHoles();
if (Utils.isNullOrEmptyString(str)) {
return strAndHoles;
}
let index = str.lastIndexOf(find);
if (index == -1) {
strAndHoles.value = str;
return strAndHoles;
}
const holes: IStringHole[] = [];
while (index >= 0) {
const hole = <IStringHole>{
begin: index,
size: find.length,
replacementSize: replace.length
};
holes.push(hole);
str = str.slice(0, index) + replace + str.slice(index + find.length);
index = str.lastIndexOf(find);
}
strAndHoles.holes = holes;
strAndHoles.value = str;
return strAndHoles;
}
/**
* Shorten the passed path intelligently (path-aware).
* Works with *local paths* and *network paths*
* @param uriOrig The path to shorten
* @param length The length to which the path will be shortened.
*/
static shortenPath(uriOrig: string, length: number): StringAndHoles {
const strAndHoles = new StringAndHoles();
let uri = uriOrig;
if (Utils.isNullOrEmptyString(uri) || uri.length <= length) {
strAndHoles.value = uri;
return strAndHoles;
}
const holes: IStringHole[] = [];
let first = -1;
if (Utils.stringStartsWith(uri, '\\\\')) {
first = uri.indexOf('\\', first + 2);
} else {
first = uri.indexOf('\\');
}
if (first !== -1) {
let removed = 0;
let next = uri.indexOf('\\', first + 1);
while (next !== -1 && uri.length - removed + StringAndHoles.SHORTEN_END.length > length) {
removed = next - first - 1;
next = uri.indexOf('\\', next + 1);
}
if (removed > 0) {
uri = uri.slice(0, first + 1) + StringAndHoles.SHORTEN_END + uri.slice(removed);
const hole = <IStringHole>{
begin: first + 1,
size: removed - StringAndHoles.SHORTEN_END.length,
replacementSize: StringAndHoles.SHORTEN_END.length
};
holes.push(hole);
}
}
if (uri.length > length) {
const over = uri.length - length + StringAndHoles.SHORTEN_END.length;
const start = uri.length - over;
uri = uri.slice(0, start) + StringAndHoles.SHORTEN_END;
const hole = <IStringHole>{
begin: start,
size: over,
replacementSize: StringAndHoles.SHORTEN_END.length
};
holes.push(hole);
}
strAndHoles.holes = holes;
strAndHoles.value = uri;
return strAndHoles;
}
/**
* Shorten the passed string.
* @param toShortenOrig The string to shorten
* @param length The length to which the string will be shortened.
* @param toAppend The string to append at the end (usually, it is set to '...')
*/
static shortenString(toShortenOrig: string, length: number = 200, toAppend?: string): StringAndHoles {
const toShorten = toShortenOrig;
toAppend = Utils.toNotNullString(toAppend);
const strAndHoles = new StringAndHoles();
if (Utils.isNullOrEmptyString(toShorten) || length <= toAppend.length) {
strAndHoles.value = toShorten;
return strAndHoles;
}
if (toShorten.length <= length) {
strAndHoles.value = toShorten;
return strAndHoles;
}
let str = toShorten;
length = length - toAppend.length;
str = str.slice(0, length);
if (toShorten.charAt(str.length) !== ' ') {
const pos = str.lastIndexOf(' ');
if (pos !== -1 && str.length - pos < StringAndHoles.WORD_SHORTER) {
str = str.slice(0, pos);
}
}
const holes: IStringHole[] = [];
holes[0] = <IStringHole>{
begin: str.length,
size: toShorten.length - str.length,
replacementSize: toAppend.length
};
str += toAppend;
strAndHoles.value = str;
strAndHoles.holes = holes;
return strAndHoles;
}
/**
* Shorten the passed URI intelligently (path-aware).
* @param toShortenOrig The URI to shorten
* @param length The length to which the URI will be shortened.
*/
static shortenUri(uri: string, length: number): StringAndHoles {
const strAndHoles = new StringAndHoles();
if (Utils.isNullOrEmptyString(uri) || uri.length <= length) {
strAndHoles.value = uri;
return strAndHoles;
}
const holes: IStringHole[] = [];
let first = uri.indexOf('//');
if (first !== -1) {
first = uri.indexOf('/', first + 2);
}
if (first !== -1) {
let removed = 0;
let next = uri.indexOf('/', first + 1);
while (next !== -1 && uri.length - removed + StringAndHoles.SHORTEN_END.length > length) {
removed = next - first - 1;
next = uri.indexOf('/', next + 1);
}
if (removed > 0) {
uri = uri.slice(0, first + 1) + StringAndHoles.SHORTEN_END + uri.slice(first + 1 + removed);
const hole = <IStringHole>{
begin: first + 1,
size: removed,
replacementSize: StringAndHoles.SHORTEN_END.length
};
holes.push(hole);
}
}
if (uri.length > length) {
const over = uri.length - length + StringAndHoles.SHORTEN_END.length;
const start = uri.length - over;
uri = uri.slice(0, start) + StringAndHoles.SHORTEN_END;
const hole = <IStringHole>{
begin: start,
size: over,
replacementSize: StringAndHoles.SHORTEN_END.length
};
holes.push(hole);
}
strAndHoles.holes = holes;
strAndHoles.value = uri;
return strAndHoles;
}
}
export class HighlightUtils {
/**
* Highlight the passed string using specified highlights and holes.
* @param content The string to highlight items in.
* @param highlights The highlighted positions to highlight in the string.
* @param holes Possible holes which are used to skip highlighting.
* @param cssClass The css class to use on the highlighting `span`.
*/
static highlightString(content: string, highlights: IHighlight[], holes: IStringHole[], cssClass: string): string {
Assert.isNotUndefined(highlights);
Assert.isNotNull(highlights);
Assert.isNonEmptyString(cssClass);
if (Utils.isNullOrEmptyString(content)) {
return content;
}
const maxIndex = content.length;
let highlighted = '';
let last = 0;
for (let i = 0; i < highlights.length; i++) {
const highlight = highlights[i];
let start: number = highlight.offset;
let end: number = start + highlight.length;
if (holes !== null) {
let skip = false;
for (let j = 0; j < holes.length; j++) {
const hole = holes[j];
const holeBegin = hole.begin;
const holeEnd = holeBegin + hole.size;
if (start < holeBegin && end >= holeBegin && end < holeEnd) {
end = holeBegin;
} else if (start >= holeBegin && end < holeEnd) {
skip = true;
break;
} else if (start >= holeBegin && start < holeEnd && end >= holeEnd) {
start = holeBegin + hole.replacementSize;
end -= hole.size - hole.replacementSize;
} else if (start < holeBegin && end >= holeEnd) {
end -= hole.size - hole.replacementSize;
} else if (start >= holeEnd) {
const offset = hole.size - hole.replacementSize;
start -= offset;
end -= offset;
}
}
if (skip || start === end) {
continue;
}
}
if (end > maxIndex) {
break;
}
if (last <= start) {
highlighted += _.escape(content.slice(last, start));
highlighted += `<span class="${cssClass}"`;
if (highlight.dataHighlightGroup) {
highlighted += ` data-highlight-group="${highlight.dataHighlightGroup.toString()}"`;
}
if (highlight.dataHighlightGroupTerm) {
highlighted += ` data-highlight-group-term="${highlight.dataHighlightGroupTerm}"`;
}
highlighted += '>';
highlighted += _.escape(content.slice(start, end));
highlighted += '</span>';
}
last = end;
}
if (last != maxIndex) {
highlighted += _.escape(content.slice(last));
}
return highlighted;
}
static highlight(text: string, match: string, className: string): HTMLElement[] {
const elements: HTMLElement[] = [];
const regex = RegExp(match, 'i');
const parts = text.split(regex);
const lastPart = parts.pop();
let index = 0;
parts.forEach(part => {
if (part) {
const unhighlighted = createSpanWithText(part);
elements.push(unhighlighted.el);
index += part.length;
}
const matchedSubstring = text.substring(index, index + match.length);
const highlighted = createSpanWithText(matchedSubstring);
highlighted.addClass(className);
elements.push(highlighted.el);
index += match.length;
});
if (lastPart) {
const last = createSpanWithText(lastPart);
elements.push(last.el);
}
return elements;
}
}
function createSpanWithText(text: string) {
const span = $$('span');
span.text(text);
return span;
}