react-ai-highlight-parser
Version:
Parse and render AI streaming responses with semantic highlighting. Supports multiple modes (highlights, underline, both) and color palettes.
266 lines (262 loc) • 7.84 kB
JavaScript
// src/types.ts
var HIGHLIGHT_MEANINGS = {
"Y": "Important/Key points",
"B": "Concepts/Definitions",
"O": "Steps/Sequences",
"G": "Success/Positive",
"R": "Warnings/Errors",
"P": "Examples",
"L": "Data/Numbers",
"GR": "Code/Technical",
"H": "Emphasis/Highlights",
"BR": "Context/Background"
};
// src/palettes.ts
var VIBRANT = {
background: {
"Y": "#FFF4C3",
// Yellow - Important/key points
"B": "#D5FEFF",
// Blue - Concepts/definitions
"O": "#FFD5C3",
// Orange - Steps/sequences
"G": "#DCFCE7",
// Green - Success/positive
"R": "#fee2e2",
// Red - Warnings/errors
"P": "#FEECFF",
// Pink - Examples
"L": "#E6F3FF",
// Light blue - Data/numbers
"GR": "#E8E6E5",
// Gray - Code/technical
"H": "#ede9fe",
// Purple - Emphasis/highlights
"BR": "#f5e8dd"
// Brown - Context/background info
},
underline: {
"Y": "#FFC41A",
"B": "#5DCFFF",
"O": "#FF7744",
"G": "#22C55E",
"R": "#ef4444",
"P": "#FC90FF",
"L": "#8DC5FF",
"GR": "#ACA8A4",
"H": "#8b5cf6",
"BR": "#92400e"
}
};
var NATURAL = {
background: {
"Y": "#F5F0E8",
// Beige/cream
"B": "#E8F0F4",
// Steel blue light
"O": "#F5E8DD",
// Tan light
"G": "#E8EDE6",
// Sage green
"R": "#F5E8EA",
// Dusty rose
"P": "#F0EAF5",
// Lavender light
"L": "#E6EEF3",
// Slate light
"GR": "#E8E6E5",
// Gray
"H": "#EAE8F0",
// Soft purple
"BR": "#F0E8E0"
// Warm beige
},
underline: {
"Y": "#9A8B7A",
"B": "#2C5F6F",
"O": "#92400E",
"G": "#6B7056",
"R": "#7C2D3F",
"P": "#9B8BA8",
"L": "#5C7B8B",
"GR": "#ACA8A4",
"H": "#7C6B8A",
"BR": "#8B6B47"
}
};
var PALETTES = {
vibrant: VIBRANT,
natural: NATURAL
};
function getPalette(name = "vibrant") {
return PALETTES[name] || PALETTES.vibrant;
}
function getBackgroundColor(palette, code) {
return getPalette(palette).background[code] || getPalette(palette).background["Y"];
}
function getUnderlineColor(palette, code) {
return getPalette(palette).underline[code] || getPalette(palette).underline["O"];
}
// src/parser.ts
var VALID_CODES = ["Y", "B", "O", "G", "R", "P", "L", "GR", "H", "BR"];
function processResponse(text) {
if (!text) return text;
let processed = text;
const codeBlocks = [];
processed = processed.replace(/(```[\s\S]*?```)/g, (match) => {
const idx = codeBlocks.length;
codeBlocks.push(match);
return `___CODE_BLOCK_${idx}___`;
});
processed = processed.replace(/\[GREEN\]([^\[]*)\[\/GREEN\]/gi, "$1").replace(/\[RED\]([^\[]*)\[\/RED\]/gi, "$1").replace(/\[BLUE\]([^\[]*)\[\/BLUE\]/gi, "$1").replace(/\[YELLOW\]([^\[]*)\[\/YELLOW\]/gi, "$1").replace(/\[ORANGE\]([^\[]*)\[\/ORANGE\]/gi, "$1").replace(/\[PURPLE\]([^\[]*)\[\/PURPLE\]/gi, "$1");
VALID_CODES.forEach((code) => {
const validPairRegex = new RegExp(`\\[${code}\\]([^\\[]*)\\[\\/${code}\\]`, "g");
const validPairs = [];
let tempText = processed.replace(validPairRegex, (match) => {
const idx = validPairs.length;
validPairs.push(match);
return `___VALID_${code}_${idx}___`;
});
tempText = tempText.replace(new RegExp(`\\[${code}\\]`, "g"), "").replace(new RegExp(`\\[\\/${code}\\]`, "g"), "");
validPairs.forEach((pair, idx) => {
tempText = tempText.replace(`___VALID_${code}_${idx}___`, pair);
});
processed = tempText;
});
codeBlocks.forEach((block, idx) => {
processed = processed.replace(`___CODE_BLOCK_${idx}___`, block);
});
return processed;
}
function removeHighlightCodes(text) {
let cleaned = text;
VALID_CODES.forEach((code) => {
const regex = new RegExp(`\\[${code}\\]([^\\[]*)\\[\\/${code}\\]`, "g");
cleaned = cleaned.replace(regex, "$1");
});
return cleaned;
}
function wrapWithStyle(content, code, mode, palette) {
const colors = getPalette(palette);
const bgColor = colors.background[code] || colors.background["Y"];
const underlineColor = colors.underline[code] || colors.underline["O"];
if (mode === "underline") {
return `<span style="text-decoration:underline ${underlineColor};text-decoration-thickness:2px;text-underline-offset:2px;text-decoration-skip-ink:none">${content}</span>`;
}
if (mode === "highlights") {
return `<span style="background-color:${bgColor};padding:1px 3px 0 3px;border-radius:3px;display:inline">${content}</span>`;
}
if (mode === "both") {
return `<span style="background-color:${bgColor};text-decoration:underline ${underlineColor};text-decoration-thickness:2px;text-underline-offset:2px;text-decoration-skip-ink:none;padding:1px 3px 0 3px;border-radius:3px">${content}</span>`;
}
return content;
}
function parseHighlights(text, mode = "highlights", palette = "vibrant") {
if (!text || mode === "none") {
return mode === "none" ? removeHighlightCodes(text) : text;
}
let processed = processResponse(text);
processed = processed.replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
processed = processed.replace(/\*([^*]+?)\*/g, "<em>$1</em>");
processed = processed.replace(/`([^`]+)`/g, '<code style="background:#f1f1f1;padding:2px 4px;border-radius:3px;font-family:monospace;font-size:0.9em">$1</code>');
const hasHighlightCodes = new RegExp(`\\[(${VALID_CODES.join("|")})\\]`).test(processed);
if (!hasHighlightCodes) {
return processed;
}
const tokenPattern = new RegExp(`(\\[\\/?(?:${VALID_CODES.join("|")})\\])`, "g");
const tokens = processed.split(tokenPattern).filter((t) => t.length > 0);
const stack = [];
const output = [];
for (const token of tokens) {
const openMatch = token.match(new RegExp(`^\\[(${VALID_CODES.join("|")})\\]$`));
if (openMatch) {
stack.push({
code: openMatch[1],
startIndex: output.length
});
continue;
}
const closeMatch = token.match(new RegExp(`^\\[\\/(${VALID_CODES.join("|")})\\]$`));
if (closeMatch) {
const code = closeMatch[1];
let matchIndex = -1;
for (let j = stack.length - 1; j >= 0; j--) {
if (stack[j].code === code) {
matchIndex = j;
break;
}
}
if (matchIndex !== -1) {
const open = stack[matchIndex];
const content = output.slice(open.startIndex).join("");
output.splice(open.startIndex);
output.push(wrapWithStyle(content, code, mode, palette));
stack.splice(matchIndex, 1);
}
continue;
}
output.push(token);
}
return output.join("");
}
function hasHighlights(text) {
return new RegExp(`\\[(${VALID_CODES.join("|")})\\]`).test(text);
}
function extractHighlightCodes(text) {
const codes = /* @__PURE__ */ new Set();
VALID_CODES.forEach((code) => {
if (new RegExp(`\\[${code}\\]`).test(text)) {
codes.add(code);
}
});
return Array.from(codes);
}
// src/components/HighlightRenderer.tsx
import { jsx } from "react/jsx-runtime";
function HighlightRenderer({
content,
mode = "highlights",
palette = "vibrant",
className = ""
}) {
const html = parseHighlights(content, mode, palette);
return /* @__PURE__ */ jsx(
"div",
{
className,
dangerouslySetInnerHTML: { __html: html }
}
);
}
function HighlightSpan({
content,
mode = "highlights",
palette = "vibrant",
className = ""
}) {
const html = parseHighlights(content, mode, palette);
return /* @__PURE__ */ jsx(
"span",
{
className,
dangerouslySetInnerHTML: { __html: html }
}
);
}
export {
HIGHLIGHT_MEANINGS,
HighlightRenderer,
HighlightSpan,
NATURAL,
PALETTES,
VIBRANT,
HighlightRenderer as default,
extractHighlightCodes,
getBackgroundColor,
getPalette,
getUnderlineColor,
hasHighlights,
parseHighlights,
processResponse,
removeHighlightCodes
};