html_to_react/
lib.rs

1#![warn(missing_docs)]
2
3//! HTML to react transformer
4//!
5//! html-to-react is a fast html to react
6//! transformer that uses btrees to search and replace
7//! html properties to the react equivalent.
8//!
9//! # How to use html-to-react
10//!
11//! There are two ways to use html-to-react:
12//!
13//! - **Convert props React** transform html string to a react string.
14//!   - [`convert_props_react`] is used to transform :blocking.
15//! - **Convert to React**  Lets you transform the html to a react component.
16//!   - [`convert_to_react`] is used to transform :blocking.
17//!
18//! [`convert_props_react`]: html_to_react/fn.convert_props_react.html
19//! [`convert_to_react`]: html_to_react/fn.convert_to_react.html
20//! 
21//! # Basic usage
22//!
23//! First, you will need to add `html-to-react` to your `Cargo.toml`.
24//!
25//! Next, add your html to one of the transform methods to get your react,
26//! output.
27
28#[macro_use]
29extern crate lazy_static;
30
31extern crate convert_case;
32
33use convert_case::{Case, Casing};
34use std::collections::BTreeMap;
35
36/// convert props to react
37pub fn convert_props_react(ctx: String) -> String {
38    let mut context = ctx.clone();
39
40    lazy_static! {
41        /// html static list of properties that convert to camel-case [https://reactjs.org/docs/dom-elements.html]
42        static ref HTML_PROPS: BTreeMap<&'static str, &'static str> = BTreeMap::from([
43            ("acceptcharset", "acceptCharset"),
44            ("accesskey", "accessKey"),
45            ("allowfullscreen", "allowFullScreen"),
46            ("allowtransparency", "allowTransparency"),
47            ("autocomplete", "autoComplete"),
48            ("autofocus", "autoFocus"),
49            ("autoplay", "autoPlay"),
50            ("cellpadding", "cellPadding"),
51            ("cellspacing", "cellSpacing"),
52            ("charset", "charSet"),
53            ("class", "className"), // special html
54            ("classid", "classID"),
55            ("classname", "className"),
56            ("colspan", "colSpan"),
57            ("contenteditable", "contentEditable"),
58            ("contextmenu", "contextMenu"),
59            ("crossorigin", "crossOrigin"),
60            ("datetime", "dateTime"),
61            ("enctype", "encType"),
62            ("for", "htmlFor"), // special html
63            ("formaction", "formAction"),
64            ("formenctype", "formEncType"),
65            ("formmethod", "formMethod"),
66            ("formnovalidate", "formNoValidate"),
67            ("formtarget", "formTarget"),
68            ("frameborder", "frameBorder"),
69            ("hreflang", "hrefLang"),
70            ("htmlfor", "htmlFor"),
71            ("httpequiv", "httpEquiv"),
72            ("inputmode", "inputMode"),
73            ("keyparams", "keyParams"),
74            ("keytype", "keyType"),
75            ("marginheight", "marginHeight"),
76            ("marginwidth", "marginWidth"),
77            ("maxlength", "maxLength"),
78            ("mediagroup", "mediaGroup"),
79            ("minlength", "minLength"),
80            ("novalidate", "noValidate"),
81            ("radiogroup", "radioGroup"),
82            ("readonly", "readOnly"),
83            ("rowspan", "rowSpan"),
84            ("spellcheck", "spellCheck"),
85            ("srcdoc", "srcDoc"),
86            ("srclang", "srcLang"),
87            ("srcset", "srcSet"),
88            ("tabindex", "tabIndex"),
89            ("usemap", "useMap"),
90            // svg
91            ("accentheight", "accentHeight"),
92            ("alignmentbaseline", "alignmentBaseline"),
93            ("allowreorder", "allowReorder"),
94            ("arabicform", "arabicForm"),
95            ("attributename", "attributeName"),
96            ("attributetype", "attributeType"),
97            ("autoreverse", "autoReverse"),
98            ("basefrequency", "baseFrequency"),
99            ("baseprofile", "baseProfile"),
100            ("baselineshift", "baselineShift"),
101            ("calcmode", "calcMode"),
102            ("capheight", "capHeight"),
103            ("clippath", "clipPath"),
104            ("clippathunits", "clipPathUnits"),
105            ("cliprule", "clipRule"),
106            ("colorinterpolation", "colorInterpolation"),
107            ("colorinterpolationfilters", "colorInterpolationFilters"),
108            ("colorprofile", "colorProfile"),
109            ("colorrendering", "colorRendering"),
110            ("contentscripttype", "contentScriptType"),
111            ("contentstyletype", "contentStyleType"),
112            ("diffuseconstant", "diffuseConstant"),
113            ("dominantbaseline", "dominantBaseline"),
114            ("edgemode", "edgeMode"),
115            ("enablebackground", "enableBackground"),
116            ("externalresourcesrequired", "externalResourcesRequired"),
117            ("fillopacity", "fillOpacity"),
118            ("fillrule", "fillRule"),
119            ("filterres", "filterRes"),
120            ("filterunits", "filterUnits"),
121            ("floodcolor", "floodColor"),
122            ("floodopacity", "floodOpacity"),
123            ("fontfamily", "fontFamily"),
124            ("fontsize", "fontSize"),
125            ("fontsizeadjust", "fontSizeAdjust"),
126            ("fontstretch", "fontStretch"),
127            ("fontstyle", "fontStyle"),
128            ("fontvariant", "fontVariant"),
129            ("fontweight", "fontWeight"),
130            ("glyphname", "glyphName"),
131            ("glyphorientationhorizontal", "glyphOrientationHorizontal"),
132            ("glyphorientationvertical", "glyphOrientationVertical"),
133            ("glyphref", "glyphRef"),
134            ("gradienttransform", "gradientTransform"),
135            ("gradientunits", "gradientUnits"),
136            ("horizadvx", "horizAdvX"),
137            ("horizoriginx", "horizOriginX"),
138            ("imagerendering", "imageRendering"),
139            ("kernelmatrix", "kernelMatrix"),
140            ("kernelunitlength", "kernelUnitLength"),
141            ("keypoints", "keyPoints"),
142            ("keysplines", "keySplines"),
143            ("keytimes", "keyTimes"),
144            ("lengthadjust", "lengthAdjust"),
145            ("letterspacing", "letterSpacing"),
146            ("lightingcolor", "lightingColor"),
147            ("limitingconeangle", "limitingConeAngle"),
148            ("markerend", "markerEnd"),
149            ("markerheight", "markerHeight"),
150            ("markermid", "markerMid"),
151            ("markerstart", "markerStart"),
152            ("markerunits", "markerUnits"),
153            ("markerwidth", "markerWidth"),
154            ("maskcontentunits", "maskContentUnits"),
155            ("maskunits", "maskUnits"),
156            ("numoctaves", "numOctaves"),
157            ("overlineposition", "overlinePosition"),
158            ("overlinethickness", "overlineThickness"),
159            ("paintorder", "paintOrder"),
160            ("pathlength", "pathLength"),
161            ("patterncontentunits", "patternContentUnits"),
162            ("patterntransform", "patternTransform"),
163            ("patternunits", "patternUnits"),
164            ("pointerevents", "pointerEvents"),
165            ("pointsatx", "pointsAtX"),
166            ("pointsaty", "pointsAtY"),
167            ("pointsatz", "pointsAtZ"),
168            ("preservealpha", "preserveAlpha"),
169            ("preserveaspectratio", "preserveAspectRatio"),
170            ("primitiveunits", "primitiveUnits"),
171            ("refx", "refX"),
172            ("refy", "refY"),
173            ("renderingintent", "renderingIntent"),
174            ("repeatcount", "repeatCount"),
175            ("repeatdur", "repeatDur"),
176            ("requiredextensions", "requiredExtensions"),
177            ("requiredfeatures", "requiredFeatures"),
178            ("shaperendering", "shapeRendering"),
179            ("specularconstant", "specularConstant"),
180            ("specularexponent", "specularExponent"),
181            ("spreadmethod", "spreadMethod"),
182            ("startoffset", "startOffset"),
183            ("stddeviation", "stdDeviation"),
184            ("stitchtiles", "stitchTiles"),
185            ("stopcolor", "stopColor"),
186            ("stopopacity", "stopOpacity"),
187            ("strikethroughposition", "strikethroughPosition"),
188            ("strikethroughthickness", "strikethroughThickness"),
189            ("strokedasharray", "strokeDasharray"),
190            ("strokedashoffset", "strokeDashoffset"),
191            ("strokelinecap", "strokeLinecap"),
192            ("strokelinejoin", "strokeLinejoin"),
193            ("strokemiterlimit", "strokeMiterlimit"),
194            ("strokeopacity", "strokeOpacity"),
195            ("strokewidth", "strokeWidth"),
196            ("surfacescale", "surfaceScale"),
197            ("systemlanguage", "systemLanguage"),
198            ("tablevalues", "tableValues"),
199            ("targetx", "targetX"),
200            ("targety", "targetY"),
201            ("textanchor", "textAnchor"),
202            ("textdecoration", "textDecoration"),
203            ("textlength", "textLength"),
204            ("textrendering", "textRendering"),
205            ("underlineposition", "underlinePosition"),
206            ("underlinethickness", "underlineThickness"),
207            ("unicodebidi", "unicodeBidi"),
208            ("unicoderange", "unicodeRange"),
209            ("unitsperem", "unitsPerEm"),
210            ("valphabetic", "vAlphabetic"),
211            ("vhanging", "vHanging"),
212            ("videographic", "vIdeographic"),
213            ("vmathematical", "vMathematical"),
214            ("vectoreffect", "vectorEffect"),
215            ("vertadvy", "vertAdvY"),
216            ("vertoriginx", "vertOriginX"),
217            ("vertoriginy", "vertOriginY"),
218            ("viewbox", "viewBox"),
219            ("viewtarget", "viewTarget"),
220            ("wordspacing", "wordSpacing"),
221            ("writingmode", "writingMode"),
222            ("xchannelselector", "xChannelSelector"),
223            ("xheight", "xHeight"),
224            ("xlinkactuate", "xlinkActuate"),
225            ("xlinkarcrole", "xlinkArcrole"),
226            ("xlinkhref", "xlinkHref"),
227            ("xlinkrole", "xlinkRole"),
228            ("xlinkshow", "xlinkShow"),
229            ("xlinktitle", "xlinkTitle"),
230            ("xlinktype", "xlinkType"),
231            ("xmlnsxlink", "xmlnsXlink"),
232            ("xmlbase", "xmlBase"),
233            ("xmllang", "xmlLang"),
234            ("xmlspace", "xmlSpace"),
235            ("ychannelselector", "yChannelSelector"),
236            ("zoomandpan", "zoomAndPan"),
237            // events
238            ("onabort", "onAbort"),
239            ("onanimationend", "onAnimationEnd"),
240            ("onanimationiteration", "onAnimationIteration"),
241            ("onanimationstart", "onAnimationStart"),
242            ("onblur", "onBlur"),
243            ("oncanplay", "onCanPlay"),
244            ("oncanplaythrough", "onCanPlayThrough"),
245            ("onchange", "onChange"),
246            ("onclick", "onClick"),
247            ("oncompositionend", "onCompositionEnd"),
248            ("oncompositionstart", "onCompositionStart"),
249            ("oncompositionupdate", "onCompositionUpdate"),
250            ("oncontextmenu", "onContextMenu"),
251            ("oncopy", "onCopy"),
252            ("oncut", "onCut"),
253            ("ondoubleclick", "onDoubleClick"),
254            ("ondrag", "onDrag"),
255            ("ondragend", "onDragEnd"),
256            ("ondragenter", "onDragEnter"),
257            ("ondragexit", "onDragExit"),
258            ("ondragleave", "onDragLeave"),
259            ("ondragover", "onDragOver"),
260            ("ondragstart", "onDragStart"),
261            ("ondrop", "onDrop"),
262            ("ondurationchange", "onDurationChange"),
263            ("onemptied", "onEmptied"),
264            ("onencrypted", "onEncrypted"),
265            ("onended", "onEnded"),
266            ("onerror", "onError"),
267            ("onfocus", "onFocus"),
268            ("oninput", "onInput"),
269            ("onkeydown", "onKeyDown"),
270            ("onkeypress", "onKeyPress"),
271            ("onkeyup", "onKeyUp"),
272            ("onload", "onLoad"),
273            ("onloadeddata", "onLoadedData"),
274            ("onloadedmetadata", "onLoadedMetadata"),
275            ("onloadstart", "onLoadStart"),
276            ("onmousedown", "onMouseDown"),
277            ("onmouseenter", "onMouseEnter"),
278            ("onmouseleave", "onMouseLeave"),
279            ("onmousemove", "onMouseMove"),
280            ("onmouseout", "onMouseOut"),
281            ("onmouseover", "onMouseOver"),
282            ("onmouseup", "onMouseUp"),
283            ("onpaste", "onPaste"),
284            ("onpause", "onPause"),
285            ("onplay", "onPlay"),
286            ("onplaying", "onPlaying"),
287            ("onprogress", "onProgress"),
288            ("onratechange", "onRateChange"),
289            ("onscroll", "onScroll"),
290            ("onseeked", "onSeeked"),
291            ("onseeking", "onSeeking"),
292            ("onselect", "onSelect"),
293            ("onstalled", "onStalled"),
294            ("onsubmit", "onSubmit"),
295            ("onsuspend", "onSuspend"),
296            ("ontimeupdate", "onTimeUpdate"),
297            ("ontouchcancel", "onTouchCancel"),
298            ("ontouchend", "onTouchEnd"),
299            ("ontouchmove", "onTouchMove"),
300            ("ontouchstart", "onTouchStart"),
301            ("ontransitionend", "onTransitionEnd"),
302            ("onvolumechange", "onVolumeChange"),
303            ("onwaiting", "onWaiting"),
304            ("onwheel", "onWheel")
305        ]);
306    };
307
308    let props: Vec<String> = extract_html_props(&context);
309
310    for item in props.iter() {
311        if item == "style" {
312            context = create_style_object(&mut context);
313        } else {
314            let value = HTML_PROPS.get(&*item.to_owned()).unwrap_or(&"");
315
316            if !value.is_empty() {
317                let v = format!("{}=", item);
318                let rp = format!("{}=", value);
319                context = context.replace(&v, &rp);
320            }
321        }
322    }
323
324    context
325}
326
327/// extract all html props into a vector
328fn extract_html_props(context: &String) -> Vec<String> {
329    let mut props: Vec<String> = vec![];
330    let mut current_prop = String::from("");
331    let mut space_before_text = false;
332    let mut inside_tag = false;
333
334    // get all html props into a vec
335    for c in context.chars() {
336        if inside_tag {
337            if c == '=' {
338                space_before_text = false;
339                props.push((*current_prop).to_string());
340                current_prop.clear();
341            }
342            if space_before_text {
343                current_prop.push(c);
344            }
345            if c == ' ' {
346                space_before_text = true;
347            }
348        }
349        if c == '<' {
350            inside_tag = true;
351        }
352        if c == '>' {
353            inside_tag = false;
354        }
355    }
356
357    // sort the vec for btree linear lookup performance
358    props.sort();
359
360    props
361}
362
363/// manipulate the style properties to react
364pub fn create_style_object(ctx: &mut String) -> String {
365    let style_matcher = if ctx.contains("style='") {
366        r#"'"#
367    } else {
368        r#"""#
369    };
370
371    let style_start = format!(r#"style={}"#, style_matcher);
372    let (style_string, start_idx, end_idx) = text_between(&ctx, &style_start, style_matcher);
373
374    let mut current_prop = String::from("");
375    let mut space_before_text = false;
376
377    let mut style_replacer = style_string.clone();
378
379    // get all html props into a vec
380    for c in style_string.chars() {
381        if space_before_text {
382            current_prop.push(c);
383        }
384        if c == ';' {
385            space_before_text = true;
386            style_replacer = style_replacer.replace(";", ",");
387            current_prop.clear();
388        }
389        if c == ':' {
390            let clp = &current_prop.trim();
391            let camel_style = &clp.to_case(Case::Camel);
392
393            style_replacer = style_replacer.replace(&*clp, &camel_style);
394            space_before_text = false;
395            current_prop.clear();
396        }
397    }
398
399    let mut space_before_text = false;
400    let mut current_prop = String::from("");
401
402    let mut style_clone = style_replacer.clone().to_owned();
403
404    // add double quotes to react props style values
405    for (i, c) in style_replacer.chars().enumerate() {
406        if space_before_text && c != ',' {
407            current_prop.push(c);
408        }
409
410        if space_before_text && c == ',' {
411            let current = current_prop.trim();
412            style_clone = style_clone.replace(&current, &format!(r#""{}""#, current).to_string());
413            space_before_text = false;
414        }
415
416        if c == ':' {
417            space_before_text = true;
418            current_prop.clear();
419        }
420
421        if i + 1 == style_replacer.len() {
422            let current = current_prop.trim();
423            style_clone = style_clone.replace(&current, &format!(r#""{}""#, current).to_string());
424            space_before_text = false;
425        }
426    }
427
428    let style_replacer = format!("{}{}{}", "style={{", style_clone, "}}");
429
430    ctx.replace_range(start_idx - 7..start_idx + end_idx + 1, &style_replacer);
431
432    ctx.to_owned()
433}
434
435/// get the text between two strings
436fn text_between(search_str: &String, start_str: &String, end_str: &str) -> (String, usize, usize) {
437    let start_idx = {
438        let start_point = search_str.find(start_str);
439        start_point.unwrap() + start_str.len()
440    };
441
442    let remaining = &search_str[start_idx..];
443    let end_idx = remaining.find(&end_str).unwrap_or(remaining.len());
444
445    (remaining[..end_idx].to_string(), start_idx, end_idx)
446}
447
448/// convert props to a react component
449pub fn convert_to_react(ctx: String, component_name: String) -> String {
450    let react_html = convert_props_react(ctx);
451    let mut react_html = react_html.trim().to_owned();
452    
453    // remove html tags
454    if react_html.starts_with("<!DOCTYPE html>") {
455        react_html = react_html.replace("<!DOCTYPE html>", "");
456    }
457    if react_html.starts_with("<html>") {
458        react_html = react_html.replace("<html>", "");
459        react_html = react_html.replace("</html>", "");
460    }
461
462    let component_name = format!(" {}", component_name.trim());
463
464    let component = format!(
465        r#"import React from "react"
466    
467function{}() {{
468    return (
469        {}
470    )
471}}"#,
472        component_name, react_html
473    );
474
475    component
476}
477
478#[test]
479fn convert_props_react_test() {
480    // convert special props class
481    let html = r#"<img class="something">"#;
482    let react_html = convert_props_react(html.to_string());
483
484    assert_eq!("<img className=\"something\">", react_html);
485
486    // convert special props class and for
487    let html = r#"<img class="something" for="mystuff">"#;
488    let react_html = convert_props_react(html.to_string());
489
490    assert_eq!(
491        "<img className=\"something\" htmlFor=\"mystuff\">",
492        react_html
493    );
494
495    // convert special props class, for, and other props
496    let html = r#"<img class="something" for="mystuff" tabindex="2">"#;
497    let react_html = convert_props_react(html.to_string());
498
499    assert_eq!(
500        "<img className=\"something\" htmlFor=\"mystuff\" tabIndex=\"2\">",
501        react_html
502    );
503}
504
505#[test]
506fn extract_html_props_test() {
507    let html = r#"<img class="something" for="mystuff" tabindex="2">"#;
508    let props = extract_html_props(&html.to_string());
509
510    assert_eq!(props, vec!["class", "for", "tabindex"]);
511}
512
513#[test]
514fn convert_props_react_styles_test() {
515    let html = r#"<img style="color: white; background-color: black">"#;
516    let props = convert_props_react(html.to_string());
517
518    assert_eq!(
519        r#"<img style={{color: "white", backgroundColor: "black"}}>"#,
520        props
521    );
522}
523
524#[test]
525fn convert_props_react_children_test() {
526    let html = r#"<div class="something" for="mystuff" tabindex="2" style="color: white; background-color: black">
527    <div class="child" for="mychildstuff" tabindex="2" style="color: white; background-color: black">
528        child
529    </div>
530</div>"#;
531    let props = convert_props_react(html.to_string());
532
533    assert_eq!(
534        r###"<div className="something" htmlFor="mystuff" tabIndex="2" style={{color: "white", backgroundColor: "black"}}>
535    <div className="child" htmlFor="mychildstuff" tabIndex="2" style={{color: "white", backgroundColor: "black"}}>
536        child
537    </div>
538</div>"###,
539        props
540    );
541}
542
543#[test]
544fn convert_react_component_test() {
545    let html = r#"<div class="something" for="mystuff" tabindex="2" style="color: white; background-color: black">
546            <div class="child" for="mychildstuff" tabindex="2" style="color: white; background-color: black">
547                child
548            </div>
549        </div>"#;
550
551    let props = convert_to_react(html.trim().to_string(), "Something".to_string());
552
553    let style_object = r#"style={{color: "white", backgroundColor: "black"}}"#;
554
555    assert_eq!(
556        format!(
557            r###"import React from "react"
558    
559function Something() {{
560    return (
561        <div className="something" htmlFor="mystuff" tabIndex="2" {}>
562            <div className="child" htmlFor="mychildstuff" tabIndex="2" {}>
563                child
564            </div>
565        </div>
566    )
567}}"###,
568            style_object, style_object
569        ),
570        props
571    );
572}