Tabbed list to HTML in CoffeeScript

29 Apr 2013

A friend asked for a script to converted a tabbed list of data to a nested HTML list. Before working on this, I didn’t know CodePen had live CoffeeScript editing. That feature moves it into the favorite spot above jsfiddle for live code editing.

Given a sample of text like this:

  Puppy 1
  Puppy 2
    Page 1
    Page 2
  Puppy 3
    Page 1
    Page 2
    Page 3
    Page 4
  Puppy 4
  Kitty 1
  Kitty 2
    Page 1
      Paragraph 1
      Paragraph 2
    Page 2
  Kitty 3
  Pony 1
  Pony 2
  Pony 3

Convert it to an equivalent HTML list like this:

    <li> Puppy 1</li>
    <li> Puppy 2</li>
      <li> Page 1</li>
      <li> Page 2</li>
    <li> Puppy 3</li>
      <li> Page 1</li>
      <li> Page 2</li>
      <li> Page 3</li>
      <li> Page 4</li>
    <li> Puppy 4</li>
    <li> Kitty 1</li>
    <li> Kitty 2</li>
      <li> Page 1</li>
        <li> Paragraph 1</li>
        <li> Paragraph 2</li>
      <li> Page 2</li>
    <li> Kitty 3</li>
    <li> Pony 1</li>
    <li> Pony 2</li>
    <li> Pony 3</li>

And here’s the relevant bit of CoffeeScript to do it:

# Call `convert`!
# Converts tabbed-text to HTML
convert = (text) ->
  parse text.split '\n'
# Creates a list item element from a piece of text
li = (t) ->
  html = "<li>#{t['line']}</li>"
  ptAccum.push html
# Creates a start UL tag
ul = (t) ->
  ptAccum.push "<ul>#{li(t)}"
# Creates an ending UL tag
ulEnd = ->
  ptAccum.push "</ul>"
# Will be used to store the generated HTML
ptAccum = []
# Will be used to track progress
index = 0
# Begins the parsing procedure
parse = (lines) ->
  ts = linesToMaps lines
  ptAccum = ["<ul>"]
  index = 0
  parseTuples ts, 0
  ptAccum.join "\n"
# Does the bulk of the parsing job, keeping track 
# of the indentation level
parseTuples = (tuples, level) ->
  stop = false
  _p = ->
    t = tuples[index]
    curLevel = t['level']
    if curLevel == level
      # sibling, process the current 
      #at the same level
    else if curLevel < level
      # we want to unindent
      # dont do anything here
      stop = true
      # we are at the first child
      parseTuples tuples, level + 1
  _p() while !stop && index < tuples.length
# Returns the number of tabs that prefix a line
tabCount = (line) ->
  tc = 0
  c = '\t'
  count = 0
  count = line.length if line
  i = 0
  isTab = ->
    c == '\t'
  inc = ->
    c = line.charAt(i)
    tc++ if isTab()
  inc() while isTab() && i < count
# Converts the passed line to a map containing the 
# line and meta-data about the line
lineToMap = (line) ->
  line: line
  level: tabCount line
# Returns true if the string is blank
blank = (line) ->
  !line || line.length == 0 || line.match /^ *$/
# Converts the passed lines into maps 
# representing the line
linesToMaps = (lines) ->
  lineToMap line for line in lines when !(blank line)
Looking for more content? Check out other posts with the same tags: