How can a table be split horizontally?

To get back to your original approach, I cooked up something:

#let repeated-table(num-repeats: 2, ..args) = {
  let options = args.named()
  let data = args.pos()

  // STEP 1: transform table options to apply to a multiple of the original columns

  let columns = options.at("columns", default: ())
  let (column-count, columns) = if type(columns) == int {
    // for numbers, that's number of columns
    (columns, columns * num-repeats)
  } else if type(columns) == array and columns != () {
    // for arrays with elements, the number of elements is number of columns
    (columns.len(), columns * num-repeats)
  } else {
    // lengths, auto or an empty array mean there's one column
    (1, (auto,) * num-repeats)
  }
  options.columns = columns

  // TODO transform other per-column fields, such as align

  // STEP 2: separate header and footer from the table body, with repeated cells

  let header = if data.len() > 0 and type(data.first()) == content and data.first().func() == table.header {
    let (children, ..args) = data.remove(0).fields()
    table.header(..args, ..children * num-repeats)
  }

  let footer = if data.len() > 0 and type(data.last()) == content and data.last().func() == table.footer {
    let (children, ..args) = data.pop().fields()
    table.footer(..args, ..children * num-repeats)
  }

  // STEP 3: rearrange the data, so that after a number of rows the next repetition begins

  // split into rows
  let rows = data.chunks(column-count)
  // split into repeats of rows
  let num-rows = calc.ceil(rows.len() / num-repeats)
  let repeats = rows.chunks(num-rows)
  // pad the last repeat so that all have the same number of rows
  let empty-row = (none,) * column-count
  repeats.last() += (empty-row,) * (num-rows - repeats.last().len())
  // join repeats into combined rows
  let rows = array.zip(..repeats)
  // combine into flat data
  data = rows.flatten()

  // STEP 4: re-add header and footer to the data

  if header != none {
    data.insert(0, header)
  }
  if footer != none {
    data.push(footer)
  }

  // STEP 5: produce table
  
  table(..options, ..data)
}

#align(center,
  repeated-table(
    num-repeats: 3,
    align: center + horizon,
    columns: (10%, 15%),
    table.header("Left", "Right", repeat: true),
    ..array.range(1, 29).map(n => ([l#n], [r#n])).flatten(),
    table.footer("Left", "Right"),
  )
)

It’s inferior to eventually having balanced columns, but it works right now ;) there is a big TODO in the code; if you use per-column align, you still need to fix that, but I think the principle is clear from how columns is transformed for this to work, and the hard part of transforming the data is done.

For nicer styling, I would recommend looking at double strokes in the table guide; by using the correct column gutters, you can separate the “repeats” of the table and achieve nice double strokes at the same time.

3 Likes