ElectronicBabylonianLiterature/ebl-frontend

View on GitHub
src/fragmentarium/ui/fragment/PdfExport.tsx

Summary

Maintainability
F
1 wk
Test Coverage
B
87%
import React from 'react'
import { Fragment } from 'fragmentarium/domain/fragment'
import Record from 'fragmentarium/ui/info/Record'

import { ReactElement } from 'react'
import TransliterationLines from 'transliteration/ui/TransliterationLines'
import TransliterationNotes from 'transliteration/ui/TransliterationNotes'
import { Glossary } from 'transliteration/ui/Glossary'
import { renderToString } from 'react-dom/server'
import $ from 'jquery'
import rgbHex from 'rgb-hex'
import WordService from 'dictionary/application/WordService'
import GlossaryFactory from 'transliteration/application/GlossaryFactory'
import { MemoryRouter } from 'react-router-dom'
import getJunicodeRegular from './pdf-fonts/Junicode'
import getJunicodeBold from './pdf-fonts/JunicodeBold'
import getJunicodeItalic from './pdf-fonts/JunicodeItalic'

import { jsPDF } from 'jspdf'
import { DictionaryContext } from 'dictionary/ui/dictionary-context'
import { fixHtmlParseOrder } from 'common/HtmlParsing'
import { getLineTypeByHtml } from 'common/HtmlLineType'

export async function pdfExport(
  fragment: Fragment,
  wordService: WordService,
  jQueryRef: JQuery
): Promise<jsPDF> {
  const tableHtml: JQuery = $(
    renderToString(
      <MemoryRouter>
        <DictionaryContext.Provider value={wordService}>
          <TransliterationLines text={fragment.text} />
        </DictionaryContext.Provider>
      </MemoryRouter>
    )
  )
  const notesHtml: JQuery = $(
    renderToString(
      <DictionaryContext.Provider value={wordService}>
        <TransliterationNotes notes={fragment.text.notes} />
      </DictionaryContext.Provider>
    )
  )

  const pdf = getPdfDoc(tableHtml, notesHtml, jQueryRef, wordService, fragment)

  jQueryRef.hide()

  return pdf
}

async function getPdfDoc(
  tableHtml: JQuery,
  notesHtml: JQuery,
  jQueryRef: JQuery,
  wordService: WordService,
  fragment: Fragment
): Promise<jsPDF> {
  const doc = new jsPDF()

  addCustomFonts(doc)

  const initialPos = 15

  tableHtml.show()
  jQueryRef.show()

  let posAfterHeadline = addPdfHeadLine(doc, fragment, initialPos)
  posAfterHeadline += 10

  const posAfterTable = addMainTableWithFootnotes(
    tableHtml,
    notesHtml,
    jQueryRef,
    posAfterHeadline,
    doc
  )

  const glossaryHtml = await getGlossaryHtml(wordService, fragment)

  if (glossaryHtml.find('div').length > 0)
    addGlossary(glossaryHtml, jQueryRef, posAfterTable, doc)

  return doc
}

function addCustomFonts(doc: jsPDF) {
  doc.addFileToVFS('Junicode.ttf', getJunicodeRegular())
  doc.addFont('Junicode.ttf', 'Junicode', 'normal')

  doc.addFileToVFS('JunicodeBold.ttf', getJunicodeBold())
  doc.addFont('JunicodeBold.ttf', 'JunicodeBold', 'normal')

  doc.addFileToVFS('JunicodeItalic.ttf', getJunicodeItalic())
  doc.addFont('JunicodeItalic.ttf', 'JunicodeItalic', 'normal')
}

async function getGlossaryHtml(
  wordService: WordService,
  fragment: Fragment
): Promise<JQuery> {
  const glossaryFactory: GlossaryFactory = new GlossaryFactory(wordService)
  const glossaryJsx: JSX.Element = await glossaryFactory
    .createGlossary(fragment.text)
    .then((glossaryData) => {
      return Glossary({ data: glossaryData })
    })

  const glossaryHtml: JQuery = $(
    renderToString(
      wrapWithMemoryRouter(
        <DictionaryContext.Provider value={wordService}>
          {glossaryJsx}
        </DictionaryContext.Provider>
      )
    )
  )

  return glossaryHtml
}

