ng-text-highlight
Version:
A lightweight standalone Angular 19 component/module for highlighting text.
248 lines (241 loc) • 11.9 kB
JavaScript
import * as i0 from '@angular/core';
import { Input, Component, NgModule } from '@angular/core';
import { NgFor, NgIf, NgClass, NgStyle } from '@angular/common';
/**
* Finds all matching chunks of text based on the provided search words.
* It processes the text using the provided options (e.g., case sensitivity, escaping).
* @param autoEscape - Whether to escape special characters in search words.
* @param caseSensitive - Whether the search should be case-sensitive.
* @param findChunks - Custom function to find chunks (default is `defaultChunkFinder`).
* @param sanitize - Function to sanitize the input text and search words.
* @param searchWords - Array of keywords to search for in the text.
* @param textToSearch - The main text where the search is performed.
* @returns Array of `TextChunk` objects representing the highlighted and non-highlighted segments.
*/
const findAllMatches = ({ autoEscape, caseSensitive = false, findChunks = defaultChunkFinder, sanitize, searchWords, textToSearch, }) => fillChunks({
chunksToHighlight: mergeChunks({
chunks: findChunks({
autoEscape,
caseSensitive,
sanitize,
searchWords,
textToSearch,
}),
}),
totalLength: textToSearch ? textToSearch.length : 0,
});
/**
* Merges overlapping or adjacent chunks into a single chunk.
* Ensures that the chunks are non-overlapping and sorted.
* @param chunks - Array of `TextChunk` objects to merge.
* @returns Array of merged `TextChunk` objects.
*/
const mergeChunks = ({ chunks, }) => {
chunks = chunks
.sort((first, second) => first.start - second.start)
.reduce((mergedChunks, nextChunk) => {
if (mergedChunks.length === 0) {
return [nextChunk];
}
else {
const previousChunk = mergedChunks.pop();
if (nextChunk.start <= previousChunk.end) {
const endIndex = Math.max(previousChunk.end, nextChunk.end);
mergedChunks.push({
highlight: false,
start: previousChunk.start,
end: endIndex,
});
}
else {
mergedChunks.push(previousChunk, nextChunk);
}
return mergedChunks;
}
}, []);
return chunks;
};
/**
* Finds chunks of text that match the search words.
* Uses regular expressions to locate the matches in the text.
* @param autoEscape - Whether to escape special characters in search words.
* @param caseSensitive - Whether the search should be case-sensitive.
* @param sanitize - Function to sanitize the input text and search words.
* @param searchWords - Array of keywords to search for in the text.
* @param textToSearch - The main text where the search is performed.
* @returns Array of `TextChunk` objects representing the matching segments.
*/
const defaultChunkFinder = ({ autoEscape, caseSensitive, sanitize = defaultSanitize, searchWords, textToSearch, }) => {
textToSearch = sanitize(textToSearch);
return searchWords
.filter((searchWord) => searchWord)
.reduce((chunks, searchWord) => {
searchWord = sanitize(searchWord);
if (autoEscape) {
searchWord = escapeRegExp(searchWord);
}
const regex = new RegExp(searchWord, caseSensitive ? "g" : "gi");
let match;
while ((match = regex.exec(textToSearch))) {
let start = match.index;
let end = regex.lastIndex;
if (end > start) {
chunks.push({ highlight: false, start, end });
}
if (match.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return chunks;
}, []);
};
/**
* Fills in the gaps between highlighted chunks to create a complete representation of the text.
* Ensures that non-highlighted segments are included between highlighted ones.
* @param chunksToHighlight - Array of `TextChunk` objects representing highlighted segments.
* @param totalLength - Total length of the text being processed.
* @returns Array of `TextChunk` objects representing the full text with highlighted and non-highlighted segments.
*/
const fillChunks = ({ chunksToHighlight, totalLength, }) => {
const allChunks = [];
const appendChunk = (start, end, highlight) => {
if (end - start > 0) {
allChunks.push({
start,
end,
highlight,
});
}
};
if (chunksToHighlight.length === 0) {
appendChunk(0, totalLength, false);
}
else {
let lastIndex = 0;
chunksToHighlight.forEach((chunk) => {
appendChunk(lastIndex, chunk.start, false);
appendChunk(chunk.start, chunk.end, true);
lastIndex = chunk.end;
});
appendChunk(lastIndex, totalLength, false);
}
return allChunks;
};
/**
* Default sanitization function that returns the input string as-is.
* Can be overridden to implement custom sanitization logic.
* @param string - The input string to sanitize.
* @returns The sanitized string.
*/
function defaultSanitize(string) {
return string;
}
/**
* Escapes special characters in a string to make it safe for use in a regular expression.
* @param string - The input string to escape.
* @returns The escaped string.
*/
function escapeRegExp(string) {
return string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
/**
* TextHighlightComponent
* A standalone Angular component that highlights specified keywords within a block of text.
*/
class TextHighlightComponent {
constructor() {
/**
* The main text where keywords will be highlighted.
*/
this.fullText = "";
/**
* Whether the search should be case-sensitive.
*/
this.caseSensitive = false;
/**
* Custom CSS class for highlighted text.
*/
this.highlightClass = "highlight";
/**
* Inline styles for highlighted text.
*/
this.highlightStyle = {};
}
/**
* Lifecycle hook that is triggered when any data-bound property changes.
* Calls the method to generate highlighted segments.
* @param changes - The changes to the input properties.
*/
ngOnChanges(changes) {
this.generateHighlightedSegments();
}
/**
* Generates the highlighted segments of the text based on the provided keywords.
* If no keywords are provided, the entire text is treated as unhighlighted.
*/
generateHighlightedSegments() {
this.highlightedSegments = [];
if (!this.keywords || this.keywords.length === 0) {
this.highlightedSegments = [
{
text: this.fullText,
isHighlighted: false,
},
];
return;
}
// Find all matching chunks in the text
const textChunks = findAllMatches({
searchWords: this.keywords,
textToSearch: this.fullText,
autoEscape: true,
caseSensitive: this.caseSensitive,
});
// Map chunks to highlighted segments
textChunks.forEach((chunk) => {
const { end, highlight, start } = chunk;
const segmentText = this.fullText.substring(start, end);
this.highlightedSegments.push({
text: segmentText,
isHighlighted: highlight,
});
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: TextHighlightComponent, isStandalone: true, selector: "ng-text-highlight", inputs: { fullText: "fullText", keywords: "keywords", caseSensitive: "caseSensitive", highlightClass: "highlightClass", highlightStyle: "highlightStyle" }, usesOnChanges: true, ngImport: i0, template: "<span *ngFor=\"let segment of highlightedSegments\">\n <span\n *ngIf=\"segment.isHighlighted; else normalText\"\n [ngClass]=\"highlightClass\"\n [ngStyle]=\"highlightStyle\"\n >{{ segment.text }}</span>\n <ng-template #normalText>{{ segment.text }}</ng-template>\n</span>\n", styles: [".highlight{background-color:#ff0;color:#000;font-weight:700}.highlight-green{background-color:#90ee90;color:#000;font-weight:700}.highlight-blue{background-color:#add8e6;color:#000;font-style:italic}.highlight-red{background-color:#f08080;color:#fff;font-weight:700;text-decoration:underline}.highlight-purple{background-color:#e6e6fa;color:#9400d3;font-weight:700;font-style:italic}.highlight-orange{background-color:orange;color:#000;font-weight:700}\n"], dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightComponent, decorators: [{
type: Component,
args: [{ selector: "ng-text-highlight", imports: [NgFor, NgIf, NgClass, NgStyle], standalone: true, template: "<span *ngFor=\"let segment of highlightedSegments\">\n <span\n *ngIf=\"segment.isHighlighted; else normalText\"\n [ngClass]=\"highlightClass\"\n [ngStyle]=\"highlightStyle\"\n >{{ segment.text }}</span>\n <ng-template #normalText>{{ segment.text }}</ng-template>\n</span>\n", styles: [".highlight{background-color:#ff0;color:#000;font-weight:700}.highlight-green{background-color:#90ee90;color:#000;font-weight:700}.highlight-blue{background-color:#add8e6;color:#000;font-style:italic}.highlight-red{background-color:#f08080;color:#fff;font-weight:700;text-decoration:underline}.highlight-purple{background-color:#e6e6fa;color:#9400d3;font-weight:700;font-style:italic}.highlight-orange{background-color:orange;color:#000;font-weight:700}\n"] }]
}], propDecorators: { fullText: [{
type: Input
}], keywords: [{
type: Input
}], caseSensitive: [{
type: Input
}], highlightClass: [{
type: Input
}], highlightStyle: [{
type: Input
}] } });
class TextHighlightModule {
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightModule, imports: [TextHighlightComponent], exports: [TextHighlightComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: TextHighlightModule, decorators: [{
type: NgModule,
args: [{
declarations: [],
imports: [TextHighlightComponent],
exports: [TextHighlightComponent],
}]
}] });
/*
* Public API Surface of ng-text-highlight
*/
/**
* Generated bundle index. Do not edit.
*/
export { TextHighlightComponent, TextHighlightModule };
//# sourceMappingURL=ng-text-highlight.mjs.map