@erosnicolau/animated-text
Version:
Advanced React animated text component with comprehensive animation effects
139 lines (138 loc) • 4.73 kB
JavaScript
// Text processing and tokenization utilities
// Allowed HTML tags for security
const ALLOWED_HTML_TAGS = ['br', 'strong', 'i', 'b', 'em', 'italic'];
// HTML sanitization function
export const sanitizeHtml = (content) => {
if (!content)
return '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
const cleanNode = (node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = node;
const tagName = element.tagName.toLowerCase();
if (!ALLOWED_HTML_TAGS.includes(tagName)) {
const textNode = document.createTextNode(element.textContent || '');
element.parentNode?.replaceChild(textNode, element);
return;
}
Array.from(element.attributes).forEach(attr => {
element.removeAttribute(attr.name);
});
Array.from(element.childNodes).forEach(cleanNode);
}
};
Array.from(tempDiv.childNodes).forEach(cleanNode);
return tempDiv.innerHTML;
};
// Tokenize content into words
export const tokenizeContent = (content) => {
const tokens = [];
let wordIndex = 1;
const regex = /(<[^>]+>)|(\S+)|(\s+)/g;
let match;
while ((match = regex.exec(content)) !== null) {
if (match[1]) {
tokens.push({ type: 'html', content: match[1] });
}
else if (match[2]) {
tokens.push({ type: 'word', content: match[2], wordIndex: wordIndex++ });
}
else if (match[3]) {
tokens.push({ type: 'space', content: match[3] });
}
}
return tokens;
};
// Tokenize content into phrases separated by "|"
export const tokenizePhrases = (content) => {
const phrases = content.split('|');
const tokens = [];
phrases.forEach((phrase, phraseIndex) => {
const regex = /(<[^>]+>)|([^<]+)/g;
let match;
while ((match = regex.exec(phrase)) !== null) {
if (match[1]) {
tokens.push({ type: 'html', content: match[1] });
}
else if (match[2]) {
const trimmedContent = match[2].trim();
if (trimmedContent) {
tokens.push({
type: 'phrase',
content: trimmedContent,
phraseIndex: phraseIndex + 1
});
}
}
}
if (phraseIndex < phrases.length - 1) {
tokens.push({ type: 'space', content: ' ' });
}
});
return tokens;
};
// Tokenize content into individual words for sequence animation
export const tokenizeWords = (content) => {
const tokens = [];
let wordIndex = 1;
const words = content.trim().split(/\s+/);
words.forEach((word, index) => {
if (word.trim()) {
tokens.push({ type: 'word', content: word, wordIndex: wordIndex++ });
// Add space after each word except the last one
if (index < words.length - 1) {
tokens.push({ type: 'space', content: ' ', wordIndex: 0 });
}
}
});
return tokens;
};
// Tokenize content into individual letters for typewriter animation
export const tokenizeLetters = (content) => {
const tokens = [];
let letterIndex = 1;
for (const char of content) {
if (char === ' ') {
tokens.push({ type: 'space', content: ' ', letterIndex: 0 });
}
else {
tokens.push({ type: 'letter', content: char, letterIndex: letterIndex++ });
}
}
return tokens;
};
// Tokenize content for word-typewriter animation (words with individual letters)
export const tokenizeWordTypewriter = (content) => {
const tokens = [];
let letterIndex = 1;
// Split content into words and spaces while preserving spaces
const parts = content.split(/(\s+)/);
for (const part of parts) {
if (/^\s+$/.test(part)) {
// It's whitespace (spaces, tabs, etc.)
tokens.push({
type: 'space',
content: part,
letterIndex: 0,
isWordBoundary: true
});
}
else if (part.length > 0) {
// It's a word - create word token with individual letters
const letters = [];
for (const char of part) {
letters.push({
content: char,
letterIndex: letterIndex++
});
}
tokens.push({
type: 'word',
content: part,
letters: letters
});
}
}
return tokens;
};