function addPdfHeadLine(doc: jsPDF, fragment: Fragment, yPos: number): number {
  const headlineParts = getPdfHeadline(fragment)
  const outerPaddingForCredits = 20

  doc.setFont('JunicodeBold', 'normal')
  doc.setFontSize(16)
  doc.text(headlineParts[0], centerText(doc, headlineParts[0]), yPos)

  doc.setFont('Junicode', 'normal')
  doc.setFontSize(11)
  doc.setTextColor('#007bff')

  yPos += 5.5
  doc.text(headlineParts[2], centerText(doc, headlineParts[2]), yPos)

  doc.setTextColor('black')
  doc.setFontSize(8)
  yPos += 5.5

  const currentLineHeight = getLineHeight(doc)
  const creditsSplit = doc.splitTextToSize(
    headlineParts[1],
    doc.internal.pageSize.width - outerPaddingForCredits
  )

  for (const key in creditsSplit) {
    doc.text(creditsSplit[key], centerText(doc, creditsSplit[key]), yPos)
    yPos += currentLineHeight
    if (checkIfNewPage(yPos, doc)) {
      doc.addPage()
    }
  }

  return yPos
}

function getLineHeight(doc: any): number {
  return doc.internal.getFontSize() / 2
}

function centerText(doc: jsPDF, text: string): number {
  const textWidth = getTextWidth(doc, text)
  const textOffset = (doc.internal.pageSize.width - textWidth) / 2
  return textOffset
}

function getTextWidth(doc, text: string): number {
  return text
    ? (doc.getStringUnitWidth(text) * doc.internal.getFontSize()) /
        doc.internal.scaleFactor
    : 0
}

function getTextHeight(doc: jsPDF, text: string): number {
  return doc.getTextDimensions(text).h
}

function getPdfHeadline(fragment: Fragment): [string, string, string] {
  const records: JQuery = $(
    renderToString(Record({ record: fragment.uniqueRecord }))
  )
  const credit = getCredit(records)
  const link = getHyperLink(fragment)

  return [fragment.number, credit, link]
}

function wrapWithMemoryRouter(component: JSX.Element): ReactElement {
  return <MemoryRouter>{component}</MemoryRouter>
}

function addMainTableWithFootnotes(
  table: JQuery,
  notes: JQuery,
  jQueryRef: any,
  yPos: number,
  doc: any
): number {
  // table.hide()

  jQueryRef.append(table)

  const tablelines: JQuery = table.find('tr')
  fixHtmlParseOrder(tablelines)

  doc.setFontSize(10)

  const outerPaddingForTable = 17
  const firstColumnMinWidth = getFirstColumnSize(tablelines, doc)
  const firstColumnSize = outerPaddingForTable + firstColumnMinWidth

  let maxRowOffset = 0
  const linePositions = {}
  let maxXPos = 0
  const columnSizes = getColumnSizes(
    table,
    jQueryRef,
    outerPaddingForTable,
    firstColumnMinWidth,
    doc
  )

  let xPos = 0

  tablelines.each((i, el) => {
    const lineType = getLineTypeByHtml($(el))
    const nextElement = $(el).next()
    const nextLineType = getLineTypeByHtml(nextElement)

    if (lineType === 'emptyLine') return

    if (xPos > maxRowOffset) {
      maxRowOffset = xPos
    }

    const originalYPos = {
      coords: yPos,
      page: doc.internal.getCurrentPageInfo().pageNumber,
    }
    const maxYPos = {
      coords: yPos,
      page: doc.internal.getCurrentPageInfo().pageNumber,
    }
    let newPageStarted = false
    let lastElement = false

    $(el)
      .find('td')
      .each((i, el) => {
        const columnDefs = columnSizes[i]
        xPos = columnDefs['startpos']
        const tdIdx = i

        if ($(el).next().length === 0 || $(el).next().next().length === 0)
          lastElement = true

        const colspan: string | undefined = $(el).is('[colspan]')
          ? $(el).attr('colspan')
          : '1'
        const colspanInt: number = colspan ? parseInt(colspan) : 1

        setDocStyle($(el), doc)

        if (lineType === 'textLine') {
          $(el)
            .find('span,em,sup')
            .each((i, el) => {
              if (
                $(el).contents().text().length > 0 &&
                $(el).contents()[0].nodeType === 3
              ) {
                if (i === 0 && $(el).text() === ' ') return

                const test =
                  xPos + getTransliterationText(el, doc, xPos, yPos, false)

                const cellEndpos = getCellEndpos(
                  columnSizes,
                  tdIdx,
                  columnDefs['endpos'],
                  colspanInt
                )

                if (test >= cellEndpos && !columnDefs['firstElement']) {
                  yPos = moveOneRowDown(yPos, doc)

                  if (!newPageStarted) {
                    if (checkIfNewPage(yPos, doc)) {
                      newPageStarted = true
                      doc.addPage()
                      yPos = 15
                      maxYPos.coords = 15
                      maxYPos.page = doc.internal.getCurrentPageInfo().pageNumber
                    }
                  }

                  xPos = columnDefs['startpos']
                  if (yPos > maxYPos.coords && !newPageStarted)
                    maxYPos.coords = yPos
                }
                xPos += getTransliterationText(el, doc, xPos, yPos, true)
                if (xPos > maxXPos) maxXPos = xPos
              }
            })
        } //textLine
        else if (lineType === 'rulingDollarLine') {
          const num = $(el)
            .parent()
            .find('.Transliteration__RulingDollarLine')
            .children('div').length
          linePositions[yPos] = {}
          linePositions[yPos][
            'page'
          ] = doc.internal.getCurrentPageInfo().pageNumber
          linePositions[yPos]['num'] = num
        } else if (lineType !== 'rulingDollarLine') {
          doc.text($(el).text(), xPos, yPos)
        }

        if (isNoteCell($(el))) {
          const noteNumber = $(el).text()
          doc.setFontSize(7)
          doc.text(
            noteNumber,
            doc.internal.pageSize.width - outerPaddingForTable,
            originalYPos.coords - getTextHeight(doc, noteNumber) / 2
          )
          doc.setFontSize(10)
        }

        if (!lastElement) {
          yPos = originalYPos.coords
          if (
            doc.internal.getCurrentPageInfo().pageNumber !== originalYPos.page
          )
            doc.setPage(originalYPos.page)
        } else {
          yPos = maxYPos.coords
          if (newPageStarted) doc.setPage(maxYPos.page)
        }
      }) //td

    if (nextLineType === 'rulingDollarLine') {
      yPos += getLineHeight(doc) / 2
    } else {
      yPos = moveOneRowDown(yPos, doc)

      if (checkIfNewPage(yPos, doc)) {
        doc.addPage()
        yPos = 15
      }
    }
  }) //tr

  table.remove()

  const savedPage = doc.getCurrentPageInfo().pageNumber
  addLines(linePositions, maxXPos, firstColumnSize, doc)
  doc.setPage(savedPage)

  yPos = addFootnotes(notes, outerPaddingForTable, jQueryRef, yPos, doc)

  return yPos
}

