1+ import type { Element , Root , RootContent } from "hast" ;
12import { renderToStaticMarkup } from "react-dom/server" ;
23import { describe , expect , test } from "vitest" ;
34import {
45 rehypeNeutralizeEmails ,
56 splitEmails ,
7+ splitEmailText ,
68} from "@/app/_components/toolkit-docs/lib/neutralize-emails" ;
79
810/**
@@ -39,27 +41,51 @@ describe("splitEmails", () => {
3941 } ) ;
4042} ) ;
4143
42- type HastNode = {
43- type : string ;
44- value ?: string ;
45- tagName ?: string ;
46- properties ?: Record < string , unknown > ;
47- children ?: HastNode [ ] ;
48- } ;
44+ describe ( "splitEmailText" , ( ) => {
45+ test ( "returns one text node when there are no emails" , ( ) => {
46+ expect ( splitEmailText ( "just some text" ) ) . toEqual ( [
47+ { type : "text" , value : "just some text" } ,
48+ ] ) ;
49+ } ) ;
50+
51+ test ( "splits every email while preserving the text" , ( ) => {
52+ const nodes = splitEmailText (
53+ "contact jane@example.com or sam@example.org today"
54+ ) ;
55+
56+ expect ( nodes ) . toEqual ( [
57+ { type : "text" , value : "contact jane" } ,
58+ { type : "element" , tagName : "wbr" , properties : { } , children : [ ] } ,
59+ { type : "text" , value : "@example.com or sam" } ,
60+ { type : "element" , tagName : "wbr" , properties : { } , children : [ ] } ,
61+ { type : "text" , value : "@example.org today" } ,
62+ ] ) ;
63+ expect (
64+ nodes
65+ . filter ( ( node ) => node . type === "text" )
66+ . map ( ( node ) => node . value )
67+ . join ( "" )
68+ ) . toBe ( "contact jane@example.com or sam@example.org today" ) ;
69+ } ) ;
70+ } ) ;
4971
50- const collectText = ( node : HastNode ) : string =>
51- node . type === "text"
52- ? ( node . value ?? "" )
53- : ( node . children ?? [ ] ) . map ( collectText ) . join ( "" ) ;
72+ const collectText = ( node : Root | RootContent ) : string => {
73+ if ( node . type === "text" ) {
74+ return node . value ;
75+ }
76+ return "children" in node ? node . children . map ( collectText ) . join ( "" ) : "" ;
77+ } ;
5478
55- const hasContiguousEmail = ( node : HastNode ) : boolean =>
56- node . type === "text"
57- ? EMAIL . test ( node . value ?? "" )
58- : ( node . children ?? [ ] ) . some ( hasContiguousEmail ) ;
79+ const hasContiguousEmail = ( node : Root | RootContent ) : boolean => {
80+ if ( node . type === "text" ) {
81+ return EMAIL . test ( node . value ) ;
82+ }
83+ return "children" in node ? node . children . some ( hasContiguousEmail ) : false ;
84+ } ;
5985
6086describe ( "rehypeNeutralizeEmails" , ( ) => {
6187 test ( "splits email text nodes and inserts a <wbr>, losslessly" , ( ) => {
62- const tree : HastNode = {
88+ const tree : Root = {
6389 type : "root" ,
6490 children : [
6591 {
@@ -73,13 +99,43 @@ describe("rehypeNeutralizeEmails", () => {
7399
74100 rehypeNeutralizeEmails ( ) ( tree ) ;
75101
76- const paragraph = tree . children ?. [ 0 ] ;
77- expect ( paragraph ?. children ?. some ( ( child ) => child . tagName === "wbr" ) ) . toBe (
78- true
79- ) ;
102+ const paragraph = tree . children [ 0 ] as Element ;
103+ expect (
104+ paragraph . children . some (
105+ ( child ) => child . type === "element" && child . tagName === "wbr"
106+ )
107+ ) . toBe ( true ) ;
80108 // No single text node still holds a full email...
81109 expect ( hasContiguousEmail ( tree ) ) . toBe ( false ) ;
82110 // ...and the concatenated text is unchanged.
83111 expect ( collectText ( tree ) ) . toBe ( "reach user@example.com now" ) ;
84112 } ) ;
113+
114+ test ( "visits nested elements and every matching text node" , ( ) => {
115+ const tree : Root = {
116+ type : "root" ,
117+ children : [
118+ {
119+ type : "element" ,
120+ tagName : "blockquote" ,
121+ properties : { } ,
122+ children : [
123+ {
124+ type : "element" ,
125+ tagName : "em" ,
126+ properties : { } ,
127+ children : [ { type : "text" , value : "jane@example.com" } ] ,
128+ } ,
129+ { type : "text" , value : " and sam@example.org" } ,
130+ ] ,
131+ } ,
132+ ] ,
133+ } ;
134+
135+ rehypeNeutralizeEmails ( ) ( tree ) ;
136+
137+ expect ( hasContiguousEmail ( tree ) ) . toBe ( false ) ;
138+ expect ( collectText ( tree ) ) . toBe ( "jane@example.com and sam@example.org" ) ;
139+ expect ( JSON . stringify ( tree ) . match ( / " t a g N a m e " : " w b r " / g) ) . toHaveLength ( 2 ) ;
140+ } ) ;
85141} ) ;
0 commit comments