Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md

Large diffs are not rendered by default.

486 changes: 486 additions & 0 deletions _plans/055_raw-tables-round-trip.md

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions docs/confluence/storage-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ should look like.
| `center` | kept | `center` |
| `wide` | kept | `wide` |
| `full-width` | kept | `full-width` |
| `default` (verified 2026-09-25) | kept | `default` |
| `bogus-value` | **kept verbatim** | **`None`** — silently dropped |
| *(absent)* | absent | absent |

Expand Down Expand Up @@ -196,6 +197,102 @@ back as `---`. Only center and right make the round trip.
> `text-align` survive, because the legacy renderer echoes them. That is wrong.
> Use ADF.

### A paragraph's alignment and its cell's

**Verified 2026-09-25**, one cell per hypothesis on a scratch page, storage and
ADF read back, the page trashed:

| written | stored | ADF paragraph mark |
|---|---|---|
| centred cell, `<p style="text-align: start;">` | `<p style="">` | `center` |
| centred cell, `<p style="text-align: justify;">` | kept | `center` |
| centred cell, `<p style="text-align: left;">` | kept | `center` |
| centred cell, `<p style="text-align: end;">` | `<p style="">` | `center` |
| centred cell, `<p style="text-align: right;">` | kept | `end` |
| right cell, `<p style="text-align: center;">` | kept | `center` |
| centred cell, loose text | kept | `center` |
| `<p style="text-align: end;">` in a plain cell | `<p style="">` | none |
| `<td style="text-align: start;">` or `end` | `<td style="">` | none |
| `<p style="">` | kept | none |

So only `center` and `right` do anything, on a paragraph or a cell, and a
paragraph saying one of them overrides its cell's. A paragraph saying `left` or
`justify` (which do nothing) or `start` or `end` (which the sanitizer strips) is
aligned by its cell, as a paragraph saying nothing is. `end` is ADF's name for
right, and not a storage value that means it. `read` follows this
(`internal/convert/storage_to_md_table.go`), and drops the four on the way back,
since `start` and `end` written back would be stripped and read differently.

## Table markup

What else a table may carry, one hypothesis per table.

**Verified 2026-09-25** on a scratch page in the personal space, storage read
back and then ADF, the page trashed afterwards:

| written | stored | takes effect (ADF) |
|---|---|---|
| `<th>` in the first row | kept | header row |
| `<th>` first in every row | kept | header column |
| two header rows in `<thead>` | kept | two header rows |
| `<tfoot>` | kept | an ordinary last row; no footer |
| `<caption>` | **tag dropped, its text left loose** | the caption text becomes a paragraph above the table |
| `colspan`, `rowspan` on `<th>`/`<td>` | kept | yes |
| `style="background-color: …"` on a cell | kept, as `rgb()` | **no** |
| `class="highlight-blue"` on a cell | kept | no |
| `valign="bottom"` on a cell | kept | **yes**, cell `valign` |
| `style="vertical-align: top;"` on a cell | kept | no |
| `scope="col"` on a `<th>` | kept | no |
| `data-colwidth` on a cell | **dropped** | no |
| `data-table-display-mode="fixed"` | kept | `displayMode: fixed` |
| `data-number-column="true"` | **dropped** | no |
| `class="numberingColumn"` on each row's first cell | kept | **numbered column**, those cells removed from ADF |
| `class`, `border`, `width`, `style="width: …"` on `<table>` | kept | no (the `style` width induced `layout: default`) |
| a table inside a cell | kept | **not a table**: a `nested-table` migration extension, "A table in a table cell can't be created or edited in the new editor" |

Colour, alignment, layout and widths are in their own sections here. So the
markup that works is header rows and columns, `colspan`/`rowspan`,
`data-highlight-colour`, `text-align`, `valign`, a `<colgroup>`,
`data-layout`, `data-table-width`, `data-table-display-mode`, and a numbered
column spelled as `numberingColumn` cells. Everything else is stored and
ignored, or dropped; `<caption>` and a nested table do harm.

### What the editor writes on a table

**Verified 2026-09-25** on page 2913502220, three tables made in the editor:
every table carries `data-table-width` (1110 on two, 778 on the third) and
`ac:local-id`, every row and cell carries `ac:local-id`, and one table carries
`data-table-display-mode="default"`. None has a `<colgroup>`.

**Verified 2026-09-25** on page 3109814418, a table `create` published and
then saved in the browser editor. An edit elsewhere on the page, leaving the
table alone, added `ac:local-id` to the table, rows and cells, a bare
`local-id` to every paragraph, `data-table-width="229"`, and a `<colgroup>` of
pixel widths (72, 97, 60) that sum to it -- widths the editor measured, on the
unchanged `data-layout="align-start"`. Colours and alignments were untouched.
Dragging one column border then changed the `<col>` widths (72, 154, 48) and
left `data-table-width` at 229. A save through the API instead (the page's ADF
`PUT` back) added none of this.

So those attributes say nothing about what an author chose. `read` ignores
them when deciding whether a table can be a GFM table (#55,
`internal/convert/storage_to_md_table.go`), along with a span of 1 -- and a
`<colgroup>` of pixel widths only on an `align-start` table, which is what
keeps a markfluence table a GFM table after an editor save. Two are costly:
`data-table-width` and that `<colgroup>` are also what a hand resize records,
so a resized table that reads back as GFM loses the resize on its next
publish. The only sign of a resize is a `<col>` sum that no longer matches
`data-table-width`, seen once and not relied on.

**Tables markfluence did not write are mostly different.** Of 152 tables on
50 pages edited since June 2026 (a CQL sample, 2026-09-25), 83 carry
`data-layout="default"`, 27 `full-width`, and 35 a `<colgroup>`; 27 read back
as GFM. Of 109 tables on 50 pages last edited before 2019, 6 do, and the rest
mostly for structure GFM cannot hold -- no header row (48), merged cells (33),
block content in a cell. `read` keeps a layout other than `align-start` as a
reason to write a table raw: it is visible, and republishing a GFM table would
replace it with `align-start`.

## Cell background colors

`data-highlight-colour` on a `<td>`/`<th>` sets a cell background. It reaches ADF
Expand Down
120 changes: 119 additions & 1 deletion docs/markdown-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,8 @@ use a real newline, because a GFM table row must stay on one physical line.
In storage format, the editor of Confluence records a multi-line cell as
separate paragraphs, and not with `<br>`. `read` and `export` change that back
to the `<br>` form above.
That form publishes back to the same paragraphs.
That form publishes as line breaks in one paragraph, which look the same in a
cell.

#### Lists in cells

Expand All @@ -321,6 +322,120 @@ line, and a table row cannot do that. Thus you cannot use it here.
`read` and `export` get back the same tags. They do not change them to anything
else.

#### Column alignment

To align a column, use the delimiter row of GFM: `:---:` centers a column and
`---:` aligns it to the right.

```markdown
| Service | Errors |
| ------- | -----: |
| auth | 3 |
```

Confluence has no explicit left alignment. Left is its default. Thus `:---`
publishes the same as `---`, and `read` gives back `---`.

#### Tables that Markdown cannot express

A GFM table cannot express some things that a Confluence table can: column
widths, a layout other than the default, merged cells, a table with no header
row, a header column, or a code block or a heading in a cell. For these, write
the table as raw storage format. markfluence publishes it with no change. See
[Raw Confluence storage format](#raw-confluence-storage-format).

Put each table, row, and cell tag on its own line. Put a blank line before and
after the content of a cell. Then markfluence converts the content of the cell
as Markdown. Without the blank lines, the content is storage format, and
markfluence does not convert it.

```
<table data-layout="center" data-table-width="900">
<colgroup>
<col style="width: 300px;" />
<col style="width: 600px;" />
</colgroup>
<tbody>
<tr>
<th colspan="2">

Q3 results

</th>
</tr>
<tr>
<td rowspan="2" data-highlight-colour="#e3fcef">

**auth** is [up](https://status.example.com)

</td>
<td>

99.9%

</td>
</tr>
<tr>
<td>

99.8%

</td>
</tr>
</tbody>
</table>
```

This is the table markup that has an effect in Confluence:

| markup | effect |
| --- | --- |
| `<th>` cells in the first row | a header row |
| a `<th>` cell first in each row | a header column |
| `colspan` and `rowspan` on a cell | merged cells |
| `data-highlight-colour="#rrggbb"` on a cell | the background color of the cell |
| `style="text-align: center;"` or `right` on a cell, or on a `<p>` in a cell | alignment |
| `valign` on a cell, such as `valign="bottom"` | vertical alignment |
| `<colgroup>` with a `<col style="width: 300px;" />` for each column | column widths |
| `data-layout` on the table: `align-start`, `align-end`, `center`, `default`, `wide`, or `full-width` | the layout of the table |
| `data-table-width` on the table, in pixels | the width of the table |
| `data-table-display-mode="fixed"` on the table | the fixed display mode of the editor |
| `class="numberingColumn"` on the first cell of each row | a numbered column |

Details:

- markfluence does not add `data-layout="align-start"` to a raw table, as it
does to a GFM table. You control all the attributes.
- If a table has a `<colgroup>` and no `data-layout`, Confluence picks a layout
from the total width of the columns. Wide columns make the table
`full-width`. Thus, if you give column widths, also give a `data-layout`.
- Column widths in pixels always work. Column widths in percent work only if
the table also has `data-table-width`.
- Do not use `<caption>`. Confluence removes the tag and puts its text in a
paragraph above the table.
- Do not put a table in a table cell. The Confluence editor cannot edit a
nested table.
- Colors in `style` or `class` do nothing. Use `data-highlight-colour`.

`read` and `export` give back a GFM table when GFM can express the whole
table. Otherwise, they give back a raw table in the form above, with the
content of each cell as Markdown. A Markdown paragraph has no alignment. Thus,
if all the paragraphs in a cell have the same alignment, `read` puts the
alignment on the cell, as `<td style="text-align: center;">`. If the
paragraphs in a cell have different alignments, an aligned paragraph stays
storage format. A table in a raw cell also stays a raw table.

A table that markfluence published stays a GFM table after someone edits the
page in Confluence. The Confluence editor adds attributes to every table that
it saves: IDs, a `data-table-width`, and, on a table with the `align-start`
layout that markfluence uses, a `<colgroup>` with the column widths that it
measured. `read` ignores these. The editor writes the same `<colgroup>` when
someone changes a column width by hand. Thus that change is lost when you
publish the file again, and the columns fit their content again. (Column
widths that add up to more than the page give the table a horizontal scroll
bar, so this is often what you want.) A table with any other layout and a
`<colgroup>` comes back as a raw table.

### GitHub alerts

GitHub alerts become Confluence panels in the color that GitHub gives them. The
Expand Down Expand Up @@ -557,3 +672,6 @@ Right column.

Storage markup in a fenced code block stays literal. markfluence does not
activate it.

A table uses the same conventions. See
[Tables that Markdown cannot express](#tables-that-markdown-cannot-express).
Loading
Loading