aboutsummaryrefslogtreecommitdiff
path: root/src/services/html_converter/html_line_converter.service.js
blob: 80482c9aedd5a69821040d2e803a42cb899d1b1d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
 * This is a tiny purpose-built HTML parser/processor. This basically detects
 * any type of visual newline and converts entire HTML into a array structure.
 *
 * Text nodes are represented as object with single property - text - containing
 * the visual line. Intended usage is to process the array with .map() in which
 * map function returns a string and resulting array can be converted back to html
 * with a .join('').
 *
 * Generally this isn't very useful except for when you really need to either
 * modify visual lines (greentext i.e. simple quoting) or do something with
 * first/last line.
 *
 * known issue: doesn't handle CDATA so nested CDATA might not work well
 *
 * @param {Object} input - input data
 * @return {(string|{ text: string })[]} processed html in form of a list.
 */
export const convertHtmlToLines = (html) => {
  const handledTags = new Set(['p', 'br', 'div'])
  const openCloseTags = new Set(['p', 'div'])

  let buffer = [] // Current output buffer
  const level = [] // How deep we are in tags and which tags were there
  let textBuffer = '' // Current line content
  let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag

  // Extracts tag name from tag, i.e. <span a="b"> => span
  const getTagName = (tag) => {
    const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
    return result && (result[1] || result[2])
  }

  const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
    if (textBuffer.trim().length > 0) {
      buffer.push({ text: textBuffer })
    } else {
      buffer.push(textBuffer)
    }
    textBuffer = ''
  }

  const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
    flush()
    buffer.push(tag)
  }

  const handleOpen = (tag) => { // handles opening tags
    flush()
    buffer.push(tag)
    level.push(tag)
  }

  const handleClose = (tag) => { // handles closing tags
    flush()
    buffer.push(tag)
    if (level[level.length - 1] === tag) {
      level.pop()
    }
  }

  for (let i = 0; i < html.length; i++) {
    const char = html[i]
    if (char === '<' && tagBuffer === null) {
      tagBuffer = char
    } else if (char !== '>' && tagBuffer !== null) {
      tagBuffer += char
    } else if (char === '>' && tagBuffer !== null) {
      tagBuffer += char
      const tagFull = tagBuffer
      tagBuffer = null
      const tagName = getTagName(tagFull)
      if (handledTags.has(tagName)) {
        if (tagName === 'br') {
          handleBr(tagFull)
        } else if (openCloseTags.has(tagName)) {
          if (tagFull[1] === '/') {
            handleClose(tagFull)
          } else if (tagFull[tagFull.length - 2] === '/') {
            // self-closing
            handleBr(tagFull)
          } else {
            handleOpen(tagFull)
          }
        }
      } else {
        textBuffer += tagFull
      }
    } else if (char === '\n') {
      handleBr(char)
    } else {
      textBuffer += char
    }
  }
  if (tagBuffer) {
    textBuffer += tagBuffer
  }

  flush()

  return buffer
}