Skip to content

Commit

Permalink
Merge pull request #66 from inokawa/merge
Browse files Browse the repository at this point in the history
Improve logic of createRegexRenderer
  • Loading branch information
inokawa authored Mar 18, 2023
2 parents 9a8d5d9 + b36be22 commit f3a99a8
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 137 deletions.
144 changes: 120 additions & 24 deletions src/__snapshots__/textarea.spec.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,85 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`match double matchers 1`] = `
<DocumentFragment>
<div
style="display: inline-block; position: relative; width: 0px; height: 0px;"
>
<div
style="position: absolute; overflow: hidden; top: 0px; left: 0px; width: 0px; height: 0px;"
>
<div
aria-hidden="true"
style="width: 0px; transform: translate(0px, 0px); pointer-events: none; user-select: none; box-sizing: content-box; padding: 2px 2px 2px 2px; margin: 0px 0px 0px 0px; border: 1px solid; border-width: 1px; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-style: solid; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; font-family: -webkit-small-control; text-align: start; text-transform: none; text-indent: 0; letter-spacing: normal; word-spacing: normal; line-height: normal; white-space: pre-wrap; border-color: transparent;"
>
L
<span
style="color: blue; border: 1px solid blue;"
>
<span
style="color: red; background: red;"
>
o
</span>
</span>
<span
style="color: red; background: red;"
>
r
</span>
<span
style="color: blue; border: 1px solid blue;"
>
e
</span>
m ipsum d
<span
style="color: blue; border: 1px solid blue;"
>
<span
style="color: red; background: red;"
>
o
</span>
</span>
l
<span
style="color: blue; border: 1px solid blue;"
>
<span
style="color: red; background: red;"
>
o
</span>
</span>
<span
style="color: red; background: red;"
>
r
</span>
sit am
<span
style="color: blue; border: 1px solid blue;"
>
e
</span>
t
<span
style="color: transparent;"
>
</span>
</div>
</div>
<textarea
style="background: transparent; margin: 0px; vertical-align: top; color: transparent;"
>
Lorem ipsum dolor sit amet
</textarea>
</div>
</DocumentFragment>
`;

exports[`match emoji 1`] = `
<DocumentFragment>
<div
Expand Down Expand Up @@ -403,7 +483,7 @@ exports[`match match one 1`] = `
</DocumentFragment>
`;

exports[`match multiple matchers 1`] = `
exports[`match triple matchers 1`] = `
<DocumentFragment>
<div
style="display: inline-block; position: relative; width: 0px; height: 0px;"
Expand All @@ -415,54 +495,70 @@ exports[`match multiple matchers 1`] = `
aria-hidden="true"
style="width: 0px; transform: translate(0px, 0px); pointer-events: none; user-select: none; box-sizing: content-box; padding: 2px 2px 2px 2px; margin: 0px 0px 0px 0px; border: 1px solid; border-width: 1px; border-top-width: 1px; border-bottom-width: 1px; border-left-width: 1px; border-right-width: 1px; border-style: solid; border-top-style: solid; border-bottom-style: solid; border-left-style: solid; border-right-style: solid; font-family: -webkit-small-control; text-align: start; text-transform: none; text-indent: 0; letter-spacing: normal; word-spacing: normal; line-height: normal; white-space: pre-wrap; border-color: transparent;"
>
L
<span
style="color: red; background: red;"
style="background-color: lightgray;"
>
<span
style="color: blue; border: 1px solid blue;"
>
o
</span>
Lor
</span>
<span
style="color: red; background: red;"
style="color: red; font-weight: bold;"
>
r
<span
style="background-color: lightgray;"
>
e
</span>
</span>
<span
style="color: blue; border: 1px solid blue;"
style="background-color: lightgray;"
>
e
m
</span>
m ipsum d
<span
style="color: red; background: red;"
style="color: blue; text-decoration: underline wavy;"
>
<span
style="color: blue; border: 1px solid blue;"
style="color: red; font-weight: bold;"
>
o
i
</span>
</span>
l
<span
style="color: red; background: red;"
style="color: blue; text-decoration: underline wavy;"
>
<span
style="color: blue; border: 1px solid blue;"
style="color: red; font-weight: bold;"
>
o
p
</span>
</span>
<span
style="color: red; background: red;"
style="color: blue; text-decoration: underline wavy;"
>
r
sum
</span>
sit am
<span
style="color: blue; border: 1px solid blue;"
style="color: red; font-weight: bold;"
>
d
</span>
olor s
<span
style="color: red; font-weight: bold;"
>
i
</span>
t
<span
style="color: red; font-weight: bold;"
>
a
</span>
m
<span
style="color: red; font-weight: bold;"
>
e
</span>
Expand Down
102 changes: 79 additions & 23 deletions src/renderers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { execReg } from "./regex";
import type { Renderer } from "./types";
import { RangeChunk, merge } from "./utils";

export type StyleOrRender =
| React.CSSProperties
Expand All @@ -10,39 +9,85 @@ export type StyleOrRender =
key?: string | undefined;
}) => React.ReactNode);

type RangeChunk = [start: number, end: number];

/**
* An utility to create renderer function with regex.
*
* The priority is descending order.
*/
export const createRegexRenderer = (
matchers: [RegExp, StyleOrRender][]
): Renderer => {
return (value) => {
const styles: { [key: string]: StyleOrRender } = {};
const ranges: RangeChunk[] = [];
matchers.forEach(([matcher, style], i) => {
ranges.push(
...execReg(matcher, value).map((m): RangeChunk => {
const start = m.index;
const end = m.index + m[0]!.length;
return [start, end, i];
})
);
styles[String(i)] = style;
const matches = matchers.map(
([matcher, style]): [RangeChunk[], StyleOrRender] => {
return [
execReg(matcher, value).map((m): RangeChunk => {
return [m.index, m.index + m[0]!.length];
}),
style,
];
}
);
const [indexSet, startToStyleMap, endToStyleMap] = matches.reduce(
(acc, [ranges, style]) => {
ranges.forEach(([start, end]) => {
acc[0].add(start).add(end);
let startStyles = acc[1].get(start);
let endStyles = acc[2].get(end);
if (!startStyles) {
acc[1].set(start, (startStyles = []));
}
if (!endStyles) {
acc[2].set(end, (endStyles = []));
}
startStyles.push(style);
endStyles.push(style);
});
return acc;
},
[
new Set<number>(),
new Map<number, StyleOrRender[]>(),
new Map<number, StyleOrRender[]>(),
] as const
);
const indexes = Array.from(indexSet);
indexes.sort((a, b) => {
return a - b;
});

const chunks = merge(ranges);
const res: React.ReactNode[] = [];
let prevEnd = 0;
// let prevStart = 0;
for (let i = 0; i < chunks.length; i++) {
const [start, end, styleIds] = chunks[i]!;
res.push(value.slice(prevEnd, start));
const activeStyles = new Set<StyleOrRender>();
const res: React.ReactNode[] = [];
for (let i = 0; i < indexes.length; i++) {
const start = indexes[i]!;
const end = indexes[i + 1] ?? value.length;
if (start === end) continue;
const headValue = value.slice(prevEnd, start);
if (headValue) {
res.push(headValue);
}
const startStyles = startToStyleMap.get(start);
const endStyles = endToStyleMap.get(end);
if (startStyles) {
startStyles.forEach((s) => {
activeStyles.add(s);
});
}

const v = value.slice(start, end);
const sortedStyles = Array.from(activeStyles).sort((a, b) => {
return (
matchers.findIndex(([, s]) => s === b) -
matchers.findIndex(([, s]) => s === a)
);
});

res.push(
Object.keys(styleIds).reduceRight((acc, si, index) => {
const styleOrRender = styles[si];
const key = index === 0 ? String(start) : undefined;
sortedStyles.reduceRight((acc, styleOrRender, j) => {
const key = j === 0 ? String(start) : undefined;
if (typeof styleOrRender === "function") {
return styleOrRender({ children: acc, value: v, key });
} else {
Expand All @@ -54,10 +99,21 @@ export const createRegexRenderer = (
}
}, v as React.ReactNode)
);

if (endStyles) {
endStyles.forEach((s) => {
activeStyles.delete(s);
});
}

prevEnd = end;
// prevStart = start;
}
res.push(value.slice(prevEnd));

const tailValue = value.slice(prevEnd);
if (tailValue) {
res.push(tailValue);
}

return res;
};
};
15 changes: 14 additions & 1 deletion src/textarea.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ describe("match", () => {
expect(asFragment()).toMatchSnapshot();
});

it("multiple matchers", () => {
it("double matchers", () => {
const { asFragment } = render(
<RichTextarea value={"Lorem ipsum dolor sit amet"} onChange={NOP}>
{createRegexRenderer([
Expand All @@ -180,6 +180,19 @@ describe("match", () => {
expect(asFragment()).toMatchSnapshot();
});

it("triple matchers", () => {
const { asFragment } = render(
<RichTextarea value={"Lorem ipsum dolor sit amet"} onChange={NOP}>
{createRegexRenderer([
[/[A-Z][a-z]+/g, { backgroundColor: "lightgray" }],
[/[abcdeip]/g, { color: "red", fontWeight: "bold" }],
[/ipsum/g, { color: "blue", textDecoration: "underline wavy" }],
])}
</RichTextarea>
);
expect(asFragment()).toMatchSnapshot();
});

it("japanese", () => {
const { asFragment } = render(
<RichTextarea
Expand Down
33 changes: 0 additions & 33 deletions src/utils.spec.ts

This file was deleted.

Loading

0 comments on commit f3a99a8

Please sign in to comment.