Extended Table Markup ▦▤▥

3

The first significant changes to the table syntax are already available in version 6.2.1.

1. Introduction


Introduce a flexible markup for data tables. Any kind of tables allowed by HTML5 can be created using this markup, from the most basic examples (e.g. simple rows of cells) to complex tables with full support for accessibility options.


Backport and adaption of WikkaWiki Table Markup. Requires evaluation and changes, not all features are reasonable to implement.

2. Implementation Notes

2.1. HTML5 Table Cell Attributes


HTML5 table cells, defined by the <td> element, support several attributes for controlling layout, accessibility, and relationships with header cells. Many presentational attributes from earlier HTML versions are now deprecated in favor of CSS.

2.1.1. Core Supported Attributes


These attributes are valid in HTML5 and should be used for modern web development:


  • colspan: Specifies how many columns a cell should span.
    • Example: <td colspan="2"> makes the cell span two columns.
  • rowspan: Specifies how many rows a cell should span.
    • Example: <td rowspan="3"> makes the cell span three rows.
  • headers: Associates a data cell with one or more header cells (<th>) by referencing their id values. This improves accessibility for screen readers.
    • Example: <td headers="header1 header2">.

2.1.2. Deprecated Attributes (Use CSS Instead)


The following attributes are obsolete in HTML5. Their functionality should be achieved using CSS:


  • align → Use text-align or margin in CSS.
  • valign → Use vertical-align in CSS.
  • bgcolor → Use background-color in CSS.
  • width → Use the width CSS property.
  • height → Use the height CSS property.
  • nowrap → Use white-space: nowrap in CSS.

2.1.3. Global Attributes


The <td> element also supports all global HTML attributes, such as class, id, style, data-*, and aria-* attributes for styling, scripting, and enhanced accessibility.


2.2. Considerations

  1. use WackoWiki parameter pattern used by actions and formatters
    • (x=1 y=name)
  2. ^ conflicts with ^^superscript^^ or ^C in cell content
    • The table cell separator is of utmost importance and should never conflict with other wiki markups.
    • I like the use of the caret ^ (CIRCUMFLEX ACCENT) here, maybe there is a simple workaround to mitigate the conflict with other syntax.
      • (?<!\^)\^(?!\^) ensures the ^ is not preceded or followed by another ^, matches only isolated ^ characters
  3. use of full parameter names
    • e.g. id=tbl1_header or rowspan=2
  4. support only indirect inline CSS (see Wrappers around formatters)
    • add parameter width= and support px as well as %
    • add align= parameter
    • mainly a sanitization and validation issue
    •  <td class="[ ' class ' ][ ' bgcolor ' ][ ' align ' ]" style="width: [ ' width ' ];">	
  5. current regex does not allow quotes like align="left"
    • for now it skips that for simplicity, if spaces become an issue for something like class="style-1 style-2"
      • $param_pattern  = '/(\w+)\s*=\s*(?:\"([^\"]+)\"|([^)\s]+))/';
        // ...
        $params[$pair[1]] = $pair[2] ?: $pair[3]; // key => value	
  6. to keep it modest, omit some attributes
    • title
    • height
    • headers
  7. add attributes also to table and caption
    • for second patch, can't use table_rows() function but uses same pattern, avoid spaghetti code and add table_attr()
      • table: id, class, width, align (float)
      • col: id, class, span, width, bgcolor
      • caption: id, class, side, bgcolor
      • cell: id, class, width, bgcolor, align, valign, colspan, rowspan, scope
    • table: #|(float=right)
      • float (same as wrapper- CSS classes), class, id
    • caption: ?|(side=bottom) Caption |?
      • id, side (caption-side: top | bottom)
    • col: !|(width=20%) |(width=20%) |(width=40% bgcolor=green) |(width=20% align=right) |!
      • id, class, span, width, align, valign, bgcolor
  8. additional attributes are thinkable for font-size, nowrap and others all translating in adding an additional CSS class to syle="style1 sytyle2 ..."
  9. catch logical errors and mitigate them
    • there can be only one caption and only at the top of the table
  10. error sources
    • mistakenly using the wrong attribute name, row instead of col
    • writing the attribute name wrong, leaving out a letter or messing up letters when writing
    • data cells in the wrong column (e.g. broken legacy tables) due to missing cell spans

2.3. Rational

The parser breaks down the pipe syntax by row and then each cell. The row and cell syntax delimiter | determines each element and process it recursively.


row 1
→ cell 1 → content
→ cell 2 → content
row 2
→ cell 1 → content
→ cell 2 → content


To avoid syntax conflicts, the delimiter | is always on the inside.

#|
?| Fruit production in 2026 |?
*| | Apples | Pears |*
^| Mary | 300Kg | 320Kg ||
^| John | 400Kg | 630Kg ||
|#	

2.4. Syntax

Character Name Alias names Unicode Wikipedia
| Pipe vertical bar, vertical line U+007C wikipedia:Vertical_bar
^ Caret circumflex accent U+005E wikipedia:Caret

Syntax Notes
Start #| Besides beginning the table, this is also where the table's class is defined – for example, class="usertable". A table's "class" applies standard WackWiki formatting to that table.
Caption ?| |? Required for accessibility purposes on data tables, and placed only between the table start and the first table row.
?| Caption |?
Thead *| |* head columns, entire row
*| Header 1 | Header 2 |*
Header cell ^| Optional. Each header cell starts with a new line and a ^|, or several header cells can be placed consecutively on the same line, separated by a single caret ^.
^| Header | Cell ||
|| Cell ^ Header ||
Row || To begin and end a new row of cells, use a double vertical bar ||.
|| Cell 1 | Cell 2 ||
Cell | To add a new cell in a row, start each new cell with a single vertical bar |, several cells can be placed consecutively on the same line or a new line, separated by single vertical bar |.
Attribute (attribute=value) cell attributes
||(colspan=2 rowspan=2) 2x2 | 1x1 ||
End |# To end the table.

The table cells as well as the caption are fully editable via wiki syntax.

2.4.1. Attributes


Attribute Values Notes
align center, left, right, justify Specifies the horizontal alignment of the data cell. The possible enumerated values are left, center, right and justify.
(align=right)
valign top, middle, bottom Specifies the vertical alignment of the data cell. The possible enumerated values are baseline, bottom, middle, and top.
(valign=middle)
bgcolor blue, red, green, x11colors Defines the background color of the data cell. The value is an HTML color; a color keyword.
(bgcolor=green)
scope row, col, rowgroup, colgroup

Optional. Defines the cells that the header (defined in the <th>) element relates to. Possible enumerated values are:

  • row: the header relates to all cells of the row it belongs to;
  • col: the header relates to all cells of the column it belongs to;
  • rowgroup: the header belongs to a rowgroup and relates to all of its cells;
  • colgroup: the header belongs to a colgroup and relates to all of its cells.

^|(scope=row)

id id_name Defines a unique identifier (ID) which must be unique in the whole document.
(id=tbl1_summary)
width number[px|%|em|rem] Defines a recommended data cell width.
(width=50%)
colspan number Contains a non-negative integer value that indicates how many columns the data cell spans or extends.
||(colspan=2) 2x1 | 1x1 ||
rowspan number Contains a non-negative integer value that indicates for how many rows the data cell spans or extends.
||(rowspan=2) 1x2 | 1x1 ||
class class_name

  • thead / tfoot / tbody
  • colgroup / col
  • CSS: color, styles, width
    • via predefined classes
  • Global Attributes: id, class, title, lang, data-*

2.4.2. Spaces

|| cell ||
||(pattern) cell ||


Pattern Output Note
() ()
(hello) (hello)
(pattern=value) matches valid cell attribute pattern
(pattern = value) matches valid cell attribute pattern
text text
␣text text ignores heading space
␣␣text
text
text indention (<div class="indent"></div>)
␣␣␣␣text
text
text indention
(align=right) text text
(align=justify) text HTML preserves all whitespace in the source code as text nodes in the DOM, but CSS applies a default whitespace processing algorithm that collapses multiple spaces, tabs, and line breaks into a single space.
2.4.2.1. Table Space Ignored

HTML and CSS whitespace handling explains why browsers ignore leading spaces in table cells. HTML preserves all whitespace in the source code as text nodes in the DOM, but CSS applies a default whitespace processing algorithm that collapses multiple spaces, tabs, and line breaks into a single space. This behavior is consistent across all inline content, including table cells.


When you have leading spaces in a <td> element, such as <td> Hello</td>, the browser processes the whitespace as follows:

  • Leading and trailing whitespace (spaces, tabs, line breaks) before and after text is ignored in rendering.
  • Consecutive whitespace characters (spaces, tabs, line feeds) are collapsed into a single space.
  • Only the first space between words is preserved; extra spaces are discarded.

This is not a bug—it's by design. The goal is to ensure readable text formatting without requiring developers to worry about code indentation affecting the displayed layout. For example, the following HTML:

<td>   Hello World!</td>

is rendered as:

Hello World!	

with only a single space between "Hello" and "World!".

2.4.2.2. Solutions to preserve spacing

If you need to display multiple spaces (e.g., for monospaced text or formatting), use one of these methods:

  • Use white-space: pre in CSS to preserve all whitespace:
  td {
    white-space: pre;
  }

  • Use non-breaking spaces (&nbsp;) to force visible spacing:
  <td>&nbsp;&nbsp;Hello</td>

  • Wrap text in a <pre> tag if you want to preserve all formatting exactly as written.

This behavior applies uniformly across modern browsers and is part of the standard HTML and CSS specification.

2.5. Backwards Compatibility

The old colspan behaviour is not compatible with the new colspan or rowspan attributes, it would add a colspan where it not belongs.


It once expanded omitted cells at the end of a table row for the last cell to fit the previous rows columns.


By removing this behaviour, existing col spans will shrink to their original colspan which is 1. Just add the new colspan attribute to the cell to restore the intended colspan.


<?php
// legacy colspan behaviour
if (   ($i == $count)
    && ($this->cols <> 0)
    && ($count < $this->cols))
{
    $colspan = ' colspan="' . ($this->cols - $count + 1) . '"';
}

2.6. Sanitization & Validation

  • whitelist

2.7. Documentation


  1. update Table section in Text Formatting page
  2. add Table Markup Guide (draft)

2.8. Resources

3. Examples

#| 
|| ^ Heading 1 ^ Heading 2 ||
^| Heading 3    | Row 1 Col 2          | Row 1 Col 3 ||
^| Heading 4    | no colspan | ||
^| Heading 5    | Row 3 Col 2          | Row 3 Col 3 ||
|#	

Heading 1 Heading 2
Heading 3 Row 1 Col 2 Row 1 Col 3
Heading 4 no colspan
Heading 5 Row 3 Col 2 Row 3 Col 3

#|
?| Fruit production in the last **two** years |?
*| |(colspan=2) Apples |(colspan=2) Pears |*
*| | 2025 | 2026 | 2025 | 2026 |*
^| Mary | 300Kg | 320Kg | 400kg | 280Kg ||
^| John | 400Kg | 630Kg | 210Kg | 300Kg ||
|#	

Fruit production in the last two years
Apples Pears
2025 2026 2025 2026
Mary 300Kg 320Kg 400kg 280Kg
John 400Kg 630Kg 210Kg 300Kg

#|
^|(colspan=2 rowspan=2) 2x2 |(colspan=2) 2x1 |(rowspan=2) 1x2 ||
||(rowspan=2) 1x2 | 1x1 ||
|| 1x1 | 1x1 |(colspan=2) 2x1 ||
|#	

2x2 2x1 1x2
1x2 1x1
1x1 1x1 2x1

4. Patch

This is a very early patch for demonstration purposes allowing table caption and cell spans, scope, width and id.

diff --git a/src/formatter/class/wackoformatter.php b/src/formatter/class/wackoformatter.php
index 06165df..b231c4b 100644
--- a/src/formatter/class/wackoformatter.php
+++ b/src/formatter/class/wackoformatter.php
@@ -124,6 +124,8 @@
             "\|\#|" .
             "\|\|.*?\|\||" .
             "\*\|.*?\|\*|" .
+            "\^\|.*?\|\||" .
+            "\?\|.*?\|\?|" .
             // symbols < or >
             "<|>|" .
             // italic //...//
@@ -436,10 +438,20 @@
 
             return '</table>';
         }
-        // table head
+        // table head columns
         else if (preg_match('/^\*\|(.*?)\|\*$/us', $thing, $matches) && $this->table_scope)
         {
-            return $this->table_rows($matches[1], $callback, true);
+            return $this->table_rows($matches[1], $callback, 1);
+        }
+        // table head column or head row
+        else if (preg_match('/^\^\|(.*?)\|\|$/us', $thing, $matches) && $this->table_scope)
+        {
+            return $this->table_rows($matches[1], $callback, 2);
+        }
+        // table caption
+        else if (preg_match('/^\?\|(.*?)\|\?$/us', $thing, $matches) && $this->table_scope)
+        {
+            return "\t" . '<caption>' . preg_replace_callback($this->LONG_REGEX, $callback, $matches[1]) . '</caption>';
         }
         // table row and cells
         else if (preg_match('/^\|\|(.*?)\|\|$/us', $thing, $matches) && $this->table_scope)
@@ -798,7 +810,7 @@
             if (!$new_indent_type)
             {
                 $opener        = '<div class="indent">';
-                $closer        = '</div>' . "\n";
+                $closer        = '</div>';
                 $this->br    = true;
                 $new_type    = 'i';
             }
@@ -923,8 +935,10 @@
         return Ut::html($thing);
     }
 
-    public function table_rows($matches, array $callback, bool $header = false): string
+    public function table_rows($string, array $callback, int $header = 0): string
     {
+        $wacko        = & $this->object;
+
         $strip_delimiter = function ($string): string
         {
             return str_replace("\u{2592}", '',
@@ -936,9 +950,33 @@
         $this->intable        = true;
         $this->intable_br    = false;
 
-        $tag        = $header ? 'th' : 'td';
-        $output        = '<tr>';
-        $cells        = preg_split('/\|/', $matches);
+        $tag        = $header === 1 ? 'th' : 'td';
+        $output        = "\t" . '<tr>';
+
+        // ensures the ^ is not preceded or followed by another ^, matches only isolated ^ characters
+        $pattern    = '/((?<!\^)\^(?!\^)|\|)/';
+        $result        = preg_split($pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+        // remove empty values if any
+        $result        = array_filter($result, 'strlen');
+
+        // rebuild into [key][delimiter, result]
+        $cells        = [];
+        $delimiter    = null;
+
+        foreach ($result as $token)
+        {
+            if (in_array($token, ['^', '|']))
+            {
+                $delimiter    = $token;
+            }
+            else
+            {
+                $cells[]    = [$delimiter, $token];
+                $delimiter    = null;
+            }
+        }
+
         $count        = count($cells);
         $count--;
 
@@ -946,21 +984,128 @@
         {
             $this->tdold_indent_level    = 0;
             $this->tdindent_closers        = [];
+
+            // supported attributes
+            $align                        = '';
+            $bgcolor                    = '';
+            $class                        = '';
             $colspan                    = '';
+            $id                            = '';
+            $rowspan                    = '';
+            $scope                        = '';
+            $valign                        = '';
+            $width                        = '';
 
-            if ($cell[0] == "\n")
+            if (!$header)
             {
-                $cell = substr($cell, 1);
+                $tag = match($cell[0])
+                {
+                    '^'            => 'th',
+                    default        => 'td'
+                };
+            }
+            else if ($header === 2)
+            {
+                $tag = $i === 0 ? 'th' : 'td';
             }
 
-            if (($i == $count) && ($this->cols <> 0) && ($count < $this->cols))
+            // attributes
+            $pattern = '/^\(((?:\s*(\w+)\s*=\s*([^)\s]+)\s*)+)\)(.*)/usm';
+
+            if (preg_match($pattern, $cell[1], $matches))
             {
-                $colspan = ' colspan="' . ($this->cols - $count + 1) . '"';
+                $params_str        = $matches[1];
+                $param_pattern    = '/(\w+)\s*=\s*([^)\s]+)/';
+
+                preg_match_all($param_pattern, $params_str, $param_matches, PREG_SET_ORDER);
+
+                $params = [];
+
+                foreach ($param_matches as $pair)
+                {
+                    $params[$pair[1]] = $pair[2];
+                }
+
+                // align
+                if (isset($params['align'])
+                    && in_array($params['align'], ['center', 'left', 'right', 'justify']))
+                {
+                    $align = ' text-' . $params['align'];
+                }
+
+                // bgcolor
+                if (isset($params['bgcolor'])
+                    && in_array($params['bgcolor'], ($wacko->db->allow_x11colors ? $this->x11_colors : $this->colors)))
+                {
+                    $bgcolor = ' mark-' . $params['bgcolor'];
+                }
+
+                // colspan
+                if (isset($params['colspan']))
+                {
+                    $colspan = ' colspan="' . (int)    $params['colspan'] . '"';
+                }
+
+                // id
+                if (isset($params['id'])
+                    && preg_match('/^[\w-]+$/', $params['id']))
+                {
+                    $id = ' id="' . $params['id'] . '"';
+                }
+
+                // rowspan
+                if (isset($params['rowspan']))
+                {
+                    $rowspan = ' rowspan="' . (int)    $params['rowspan'] . '"';
+                }
+
+                // scope
+                if (isset($params['scope'])
+                    && in_array($params['scope'], ['row', 'col', 'rowgroup', 'colgroup']))
+                {
+                    $scope = ' scope="' . $params['scope'] . '"';
+                }
+
+                // valign
+                if (isset($params['valign'])
+                    && in_array($params['valign'], ['top', 'middle', 'bottom']))
+                {
+                    $valign = ' vertical-' . $params['valign'];
+                }
+
+                // width
+                if (isset($params['width'])
+                    && preg_match('/^(?:\d+|0\.\d+)(?:px|%|em|rem)$/', $params['width']))
+                {
+                    $width = ' style="width: ' . $params['width'] . ';"';
+                }
+
+                // class
+                if ($align || $bgcolor || $valign)
+                {
+                    $class = ' class="' . trim($align . $bgcolor . $valign) . '"';
+                }
+
+                $cell[1]    = $matches[4] ?? '';
             }
 
-            $output .= $strip_delimiter(
-                '<' . $tag . $colspan . '>' .
-                preg_replace_callback($this->LONG_REGEX, $callback, "\u{2592}\n" . $cell));
+            if ($cell[1][0] == "\n")
+            {
+                $cell[1] = substr($cell[1], 1);
+            }
+
+            $output .=
+                '<' .
+                    $tag .            // <- must be first!
+                    $id .
+                    $class .
+                    $colspan .
+                    $rowspan .
+                    $scope .
+                    $width .
+                '>' .
+                $strip_delimiter(
+                    preg_replace_callback($this->LONG_REGEX, $callback, "\u{2592}\n" . $cell[1]));
 
             if ($i != $count)
             {

diff --git a/src/theme/default/css/wacko.css b/src/theme/default/css/wacko.css
index 7b81d44..b8ff265 100644
--- a/src/theme/default/css/wacko.css
+++ b/src/theme/default/css/wacko.css
@@ -166,10 +166,18 @@
     background-color: #eee;
 }
 
+.usertable caption {
+    border: 1px solid #ccc;
+    font-size: 95%;
+    color: #666;
+    margin: 5px 0;
+    padding: 2px;
+}
+
 .usertable td,
 .usertable th {
     border: 1px solid #ccc;
-    padding: 4px;
+    padding: .3em .5em;
     vertical-align: top;
 }

5. Feedback

Please provide feedback.

Comments

  1. text centering question

    Thank you so much for this improvement to the tables, but how do you apply a colspan with text centered in a cell?
    #||
    ||(colspan=2) |(colspan=3 align=center)Despliegue de la pantalla|(colspan=3 align=center)Lista de Despliegue ||
    ||(colspan=8) ----||
    ||GR|Ventana de texto|siempre sin uso|bytes condicionales|uso de la pantalla|bytes sin uso|bytes usados|Total||
    ||(colspan=8) ----||
    ||#	
  2. Re: text centering question

    (colspan=3 align=center) works, doesn't it?

    Despliegue de la pantallaLista de Despliegue
    GRVentana de textosiempre sin usobytes condicionalesuso de la pantallabytes sin usobytes usadosTotal

    Despliegue de la pantallaLista de Despliegue

    GRVentana de textosiempre sin usobytes condicionalesuso de la pantallabytes sin usobytes usadosTotal

    • WikiAdmin
    • 03/08/2026 21:06 edited
  3. Comentario 4552

    It works perfectly; it was my mistake since I'm using an older 6.2.0 theme :)