function addLines(
  linePositions: any,
  maxXPos: number,
  endposFirstColumn: number,
  doc: jsPDF
) {
  for (const yPos in linePositions) {
    addUnderLine(
      yPos,
      maxXPos,
      linePositions[yPos]['num'],
      linePositions[yPos]['page'],
      endposFirstColumn,
      doc
    )
  }
}

function getFirstColumnSize(tablelines: JQuery, doc: jsPDF): number {
  let maxCharsInFirstTd = 0
  let longestFirstTd
  tablelines.each((i, el) => {
    const text = $(el).find('td').first().text()
    if (text.length > maxCharsInFirstTd) {
      maxCharsInFirstTd = text.length
      longestFirstTd = text
    }
  })

  return Math.ceil(getTextWidth(doc, longestFirstTd)) + 0.5
}

function getCellEndpos(
  columnSizes: any,
  tdIdx: number,
  endpos: number,
  colspanInt: number
) {
  if (colspanInt > 1) {
    return columnSizes[tdIdx + colspanInt - 1]['endpos']
  } else {
    return endpos
  }
}

function moveOneRowDown(yPos: number, doc: jsPDF) {
  yPos += getLineHeight(doc) + 0.6
  return yPos
}

function getColumnSizes(
  table: JQuery,
  jQueryRef: JQuery,
  outerPaddingForTable: number,
  firstColumnMinWidth: number,
  doc: jsPDF
): any {
  const columnSizes = {}

  setJQueryRefTo1000Px(jQueryRef)

  let maxChild = 0
  let firstRowWithMaxChildren

  table.find('tr').each((i, el) => {
    if ($(el).children('td').length > maxChild) {
      maxChild = $(el).children('td').length
      firstRowWithMaxChildren = el
    }
  })

  let startpos = outerPaddingForTable

  $(firstRowWithMaxChildren)
    .find('td')
    .each((i, el) => {
      const outerWidth = $(el).outerWidth()
      const tableWidth = table.find('tbody').outerWidth()
      let percentage = 1
      if (outerWidth && tableWidth) percentage = outerWidth / tableWidth
      let docWidth = doc.internal.pageSize.width - outerPaddingForTable * 2

      if (i === 0) {
        docWidth = firstColumnMinWidth
        percentage = 1
      }

      columnSizes[i] = {}
      columnSizes[i]['startpos'] = startpos
      columnSizes[i]['width'] = docWidth * percentage
      columnSizes[i]['endpos'] = startpos + docWidth * percentage

      columnSizes[i]['firstElement'] = i === 0 ? true : false
      startpos += docWidth * percentage
    })

  unSetJQueryRef1000Px(jQueryRef)

  return columnSizes
}

function setJQueryRefTo1000Px(jQueryRef: JQuery) {
  jQueryRef.css('position', 'absolute')
  jQueryRef.css('width', '1000px')
}

function unSetJQueryRef1000Px(jQueryRef: JQuery) {
  jQueryRef.css('position', '')
  jQueryRef.css('width', '')
}

function checkIfNewPage(yPos: number, doc: jsPDF): boolean {
  if (yPos >= doc.internal.pageSize.height - 10) {
    return true
  } else return false
}

function addFootnotes(
  notes: JQuery,
  padding: number,
  jQueryRef: JQuery,
  yPos: number,
  doc: jsPDF
) {
  notes.hide()
  jQueryRef.append(notes)

  fixHtmlParseOrder(notes)

  const lis: JQuery = notes.find('li')

  lis.each((i, el) => {
    let linePos = padding

    $(el)
      .contents()
      .each((i, el) => {
        if (linePos > doc.internal.pageSize.width - padding) {
          linePos = padding
          yPos += 5.6
          if (checkIfNewPage(yPos, doc)) {
            doc.addPage()
            yPos = 15
          }
        }
        linePos += dealWithFootNotesHtml(el, doc, linePos, yPos)
      })
    yPos += 5.6
    if (checkIfNewPage(yPos, doc)) {
      doc.addPage()
      yPos = 15
    }
  })

  notes.remove()

  return yPos
}

function addGlossary(
  glossaryHtml: JQuery,
  jQueryRef: JQuery,
  yPos: number,
  doc: any
): void {
  glossaryHtml.hide()
  jQueryRef.append(glossaryHtml)

  const paddingForGlossary = 17

  yPos += 3

  const divs: JQuery = glossaryHtml.find('div')
  fixHtmlParseOrder(divs)

  const headline: string = glossaryHtml.find('h4').text()

  doc.setFont('JunicodeBold', 'normal')
  doc.setFontSize(14)

  doc.text(paddingForGlossary, yPos, headline)

  doc.setFont('Junicode', 'normal')
  doc.setFontSize(10)

  yPos += getLineHeight(doc) + 0.5

  divs.each((i, el) => {
    let linePos = paddingForGlossary

    $(el)
      .contents()
      .each((i, el) => {
        if (linePos > doc.internal.pageSize.width - paddingForGlossary) {
          linePos = paddingForGlossary
          yPos += 5.6
          if (checkIfNewPage(yPos, doc)) {
            doc.addPage()
            yPos = 15
          }
        }
        linePos += dealWithGlossaryHtml(el, doc, linePos, yPos)
      })
    yPos += 5.6
    if (checkIfNewPage(yPos, doc)) {
      doc.addPage()
      yPos = 15
    }
  })

  glossaryHtml.remove()
}

function addText(text: any, xPos: any, yPos: any, doc: jsPDF) {
  doc.text(xPos, yPos, text)
  return getTextWidth(doc, text)
}

function dealWithFootNotesHtml(
  el: HTMLElement | Text | Comment | Document,
  doc: jsPDF,
  xPos: number,
  yPos: number
): number {
  let wordLength = 0
  const text = $(el).text()

  if ($(el).is('a')) {
    setDocStyle($(el), doc)
    wordLength = addText(text + ' ', xPos, yPos, doc)
  } else if ($(el).is('span.Transliteration__NoteLine')) {
    let subWordLength = xPos
    $(el)
      .find('span,em,sup')
      .each((i, el) => {
        if ($(el).is('span.Transliteration__Word')) {
          if (
            $(el).contents().text().length > 0 &&
            $(el).contents()[0].nodeType === 3
          ) {
            subWordLength += getTransliterationText(
              el,
              doc,
              subWordLength,
              yPos,
              true
            )
          }
        } else {
          if (
            $(el).contents().text().length > 0 &&
            $(el).contents()[0].nodeType === 3
          ) {
            if ($(el).text() !== ' ') {
              setDocStyle($(el), doc)
              subWordLength += addText($(el).text(), subWordLength, yPos, doc)
            }
          }
        }
      })
    wordLength = subWordLength - xPos
  }
  // else if ($(el)[0].nodeType === 3){
  //   setDocStyle($(el).parent(),doc)
  //  wordLength = addText(text,xPos,yPos,doc)
  // }
  else if ($(el).is('sup')) {
    setDocStyle($(el), doc)
    wordLength = addText(text, xPos, yPos - getTextHeight(doc, text) / 2, doc)
  }

  return wordLength
}

function dealWithGlossaryHtml(
  el: HTMLElement | Text | Comment | Document,
  doc: jsPDF,
  xPos: number,
  yPos: number
) {
  let wordLength = 0
  const text = $(el).text()

  if ($(el).is('a')) {
    setDocStyle($(el), doc)
    wordLength = addText(text, xPos, yPos, doc)
  } else if ($(el)[0].nodeType === 3) {
    setDocStyle($(el).parent(), doc)
    wordLength = addText(text, xPos, yPos, doc)
  } else if ($(el).is('span.Transliteration')) {
    let subWordLength = xPos
    $(el)
      .find('span,sup')
      .each((i, el) => {
        if (
          $(el).contents().text().length > 0 &&
          $(el).contents()[0].nodeType === 3
        ) {
          subWordLength += getTransliterationText(
            el,
            doc,
            subWordLength,
            yPos,
            true
          )
        }
      })
    wordLength = subWordLength - xPos
  } else if ($(el).is('sup')) {
    setDocStyle($(el), doc)
    wordLength = addText(text, xPos, yPos - getTextHeight(doc, text) / 2, doc)
  }

  return wordLength
}

function getTransliterationText(
  el: HTMLElement | Text | Comment | Document,
  doc: jsPDF,
  xPos: number,
  yPos: number,
  add: boolean
): number {
  setDocStyle($(el), doc)

  const superScript: boolean = $(el).is('sup') ? true : false

  let wordLength = 0

  if (
    ($(el).children().length === 0 &&
      $(el).text().trim().length &&
      $(el).parent().css('display') !== 'none') ||
    $(el).hasClass('Transliteration__wordSeparator')
  ) {
    const text = $(el).text()
    for (let i = 0; i < text.length; i++) {
      const char = text[i]
      const width = getTextWidth(doc, char)
      if (superScript) {
        if (add)
          doc.text(char, xPos + wordLength, yPos - getTextHeight(doc, char) / 2)
      } else {
        if (add) doc.text(char, xPos + wordLength, yPos)
      }
      wordLength += width + 0.2
    }
  }

  return wordLength
}

function addUnderLine(
  yPos: any,
  endpos: number,
  num: number,
  page: number,
  endposFirstColumn: number,
  doc: jsPDF
) {
  doc.setPage(page)

  const smallLineStep = 1

  if (num === 3) {
    doc.line(endposFirstColumn, yPos, endpos, yPos)
    doc.line(
      endposFirstColumn,
      yPos + smallLineStep,
      endpos,
      yPos + smallLineStep
    )
    doc.line(
      endposFirstColumn,
      yPos + smallLineStep * 2,
      endpos,
      yPos + smallLineStep * 2
    )
  } else if (num === 2) {
    doc.line(endposFirstColumn, yPos, endpos, yPos)
    doc.line(
      endposFirstColumn,
      yPos + smallLineStep,
      endpos,
      yPos + smallLineStep
    )
  } else doc.line(endposFirstColumn, yPos, endpos, yPos)

  return yPos
}

function isNoteCell(element: JQuery): boolean {
  return element.find('.Transliteration__NoteLink').length > 0 ? true : false
}

function getHyperLink(fragment: Fragment): string {
  return 'https://www.ebl.lmu.de/fragmentarium/' + fragment.number
}

function getCredit(records: JQuery): string {
  return (
    'Credit: electronic Babylonian Library Project; ' +
    records
      .find('.Record__entry')
      .map((i, el) => $(el).text() + ', ')
      .get()
      .join('')
      .slice(0, -2)
  )
}

function setDocStyle(el: any, doc: jsPDF) {
  const superScript = el.is('sup')
  const allSmall = el.css('font-variant') === 'all-small-caps'

  if (el.css('font-style') === 'italic' || el.is('em'))
    doc.setFont('JunicodeItalic', 'normal')
  else doc.setFont('Junicode', 'normal')

  if (allSmall) doc.setFontSize(7)
  if (superScript) doc.setFontSize(7)
  if (!superScript && !allSmall) doc.setFontSize(10)

  doc.setTextColor(el.css('color') ? '#' + rgbHex(el.css('color')) : '#ffffff')
}