AsciiDocの表(Table)のcols属性の仕様&Asciidoctorの内部実装について調べてみた のセル版。
参考サイト1 Asciidoctor User Manual #cell に割と詳しく書いてありそう。
セル結合の記述例
セル(|で始まる)の直前で指定する。下記の例でいうと、.2+^.^ や 3+^ の部分。
AsciiDoc
[options="header"]
|===
      |Date          |Duration | Location | Plan
.2+^.^|2021.01.04    |08:00    | XXX      | game
                     |09:00    | YYY      | coding
      |2021.01.05    |08:00    | XXX      | no plan
      |2021.01.06 3+^| work
|===
内部実装を追う
parser.rb の self.parse_tableメソッドから呼ばれている parse_cellspecメソッド
parser.rb(抜粋&行番号補足)
# Internal: Parse the cell specs for the current cell.
#
# The cell specs dictate the cell's alignments, styles or filters,
# colspan, rowspan and/or repeating content.
#
# The default spec when pos == :end is {} since we already know we're at a
# delimiter. When pos == :start, we *may* be at a delimiter, nil indicates
# we're not.
#
# returns the Hash of attributes that indicate how to layout
# and style this cell in the table.
def self.parse_cellspec(line, pos = :end, delimiter = nil)             #  1
  m, rest = nil, ''                                                    #  2
                                                                       #  3
  if pos == :start                                                     #  4
    if line.include? delimiter                                         #  5
      spec_part, delimiter, rest = line.partition delimiter            #  6
      if (m = CellSpecStartRx.match spec_part)                         #  7
        return [{}, rest] if m[0].empty?                               #  8
      else                                                             #  9
        return [nil, line]                                             # 10
      end                                                              # 11
    else                                                               # 12
      return [nil, line]                                               # 13
    end                                                                # 14
  else # pos == :end                                                   # 15
    if (m = CellSpecEndRx.match line)                                  # 16
      # NOTE return the line stripped of trailing whitespace if no cellspec is found in this case # 17
      return [{}, line.rstrip] if m[0].lstrip.empty?                   # 18
      rest = m.pre_match                                               # 19
    else                                                               # 20
      return [{}, line]                                                # 21
    end                                                                # 22
  end                                                                  # 23
                                                                       # 24
  spec = {}                                                            # 25
  if m[1]                                                              # 26
    colspec, rowspec = m[1].split '.'                                  # 27
    colspec = colspec.nil_or_empty? ? 1 : colspec.to_i                 # 28
    rowspec = rowspec.nil_or_empty? ? 1 : rowspec.to_i                 # 29
    if m[2] == '+'                                                     # 30
      spec['colspan'] = colspec unless colspec == 1                    # 31
      spec['rowspan'] = rowspec unless rowspec == 1                    # 32
    elsif m[2] == '*'                                                  # 33
      spec['repeatcol'] = colspec unless colspec == 1                  # 34
    end                                                                # 35
  end                                                                  # 36
                                                                       # 37
  if m[3]                                                              # 38
    colspec, rowspec = m[3].split '.'                                  # 39
    if !colspec.nil_or_empty? && TableCellHorzAlignments.key?(colspec) # 40
      spec['halign'] = TableCellHorzAlignments[colspec]                # 41
    end                                                                # 42
    if !rowspec.nil_or_empty? && TableCellVertAlignments.key?(rowspec) # 43
      spec['valign'] = TableCellVertAlignments[rowspec]                # 44
    end                                                                # 45
  end                                                                  # 46
                                                                       # 47
  if m[4] && TableCellStyles.key?(m[4])                                # 48
    spec['style'] = TableCellStyles[m[4]]                              # 49
  end                                                                  # 50
                                                                       # 51
  [spec, rest]                                                         # 52
end                                                                    # 53
7行目および16行目でマッチング処理をしている。使用している正規表現は下記。
rx.rb(抜粋&コメント一部省略&補足)
# Examples
#   2.3+<.>m
#                            m[1]                         m[2]    m[3]                                    m[4]
#                            <---------------------------><---->  <-------------------------------------> <----->
CellSpecStartRx = /^[ \t]*(?:(\d+(?:\.\d*)?|(?:\d*\.)?\d+)([*+]))?([<^>](?:\.[<^>]?)?|(?:[<^>]?\.)?[<^>])?([a-z])?$/
CellSpecEndRx   =  /[ \t]+(?:(\d+(?:\.\d*)?|(?:\d*\.)?\d+)([*+]))?([<^>](?:\.[<^>]?)?|(?:[<^>]?\.)?[<^>])?([a-z])?$/
CellSpecStartRxは行頭用、CellSpecEndRxはそれ以外用っぽい。
- m[1]m[2]:
整数.整数+の場合・・・
結合する列数(横方向)と結合する行数(縦方向)。.以降を省略可。もしくは、.の前を省略することもできる。 - m[1]m[2]:
整数*の場合・・・ 指定した列数だけ同じセルを繰り返す(参考サイト1参照)。 - m[3]:横方向(
<or^or>).縦方向(<or^or>)・・・ アライメント 横(左寄せ、中央寄せ、右寄せ) と 縦(上寄せ、中央寄せ、下寄せ)。.以降を省略可。もしくは、.の前を省略することもできる。 - m[4]:
文字・・・書式指定 
