+
+
+
+
+
+
diff --git a/html-to-j2html/settings.gradle.kts b/html-to-j2html/settings.gradle.kts
new file mode 100644
index 00000000..d97ad3ff
--- /dev/null
+++ b/html-to-j2html/settings.gradle.kts
@@ -0,0 +1,12 @@
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
+}
+
+rootProject.name = "html-to-j2html"
\ No newline at end of file
diff --git a/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/SupportedJ2HtmlAttributes.kt b/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/SupportedJ2HtmlAttributes.kt
new file mode 100644
index 00000000..8997bb84
--- /dev/null
+++ b/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/SupportedJ2HtmlAttributes.kt
@@ -0,0 +1,227 @@
+package com.html.to.j2html.common
+/**
+ * attributes that require specific treatment
+ */
+fun specialAttributes() = listOf (
+ "class", // withClasses(val1, val2, val3) for each space separated token // DONE
+ "data-*", // the * is the key and the value is the value, and becomes .withData("key", "value") // DONE
+
+)
+
+/**
+ * we produce raw html for these
+ */
+val unsupportedTags : List = listOf(
+ "svg",
+ "path",
+)
+
+/**
+ * Not much to do with those, just taking them out of the supported list without losing track of them
+ *
+ * we default to attr("key", "value") for those
+ *
+ */
+val unsupportedAttributes : List = listOf(
+ "accept-charset", // valid html5
+ "http-equiv", // valid html5
+ "inputmode", // valid html5
+ "popovertarget", // valid html5
+ "popovertargetaction", // valid html5
+ "inert", // valid html5
+ "enterkeyhint", // valid html5
+ "onblur", // valid html5
+ "onchange", // valid html5
+ "onclick", // valid html5
+ "oncontextmenu", // valid html5
+ "oncopy", // valid html5
+ "oncut", // valid html5
+ "ondblclick", // valid html5
+ "ondrag", // valid html5
+ "ondragend", // valid html5
+ "ondragenter", // valid html5
+ "ondragleave", // valid html5
+ "ondragover", // valid html5
+ "ondragstart", // valid html5
+ "ondrop", // valid html5
+ "onfocus", // valid html5
+ "oninput", // valid html5
+ "oninvalid", // valid html5
+ "onkeydown", // valid html5
+ "onkeypress", // valid html5
+ "onkeyup", // valid html5
+ "onmousedown", // valid html5
+ "onmousemove", // valid html5
+ "onmouseout", // valid html5
+ "onmouseover", // valid html5
+ "onmouseup", // valid html5
+ "onmousewheel", // valid html5
+ "onpaste", // valid html5
+ "onscroll", // valid html5
+ "onselect", // valid html5
+ "onwheel", // valid html5
+ "popover", // valid html5
+
+)
+
+/**
+ * we default to attr("key", "value") for those
+ *
+ */
+val buggedAttributes : List = listOf(
+ "autocomplete", // input().withCondAutocomplete(true) but mozilla docs say it's not just a boolean: valid values are "off", "on", "name", "email", etc
+ "onvolumechange", // audio().withOnvolumechanged("value") exists instead of withOnvolumechange
+)
+
+/**
+ * list of HTML integer attributes
+ *
+ * these have been tested to exist in j2html with the associated comment code
+ */
+val knownIntegerHtmlAttributes : List = listOf(
+ "tabindex", // div().withTabindex(5) // tabindex
+)
+
+/**
+ * list of HTML boolean attributes copied on 28-02-2024 from
+ * https://meiert.com/en/blog/boolean-attributes-of-html/
+ * with a few that I found myself during tests, and a few taken out because unsupported
+ *
+ * these have been tested to exist in j2html with the associated comment code
+ */
+val knownBooleanHtmlAttributes : List = listOf(
+ "async", // script().withCondAsync(true), // async
+ "autofocus", // input().withCondAutofocus(true), // autofocus
+ "autoplay", // video().withCondAutoplay(true), // autoplay
+ "checked", // input().withCondChecked(true), // checked
+ "controls", // audio().withCondControls(true), // controls
+ "default", // track().withCondDefault(true), // default
+ "defer", // script().withCondDefer(true), // defer
+ "disabled", // input().withCondDisabled(true), // disabled
+ "ismap", // img().withCondIsmap(true), // ismap
+ "loop", // audio().withCondLoop(true), // loop
+ "multiple", // select().withCondMultiple(true), // multiple
+ "muted", // video().withCondMuted(true), // muted
+ "novalidate", // form().withCondNovalidate(true), // novalidate
+ "open", // details().withCondOpen(true), // open
+ "readonly", // input().withCondReadonly(true), // readonly
+ "required", // select().withCondRequired(true), // required
+ "reversed", // ol().withCondReversed(true), // reversed
+ "selected", // option().withCondSelected(true), // selected
+ // boolean attributes I found during tests:
+ "contenteditable", // div().withCondContenteditable(true) // contenteditable
+ "download", // area().withCondDownload(true), // download
+ "draggable", // img().withCondDraggable(true), // draggable
+ "hidden", // img().withCondHidden(true), // hidden
+ "sandbox", // iframe().withCondSandbox(true), // sandbox
+ "spellcheck", // input().withCondSpellcheck(true), // spellcheck
+ "translate", // input().withCondTranslate(true), // translate
+)
+
+
+/**
+ * these have been tested to exist in j2html with the associated comment code
+ */
+val knownStringHtmlAttributes : List = listOf(
+ "accept", // input().withAccept("value"), // accept
+ "accesskey", // div().withAccesskey("value"), // accesskey
+ "action", // form().withAction("value"), // action
+ "alt", // input().withAlt("value"), // alt
+ "charset", // meta().withCharset("value"), // charset
+ "cite", // del().withCite("value"), // cite
+ "cols", // textarea().withCols("value"), // cols
+ "colspan", // td().withColspan("value"), // colspan
+ "content", // meta().withContent("value"), // content
+ "coords", // area().withCoords("value"), // coords
+ "datetime", // time().withDatetime("value"), // datetime
+ "dir", // div().withDir("value"), // dir
+ "dirname", // input().withDirname("value"), // dirname
+ "enctype", // form().withEnctype("value"), // enctype
+ "for", // label().withFor("value"), // for
+ "form", // input().withForm("value"), // form
+ "formaction", // button().withFormaction("value"), // formaction
+ "headers", // td().withHeaders("value"), // headers
+ "height", // canvas().withHeight("value"), // height
+ "high", // meter().withHigh("value"), // high
+ "href", // a().withHref("value"), // href
+ "hreflang", // a().withHreflang("value"), // hreflang
+ "id", // div().withId("value"), // id
+ "kind", // track().withKind("value"), // kind
+ "label", // option().withLabel("value"), // label
+ "lang", // div().withLang("value"), // lang
+ "list", // input().withList("value"), // list
+ "low", // meter().withLow("value"), // low
+ "max", // input().withMax("value"), // max
+ "maxlength", // input().withMaxlength("value"), // maxlength
+ "media", // a().withMedia("value"), // media
+ "method", // form().withMethod("value"), // method
+ "min", // input().withMin("value"), // min
+ "name", // input().withName("value"), // name
+ "optimum", // meter().withOptimum("value"), // optimum
+ "onabort", // img().withOnabort("value"), // onabort
+ "onafterprint", // body().withOnafterprint("value"), // onafterprint
+ "onbeforeprint", // body().withOnbeforeprint("value"), // onbeforeprint
+ "onbeforeunload", // body().withOnbeforeunload("value"), // onbeforeunload
+ "oncanplay", // video().withOncanplay("value"), // oncanplay
+ "oncanplaythrough", // video().withOncanplaythrough("value"), // oncanplaythrough
+ "oncuechange", // track().withOncuechange("value"), // oncuechange
+ "ondurationchange", // video().withOndurationchange("value"), // ondurationchange
+ "onemptied", // video().withOnemptied("value"), // onemptied
+ "onended", // video().withOnended("value"), // onended
+ "onerror", // video().withOnerror("value"), // onerror
+ "onhashchange", // body().withOnhashchange("value"), // onhashchange
+ "onload", // input().withOnload("value"), // onload
+ "onloadeddata", // video().withOnloadeddata("value"), // onloadeddata
+ "onloadedmetadata", // video().withOnloadedmetadata("value"), // onloadedmetadata
+ "onloadstart", // video().withOnloadstart("value"), // onloadstart
+ "onoffline", // body().withOnoffline("value"), // onoffline
+ "ononline", // body().withOnonline("value"), // ononline
+ "onpagehide", // body().withOnpagehide("value"), // onpagehide
+ "onpageshow", // body().withOnpageshow("value"), // onpageshow
+ "onpause", // video().withOnpause("value"), // onpause
+ "onplay", // video().withOnplay("value"), // onplay
+ "onplaying", // video().withOnplaying("value"), // onplaying
+ "onpopstate", // body().withOnpopstate("value"), // onpopstate
+ "onprogress", // video().withOnprogress("value"), // onprogress
+ "onratechange", // video().withOnratechange("value"), // onratechange
+ "onreset", // form().withOnreset("value"), // onreset
+ "onresize", // body().withOnresize("value"), // onresize
+ "onsearch", // input().withOnsearch("value"), // onsearch
+ "onseeked", // video().withOnseeked("value"), // onseeked
+ "onseeking", // video().withOnseeking("value"), // onseeking
+ "onstalled", // video().withOnstalled("value"), // onstalled
+ "onstorage", // body().withOnstorage("value"), // onstorage
+ "onsubmit", // form().withOnsubmit("value"), // onsubmit
+ "onsuspend", // video().withOnsuspend("value"), // onsuspend
+ "ontimeupdate", // video().withOntimeupdate("value"), // ontimeupdate
+ "ontoggle", // details().withOntoggle("value"), // ontoggle
+ "onunload", // body().withOnunload("value"), // onunload
+ "onwaiting", // video().withOnwaiting("value"), // onwaiting
+ "pattern", // input().withPattern("value"), // pattern
+ "placeholder", // input().withPlaceholder("value"), // placeholder
+ "poster", // video().withPoster("value"), // poster
+ "preload", // video().withPreload("value"), // preload
+ "rel", // form().withRel("value"), // rel
+ "rows", // textarea().withRows("value"), // rows
+ "rowspan", // td().withRowspan("value"), // rowspan
+ "scope", // th().withScope("value"), // scope
+ "shape", // area().withShape("value"), // shape
+ "size", // input().withSize("value"), // size
+ "sizes", // img().withSizes("value"), // sizes
+ "span", // col().withSpan("value"), // span
+ "src", // input().withSrc("value"), // src
+ "srcdoc", // iframe().withSrcdoc("value"), // srcdoc
+ "srclang", // track().withSrclang("value"), // srclang
+ "srcset", // img().withSrcset("value"), // srcset
+ "start", // ol().withStart("value"), // start
+ "step", // input().withStep("value"), // step
+ "style", // div().withStyle("value"), // style
+ "target", // form().withTarget("value"), // target
+ "title", // div().withTitle("value"), // title
+ "type", // input().withType("value"), // type
+ "usemap", // img().withUsemap("value"), // usemap
+ "value", // input().withValue("value"), // value
+ "width", // input().withWidth("value"), // width
+ "wrap" // textarea().withWrap("value") // wrap
+)
+
diff --git a/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/converter.kt b/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/converter.kt
new file mode 100644
index 00000000..55882a96
--- /dev/null
+++ b/html-to-j2html/src/commonMain/kotlin/com/html/to/j2html/common/converter.kt
@@ -0,0 +1,437 @@
+package com.html.to.j2html.common
+import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlHandler
+import com.mohamedrejeb.ksoup.html.parser.KsoupHtmlParser
+
+data class Tag(val name : String, val attributes : Map, val children : MutableList, val parent: Tag?) {
+ override fun toString(): String {
+ return " name($name), attributes($attributes}, children($children)"
+ }
+}
+
+class TagOrString private constructor(private val tag: Tag?, private val string: String?) {
+ constructor(string: String) : this(null, string)
+ constructor(tag: Tag) : this(tag, null)
+ fun getTag() : Tag?{
+ return tag
+ }
+ fun getString() : String?{
+ return string
+ }
+ override fun toString(): String {
+ val s1 : String = string ?: ""
+ val s2 : String = tag?.toString() ?: ""
+ return "$s1$s2"
+ }
+}
+
+fun convert(input: String): String {
+
+ val mainLevelTagsOrStrings: List = extractAndSanitize(input)
+
+ val tabStr = " "
+
+ var output = ""
+ val howManyNonCommentTagsOrStringsWeHave = mainLevelTagsOrStrings.filter { "comment" != it.getTag()?.name }.size
+ var nonCommentIndex = 0
+
+ for ((index, mainLevelTagOrString) in mainLevelTagsOrStrings.withIndex()) {
+ val isLastNonComment = nonCommentIndex >= (howManyNonCommentTagsOrStringsWeHave - 1)
+ val isLast = index >= (mainLevelTagsOrStrings.size - 1)
+
+ if( mainLevelTagOrString.getTag() != null){
+
+ val mainLevelTag = mainLevelTagOrString.getTag()!!
+ output += outputTag(mainLevelTag, 0, tabStr)
+
+ }
+ else if( mainLevelTagOrString.getString() != null ) {
+
+ output += outputStringValue(true, tabStr, 0, true, mainLevelTagOrString.getString()!!)
+
+ }
+
+ if (!isLastNonComment && "comment" != mainLevelTagOrString.getTag()?.name) {
+ output += ","
+ }
+ if (!isLast && "comment" != mainLevelTagOrString.getTag()?.name) {
+ output += "\n"
+ }
+ if ("comment" != mainLevelTagOrString.getTag()?.name) {
+ nonCommentIndex++
+ }
+ }
+
+ return output
+
+}
+
+private fun extractAndSanitize(input: String): List {
+ val mainLevelTagsOrStrings: MutableList = mutableListOf()
+ var currentTag: Tag? = null
+
+ val handler = KsoupHtmlHandler
+ .Builder()
+ .onOpenTag { name, attributes, _ ->
+
+ if (currentTag != null
+ && "comment" != currentTag!!.name
+ ) {
+ val newTag = Tag(name, attributes, mutableListOf(), currentTag)
+ currentTag!!.children += TagOrString(newTag)
+ currentTag = newTag
+ } else {
+ currentTag = Tag(name, attributes, mutableListOf(), null)
+ mainLevelTagsOrStrings += TagOrString(currentTag!!)
+ }
+ }
+ .onComment { comment ->
+ if (comment.isNotBlank()) {
+ val newTag = Tag("comment", mapOf(), mutableListOf(), currentTag)
+ newTag.children += TagOrString(comment.trim())
+ if (currentTag != null) {
+ currentTag!!.children += TagOrString(newTag)
+ } else {
+ mainLevelTagsOrStrings += TagOrString(newTag)
+ }
+ }
+ }
+ .onText { text ->
+
+ if (text.isNotBlank()) {
+ val tagOrString = TagOrString(sanitizeWhitespace(text))
+
+ if (currentTag != null
+ && "comment" != currentTag!!.name
+ ) {
+ currentTag!!.children += tagOrString
+ } else {
+ mainLevelTagsOrStrings += tagOrString
+ }
+ }
+ }
+ .onCloseTag { _, _ ->
+ // we go back to the parent
+ currentTag = currentTag?.parent
+ }
+ .build()
+
+ val parser = KsoupHtmlParser(handler)
+ parser.write(input)
+ parser.end()
+ return mainLevelTagsOrStrings
+}
+
+fun sanitizeWhitespace(text: String): String{
+
+ // If we don't detect any newlines we assume unformatted input and leave any excess whitespace in
+ if ( ! text.contains("\n")){
+ return text;
+ }
+ else {
+ // if we detect newlines, we strip out the unnecessary whitespace from the inputs
+
+ // this seems to work, but it's hard to not lose a space when they're meant to be there
+ // while removing all the html indentation spaces for every case
+ val startsWithReturn = text.startsWith("\n")
+ val lines = text.split("\n")
+ var endsWithReturnAndMaybeSpace = false
+ if(lines.isNotEmpty()){
+ endsWithReturnAndMaybeSpace = lines.last().isBlank()
+ }
+
+ var sanitized = text
+
+ // meant to keep " text" as is but change "\n text" into "text"
+ if(startsWithReturn){
+ sanitized = sanitized.trimStart('\n', ' ')
+ }
+
+ // meant to keep "text " as is but change "text\n " into "text"
+ if(endsWithReturnAndMaybeSpace){
+ sanitized = sanitized.trimEnd('\n', ' ')
+ }
+
+ // meant to keep the browser-rendered html identical between the input html and the html produced by j2html
+ // either though render() or renderFormatter()
+ // https://stackoverflow.com/questions/588356/why-does-the-browser-renders-a-newline-as-space
+ // Browsers condense multiple whitespace characters (including newlines)
+ // to a single space when rendering. The only exception is within
elements
+ // or those that have the CSS property white-space set to
+ // pre or pre-wrap set. (Or in XHTML, the xml:space="preserve" attribute.)
+ sanitized = sanitized.replace("\\s+".toRegex(), " ")
+
+
+ return sanitized
+
+ }
+
+}
+
+fun printRawHtmlReturnsAndIndents() : Boolean {
+ return true
+}
+
+fun maxCharactersBeforeSwitchingToMultiline(): Int {
+ return 150
+}
+
+fun outputTag(tag: Tag, indentationLevel : Int, tab : String) : String {
+ var output = ""
+ if ("comment" == tag.name) {
+ // we print each line of multi-line comments on a separate java comment
+ for( commentLine in tag.children[0].getString()!!.split("\n") ){
+ output += tab.repeat(indentationLevel) + "// " + commentLine.trimStart(' ') + "\n"
+ }
+
+ } else if (tag.name in unsupportedTags) {
+ // currently unsupported tags such as