Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/abc/Renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { Component } from "react";
import { AsabReactJson } from "../components/AsabReactJson/AsabReactJson.jsx";
import { RendererWrapper } from '../components/RendererWrapper/RendererWrapper.jsx';

import './Renderer.scss';

export class Renderer extends Component {
// Renderer defaults
constructor(app) {
Expand Down Expand Up @@ -32,11 +34,15 @@ export class Renderer extends Component {
}

// Render span with value inside as a default
return (<RendererWrapper
return (
<RendererWrapper
data-value={value} // Passing value (to eventually work with in the external wrapper)
data-key={key} // Passing key (to eventually work with in the external wrapper)
component={params?.WrapperComponent || "span"}
>{value}</RendererWrapper>);
>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indentation

{value}
</RendererWrapper>
);
}

plain(key, value, schemaField) {
Comment thread
yanabel1996 marked this conversation as resolved.
Expand Down
5 changes: 5 additions & 0 deletions src/abc/Renderer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.renderer-whitespace {
Comment thread
yanabel1996 marked this conversation as resolved.
color: var(--slate-smoke);
-webkit-user-select: none;
user-select: none;
}
7 changes: 5 additions & 2 deletions src/components/RendererWrapper/RendererWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import React, { useMemo } from 'react';
import { visualizeUnicodeChildren } from '../../utils/visualization/visualizeWhitespaces';

// Renderer wraper
export function RendererWrapper({
children,
component: Component = 'span', // Default tag, can be overloaded
...rest
}) {
const HighlightedChildren = useMemo(() => visualizeUnicodeChildren(children), [children]);

return (
<Component
{...rest}
>
{children}
{HighlightedChildren}
</Component>
);
}
10 changes: 10 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ export { AsabReactJson } from './components/AsabReactJson/AsabReactJson.jsx';
export { FullscreenButton } from './components/FullscreenButton.jsx';
export { AttentionBadge } from './components/AttentionRequired/AttentionRequiredBadge.jsx';
export { RendererWrapper } from './components/RendererWrapper/RendererWrapper.jsx';
export {
visualizeWhitespaces,
visualizeUnicodeChildren,
highlightFulltextInNodes,
createUnicodeVisualizeWrapper,
} from './utils/visualization/visualizeWhitespaces.js';
export { highlightSearchedFulltexts } from './utils/highlighting/highlightSearchedFulltexts.js';
export {
highlightChildren,
} from './utils/highlighting/highlightChildren.jsx';
export { ASABProgress } from './components/Progress/ASABProgress.jsx';
export { FlowbiteIllustration } from './components/FlowbiteIllustration.jsx';
export { PubSubProvider, usePubSub } from './components/Context/PubSubContext';
Expand Down
71 changes: 71 additions & 0 deletions src/utils/highlighting/highlightChildren.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { highlightSearchedFulltexts } from './highlightSearchedFulltexts.js';

const isPrimitiveHighlightValue = (value) => (
value != null && (typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint')
);

const highlightText = (text, fulltextHighlightTerms) => (
highlightSearchedFulltexts(String(text), fulltextHighlightTerms)
);

// Apply fulltext highlight to children (string, whitespace node array, or nested elements)
export const highlightChildren = (children, fulltextHighlightTerms, dataValue) => {
if (!fulltextHighlightTerms?.length) {
return children;
}

if (typeof children === 'string' || typeof children === 'number') {
return highlightText(children, fulltextHighlightTerms);
}

// Whitespace renderer output: apply fulltext to string runs, keep unicode markers as-is
if (Array.isArray(children)) {
return children.flatMap((node) => {
if (typeof node === 'string' || typeof node === 'number') {
const highlighted = highlightText(node, fulltextHighlightTerms);
return Array.isArray(highlighted) ? highlighted : [highlighted];
}

if (!React.isValidElement(node)) {
return [node];
}

const highlightedNode = highlightChildren(
node,
fulltextHighlightTerms,
dataValue,
);

return Array.isArray(highlightedNode) ? highlightedNode : [highlightedNode];
});
}

if (React.isValidElement(children)) {
const nestedDataValue = children.props?.['data-value'] ?? dataValue;

if (typeof children.type === 'string' || children.type === React.Fragment) {
return React.cloneElement(
children,
children.props,
highlightChildren(
children.props?.children,
fulltextHighlightTerms,
nestedDataValue,
),
);
}

if (isPrimitiveHighlightValue(nestedDataValue)) {
return highlightText(nestedDataValue, fulltextHighlightTerms);
}

return children;
}

if (isPrimitiveHighlightValue(dataValue)) {
return highlightText(dataValue, fulltextHighlightTerms);
}

return children;
};
51 changes: 51 additions & 0 deletions src/utils/highlighting/highlightSearchedFulltexts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';

import './highlightSearchedFulltexts.scss';

// Regex to escape special characters to prevent the app to crash in search value
const ESCAPE_REGEX = /[.*+?^${}()|[\]\\]/g;

// Normalize search terms
const normalizeSearchTerms = (searchValue) => {
if (Array.isArray(searchValue)) {
return searchValue
.map((term) => String(term).trim())
.filter(Boolean);
}

if (typeof searchValue === 'string') {
return searchValue
.trim()
.split(/\s+/)
.filter(Boolean);
}

return [];
};

// Highlight searched fulltexts in the text by replacing matched terms with a span
export const highlightSearchedFulltexts = (text, searchValue) => {
const sourceText = String(text ?? '');
const terms = normalizeSearchTerms(searchValue);

if (terms.length === 0 || sourceText === '') {
return sourceText;
}

const escapedTerms = [...new Set(terms.map((term) => term.replace(ESCAPE_REGEX, '\\$&')))]
.sort((a, b) => b.length - a.length);
const matchRegex = new RegExp(`(${escapedTerms.join('|')})`, 'gi');
const highlightedTerms = new Set(terms.map((term) => term.toLowerCase()));

return sourceText.split(matchRegex).map((part, index) => {
if (highlightedTerms.has(part.toLowerCase())) {
return (
<span key={`${part}-${index}`} className='search-text-highlight'>
{part}
</span>
);
}

return part;
});
};
11 changes: 11 additions & 0 deletions src/utils/highlighting/highlightSearchedFulltexts.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.search-text-highlight {
[data-bs-theme=dark] & {
color: var(--black);
background-color: rgba(var(--light-honey-rgb), 0.9);
}

[data-bs-theme=light] & {
color: var(--fiery-sunset);
background-color: rgba(var(--bright-orange-rgb), 0.25);
}
}
Loading
Loading