char/phoebe
git clone https://git.t4t.associates/char/phoebe
0ddd756
legacy
1use html5ever::{ local_name, namespace_url, ns, LocalName , QualName }; 2use kuchiki::{ iter:: NodeEdge , traits:: * , NodeData }; 3 4use super ::{ MessageComponent , MessageContent }; 5 6pub fn convert_matrix ( message : & str ) ->MessageContent { 7let dom = kuchiki:: parse_fragment ( 8QualName :: new ( None , ns! ( html), LocalName :: from ( "div" )), 9vec! [], 10) 11. one ( message); 12 13let mut parents =vec! []; 14let mut components =vec! []; 15let mut skip_text =0 ; 16let mut skip_all =0 ; 17 18for edgein dom. traverse () { 19match edge{ 20NodeEdge :: Start ( node) =>{ 21if let NodeData :: Element ( element) = node. data () { 22if element. name . local ==* "mx-reply" { 23 skip_all +=1 ; 24} 25 26if skip_all >0 { 27continue ; 28} 29 30if element. name . ns ==ns! ( html) { 31match element. name . local { 32local_name! ( "strong" ) 33 |local_name! ( "b" ) 34 |local_name! ( "em" ) 35 |local_name! ( "i" ) 36 |local_name! ( "s" ) 37 |local_name! ( "u" ) 38 |local_name! ( "a" ) 39 |local_name! ( "blockquote" ) =>{ 40 parents. push ( components); 41 components =vec! []; 42} 43 44local_name! ( "span" ) =>{ 45let attrs = element. attributes . borrow (); 46if attrs. get ( "data-mx-spoiler" ). is_some () { 47 parents. push ( components); 48 components =vec! []; 49} 50} 51 52local_name! ( "code" ) =>{ 53 skip_text +=1 ; 54} 55 56 _ =>{} 57} 58} 59} 60} 61 62NodeEdge :: End ( node) =>match node. data () { 63NodeData :: Text ( text) =>{ 64if skip_text <=0 && skip_all <=0 { 65let text = text. borrow (). lines (). collect ::< Vec < _ >>(). join ( " " ); 66 components. push ( MessageComponent :: Plain ( text)); 67} 68} 69NodeData :: Element ( element) =>{ 70if element. name . local ==* "mx-reply" { 71 skip_all -=1 ; 72} 73 74if skip_all >0 { 75continue ; 76} 77 78macro_rules ! construct_component{ 79( $f: expr) =>{{ 80let component_type = $f; 81if let Some ( mut parent_components) = parents. pop () { 82 parent_components. push (( component_type)( components)); 83 components = parent_components; 84} 85}}; 86} 87 88if element. name . ns ==ns! ( html) { 89match element. name . local { 90local_name! ( "strong" ) |local_name! ( "b" ) =>{ 91construct_component! ( MessageComponent :: Bold ) 92} 93local_name! ( "em" ) |local_name! ( "i" ) =>{ 94construct_component! ( MessageComponent :: Italic ) 95} 96local_name! ( "s" ) =>{ 97construct_component! ( MessageComponent :: Strikethrough ) 98} 99local_name! ( "u" ) =>{ 100construct_component! ( MessageComponent :: Underline ) 101} 102local_name! ( "a" ) =>{ 103if let Some ( mut parent_components) = parents. pop () { 104let attrs = element. attributes . borrow (); 105if let Some ( href) = attrs. get ( local_name! ( "href" )) { 106 parent_components. push ( MessageComponent :: Link { 107target : href. to_string (), 108text : components, 109}); 110} else { 111 parent_components. append ( & mut components); 112} 113 114 components = parent_components; 115} 116} 117local_name! ( "br" ) |local_name! ( "p" ) =>{ 118 components. push ( MessageComponent :: HardBreak ); 119} 120local_name! ( "blockquote" ) =>{ 121construct_component! ( MessageComponent :: BlockQuote ) 122} 123local_name! ( "span" ) =>{ 124let attrs = element. attributes . borrow (); 125if let Some ( spoiler_reason) = attrs. get ( "data-mx-spoiler" ) { 126construct_component! ( |inner|MessageComponent :: Spoiler { 127reason : ( !spoiler_reason. is_empty ()) 128. then ( || spoiler_reason. to_string ()), 129content : inner, 130}) 131} 132} 133local_name! ( "code" ) =>{ 134// is_code_block = whether we are the child of a <pre> tag 135let is_code_block = node 136. parent () 137. as_ref () 138. map ( |p| p. data ()) 139. and_then ( |d|match d{ 140NodeData :: Element ( e) =>{ 141Some ( e. name . local ==local_name! ( "pre" )) 142} 143 _ =>None , 144}) 145. unwrap_or ( false ); 146 147 components. push ( if is_code_block{ 148let attrs = element. attributes . borrow (); 149let lang = attrs 150. get ( local_name! ( "class" )) 151. and_then ( |lang| lang. strip_prefix ( "language-" )) 152. map ( |s| s. to_string ()); 153 154MessageComponent :: CodeBlock { 155 lang, 156source : node. text_contents (), 157} 158} else { 159MessageComponent :: Code ( node. text_contents ()) 160}); 161 162 skip_text -=1 ; 163} 164 _ =>{} 165} 166} 167} 168 _ =>{} 169}, 170}; 171} 172 173 components 174} 175 176pub fn format_matrix ( message_content : & [ MessageComponent ]) ->String { 177 message_content 178. iter () 179. map ( |component|match component{ 180MessageComponent :: Plain ( text) => html_escape:: encode_text ( text). to_string (), 181MessageComponent :: Link { target, text} =>format! ( 182r#"<a href="{}">{}</a>"# , 183 html_escape:: encode_quoted_attribute ( target), 184format_matrix ( text) 185), 186 187MessageComponent :: Italic ( inner) =>format! ( "<em>{}</em>" , format_matrix ( inner)), 188MessageComponent :: Bold ( inner) =>format! ( "<strong>{}</strong>" , format_matrix ( inner)), 189MessageComponent :: Strikethrough ( inner) =>{ 190format! ( "<del>{}</del>" , format_matrix ( inner)) 191} 192MessageComponent :: Underline ( inner) =>format! ( "<u>{}</u>" , format_matrix ( inner)), 193 194MessageComponent :: Code ( code) =>{ 195format! ( "<code>{}</code>" , html_escape:: encode_text ( code)) 196} 197MessageComponent :: CodeBlock { lang, source} =>{ 198format! ( 199r#"<pre><code{}>{}</code></pre>"# , 200 lang. as_ref () 201. map ( |lang|format! ( 202r#" class="language-{}""# , 203 html_escape:: encode_quoted_attribute ( lang) 204)) 205. unwrap_or_else ( ||"" . to_string ()), 206 source, 207) 208} 209 210MessageComponent :: Spoiler { reason, content} =>format! ( 211"<span data-mx-spoiler{}>{}</span>" , 212 reason 213. as_ref () 214. map ( |reason|format! ( r#"="{}""# , html_escape:: encode_quoted_attribute ( reason))) 215. unwrap_or_else ( ||"" . to_string ()), 216format_matrix ( content) 217), 218MessageComponent :: HardBreak =>"<br>" . to_string (), 219MessageComponent :: BlockQuote ( inner) =>{ 220format! ( "<blockquote>{}</blockquote>" , format_matrix ( inner)) 221} 222}) 223. collect () 224} 225 226# [ test ] 227fn simple_parsing () { 228use MessageComponent :: * ; 229 230let html = 231r#"<strong>hello! <i><></i></strong> <a href="https://example.com/">example</a>"# ; 232 233assert_eq! ( 234convert_matrix ( html), 235vec! [ 236Bold ( vec! [ 237Plain ( "hello! " . to_string (),), 238Italic ( vec! [ Plain ( "<>" . to_string())]), 239]), 240Plain ( " " . to_string ()), 241Link { 242target : "https://example.com/" . to_string (), 243text : vec! [ Plain ( "example" . to_string ())] 244}, 245] 246) 247} 248 249# [ test ] 250fn spoiler_parsing () { 251use MessageComponent :: * ; 252 253let html =r#"<span data-mx-spoiler>the <em>whole</em> island is populated by lesbians</span>"# ; 254 255assert_eq! ( 256convert_matrix ( html), 257vec! [ Spoiler { 258reason : None , 259content : vec! [ 260Plain ( "the " . to_string ()), 261Italic ( vec! [ Plain ( "whole" . to_string())]), 262Plain ( " island is populated by lesbians" . to_string ()) 263] 264}] 265); 266} 267 268# [ test ] 269fn code_parsing () { 270use MessageComponent :: * ; 271 272let html =r#"<code>hello_world();</code>"# ; 273 274assert_eq! ( 275convert_matrix ( html), 276vec! [ Code ( "hello_world();" . to_string ())] 277); 278 279let html =r#"<pre><code class="language-javascript">console.log("hello, world!"); 280console.table({ a: 1, b: 2, c: 3 }); 281</code></pre>"# ; 282 283assert_eq! ( 284convert_matrix ( html), 285vec! [ CodeBlock { 286lang : Some ( "javascript" . to_string ()), 287source : "console.log(\"hello, world!\");\nconsole.table({ a: 1, b: 2, c: 3 });\n" 288. to_string (), 289}] 290); 291}