All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.woojiahao.MarkdownConverter.kt Maven / Gradle / Ivy

There is a newer version: 0.2.3
Show newest version
package com.github.woojiahao

import com.github.woojiahao.modifiers.MediaReplacedElementFactory
import com.github.woojiahao.properties.DocumentProperties
import com.github.woojiahao.properties.PagePropertiesManager
import com.github.woojiahao.modifiers.renderers.ImageNodeRenderer
import com.github.woojiahao.modifiers.renderers.TaskListNodeRenderer
import com.github.woojiahao.style.Settings
import com.github.woojiahao.style.Style
import com.github.woojiahao.style.css.CssSelector
import com.github.woojiahao.style.utility.px
import com.github.woojiahao.toc.TableOfContentsVisitor
import com.github.woojiahao.toc.generateTableOfContents
import com.github.woojiahao.utility.cssColor
import com.github.woojiahao.utility.extensions.isFileType
import com.github.woojiahao.utility.getFontDirectories
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension
import org.commonmark.ext.gfm.tables.TablesExtension
import org.commonmark.node.BulletList
import org.commonmark.node.Node
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import org.xhtmlrenderer.pdf.ITextRenderer
import java.awt.Color
import java.io.File
import java.io.FileOutputStream
import com.github.kittinunf.result.Result as KResult

class MarkdownConverter private constructor(
  markdownDocument: MarkdownDocument,
  private val documentStyle: Style,
  private val targetLocation: File,
  private val documentProperties: DocumentProperties
) {

  private val extensions = listOf(
    TablesExtension.create(),
    StrikethroughExtension.create()
  )

  private val htmlRenderer = HtmlRenderer
    .builder()
    .extensions(extensions)
    .nodeRendererFactory { ImageNodeRenderer(markdownDocument.file, it) }
    .build()

  private val tableOfContentsVisitor = TableOfContentsVisitor(documentProperties.tableOfContentsSettings)

  private val parsedDocument = Parser
    .builder()
    .extensions(extensions)
    .build()
    .parse(markdownDocument.file.readText())
    .apply { accept(tableOfContentsVisitor) }

  private val pagePropertiesManager = PagePropertiesManager(documentProperties, documentStyle)

  fun convert(): KResult {
    with(ITextRenderer()) {
      sharedContext.replacedElementFactory = MediaReplacedElementFactory(
        documentProperties,
        sharedContext.replacedElementFactory
      )
      val content = generateHtml()
      setDocumentFromString(content)
      loadFontDirectories()
      layout()
      return try {
        createPDF(FileOutputStream(targetLocation))
        KResult.success(targetLocation)
      } catch (e: Exception) {
        KResult.error(e)
      }
    }
  }

  private fun generateHtml() =
    StringBuilder()
      .appendHTML()
      .html {
        head {
          style {
            unsafe {
              +wrap(documentStyle.getStyles())
              +wrap(pagePropertiesManager.toCss())
              +wrap(".table-of-contents") {
                attributes {
                  "page-break-after" to "always"
                }
              }

              // Fig caption configuration
              with(documentProperties.figcaptionSettings) {
                val figcaptionNumberLabel = "figures"

                if (isVisible) {
                  +wrap("body") {
                    attributes {
                      "counter-reset" to figcaptionNumberLabel
                    }
                  }

                  +wrap("figure") {
                    attributes {
                      "counter-increment" to figcaptionNumberLabel
                    }
                  }

                  +wrap("figure figcaption:before") {
                    attributes {
                      "content" to "'$prepend ' counter($figcaptionNumberLabel) ' $divider '"
                    }
                  }

                  +wrap("figure figcaption:after") {
                    attributes {
                      "content" to " '$append'"
                    }
                  }
                }

              }
            }
          }
        }
        body {
          with(documentProperties.tableOfContentsSettings) {
            if (isVisible) {
              div("table-of-contents") {
                h1 { +"Table of contents" }
                unsafe {
                  raw(
                    generateTableOfContents(
                      tableOfContentsVisitor.getTableOfContents(),
                      this@with
                    )
                  )
                }
              }
            }
          }

          with(documentStyle.header) {
            div("header-left") { +left.getContents() }

            div("header-center") { +center.getContents() }

            div("header-right") { +right.getContents() }
          }

          with(documentStyle.footer) {
            div("footer-left") { +left.getContents() }

            div("footer-center") { +center.getContents() }

            div("footer-right") { +right.getContents() }
          }

          div("content") {
            unsafe { +wrap(htmlRenderer.render(parsedDocument).trim()) }
          }
        }
      }
      .toString()

  private fun wrap(content: String) = "\n$content\n"

  private fun wrap(elementName: String, cssSelector: CssSelector.() -> Unit) =
    wrap(CssSelector(elementName).apply(cssSelector).toCss())

  private fun ITextRenderer.loadFontDirectories() {
    val fontDirectories = getFontDirectories()
    val hasNoFontDirectory = fontDirectories.none { File(it).exists() }
    if (hasNoFontDirectory) {
      println("Font folders could not be located on your system, fonts will default")
    }
    fontDirectories.forEach { fontResolver.addFontDirectory(it, false) }
  }

  open class Builder {
    private var document: MarkdownDocument? = null
    private var style = Style()
    private var targetLocation: String? = null
    private var documentProperties = DocumentProperties.Builder().build()

    fun document(document: MarkdownDocument): Builder {
      this.document = document
      return this
    }

    fun style(style: Style): Builder {
      this.style = style
      return this
    }

    fun targetLocation(targetLocation: String): Builder {
      this.targetLocation = targetLocation
      return this
    }

    fun documentProperties(documentProperties: DocumentProperties): Builder {
      this.documentProperties = documentProperties
      return this
    }

    fun build(): MarkdownConverter {
      check(document != null) { "Markdown document must be set using document()" }

      val targetFile = createTargetOutputFile(targetLocation)
      check(targetFile.isFileType("pdf")) { "Target location must have a .pdf extension" }

      return MarkdownConverter(
        document!!,
        style,
        targetFile,
        documentProperties
      )
    }

    private fun createTargetOutputFile(filePath: String?) =
      filePath?.let { File(it) } ?: createFileRelativeToDocument()

    private fun createFileRelativeToDocument(): File {
      with(document!!.file) {
        check(this.parentFile != null) { "File cannot have no parent folder" }
        return File(this.parentFile, "$nameWithoutExtension.pdf")
      }
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy