Skip to content

Commit

Permalink
Rewrite algorithm
Browse files Browse the repository at this point in the history
* Improve performance
* Fix a couple of bugs (such as trailing whitespace)
* Remove minimum of three character in alignment row cells
  — this isn’t needed in Markdown anymore
* Don’t double pad empty cells
* Fix tests

Related to remarkjs/remark-lint#217.
  • Loading branch information
wooorm committed Jan 23, 2020
1 parent 83d164b commit 6fbc2f8
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 169 deletions.
301 changes: 172 additions & 129 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,206 +1,249 @@
'use strict'

var repeat = require('repeat-string')

module.exports = markdownTable

var trailingWhitespace = / +$/

// Characters.
var space = ' '
var lineFeed = '\n'
var dash = '-'
var colon = ':'
var lowercaseC = 'c'
var lowercaseL = 'l'
var lowercaseR = 'r'
var verticalBar = '|'

var minCellSize = 3
var x = 0
var C = 67
var L = 76
var R = 82
var c = 99
var l = 108
var r = 114

// Create a table from a matrix of strings.
function markdownTable(table, options) {
var settings = options || {}
var padding = settings.padding === false ? '' : space
var between = padding + verticalBar + padding
var start = settings.delimiterStart === false ? '' : verticalBar + padding
var end = settings.delimiterEnd === false ? '' : padding + verticalBar
var alignment = settings.align
var calculateStringLength = settings.stringLength || lengthNoop
var cellCount = 0
var padding = settings.padding !== false
var start = settings.delimiterStart !== false
var end = settings.delimiterEnd !== false
var align = (settings.align || []).concat()
var alignDelimiters = settings.alignDelimiters !== false
var alignments = []
var stringLength = settings.stringLength || defaultStringLength
var rowIndex = -1
var rowLength = table.length
var cellMatrix = []
var sizeMatrix = []
var row = []
var sizes = []
var align
var rule
var rows
var row
var longestCellByColumn = []
var mostCellsPerRow = 0
var cells
var index
var position
var columnIndex
var columnLength
var largest
var size
var value
var spacing
var cell
var lines
var line
var before
var after
var code

alignment = alignment ? alignment.concat() : []

// This is a superfluous loop if we don’t align delimiters, but otherwise we’d
// do superfluous work when aligning, so optimize for aligning.
while (++rowIndex < rowLength) {
row = table[rowIndex]
cells = table[rowIndex]
columnIndex = -1
columnLength = cells.length
row = []
sizes = []

if (columnLength > mostCellsPerRow) {
mostCellsPerRow = columnLength
}

index = -1
while (++columnIndex < columnLength) {
cell = serialize(cells[columnIndex])

if (row.length > cellCount) {
cellCount = row.length
}
if (alignDelimiters === true) {
size = stringLength(cell)
sizes[columnIndex] = size

while (++index < cellCount) {
position = row[index] ? row[index].length : null
largest = longestCellByColumn[columnIndex]

if (!sizes[index]) {
sizes[index] = minCellSize
if (largest === undefined || size > largest) {
longestCellByColumn[columnIndex] = size
}
}

if (position > sizes[index]) {
sizes[index] = position
}
row.push(cell)
}
}

if (typeof alignment === 'string') {
alignment = pad(cellCount, alignment).split('')
cellMatrix[rowIndex] = row
sizeMatrix[rowIndex] = sizes
}

// Make sure only valid alignments are used.
index = -1

while (++index < cellCount) {
align = alignment[index]
// Figure out which alignments to use.
columnIndex = -1
columnLength = mostCellsPerRow

if (typeof align === 'string') {
align = align.charAt(0).toLowerCase()
if (typeof align === 'object' && 'length' in align) {
while (++columnIndex < columnLength) {
alignments[columnIndex] = toAlignment(align[columnIndex])
}
} else {
code = toAlignment(align)

if (align !== lowercaseL && align !== lowercaseR && align !== lowercaseC) {
align = ''
while (++columnIndex < columnLength) {
alignments[columnIndex] = code
}

alignment[index] = align
}

rowIndex = -1
rows = []

while (++rowIndex < rowLength) {
row = table[rowIndex]

index = -1
cells = []
// Inject the alignment row.
columnIndex = -1
columnLength = mostCellsPerRow
row = []
sizes = []

while (++index < cellCount) {
cells[index] = stringify(row[index])
while (++columnIndex < columnLength) {
code = alignments[columnIndex]
before = ''
after = ''

if (code === l) {
before = colon
} else if (code === r) {
after = colon
} else if (code === c) {
before = colon
after = colon
}

rows[rowIndex] = cells
}
// There *must* be at least one hyphen-minus in each alignment cell.
size = alignDelimiters
? Math.max(
1,
longestCellByColumn[columnIndex] - before.length - after.length
)
: 1

sizes = []
rowIndex = -1

while (++rowIndex < rowLength) {
cells = rows[rowIndex]
cell = before + repeat(dash, size) + after

index = -1
if (alignDelimiters === true) {
size = before.length + size + after.length

while (++index < cellCount) {
value = cells[index]

if (!sizes[index]) {
sizes[index] = minCellSize
if (size > longestCellByColumn[columnIndex]) {
longestCellByColumn[columnIndex] = size
}

size = calculateStringLength(value)

if (size > sizes[index]) {
sizes[index] = size
}
sizes[columnIndex] = size
}

row[columnIndex] = cell
}

// Inject the alignment row.
cellMatrix.splice(1, 0, row)
sizeMatrix.splice(1, 0, sizes)

rowIndex = -1
rowLength = cellMatrix.length
lines = []

while (++rowIndex < rowLength) {
cells = rows[rowIndex]
row = cellMatrix[rowIndex]
sizes = sizeMatrix[rowIndex]
columnIndex = -1
columnLength = mostCellsPerRow
line = []

while (++columnIndex < columnLength) {
cell = row[columnIndex] || ''
before = ''
after = ''

if (alignDelimiters === true) {
size = longestCellByColumn[columnIndex] - (sizes[columnIndex] || 0)
code = alignments[columnIndex]

if (code === r) {
before = repeat(space, size)
} else if (code === c) {
if (size % 2 === 0) {
before = repeat(space, size / 2)
after = before
} else {
before = repeat(space, size / 2 + 0.5)
after = repeat(space, size / 2 - 0.5)
}
} else {
after = repeat(space, size)
}
}

index = -1
if (start === true && columnIndex === 0) {
line.push(verticalBar)
}

if (settings.alignDelimiters !== false) {
while (++index < cellCount) {
value = cells[index]
if (
padding === true &&
// Don’t add the opening space if we’re not aligning and the cell is
// empty: there will be a closing space.
!(alignDelimiters === false && cell === '') &&
(start === true || columnIndex !== 0)
) {
line.push(space)
}

position = sizes[index] - (calculateStringLength(value) || 0)
spacing = pad(position)
if (alignDelimiters === true) {
line.push(before)
}

if (alignment[index] === lowercaseR) {
value = spacing + value
} else if (alignment[index] === lowercaseC) {
position /= 2
line.push(cell)

if (position % 1 === 0) {
before = position
after = position
} else {
before = position + 0.5
after = position - 0.5
}
if (alignDelimiters === true) {
line.push(after)
}

value = pad(before) + value + pad(after)
} else {
value += spacing
}
if (padding === true) {
line.push(space)
}

cells[index] = value
if (end === true || columnIndex !== columnLength - 1) {
line.push(verticalBar)
}
}

rows[rowIndex] = cells.join(between)
}
line = line.join('')

index = -1
rule = []

while (++index < cellCount) {
// When `pad` is false, make the rule the same size as the first row.
if (settings.alignDelimiters === false) {
value = table[0][index]
spacing = calculateStringLength(stringify(value))
spacing = spacing > minCellSize ? spacing : minCellSize
} else {
spacing = sizes[index]
if (end === false) {
line = line.replace(trailingWhitespace, '')
}

align = alignment[index]

// When `align` is left, don't add colons.
value = align === lowercaseR || align === '' ? dash : colon
value += pad(spacing - 2, dash)
value += align !== lowercaseL && align !== '' ? colon : dash

rule[index] = value
lines.push(line)
}

rows.splice(1, 0, rule.join(between))

return start + rows.join(end + lineFeed + start) + end
return lines.join(lineFeed)
}

function stringify(value) {
function serialize(value) {
return value === null || value === undefined ? '' : String(value)
}

// Get the length of `value`.
function lengthNoop(value) {
return String(value).length
function defaultStringLength(value) {
return value.length
}

// Get a string consisting of `length` `character`s.
function pad(length, character) {
return new Array(length + 1).join(character || space)
function toAlignment(value) {
var code = typeof value === 'string' ? value.charCodeAt(0) : x

return code === L || code === l
? l
: code === R || code === r
? r
: code === C || code === c
? c
: x
}
Loading

0 comments on commit 6fbc2f8

Please sign in to comment.