Skip to content

Commit 979f2cb

Browse files
committed
Implement TextSegmenter
1 parent ba84183 commit 979f2cb

File tree

7 files changed

+122
-49
lines changed

7 files changed

+122
-49
lines changed

example/src/App.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import React from 'react'
1+
import React from 'react';
22

3-
import { ExampleComponent } from 'react-segmenter'
4-
import 'react-segmenter/dist/index.css'
3+
import { TextSegmenter } from 'react-segmenter';
54

65
const App = () => {
7-
return <ExampleComponent text="Create React Library Example 😄" />
8-
}
6+
return (
7+
<TextSegmenter>Hello World!</TextSegmenter>)
8+
;
9+
};
910

10-
export default App
11+
export default App;

example/tsconfig.json

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
"compilerOptions": {
33
"outDir": "dist",
44
"module": "esnext",
5-
"lib": ["dom", "esnext"],
5+
"lib": [
6+
"dom",
7+
"esnext"
8+
],
69
"moduleResolution": "node",
710
"jsx": "react",
811
"sourceMap": true,
@@ -15,8 +18,21 @@
1518
"suppressImplicitAnyIndexErrors": true,
1619
"noUnusedLocals": true,
1720
"noUnusedParameters": true,
18-
"allowSyntheticDefaultImports": true
21+
"allowSyntheticDefaultImports": true,
22+
"target": "es5",
23+
"allowJs": true,
24+
"skipLibCheck": true,
25+
"strict": true,
26+
"forceConsistentCasingInFileNames": true,
27+
"resolveJsonModule": true,
28+
"isolatedModules": true,
29+
"noEmit": true
1930
},
20-
"include": ["src"],
21-
"exclude": ["node_modules", "build"]
31+
"include": [
32+
"src"
33+
],
34+
"exclude": [
35+
"node_modules",
36+
"build"
37+
]
2238
}

src/index.test.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { ExampleComponent } from '.'
1+
import { TextSegmenter } from '.'
22

3-
describe('ExampleComponent', () => {
3+
describe('TextSegmenter', () => {
44
it('is truthy', () => {
5-
expect(ExampleComponent).toBeTruthy()
5+
expect(TextSegementer).toBeTruthy()
66
})
77
})

src/index.tsx

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,92 @@
1-
import * as React from 'react'
2-
import styles from './styles.module.css'
1+
import React, { useMemo, useState, useEffect } from "react";
32

4-
interface Props {
5-
text: string
6-
}
3+
const joinWbr2StringArray = (ary: string[]) => {
4+
const res: React.ReactNode[] = ary
5+
.filter((e) => !!e)
6+
.flatMap((e, i) => [e, <wbr key={i} />]);
7+
return res;
8+
};
9+
10+
type TextSegmenterCoreProps = {
11+
children: string;
12+
locale: string;
13+
};
14+
15+
const TextSegmenterCore: React.FC<TextSegmenterCoreProps> = ({
16+
locale,
17+
children
18+
}) => {
19+
const segmenter = useMemo(
20+
() => new Intl.Segmenter(locale, { granularity: "word" }),
21+
[locale]
22+
);
23+
const splittedText = useMemo(() => {
24+
return Array.from(segmenter.segment(children)).map((s) => s.segment);
25+
}, [segmenter, children]);
26+
27+
return <>{joinWbr2StringArray(splittedText)}</>;
28+
};
29+
30+
type Props = {
31+
children: React.ReactNode;
32+
locale?: string;
33+
};
34+
35+
const TextSegmenterInner: React.FC<Required<Props>> = ({
36+
locale,
37+
children
38+
}) => {
39+
return (
40+
<>
41+
{React.Children.map(children, (child) => {
42+
if (typeof child === "string")
43+
return <TextSegmenterCore locale={locale}>{child}</TextSegmenterCore>;
44+
if (!React.isValidElement(child)) return child;
45+
if (child.props.children) {
46+
return React.cloneElement(
47+
child,
48+
{},
49+
<TextSegmenterInner locale={locale}>
50+
{child.props.children}
51+
</TextSegmenterInner>
52+
);
53+
}
54+
return child;
55+
})}
56+
</>
57+
);
58+
};
59+
60+
/**
61+
* A component to split a word by semantics for non-separated sentences like Japanese.
62+
* @param children [ReactNode] target node
63+
* @param locale [string] target locale. default is "ja-JP"
64+
* @return sentences separated by semantics and wrapped with span
65+
*/
66+
export const TextSegmenter: React.FC<Props> = ({
67+
locale = "ja-JP",
68+
children
69+
}) => {
70+
const [isClient, setIsClient] = useState(false);
71+
useEffect(() => setIsClient(true), []);
72+
73+
if (!isClient) {
74+
// two-pass rendering.
75+
return <>{children}</>;
76+
}
77+
if (Intl.Segmenter === undefined) {
78+
// Intl.Segmenter is unavailable.
79+
return <>{children}</>;
80+
}
81+
if (!Intl.Segmenter.supportedLocalesOf(locale)) {
82+
// the locale is not supported.
83+
return <>{children}</>;
84+
}
85+
86+
return (
87+
<span style={{ wordBreak: "keep-all" }}>
88+
<TextSegmenterInner locale={locale}>{children}</TextSegmenterInner>
89+
</span>
90+
);
91+
};
792

8-
export const ExampleComponent = ({ text }: Props) => {
9-
return <div className={styles.test}>Example Component: {text}</div>
10-
}

src/styles.module.css

Lines changed: 0 additions & 9 deletions
This file was deleted.

src/typings.d.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

tsconfig.test.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": "./tsconfig.json",
33
"compilerOptions": {
4-
"module": "commonjs"
4+
"module": "esnext"
55
}
6-
}
6+
}

0 commit comments

Comments
 (0)