├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── .github └── FUNDING.yml ├── README.md ├── src └── main │ ├── scala │ └── simer │ │ └── html │ │ └── converter │ │ ├── utils │ │ └── ConverterUtil.scala │ │ ├── tyrian │ │ ├── TyrianConverter.scala │ │ ├── TyrianTags.scala │ │ └── TyrianAttributes.scala │ │ ├── HTMLTemplate.scala │ │ ├── ConverterType.scala │ │ ├── AttributeType.scala │ │ └── HtmlToScalaConverter.scala │ └── resources │ ├── index-opt.html │ ├── index-dev.html │ └── style.css ├── LICENSE └── docs ├── index.html └── style.css /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.5.5 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.1") 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .idea 3 | .cache 4 | .classpath 5 | .project 6 | .settings/ 7 | .bsp 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [simerplaha] 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML converter for Scalatags, Scalajs-react, Laminar, Outwatch & Tyrian 2 | 3 | Online converter: https://simerplaha.github.io/html-to-scala-converter/ 4 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/utils/ConverterUtil.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter.utils 2 | 3 | object ConverterUtil { 4 | 5 | def tripleQuote(string: String): String = 6 | string.trim match { 7 | case string if string.contains("\"") || string.contains("\n") || string.contains("\\") => 8 | s"""\"\"\"$string\"\"\"""" 9 | 10 | case string => 11 | s""""$string"""" 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/index-opt.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML to Scala converter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | 16 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/resources/index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML to Scala converter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML to Scala converter 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/tyrian/TyrianConverter.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter.tyrian 2 | 3 | import org.scalajs.dom.ext._ 4 | import org.scalajs.dom.raw.Node 5 | import simer.html.converter.ConverterType 6 | import simer.html.converter.tyrian.TyrianAttributes.{Normal, NoValue} 7 | import simer.html.converter.tyrian.TyrianTags.{NoChildren, OptionalChildren} 8 | import simer.html.converter.utils.ConverterUtil._ 9 | 10 | import scala.scalajs.js 11 | 12 | object TyrianConverter { 13 | def toScala(node: Node, 14 | converterType: ConverterType, 15 | childrenWithoutGarbageNodes: collection.Seq[Node], 16 | children: String): String = { 17 | 18 | node.nodeName match { 19 | case "#text" => 20 | tripleQuote(node.nodeValue) 21 | case _ => 22 | 23 | val attrString = if (js.isUndefined(node.attributes) || node.attributes.isEmpty) { 24 | "" 25 | } else { 26 | val (separatorStart, separator, separatorEnd) = if (!converterType.newLineAttributes) { 27 | ("(\n", ",\n", "\n)") 28 | } else { 29 | ("(", ", ", ")") 30 | } 31 | node.attributes.map { case (attributeKey, attributeValue) => 32 | val attributeType = TyrianAttributes.attrs.find(a => a.attrName.getOrElse(a.name) == attributeKey) 33 | 34 | attributeType match { 35 | case Some(attr: NoValue) if !converterType.isBooleanTypeConversionDisabled => attr.name 36 | case Some(attr: Normal) => s"${attr.name} := ${tripleQuote(attributeValue.value)}" 37 | case _ => s"Attribute(\"$attributeKey\", ${tripleQuote(attributeValue.value)})" 38 | } 39 | }.mkString(separatorStart, separator, separatorEnd) 40 | 41 | } 42 | 43 | val tagType = TyrianTags.tags.find(t => t.tag.getOrElse(t.name) == node.nodeName.toLowerCase) 44 | 45 | val innerString = tagType match { 46 | case Some(_: NoChildren) | Some(_: OptionalChildren) => "" 47 | case _ if childrenWithoutGarbageNodes.nonEmpty => s"(\n$children)" 48 | case _ => "()" 49 | } 50 | 51 | tagType match { 52 | case Some(tag) => s"${tag.name}$attrString$innerString" 53 | case None => s"tag(\"${node.nodeName.toLowerCase}\")$attrString$innerString" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden; 6 | background-color: #000000; 7 | box-shadow: 3px 3px 4px #292323; 8 | } 9 | 10 | li { 11 | float: left; 12 | } 13 | 14 | li a { 15 | display: block; 16 | color: white; 17 | text-align: center; 18 | padding: 14px 16px; 19 | text-decoration: none; 20 | } 21 | 22 | li a:hover { 23 | background-color: #000000; 24 | } 25 | 26 | html, body { 27 | width: 100%; 28 | height: 100%; 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | #section { 34 | width: 100%; 35 | min-height: 100%; 36 | display: table; 37 | height: inherit; 38 | } 39 | 40 | .row { 41 | width: 100%; 42 | height: 100%; 43 | display: table-row; 44 | } 45 | 46 | .col-left, .col-right { 47 | width: 50%; 48 | display: table-cell; 49 | vertical-align: middle; 50 | } 51 | 52 | .col-right { 53 | text-align: center; 54 | } 55 | 56 | .content { 57 | /*border: 1px dashed red;*/ 58 | } 59 | 60 | h1 { 61 | font-family: sans-serif; 62 | } 63 | 64 | .boxsizingBorder { 65 | -webkit-box-sizing: border-box; 66 | -moz-box-sizing: border-box; 67 | box-sizing: border-box; 68 | } 69 | 70 | .button { 71 | overflow: hidden; 72 | margin: 10px; 73 | padding: 12px 12px; 74 | cursor: pointer; 75 | -webkit-user-select: none; 76 | -moz-user-select: none; 77 | -ms-user-select: none; 78 | user-select: none; 79 | -webkit-transition: all 60ms ease-in-out; 80 | transition: all 60ms ease-in-out; 81 | text-align: center; 82 | white-space: nowrap; 83 | text-decoration: none !important; 84 | text-transform: none; 85 | text-transform: capitalize; 86 | color: #fff; 87 | border: 0 none; 88 | border-radius: 4px; 89 | font-size: 13px; 90 | font-weight: 500; 91 | line-height: 1.3; 92 | -webkit-appearance: none; 93 | -moz-appearance: none; 94 | appearance: none; 95 | -webkit-box-pack: center; 96 | -ms-flex-pack: center; 97 | justify-content: center; 98 | -webkit-box-align: center; 99 | -ms-flex-align: center; 100 | align-items: center; 101 | -webkit-box-flex: 0; 102 | -ms-flex: 0 0 160px; 103 | flex: 0 0 160px; 104 | box-shadow: 2px 5px 10px rgba(22, 22, 22, 0.1); 105 | } 106 | .button:hover { 107 | -webkit-transition: all 60ms ease; 108 | transition: all 60ms ease; 109 | opacity: .85; 110 | } 111 | .button:active { 112 | -webkit-transition: all 60ms ease; 113 | transition: all 60ms ease; 114 | opacity: .75; 115 | } 116 | .button:focus { 117 | outline: 1px dotted #959595; 118 | outline-offset: -4px; 119 | } 120 | 121 | .button.-salmon { 122 | color: #FFFFFF; 123 | background: #F32C52; 124 | } 125 | 126 | .button.scalajs-react { 127 | color: black; 128 | background: #61dafb; 129 | } 130 | 131 | .button.laminar { 132 | color: #FFFFFF; 133 | background: #dc322f; 134 | } 135 | 136 | .button.outwatch { 137 | color: #FFFFFF; 138 | background: #8278fb; 139 | } 140 | 141 | .button.tyrian { 142 | color: #FFFFFF; 143 | background: #4f46e5; 144 | } 145 | 146 | .button.scalatags { 147 | color: #FFFFFF; 148 | background: rgb(31, 141, 214); 149 | } 150 | 151 | textarea { 152 | margin-top: 10px; 153 | margin-left: 5px; 154 | -moz-border-bottom-colors: none; 155 | -moz-border-left-colors: none; 156 | -moz-border-right-colors: none; 157 | -moz-border-top-colors: none; 158 | background: none repeat scroll 0 0 #fbfbfb; 159 | border-image: none; 160 | border-radius: 6px 6px 6px 6px; 161 | border-style: none solid solid none; 162 | border-width: medium 1px 1px medium; 163 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; 164 | color: #555555; 165 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 166 | font-size: 14px; 167 | line-height: 1.4em; 168 | padding: 5px 8px; 169 | transition: background-color 0.1s ease 0s; 170 | } 171 | 172 | 173 | textarea:focus { 174 | background: none repeat scroll 0 0 #FFFFFF; 175 | outline-width: 0; 176 | } 177 | 178 | 179 | h1 { 180 | text-align: center; 181 | -webkit-transition: opacity 180ms ease; 182 | transition: opacity 180ms ease; 183 | color: #161616; 184 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 185 | font-size: 32px; 186 | font-weight: 700; 187 | } 188 | h1 span { 189 | font-weight: 400; 190 | }/*Checkboxes styles*/ 191 | input[type="checkbox"] { display: none; } 192 | 193 | input[type="checkbox"] + label { 194 | position: relative; 195 | padding-left: 35px; 196 | margin-bottom: 20px; 197 | padding-top: 2px; 198 | font: 14px/20px 'Open Sans', Arial, sans-serif; 199 | 200 | cursor: pointer; 201 | -webkit-user-select: none; 202 | -moz-user-select: none; 203 | -ms-user-select: none; 204 | } 205 | 206 | input[type="checkbox"] + label:last-child { margin-bottom: 0; } 207 | 208 | input[type="checkbox"] + label:before { 209 | content: ''; 210 | width: 20px; 211 | height: 20px; 212 | border: 1px solid #000000; 213 | position: absolute; 214 | left: 0; 215 | top: 0; 216 | opacity: .6; 217 | -webkit-transition: all .12s, border-color .08s; 218 | transition: all .12s, border-color .08s; 219 | } 220 | 221 | input[type="checkbox"]:checked + label:before { 222 | width: 10px; 223 | top: -5px; 224 | left: 5px; 225 | border-radius: 0; 226 | opacity: 1; 227 | border-top-color: transparent; 228 | border-left-color: transparent; 229 | -webkit-transform: rotate(45deg); 230 | transform: rotate(45deg); 231 | } 232 | 233 | .github-button { 234 | padding-top: 10px; 235 | padding-left: 4px; 236 | } 237 | -------------------------------------------------------------------------------- /src/main/resources/style.css: -------------------------------------------------------------------------------- 1 | ul { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | overflow: hidden; 6 | background-color: #000000; 7 | box-shadow: 3px 3px 4px #292323; 8 | } 9 | 10 | li { 11 | float: left; 12 | } 13 | 14 | li a { 15 | display: block; 16 | color: white; 17 | text-align: center; 18 | padding: 14px 16px; 19 | text-decoration: none; 20 | } 21 | 22 | li a:hover { 23 | background-color: #000000; 24 | } 25 | 26 | html, body { 27 | width: 100%; 28 | height: 100%; 29 | margin: 0; 30 | padding: 0; 31 | } 32 | 33 | #section { 34 | width: 100%; 35 | min-height: 100%; 36 | display: table; 37 | height: inherit; 38 | } 39 | 40 | .row { 41 | width: 100%; 42 | height: 100%; 43 | display: table-row; 44 | } 45 | 46 | .col-left, .col-right { 47 | width: 50%; 48 | display: table-cell; 49 | vertical-align: middle; 50 | } 51 | 52 | .col-right { 53 | text-align: center; 54 | } 55 | 56 | .content { 57 | /*border: 1px dashed red;*/ 58 | } 59 | 60 | h1 { 61 | font-family: sans-serif; 62 | } 63 | 64 | .boxsizingBorder { 65 | -webkit-box-sizing: border-box; 66 | -moz-box-sizing: border-box; 67 | box-sizing: border-box; 68 | } 69 | 70 | .button { 71 | overflow: hidden; 72 | margin: 10px; 73 | padding: 12px 12px; 74 | cursor: pointer; 75 | -webkit-user-select: none; 76 | -moz-user-select: none; 77 | -ms-user-select: none; 78 | user-select: none; 79 | -webkit-transition: all 60ms ease-in-out; 80 | transition: all 60ms ease-in-out; 81 | text-align: center; 82 | white-space: nowrap; 83 | text-decoration: none !important; 84 | text-transform: none; 85 | text-transform: capitalize; 86 | color: #fff; 87 | border: 0 none; 88 | border-radius: 4px; 89 | font-size: 13px; 90 | font-weight: 500; 91 | line-height: 1.3; 92 | -webkit-appearance: none; 93 | -moz-appearance: none; 94 | appearance: none; 95 | -webkit-box-pack: center; 96 | -ms-flex-pack: center; 97 | justify-content: center; 98 | -webkit-box-align: center; 99 | -ms-flex-align: center; 100 | align-items: center; 101 | -webkit-box-flex: 0; 102 | -ms-flex: 0 0 160px; 103 | flex: 0 0 160px; 104 | box-shadow: 2px 5px 10px rgba(22, 22, 22, 0.1); 105 | } 106 | .button:hover { 107 | -webkit-transition: all 60ms ease; 108 | transition: all 60ms ease; 109 | opacity: .85; 110 | } 111 | .button:active { 112 | -webkit-transition: all 60ms ease; 113 | transition: all 60ms ease; 114 | opacity: .75; 115 | } 116 | .button:focus { 117 | outline: 1px dotted #959595; 118 | outline-offset: -4px; 119 | } 120 | 121 | .button.-salmon { 122 | color: #FFFFFF; 123 | background: #F32C52; 124 | } 125 | 126 | .button.scalajs-react { 127 | color: black; 128 | background: #61dafb; 129 | } 130 | 131 | .button.laminar { 132 | color: #FFFFFF; 133 | background: #dc322f; 134 | } 135 | 136 | .button.outwatch { 137 | color: #FFFFFF; 138 | background: #8278fb; 139 | } 140 | 141 | .button.tyrian { 142 | color: #FFFFFF; 143 | background: #4f46e5; 144 | } 145 | 146 | .button.scalatags { 147 | color: #FFFFFF; 148 | background: rgb(31, 141, 214); 149 | } 150 | 151 | textarea { 152 | margin-top: 10px; 153 | margin-left: 5px; 154 | -moz-border-bottom-colors: none; 155 | -moz-border-left-colors: none; 156 | -moz-border-right-colors: none; 157 | -moz-border-top-colors: none; 158 | background: none repeat scroll 0 0 #fbfbfb; 159 | border-image: none; 160 | border-radius: 6px 6px 6px 6px; 161 | border-style: none solid solid none; 162 | border-width: medium 1px 1px medium; 163 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12) inset; 164 | color: #555555; 165 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 166 | font-size: 14px; 167 | line-height: 1.4em; 168 | padding: 5px 8px; 169 | transition: background-color 0.1s ease 0s; 170 | } 171 | 172 | 173 | textarea:focus { 174 | background: none repeat scroll 0 0 #FFFFFF; 175 | outline-width: 0; 176 | } 177 | 178 | 179 | h1 { 180 | text-align: center; 181 | -webkit-transition: opacity 180ms ease; 182 | transition: opacity 180ms ease; 183 | color: #161616; 184 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 185 | font-size: 32px; 186 | font-weight: 700; 187 | } 188 | h1 span { 189 | font-weight: 400; 190 | }/*Checkboxes styles*/ 191 | input[type="checkbox"] { display: none; } 192 | 193 | input[type="checkbox"] + label { 194 | position: relative; 195 | padding-left: 35px; 196 | margin-bottom: 20px; 197 | padding-top: 2px; 198 | font: 14px/20px 'Open Sans', Arial, sans-serif; 199 | 200 | cursor: pointer; 201 | -webkit-user-select: none; 202 | -moz-user-select: none; 203 | -ms-user-select: none; 204 | } 205 | 206 | input[type="checkbox"] + label:last-child { margin-bottom: 0; } 207 | 208 | input[type="checkbox"] + label:before { 209 | content: ''; 210 | width: 20px; 211 | height: 20px; 212 | border: 1px solid #000000; 213 | position: absolute; 214 | left: 0; 215 | top: 0; 216 | opacity: .6; 217 | -webkit-transition: all .12s, border-color .08s; 218 | transition: all .12s, border-color .08s; 219 | } 220 | 221 | input[type="checkbox"]:checked + label:before { 222 | width: 10px; 223 | top: -5px; 224 | left: 5px; 225 | border-radius: 0; 226 | opacity: 1; 227 | border-top-color: transparent; 228 | border-left-color: transparent; 229 | -webkit-transform: rotate(45deg); 230 | transform: rotate(45deg); 231 | } 232 | 233 | .github-button { 234 | padding-top: 10px; 235 | padding-left: 4px; 236 | } 237 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/HTMLTemplate.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.html.Input 5 | import scalatags.JsDom.all._ 6 | import simer.html.converter.ConverterType.{Outwatch, Laminar, ScalaJSReact, ScalaTags, Tyrian} 7 | 8 | object HTMLTemplate { 9 | 10 | def isNewLineAttributes = !dom.document.getElementById("newlineAttributes").asInstanceOf[Input].checked 11 | 12 | def isBooleanTypeConversionDisabled = dom.document.getElementById("disableBooleanTypeConversion").asInstanceOf[Input].checked 13 | 14 | def template(onConvertClicked: ConverterType => Unit) = 15 | div( 16 | ul( 17 | li( 18 | a(href := "#")("HTML TO SCALA CONVERTER") 19 | ), 20 | li(float := "right", paddingTop := 10.px, paddingRight := 10.px, 21 | u( 22 | a( 23 | cls := "github-button", 24 | href := "https://github.com/simerplaha/html-to-scala-converter", 25 | attr("data-size") := "large", 26 | attr("data-show-count") := "true", 27 | attr("aria-label") := "Star simerplaha/html-to-scala-converter on GitHub", 28 | "Star" 29 | ) 30 | ) 31 | ) 32 | ), 33 | table(width := "100%")( 34 | tr(width := "100%")( 35 | th(width := "50%")( 36 | h4("HTML") 37 | ), 38 | th(width := "50%")( 39 | h4("Scala") 40 | ) 41 | ), 42 | tr(width := "100%")( 43 | td(width := "50%")( 44 | textarea(id := "htmlCode", cls := "boxsizingBorder", width := "100%", rows := 26, placeholder := "Enter your HTML code here.")( 45 | """
46 | |
47 | | 48 | |
49 | |
50 | | 51 | | 52 | | 53 | | 54 | | Some link 55 | | 56 | | 64 | | 65 | | 66 | | 67 | | 70 | |
""".stripMargin 71 | ) 72 | ), 73 | td(width := "50%")( 74 | textarea(id := "scalaCode", cls := "boxsizingBorder", width := "100%", rows := 26, placeholder := "Scala code will be generated here.") 75 | ) 76 | ), 77 | tr(width := "50%")( 78 | td(colspan := "1", textAlign := "right", paddingBottom := "10px", paddingRight := "25px")( 79 | input(`type` := "checkbox", id := "newlineAttributes"), 80 | label(`for` := "newlineAttributes", "Add attributes on newline"), 81 | ), 82 | td(colspan := "1", textAlign := "left", paddingBottom := "10px", paddingLeft := "10px")( 83 | input(`type` := "checkbox", id := "disableBooleanTypeConversion"), 84 | label(`for` := "disableBooleanTypeConversion", "Disable Boolean type conversion"), 85 | ) 86 | ), 87 | tr(width := "100%")( 88 | td(colspan := "3", textAlign := "center")( 89 | button(cls := "button scalajs-react center", onclick := { () => onConvertClicked(ScalaJSReact(isNewLineAttributes, isBooleanTypeConversionDisabled)) })("ScalaJS-React (1.7.7)"), 90 | span(" "), 91 | button(cls := "button scalatags center", onclick := { () => onConvertClicked(ScalaTags(isNewLineAttributes, isBooleanTypeConversionDisabled)) })("ScalaTags (0.9.4)"), 92 | span(" "), 93 | button(cls := "button laminar center", onclick := { () => onConvertClicked(Laminar(isNewLineAttributes, isBooleanTypeConversionDisabled)) })("Laminar (0.13.1)"), 94 | span(" "), 95 | button(cls := "button outwatch center", onclick := { () => onConvertClicked(Outwatch(isNewLineAttributes, isBooleanTypeConversionDisabled)) })("Outwatch (1.0.0-RC7)"), 96 | span(" "), 97 | button(cls := "button tyrian center", onclick := { () => onConvertClicked(Tyrian(isNewLineAttributes, isBooleanTypeConversionDisabled)) })("Tyrian (0.5.1)") 98 | 99 | ), 100 | ), 101 | tr(width := "100%")( 102 | td(colspan := "3", textAlign := "center")( 103 | p( 104 | strong(""""Add attributes on newline""""), 105 | " inserts a new line after each tag attribute/property.", 106 | ), 107 | p( 108 | strong(""""Disable Boolean type conversion""""), 109 | " converts ", 110 | span(backgroundColor := "#dddcdc" ,"""checked = "blah!""""), 111 | " to ", 112 | span(backgroundColor := "#dddcdc" ,"""checked := "blah!""""), 113 | " instead of ", 114 | span(backgroundColor := "#dddcdc" ,"""checked := true"""), 115 | ), 116 | p( 117 | "Type conversions are enabled for ScalaJS-React, Laminar & Outwatch. Please report any missing types." 118 | ) 119 | ) 120 | ) 121 | ) 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/tyrian/TyrianTags.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter.tyrian 2 | 3 | object TyrianTags { 4 | 5 | def tags: List[TagType] = 6 | List( 7 | HasChildren("a"), 8 | HasChildren("abbr"), 9 | HasChildren("address"), 10 | NoChildren("area"), 11 | HasChildren("article"), 12 | HasChildren("aside"), 13 | HasChildren("audio"), 14 | HasChildren("b"), 15 | NoChildren("base"), 16 | HasChildren("bdi"), 17 | HasChildren("bdo"), 18 | HasChildren("blockquote"), 19 | HasChildren("body"), 20 | NoChildren("br"), 21 | OptionalChildren("button"), 22 | HasChildren("canvas"), 23 | HasChildren("caption"), 24 | HasChildren("cite"), 25 | HasChildren("code"), 26 | NoChildren("col"), 27 | HasChildren("colgroup"), 28 | HasChildren("data"), 29 | HasChildren("datalist"), 30 | HasChildren("dd"), 31 | HasChildren("del"), 32 | HasChildren("details"), 33 | HasChildren("dfn"), 34 | HasChildren("dialog"), 35 | HasChildren("div"), 36 | HasChildren("dl"), 37 | HasChildren("dt"), 38 | HasChildren("em"), 39 | HasChildren("embed"), 40 | HasChildren("fieldset"), 41 | HasChildren("figcaption"), 42 | HasChildren("figure"), 43 | HasChildren("footer"), 44 | HasChildren("form"), 45 | HasChildren("h1"), 46 | HasChildren("h2"), 47 | HasChildren("h3"), 48 | HasChildren("h4"), 49 | HasChildren("h5"), 50 | HasChildren("h6"), 51 | HasChildren("head"), 52 | HasChildren("header"), 53 | NoChildren("hr"), 54 | HasChildren("html"), 55 | HasChildren("i"), 56 | HasChildren("iframe"), 57 | NoChildren("img"), 58 | NoChildren("input"), 59 | HasChildren("ins"), 60 | HasChildren("kbd"), 61 | HasChildren("label"), 62 | HasChildren("legend"), 63 | HasChildren("li"), 64 | NoChildren("link"), 65 | HasChildren("main"), 66 | HasChildren("map"), 67 | HasChildren("mark"), 68 | NoChildren("meta"), 69 | HasChildren("meter"), 70 | HasChildren("nav"), 71 | HasChildren("noscript"), 72 | HasChildren("`object`", "object"), 73 | HasChildren("objectTag", "object"), 74 | HasChildren("ol"), 75 | HasChildren("optgroup"), 76 | HasChildren("option"), 77 | HasChildren("output"), 78 | HasChildren("p"), 79 | NoChildren("param"), 80 | HasChildren("picture"), 81 | HasChildren("pre"), 82 | HasChildren("progress"), 83 | HasChildren("q"), 84 | HasChildren("rp"), 85 | HasChildren("rt"), 86 | HasChildren("ruby"), 87 | HasChildren("s"), 88 | HasChildren("samp"), 89 | HasChildren("script"), 90 | HasChildren("section"), 91 | HasChildren("select"), 92 | HasChildren("small"), 93 | NoChildren("source"), 94 | HasChildren("span"), 95 | HasChildren("strong"), 96 | HasChildren("style"), 97 | HasChildren("sub"), 98 | HasChildren("summary"), 99 | HasChildren("sup"), 100 | HasChildren("svg"), 101 | HasChildren("table"), 102 | HasChildren("tbody"), 103 | HasChildren("td"), 104 | HasChildren("template"), 105 | HasChildren("textarea"), 106 | HasChildren("tfoot"), 107 | HasChildren("th"), 108 | HasChildren("thead"), 109 | HasChildren("time"), 110 | HasChildren("title"), 111 | HasChildren("tr"), 112 | NoChildren("track"), 113 | HasChildren("u"), 114 | HasChildren("ul"), 115 | HasChildren("`var`", "var"), 116 | HasChildren("varTag", "var"), 117 | HasChildren("video"), 118 | HasChildren("wbr"), 119 | NoChildren("animate"), 120 | NoChildren("animateColor"), 121 | NoChildren("animateMotion"), 122 | NoChildren("animateTransform"), 123 | NoChildren("circle"), 124 | HasChildren("clipPath"), 125 | HasChildren("defs"), 126 | HasChildren("desc"), 127 | NoChildren("ellipse"), 128 | NoChildren("feBlend"), 129 | NoChildren("feColorMatrix"), 130 | HasChildren("feComponentTransfer"), 131 | NoChildren("feComposite"), 132 | NoChildren("feConvolveMatrix"), 133 | HasChildren("feDiffuseLighting"), 134 | NoChildren("feDisplacementMap"), 135 | NoChildren("feDistantLight"), 136 | NoChildren("feFlood"), 137 | NoChildren("feFuncA"), 138 | NoChildren("feFuncB"), 139 | NoChildren("feFuncG"), 140 | NoChildren("feFuncR"), 141 | NoChildren("feGaussianBlur"), 142 | NoChildren("feImage"), 143 | HasChildren("feMerge"), 144 | NoChildren("feMergeNode"), 145 | NoChildren("feMorphology"), 146 | NoChildren("feOffset"), 147 | NoChildren("fePointLight"), 148 | HasChildren("feSpecularLighting"), 149 | NoChildren("feSpotLight"), 150 | HasChildren("feTile"), 151 | NoChildren("feTurbulence"), 152 | HasChildren("filter"), 153 | HasChildren("foreignObject"), 154 | HasChildren("g"), 155 | NoChildren("image"), 156 | NoChildren("line"), 157 | HasChildren("linearGradient"), 158 | HasChildren("marker"), 159 | HasChildren("mask"), 160 | HasChildren("metadata"), 161 | NoChildren("mpath"), 162 | NoChildren("path"), 163 | HasChildren("pattern"), 164 | NoChildren("polygon"), 165 | NoChildren("polyline"), 166 | HasChildren("radialGradient"), 167 | NoChildren("rect"), 168 | NoChildren("set"), 169 | NoChildren("stop"), 170 | HasChildren("switch"), 171 | HasChildren("symbol"), 172 | HasChildren("textTag", "text"), 173 | HasChildren("textPath"), 174 | HasChildren("tspan"), 175 | NoChildren("use"), 176 | NoChildren("view") 177 | ) 178 | 179 | sealed trait TagType { 180 | def name: String 181 | 182 | def tag: Option[String] 183 | } 184 | 185 | final case class HasChildren(name: String, tag: Option[String]) extends TagType 186 | 187 | object HasChildren { 188 | def apply(name: String): HasChildren = HasChildren(name, None) 189 | 190 | def apply(name: String, tag: String): HasChildren = HasChildren(name, Some(tag)) 191 | } 192 | 193 | final case class NoChildren(name: String, tag: Option[String]) extends TagType 194 | 195 | object NoChildren { 196 | def apply(name: String): NoChildren = NoChildren(name, None) 197 | 198 | def apply(name: String, tag: String): NoChildren = NoChildren(name, Some(tag)) 199 | } 200 | 201 | final case class OptionalChildren(name: String, tag: Option[String]) extends TagType 202 | 203 | object OptionalChildren { 204 | def apply(name: String): OptionalChildren = OptionalChildren(name, None) 205 | 206 | def apply(name: String, tag: String): OptionalChildren = OptionalChildren(name, Some(tag)) 207 | } 208 | } 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/tyrian/TyrianAttributes.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter.tyrian 2 | 3 | object TyrianAttributes { 4 | def attrs: List[AttributeType] = 5 | List( 6 | Normal("accept"), 7 | Normal("accessKey"), 8 | Normal("action"), 9 | Normal("alt"), 10 | NoValue("async"), 11 | Normal("autoComplete"), 12 | NoValue("autoFocus"), 13 | NoValue("autofocus"), 14 | NoValue("autoPlay"), 15 | NoValue("autoplay"), 16 | Normal("charset"), 17 | NoValue("checked"), 18 | Normal("cite"), 19 | Normal("`class`", "class"), 20 | Normal("cls", "class"), 21 | Normal("className", "class"), 22 | Normal("classname", "class"), 23 | Normal("_class", "class"), 24 | Normal("cols").withTypes("String", "Int"), 25 | Normal("colSpan").withTypes("String", "Int"), 26 | Normal("colspan").withTypes("String", "Int"), 27 | Normal("content"), 28 | Normal("contentEditable").withTypes("String", "Boolean"), 29 | Normal("contenteditable").withTypes("String", "Boolean"), 30 | NoValue("controls"), 31 | Normal("coords"), 32 | Normal("data"), 33 | Normal("dateTime"), 34 | Normal("datetime"), 35 | NoValue("default"), 36 | NoValue("defer"), 37 | Normal("dir"), 38 | Normal("dirname"), 39 | NoValue("disabled"), 40 | NoValue("download"), 41 | Normal("draggable").withTypes("String", "Boolean"), 42 | Normal("encType"), 43 | Normal("enctype"), 44 | Normal("_for", "for"), 45 | Normal("forId", "for"), 46 | Normal("htmlFor", "for"), 47 | Normal("form"), 48 | Normal("formAction"), 49 | Normal("formaction"), 50 | Normal("headers"), 51 | Normal("height").withTypes("String", "Int"), 52 | NoValue("hidden"), 53 | Normal("high").withTypes("String", "Double"), 54 | Normal("href"), 55 | Normal("hrefLang"), 56 | Normal("hreflang"), 57 | Normal("http"), 58 | Normal("id"), 59 | NoValue("isMap"), 60 | NoValue("ismap"), 61 | Normal("kind"), 62 | Normal("label"), 63 | Normal("lang"), 64 | Normal("list"), 65 | NoValue("loop"), 66 | Normal("low").withTypes("String", "Double"), 67 | Normal("max").withTypes("String", "Int"), 68 | Normal("maxLength").withTypes("String", "Int"), 69 | Normal("maxlength").withTypes("String", "Int"), 70 | Normal("media"), 71 | Normal("method"), 72 | Normal("min").withTypes("String", "Int"), 73 | Normal("multiple"), 74 | Normal("muted"), 75 | Normal("name"), 76 | NoValue("noValidate"), 77 | NoValue("novalidate"), 78 | EventEmitting("onAbort"), 79 | EventEmitting("onAfterPrint"), 80 | EventEmitting("onBeforePrint"), 81 | EventEmitting("onBeforeUnload"), 82 | EventEmitting("onBlur"), 83 | EventEmitting("onCanPlay"), 84 | EventEmitting("onCanPlayThrough"), 85 | EventEmitting("onChange", "change"), 86 | EventEmitting("onClick", "click"), 87 | EventEmitting("onContextMenu"), 88 | EventEmitting("onCopy"), 89 | EventEmitting("onCueChange"), 90 | EventEmitting("onCut"), 91 | EventEmitting("onDblClick"), 92 | EventEmitting("onDoubleClick", "dblclick"), 93 | EventEmitting("onDrag"), 94 | EventEmitting("onDragEnd"), 95 | EventEmitting("onDragEnter"), 96 | EventEmitting("onDragLeave"), 97 | EventEmitting("onDragOver"), 98 | EventEmitting("onDragStart"), 99 | EventEmitting("onDrop"), 100 | EventEmitting("onDurationChange"), 101 | EventEmitting("onEmptied"), 102 | EventEmitting("onEnded"), 103 | EventEmitting("onError"), 104 | EventEmitting("onFocus"), 105 | EventEmitting("onHashChange"), 106 | EventEmitting("onInput"), 107 | EventEmitting("onInvalid"), 108 | EventEmitting("onKeyDown"), 109 | EventEmitting("onKeyPress"), 110 | EventEmitting("onKeyUp"), 111 | EventEmitting("onLoad"), 112 | EventEmitting("onLoadedData"), 113 | EventEmitting("onLoadedMetadata"), 114 | EventEmitting("onLoadStart"), 115 | EventEmitting("onMouseDown"), 116 | EventEmitting("onMousemove"), 117 | EventEmitting("onMouseOut"), 118 | EventEmitting("onMouseOver"), 119 | EventEmitting("onMouseUp"), 120 | EventEmitting("onMouseWheel"), 121 | EventEmitting("onOffline"), 122 | EventEmitting("onOnline"), 123 | EventEmitting("onPageHide"), 124 | EventEmitting("onPageShow"), 125 | EventEmitting("onPaste"), 126 | EventEmitting("onPause"), 127 | EventEmitting("onPlay"), 128 | EventEmitting("onPlaying"), 129 | EventEmitting("onPopState"), 130 | EventEmitting("onProgress"), 131 | EventEmitting("onRateChange"), 132 | EventEmitting("onReset"), 133 | EventEmitting("onResize"), 134 | EventEmitting("onScroll"), 135 | EventEmitting("onSearch"), 136 | EventEmitting("onSeeked"), 137 | EventEmitting("onSeeking"), 138 | EventEmitting("onSelect"), 139 | EventEmitting("onStalled"), 140 | EventEmitting("onStorage"), 141 | EventEmitting("onSubmit"), 142 | EventEmitting("onSuspend"), 143 | EventEmitting("onTimeUpdate"), 144 | EventEmitting("onToggle"), 145 | EventEmitting("onUnload"), 146 | EventEmitting("onVolumeChange"), 147 | EventEmitting("onWaiting"), 148 | EventEmitting("onWheel"), 149 | NoValue("open"), 150 | Normal("optimum").withTypes("String", "Double"), 151 | Normal("pattern"), 152 | Normal("placeholder"), 153 | Normal("poster"), 154 | Normal("preload"), 155 | NoValue("readOnly"), 156 | NoValue("readonly"), 157 | Normal("rel"), 158 | NoValue("required"), 159 | NoValue("reversed"), 160 | Normal("rows").withTypes("String", "Int"), 161 | Normal("rowSpan").withTypes("String", "Int"), 162 | Normal("rowspan").withTypes("String", "Int"), 163 | NoValue("sandbox"), 164 | Normal("scope"), 165 | NoValue("selected"), 166 | Normal("shape"), 167 | Normal("size").withTypes("String", "Int"), 168 | Normal("sizes"), 169 | Normal("span").withTypes("String", "Int"), 170 | Normal("spellCheck").withTypes("String", "Boolean"), 171 | Normal("spellcheck").withTypes("String", "Boolean"), 172 | Normal("src"), 173 | Normal("srcDoc"), 174 | Normal("srcdoc"), 175 | Normal("srcLang"), 176 | Normal("srclang"), 177 | Normal("srcSet"), 178 | Normal("srcset"), 179 | Normal("start").withTypes("String", "Int"), 180 | Normal("step").withTypes("String", "Int"), 181 | Normal("style"), 182 | Normal("tabIndex").withTypes("String", "Int"), 183 | Normal("tabindex").withTypes("String", "Int"), 184 | Normal("target"), 185 | Normal("title"), 186 | Normal("translate"), 187 | Normal("`type`", "type"), 188 | Normal("_type", "type"), 189 | Normal("typ", "type"), 190 | Normal("tpe", "type"), 191 | Normal("useMap"), 192 | Normal("usemap"), 193 | Normal("width").withTypes("String", "Int"), 194 | Normal("wrap"), 195 | Normal("value").withTypes("String", "Double", "Boolean"), 196 | Normal("cx"), 197 | Normal("cy"), 198 | Normal("d"), 199 | Normal("fill"), 200 | Normal("pathLength", "pathLength"), // svg attributes are case sensitive 201 | Normal("points"), 202 | Normal("r"), 203 | Normal("rx"), 204 | Normal("ry"), 205 | Normal("stroke"), 206 | Normal("viewBox", "viewBox"), // svg attributes are case sensitive 207 | Normal("xmlns"), 208 | Normal("x"), 209 | Normal("x1"), 210 | Normal("x2"), 211 | Normal("y"), 212 | Normal("y1"), 213 | Normal("y2") 214 | ) 215 | 216 | sealed trait AttributeType { 217 | def name: String 218 | 219 | def attrName: Option[String] 220 | } 221 | 222 | final case class Normal(name: String, attrName: Option[String], types: List[String]) extends AttributeType { 223 | def withTypes(newTypes: String*): Normal = 224 | Normal(name, attrName, newTypes.toList) 225 | } 226 | 227 | object Normal { 228 | def apply(name: String): Normal = Normal(name, None, List("String")) 229 | 230 | def apply(name: String, attrName: String): Normal = Normal(name, Some(attrName), List("String")) 231 | } 232 | 233 | final case class NoValue(name: String, attrName: Option[String]) extends AttributeType 234 | 235 | object NoValue { 236 | def apply(name: String): NoValue = NoValue(name, None) 237 | 238 | def apply(name: String, attrName: String): NoValue = NoValue(name, Some(attrName)) 239 | } 240 | 241 | final case class EventEmitting(name: String, attrName: Option[String]) extends AttributeType 242 | 243 | object EventEmitting { 244 | def apply(name: String): EventEmitting = EventEmitting(name, None) 245 | 246 | def apply(name: String, attrName: String): EventEmitting = EventEmitting(name, Some(attrName)) 247 | } 248 | 249 | 250 | } 251 | 252 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/ConverterType.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter 2 | 3 | import simer.html.converter.AttributeType.{BooleanAttribute, EventAttribute, IntAttribute, StringAttribute} 4 | 5 | sealed trait ConverterType { 6 | val attributePrefix: String 7 | val nodePrefix: String 8 | val customAttributeFunctionName: String 9 | val newLineAttributes: Boolean 10 | val isBooleanTypeConversionDisabled: Boolean 11 | //stores name conversions for html attribute names to scala names 12 | val attributeNameMap: Map[String, AttributeType] 13 | val tagNameMap: Map[String, String] 14 | } 15 | 16 | object ConverterType { 17 | 18 | object ScalaJSReact { 19 | //attributes naming and type convention found in ScalaJS-React 20 | val dataAttributes = 21 | Map( 22 | "class" -> StringAttribute("className"), 23 | "for" -> StringAttribute("`for`"), 24 | "type" -> StringAttribute("`type`"), 25 | "content" -> StringAttribute("contentAttr"), 26 | "hreflang" -> StringAttribute("hrefLang"), 27 | "tabindex" -> IntAttribute("tabIndex"), 28 | "styleTag" -> StringAttribute("styleTag"), 29 | "autocomplete" -> StringAttribute("autoComplete"), 30 | "allowfullscreen" -> BooleanAttribute("allowFullScreen"), 31 | "allowtransparency" -> BooleanAttribute("allowTransparency"), 32 | "async" -> BooleanAttribute("async"), 33 | "autocorrect" -> BooleanAttribute("autoCorrect"), 34 | "autofocus" -> BooleanAttribute("autoFocus"), 35 | "autoplay" -> BooleanAttribute("autoPlay"), 36 | "autosave" -> BooleanAttribute("autoSave"), 37 | "checked" -> BooleanAttribute("checked"), 38 | "colspan" -> IntAttribute("colSpan"), 39 | "controls" -> BooleanAttribute("controls"), 40 | "default" -> BooleanAttribute("default"), 41 | "defer" -> BooleanAttribute("defer"), 42 | "disablepictureinpicture" -> BooleanAttribute("disablePictureInPicture"), 43 | "disabled" -> BooleanAttribute("disabled"), 44 | "draggable" -> BooleanAttribute("draggable"), 45 | "formnovalidate" -> BooleanAttribute("formNoValidate"), 46 | "hidden" -> BooleanAttribute("hidden"), 47 | "itemscope" -> BooleanAttribute("itemScope"), 48 | "loop" -> BooleanAttribute("loop"), 49 | "maxlength" -> IntAttribute("maxLength"), 50 | "minlength" -> IntAttribute("minLength"), 51 | "multiple" -> BooleanAttribute("multiple"), 52 | "muted" -> BooleanAttribute("muted"), 53 | "nomodule" -> BooleanAttribute("noModule"), 54 | "novalidate" -> BooleanAttribute("noValidate"), 55 | "open" -> BooleanAttribute("open"), 56 | "playsinline" -> BooleanAttribute("playsInline"), 57 | "radiogroup" -> StringAttribute("radioGroup"), 58 | "readonly" -> BooleanAttribute("readOnly"), 59 | "required" -> BooleanAttribute("required"), 60 | "reversed" -> BooleanAttribute("reversed"), 61 | "rowspan" -> IntAttribute("rowSpan"), 62 | "rows" -> IntAttribute("rows"), 63 | "scoped" -> BooleanAttribute("scoped"), 64 | "seamless" -> BooleanAttribute("seamless"), 65 | "selected" -> BooleanAttribute("selected"), 66 | "size" -> IntAttribute("size"), 67 | "spellcheck" -> BooleanAttribute("spellCheck"), 68 | "contextmenu" -> StringAttribute("contextMenu"), 69 | "autocapitalize" -> StringAttribute("autoCapitalize"), 70 | "cellpadding" -> StringAttribute("cellPadding"), 71 | "cellspacing" -> StringAttribute("cellSpacing"), 72 | "classid" -> StringAttribute("classID"), 73 | ) 74 | } 75 | 76 | case class ScalaJSReact(newLineAttributes: Boolean, 77 | isBooleanTypeConversionDisabled: Boolean) extends ConverterType { 78 | val attributePrefix: String = "^." 79 | val nodePrefix: String = "<." 80 | val customAttributeFunctionName: String = "VdomAttr" 81 | 82 | override val tagNameMap: Map[String, String] = 83 | Map( 84 | "title" -> "titleTag", 85 | "style" -> "styleTag" 86 | ) 87 | 88 | override val attributeNameMap: Map[String, AttributeType] = 89 | ScalaJSReact.dataAttributes ++ EventAttribute.reactAndLaminarAndOutwatchEventAttributes 90 | } 91 | 92 | case class ScalaTags(newLineAttributes: Boolean, 93 | isBooleanTypeConversionDisabled: Boolean) extends ConverterType { 94 | val attributePrefix: String = "" 95 | val nodePrefix: String = "" 96 | val customAttributeFunctionName: String = "attr" 97 | 98 | override val tagNameMap: Map[String, String] = 99 | Map.empty 100 | 101 | override val attributeNameMap: Map[String, AttributeType] = 102 | Map( 103 | "class" -> StringAttribute("cls"), 104 | "for" -> StringAttribute("`for`"), 105 | "type" -> StringAttribute("`type`") 106 | ) 107 | } 108 | 109 | case class Laminar(newLineAttributes: Boolean, 110 | isBooleanTypeConversionDisabled: Boolean) extends ConverterType { 111 | val attributePrefix: String = "" 112 | val nodePrefix: String = "" 113 | val customAttributeFunctionName: String = "dataAttr" 114 | 115 | override val tagNameMap: Map[String, String] = 116 | Map( 117 | "style" -> "styleTag", 118 | "link" -> "linkTag", 119 | "param" -> "paramTag", 120 | "map" -> "mapTag", 121 | "title" -> "titleTag", 122 | "object" -> "objectTag", 123 | "noscript" -> "noScript", 124 | "textarea" -> "textArea", 125 | "optgroup" -> "optGroup", 126 | "fieldset" -> "fieldSet" 127 | ) 128 | 129 | override val attributeNameMap: Map[String, AttributeType] = 130 | ScalaJSReact.dataAttributes ++ Map( 131 | "class" -> StringAttribute("cls"), 132 | "for" -> StringAttribute("forId"), 133 | "type" -> StringAttribute("tpe"), 134 | "value" -> StringAttribute("defaultValue"), 135 | "checked" -> BooleanAttribute("defaultChecked"), 136 | "selected" -> StringAttribute("defaultSelected"), 137 | "for" -> StringAttribute("forId"), 138 | "id" -> StringAttribute("idAttr"), 139 | "max" -> StringAttribute("maxAttr"), 140 | "min" -> StringAttribute("minAttr"), 141 | "step" -> StringAttribute("stepAttr"), 142 | "offset" -> StringAttribute("offsetAttr"), 143 | "result" -> StringAttribute("resultAttr"), 144 | "loading" -> StringAttribute("loadingAttr"), 145 | "style" -> StringAttribute("styleAttr"), 146 | "content" -> StringAttribute("contentAttr"), 147 | "form" -> StringAttribute("formId"), 148 | "height" -> IntAttribute("heightAttr"), 149 | "width" -> IntAttribute("widthAttr"), 150 | "list" -> StringAttribute("listId"), 151 | "contextmenu" -> StringAttribute("contextMenuId"), 152 | ) ++ EventAttribute.reactAndLaminarAndOutwatchEventAttributes 153 | } 154 | 155 | case class Outwatch(newLineAttributes: Boolean, 156 | isBooleanTypeConversionDisabled: Boolean) extends ConverterType { 157 | val attributePrefix: String = "" 158 | val nodePrefix: String = "" 159 | val customAttributeFunctionName: String = "VModifier.attr" 160 | 161 | override val tagNameMap: Map[String, String] = 162 | Map( 163 | "style" -> "styleTag", 164 | "link" -> "linkTag", 165 | "param" -> "paramTag", 166 | "map" -> "mapTag", 167 | "title" -> "titleTag", 168 | "object" -> "objectTag", 169 | "noscript" -> "noScript", 170 | "textarea" -> "textArea", 171 | "optgroup" -> "optGroup", 172 | "fieldset" -> "fieldSet" 173 | ) 174 | 175 | override val attributeNameMap: Map[String, AttributeType] = 176 | ScalaJSReact.dataAttributes ++ Map( 177 | "class" -> StringAttribute("cls"), 178 | "for" -> StringAttribute("forId"), 179 | "type" -> StringAttribute("tpe"), 180 | "value" -> StringAttribute("value"), 181 | "checked" -> BooleanAttribute("checked"), 182 | "selected" -> StringAttribute("selected"), 183 | "for" -> StringAttribute("forId"), 184 | "id" -> StringAttribute("idAttr"), 185 | "max" -> StringAttribute("maxAttr"), 186 | "min" -> StringAttribute("minAttr"), 187 | "step" -> StringAttribute("stepAttr"), 188 | "offset" -> StringAttribute("offsetAttr"), 189 | "result" -> StringAttribute("resultAttr"), 190 | "loading" -> StringAttribute("loadingAttr"), 191 | "style" -> StringAttribute("styleAttr"), 192 | "content" -> StringAttribute("contentAttr"), 193 | "form" -> StringAttribute("formId"), 194 | "height" -> IntAttribute("heightAttr"), 195 | "width" -> IntAttribute("widthAttr"), 196 | "list" -> StringAttribute("listId"), 197 | "contextmenu" -> StringAttribute("contextMenuId"), 198 | ) ++ EventAttribute.reactAndLaminarAndOutwatchEventAttributes.map { case (key, attr) => 199 | (key, attr.copy(function = "foreach")) 200 | } 201 | } 202 | 203 | case class Tyrian(newLineAttributes: Boolean, 204 | isBooleanTypeConversionDisabled: Boolean) extends ConverterType { 205 | override val attributePrefix: String = "" 206 | override val nodePrefix: String = "" 207 | override val customAttributeFunctionName: String = "Attribute" 208 | override val attributeNameMap: Map[String, AttributeType] = Map.empty 209 | override val tagNameMap: Map[String, String] = Map.empty 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/AttributeType.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter 2 | 3 | /** 4 | * Defines mapping for HTML attribute key names to target Scala's key and function name 5 | * 6 | * For example to convert `onclick = "alert("");"` to `onClick <-- "alert("");"` 7 | * this map entry should be 8 | * {{{ 9 | * map.put("onclick", EventAttribute("onClick", "<--")) 10 | * }}} 11 | */ 12 | 13 | sealed trait AttributeType { 14 | def key: String 15 | def function: String 16 | } 17 | 18 | object AttributeType { 19 | 20 | case class StringAttribute(key: String, 21 | function: String = ":=") extends AttributeType 22 | 23 | case class IntAttribute(key: String, 24 | function: String = ":=") extends AttributeType 25 | 26 | case class DoubleAttribute(key: String, 27 | function: String = ":=") extends AttributeType 28 | 29 | case class BooleanAttribute(key: String, 30 | function: String = ":=") extends AttributeType 31 | 32 | object EventAttribute { 33 | //Event attributes shared by ScalaJS-react, Laminar and Outwatch 34 | val reactAndLaminarAndOutwatchEventAttributes = 35 | Map( 36 | s"onchange" -> EventAttribute("onChange"), 37 | s"onclick" -> EventAttribute("onClick"), 38 | s"onclickcapture" -> EventAttribute("onClickCapture"), 39 | s"onetimecode" -> EventAttribute("oneTimeCode"), 40 | s"onabort" -> EventAttribute("onAbort"), 41 | s"onabortcapture" -> EventAttribute("onAbortCapture"), 42 | s"onanimationend" -> EventAttribute("onAnimationEnd"), 43 | s"onanimationendcapture" -> EventAttribute("onAnimationEndCapture"), 44 | s"onanimationiteration" -> EventAttribute("onAnimationIteration"), 45 | s"onanimationiterationcapture" -> EventAttribute("onAnimationIterationCapture"), 46 | s"onanimationstart" -> EventAttribute("onAnimationStart"), 47 | s"onanimationstartcapture" -> EventAttribute("onAnimationStartCapture"), 48 | s"onauxclick" -> EventAttribute("onAuxClick"), 49 | s"onauxclickcapture" -> EventAttribute("onAuxClickCapture"), 50 | s"onbeforeinput" -> EventAttribute("onBeforeInput"), 51 | s"onblur" -> EventAttribute("onBlur"), 52 | s"onblurcapture" -> EventAttribute("onBlurCapture"), 53 | s"oncanplay" -> EventAttribute("onCanPlay"), 54 | s"oncanplaycapture" -> EventAttribute("onCanPlayCapture"), 55 | s"oncanplaythrough" -> EventAttribute("onCanPlayThrough"), 56 | s"oncompositionend" -> EventAttribute("onCompositionEnd"), 57 | s"oncompositionstart" -> EventAttribute("onCompositionStart"), 58 | s"oncompositionupdate" -> EventAttribute("onCompositionUpdate"), 59 | s"oncontextmenu" -> EventAttribute("onContextMenu"), 60 | s"oncontextmenucapture" -> EventAttribute("onContextMenuCapture"), 61 | s"oncopy" -> EventAttribute("onCopy"), 62 | s"oncopycapture" -> EventAttribute("onCopyCapture"), 63 | s"oncut" -> EventAttribute("onCut"), 64 | s"oncutcapture" -> EventAttribute("onCutCapture"), 65 | s"ondblclick" -> EventAttribute("onDblClick"), 66 | s"ondoubleclick" -> EventAttribute("onDoubleClick"), 67 | s"ondoubleclickcapture" -> EventAttribute("onDoubleClickCapture"), 68 | s"ondrag" -> EventAttribute("onDrag"), 69 | s"ondragcapture" -> EventAttribute("onDragCapture"), 70 | s"ondragend" -> EventAttribute("onDragEnd"), 71 | s"ondragendcapture" -> EventAttribute("onDragEndCapture"), 72 | s"ondragenter" -> EventAttribute("onDragEnter"), 73 | s"ondragentercapture" -> EventAttribute("onDragEnterCapture"), 74 | s"ondragexit" -> EventAttribute("onDragExit"), 75 | s"ondragexitcapture" -> EventAttribute("onDragExitCapture"), 76 | s"ondragleave" -> EventAttribute("onDragLeave"), 77 | s"ondragleavecapture" -> EventAttribute("onDragLeaveCapture"), 78 | s"ondragover" -> EventAttribute("onDragOver"), 79 | s"ondragovercapture" -> EventAttribute("onDragOverCapture"), 80 | s"ondragstart" -> EventAttribute("onDragStart"), 81 | s"ondragstartcapture" -> EventAttribute("onDragStartCapture"), 82 | s"ondrop" -> EventAttribute("onDrop"), 83 | s"ondropcapture" -> EventAttribute("onDropCapture"), 84 | s"ondurationchange" -> EventAttribute("onDurationChange"), 85 | s"ondurationchangecapture" -> EventAttribute("onDurationChangeCapture"), 86 | s"onemptied" -> EventAttribute("onEmptied"), 87 | s"onemptiedcapture" -> EventAttribute("onEmptiedCapture"), 88 | s"onencrypted" -> EventAttribute("onEncrypted"), 89 | s"onencryptedcapture" -> EventAttribute("onEncryptedCapture"), 90 | s"onended" -> EventAttribute("onEnded"), 91 | s"onendedcapture" -> EventAttribute("onEndedCapture"), 92 | s"onerror" -> EventAttribute("onError"), 93 | s"onerrorcapture" -> EventAttribute("onErrorCapture"), 94 | s"onfocus" -> EventAttribute("onFocus"), 95 | s"onfocuscapture" -> EventAttribute("onFocusCapture"), 96 | s"oninput" -> EventAttribute("onInput"), 97 | s"oninputcapture" -> EventAttribute("onInputCapture"), 98 | s"oninvalid" -> EventAttribute("onInvalid"), 99 | s"oninvalidcapture" -> EventAttribute("onInvalidCapture"), 100 | s"onkeydown" -> EventAttribute("onKeyDown"), 101 | s"onkeydowncapture" -> EventAttribute("onKeyDownCapture"), 102 | s"onkeypress" -> EventAttribute("onKeyPress"), 103 | s"onkeypresscapture" -> EventAttribute("onKeyPressCapture"), 104 | s"onkeyup" -> EventAttribute("onKeyUp"), 105 | s"onkeyupcapture" -> EventAttribute("onKeyUpCapture"), 106 | s"onload" -> EventAttribute("onLoad"), 107 | s"onloadcapture" -> EventAttribute("onLoadCapture"), 108 | s"onloadstart" -> EventAttribute("onLoadStart"), 109 | s"onloadstartcapture" -> EventAttribute("onLoadStartCapture"), 110 | s"onloadeddata" -> EventAttribute("onLoadedData"), 111 | s"onloadeddatacapture" -> EventAttribute("onLoadedDataCapture"), 112 | s"onloadedmetadata" -> EventAttribute("onLoadedMetadata"), 113 | s"onloadedmetadatacapture" -> EventAttribute("onLoadedMetadataCapture"), 114 | s"onmousedown" -> EventAttribute("onMouseDown"), 115 | s"onmousedowncapture" -> EventAttribute("onMouseDownCapture"), 116 | s"onmouseenter" -> EventAttribute("onMouseEnter"), 117 | s"onmouseleave" -> EventAttribute("onMouseLeave"), 118 | s"onmousemove" -> EventAttribute("onMouseMove"), 119 | s"onmousemovecapture" -> EventAttribute("onMouseMoveCapture"), 120 | s"onmouseout" -> EventAttribute("onMouseOut"), 121 | s"onmouseoutcapture" -> EventAttribute("onMouseOutCapture"), 122 | s"onmouseover" -> EventAttribute("onMouseOver"), 123 | s"onmouseovercapture" -> EventAttribute("onMouseOverCapture"), 124 | s"onmouseup" -> EventAttribute("onMouseUp"), 125 | s"onmouseupcapture" -> EventAttribute("onMouseUpCapture"), 126 | s"onpaste" -> EventAttribute("onPaste"), 127 | s"onpastecapture" -> EventAttribute("onPasteCapture"), 128 | s"onpause" -> EventAttribute("onPause"), 129 | s"onpausecapture" -> EventAttribute("onPauseCapture"), 130 | s"onplay" -> EventAttribute("onPlay"), 131 | s"onplaycapture" -> EventAttribute("onPlayCapture"), 132 | s"onplaying" -> EventAttribute("onPlaying"), 133 | s"onplayingcapture" -> EventAttribute("onPlayingCapture"), 134 | s"ongotpointercapture" -> EventAttribute("onGotPointerCapture"), 135 | s"onlostpointercapture" -> EventAttribute("onLostPointerCapture"), 136 | s"onpointercancel" -> EventAttribute("onPointerCancel"), 137 | s"onpointerdown" -> EventAttribute("onPointerDown"), 138 | s"onpointerenter" -> EventAttribute("onPointerEnter"), 139 | s"onpointerleave" -> EventAttribute("onPointerLeave"), 140 | s"onpointermove" -> EventAttribute("onPointerMove"), 141 | s"onpointerout" -> EventAttribute("onPointerOut"), 142 | s"onpointerover" -> EventAttribute("onPointerOver"), 143 | s"onpointerup" -> EventAttribute("onPointerUp"), 144 | s"onprogress" -> EventAttribute("onProgress"), 145 | s"onprogresscapture" -> EventAttribute("onProgressCapture"), 146 | s"onratechange" -> EventAttribute("onRateChange"), 147 | s"onratechangecapture" -> EventAttribute("onRateChangeCapture"), 148 | s"onreset" -> EventAttribute("onReset"), 149 | s"onresetcapture" -> EventAttribute("onResetCapture"), 150 | s"onscroll" -> EventAttribute("onScroll"), 151 | s"onscrollcapture" -> EventAttribute("onScrollCapture"), 152 | s"onseeked" -> EventAttribute("onSeeked"), 153 | s"onseekedcapture" -> EventAttribute("onSeekedCapture"), 154 | s"onseeking" -> EventAttribute("onSeeking"), 155 | s"onseekingcapture" -> EventAttribute("onSeekingCapture"), 156 | s"onselect" -> EventAttribute("onSelect"), 157 | s"onstalled" -> EventAttribute("onStalled"), 158 | s"onstalledcapture" -> EventAttribute("onStalledCapture"), 159 | s"onsubmit" -> EventAttribute("onSubmit"), 160 | s"onsubmitcapture" -> EventAttribute("onSubmitCapture"), 161 | s"onsuspend" -> EventAttribute("onSuspend"), 162 | s"onsuspendcapture" -> EventAttribute("onSuspendCapture"), 163 | s"ontimeupdate" -> EventAttribute("onTimeUpdate"), 164 | s"ontimeupdatecapture" -> EventAttribute("onTimeUpdateCapture"), 165 | s"ontouchcancel" -> EventAttribute("onTouchCancel"), 166 | s"ontouchcancelcapture" -> EventAttribute("onTouchCancelCapture"), 167 | s"ontouchend" -> EventAttribute("onTouchEnd"), 168 | s"ontouchendcapture" -> EventAttribute("onTouchEndCapture"), 169 | s"ontouchmove" -> EventAttribute("onTouchMove"), 170 | s"ontouchmovecapture" -> EventAttribute("onTouchMoveCapture"), 171 | s"ontouchstart" -> EventAttribute("onTouchStart"), 172 | s"ontouchstartcapture" -> EventAttribute("onTouchStartCapture"), 173 | s"ontransitionend" -> EventAttribute("onTransitionEnd"), 174 | s"ontransitionendcapture" -> EventAttribute("onTransitionEndCapture"), 175 | s"onvolumechange" -> EventAttribute("onVolumeChange"), 176 | s"onvolumechangecapture" -> EventAttribute("onVolumeChangeCapture"), 177 | s"onwaiting" -> EventAttribute("onWaiting"), 178 | s"onwaitingcapture" -> EventAttribute("onWaitingCapture"), 179 | s"onwheel" -> EventAttribute("onWheel"), 180 | s"onwheelcapture" -> EventAttribute("onWheelCapture") 181 | ) 182 | } 183 | case class EventAttribute(key: String, 184 | function: String = "-->") extends AttributeType 185 | } 186 | -------------------------------------------------------------------------------- /src/main/scala/simer/html/converter/HtmlToScalaConverter.scala: -------------------------------------------------------------------------------- 1 | package simer.html.converter 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.ext._ 5 | import org.scalajs.dom.html.TextArea 6 | import org.scalajs.dom.raw.{DOMParser, NamedNodeMap, Node} 7 | import simer.html.converter.ConverterType._ 8 | import simer.html.converter.tyrian.TyrianConverter 9 | import simer.html.converter.utils.ConverterUtil._ 10 | 11 | import scala.scalajs.js 12 | import scala.util.Try 13 | 14 | object HtmlToScalaConverter { 15 | 16 | def main(args: Array[String]): Unit = { 17 | val template = HTMLTemplate.template(runConverter) 18 | dom.document.getElementById("content").appendChild(template.render) 19 | } 20 | 21 | def runConverter(converterType: ConverterType): Unit = { 22 | val htmlCode = dom.document.getElementById("htmlCode").asInstanceOf[TextArea].value 23 | val parsedHtml = new DOMParser().parseFromString(htmlCode, "text/html") 24 | val scalaCodeTextArea = dom.document.getElementById("scalaCode").asInstanceOf[TextArea] 25 | val rootChildNodes = removeGarbageChildNodes(parsedHtml) 26 | //having more then one HTML tree causes the DOMParser to generate an incorrect tree. 27 | val scalaCodes = rootChildNodes.map(toScala(_, converterType)) 28 | val scalaCode = 29 | if (scalaCodes.size > 1) { 30 | val fixMe = s"""//FIXME - MULTIPLE HTML TREES PASSED TO THE CONVERTER. THIS MIGHT GENERATE UNEXPECTED CODE. Check is not in the input HTML.""" 31 | fixMe + "\n" + scalaCodes.mkString(", ") 32 | } else 33 | scalaCodes.mkString(", ") 34 | 35 | val scalaCodeWithoutParserAddedTags = removeTagsFromScalaCode(htmlCode, scalaCode, "html", "head", "body") 36 | scalaCodeTextArea.value = scalaCodeWithoutParserAddedTags.trim 37 | } 38 | 39 | def removeGarbageChildNodes(node: Node): collection.Seq[Node] = 40 | node.childNodes.filterNot(isGarbageNode) 41 | 42 | def isGarbageNode(node: Node): Boolean = 43 | js.isUndefined(node) || node.nodeName == "#comment" || (node.nodeName == "#text" && node.nodeValue.trim.isEmpty) 44 | 45 | /** 46 | * Recursively generates the output Scala code for each HTML node and it's children. 47 | * 48 | * Filters out comments and empty text's nodes (garbage nodes :D) from the input HTML before converting to Scala. 49 | */ 50 | def toScala(node: Node, converterType: ConverterType): String = { 51 | //removes all comment and empty text nodes. 52 | val childrenWithoutGarbageNodes: collection.Seq[Node] = removeGarbageChildNodes(node) 53 | 54 | val children = 55 | childrenWithoutGarbageNodes 56 | .map(toScala(_, converterType)) 57 | .mkString(",\n") 58 | 59 | toScala(node, converterType, childrenWithoutGarbageNodes, children) 60 | } 61 | 62 | /** 63 | * Converts a single HTML node, given it's child nodes's (@param children) already converted. 64 | */ 65 | private def toScala(node: Node, 66 | converterType: ConverterType, 67 | childrenWithoutGarbageNodes: collection.Seq[Node], 68 | children: String): String = { 69 | 70 | converterType match { 71 | case _: Tyrian => 72 | TyrianConverter.toScala( 73 | node = node, 74 | converterType = converterType, 75 | childrenWithoutGarbageNodes = childrenWithoutGarbageNodes, 76 | children = children 77 | ) 78 | 79 | case _ => 80 | node.nodeName match { 81 | case "#text" => 82 | tripleQuote(node.nodeValue) 83 | 84 | case _ => 85 | val scalaAttrList = 86 | toScalaAttributes( 87 | attributes = node.attributes, 88 | converterType = converterType 89 | ) 90 | 91 | val nodeNameLowerCase = node.nodeName.toLowerCase 92 | //fetch the node/tag name supplied by the ConverterType 93 | val replacedNodeName = converterType.tagNameMap.getOrElse(nodeNameLowerCase, nodeNameLowerCase) 94 | val nodeString = s"${converterType.nodePrefix}$replacedNodeName" 95 | 96 | if (scalaAttrList.isEmpty && children.isEmpty) { 97 | converterType match { 98 | case _: Laminar => 99 | s"$nodeString()" //laminar requires nodes to be closed eg: br() 100 | 101 | case _: ScalaJSReact | _: ScalaTags | _: Outwatch | _: Tyrian => 102 | nodeString 103 | 104 | } 105 | } else { //this node/tag has attributes or has children 106 | val scalaAttrString = 107 | if (scalaAttrList.isEmpty) 108 | "" 109 | else if (!converterType.newLineAttributes) 110 | scalaAttrList.mkString("\n", ",\n", "") 111 | else 112 | scalaAttrList.mkString(", ") 113 | 114 | val childrenString = 115 | if (children.isEmpty) { 116 | "" 117 | } else { 118 | //text child nodes can be a part of the same List as the attribute List. They don't have to go to a new line. 119 | val isChildNodeATextNode = childrenWithoutGarbageNodes.headOption.exists(_.nodeName == "#text") 120 | val commaMayBe = if (scalaAttrString.isEmpty) "" else "," 121 | val startNewLineMayBe = if (isChildNodeATextNode && (converterType.newLineAttributes || scalaAttrString.isEmpty)) "" else "\n" 122 | //add a newLine at the end if this node has more then one child nodes 123 | val endNewLineMayBe = if (isChildNodeATextNode && childrenWithoutGarbageNodes.size <= 1) "" else "\n" 124 | s"$commaMayBe$startNewLineMayBe$children$endNewLineMayBe" 125 | } 126 | 127 | converterType match { 128 | case _: Tyrian => 129 | s"$nodeString($scalaAttrString)($childrenString)" 130 | 131 | case _ => 132 | s"$nodeString($scalaAttrString$childrenString)" 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Converts input string of format "list-style: none; padding: 0;" 141 | * to ("list-style" -> "none", "padding" -> "0") 142 | */ 143 | def splitAttrValueToTuples(attrValueString: String) = 144 | attrValueString.split(";").map { 145 | string => 146 | val styleKeyValue = string.split(":") 147 | s""""${styleKeyValue.head.trim}" -> "${styleKeyValue.last.trim}"""" 148 | }.mkString(", ") 149 | 150 | /** 151 | * Converts HTML node attributes to Scala attributes 152 | */ 153 | def toScalaAttributes(attributes: NamedNodeMap, 154 | converterType: ConverterType): Iterable[String] = 155 | if (js.isUndefined(attributes) || attributes.isEmpty) 156 | List.empty 157 | else 158 | attributes map { 159 | case (attrKey, attrValue) => 160 | val attrValueString = attrValue.value 161 | val escapedAttrValue = tripleQuote(attrValueString) 162 | val attributeNameMapOption = converterType.attributeNameMap.get(attrKey) 163 | 164 | if (attrKey == "style") { 165 | val styleValuesDictionary = 166 | converterType match { 167 | case _: ScalaJSReact | _: ScalaTags => 168 | //if scalatags or scalajs-react convert the style value to dictionary 169 | s"js.Dictionary(${splitAttrValueToTuples(attrValueString)})" 170 | 171 | case _: Laminar | _: Outwatch | _: Tyrian => 172 | //for laminar/outwatch do not split. Simply return the javascript string value. 173 | escapedAttrValue 174 | } 175 | 176 | attributeNameMapOption match { 177 | case Some(attrNameMap) => 178 | s"""${converterType.attributePrefix}${attrNameMap.key} ${attrNameMap.function} $styleValuesDictionary""" 179 | 180 | case None => 181 | s"""${converterType.attributePrefix}$attrKey := $styleValuesDictionary""" 182 | } 183 | } else { 184 | attributeNameMapOption match { 185 | case Some(attrNameMap) => 186 | 187 | val typedValue = 188 | attrNameMap match { 189 | case _: AttributeType.StringAttribute => 190 | escapedAttrValue 191 | 192 | case _: AttributeType.IntAttribute => 193 | //if unable to convert to int or else revert back to string 194 | Try(attrValueString.toInt) getOrElse escapedAttrValue 195 | 196 | case _: AttributeType.DoubleAttribute => 197 | //if unable to convert to double or else revert back to string 198 | Try(attrValueString.toDouble) getOrElse escapedAttrValue 199 | 200 | case _: AttributeType.BooleanAttribute => 201 | //if unable to convert to boolean or else revert back to string 202 | //Note: In HTML the value of boolean attribute can be set to anything to enable it. 203 | // This should be looked into manually to handle cases where there could be some 204 | // javascript code dependant on the actual value. 205 | if (converterType.isBooleanTypeConversionDisabled) 206 | escapedAttrValue 207 | else 208 | true 209 | 210 | case _: AttributeType.EventAttribute => 211 | //Event attributes get converted to however the target library's expected syntax. 212 | //https://github.com/simerplaha/html-to-scala-converter/issues/9 213 | converterType match { 214 | case _: ScalaJSReact => 215 | s"""Callback(js.eval($escapedAttrValue))""" 216 | 217 | case _: ScalaTags | _: Tyrian => 218 | escapedAttrValue 219 | 220 | case _: Laminar | _: Outwatch => 221 | s"""(_ => js.eval($escapedAttrValue))""" 222 | 223 | } 224 | } 225 | 226 | s"${converterType.attributePrefix}${attrNameMap.key} ${attrNameMap.function} $typedValue" 227 | 228 | case None => 229 | if (attrKey.matches("[a-zA-Z0-9]*$")) 230 | s"${converterType.attributePrefix}$attrKey := $escapedAttrValue" 231 | else //else it's a custom attribute 232 | s"""${converterType.customAttributeFunctionName}("$attrKey") := $escapedAttrValue""" 233 | } 234 | } 235 | } 236 | 237 | /** 238 | * Javascript html parser seems to add , and tags the parsed tree by default. 239 | * This remove the ones that are not in the input HTML. 240 | * 241 | * Might not work for tags other then html, head and body. Have not looked into others, didn't need them so far. 242 | */ 243 | def removeTagsFromScalaCode(htmlCode: String, scalaCode: String, tagsToRemove: String*): String = 244 | tagsToRemove.foldLeft(scalaCode) { 245 | case (newScalaCode, tagToRemove) => 246 | s"(?i)<$tagToRemove".r.findFirstMatchIn(htmlCode) match { 247 | case None => 248 | val scalaCodeWithoutTag = newScalaCode.replaceFirst(s".*$tagToRemove.+", "").trim 249 | if (tagToRemove == "head") //If head if head is empty in html. Result Scalatag would be head() in Scala. 250 | scalaCodeWithoutTag 251 | else 252 | scalaCodeWithoutTag.dropRight(1) //remove the closing ')' for the tag if it's not head. 253 | 254 | case Some(_) => 255 | newScalaCode 256 | } 257 | } 258 | } 259 | --------------------------------------------------------------------------------