Skip to content

The "hug" is not valid GFM, so a multi-table example cannot survive a markdown formatter #61

Description

@aslakhellesoy

Summary

The only documented way to write an example with more than one table is the "hug" — a step paragraph placed directly after a table with no blank line between them:

A single example can carry a whole Given → table → When → doc string flow. When a following step paragraph hugs a table or fence with no blank line between them, it merges into the same example rather than starting a new one.
Examples › Tables and doc strings

That construct is not valid GFM. In GFM a table body runs until a blank line, so the hugging paragraph is parsed as another table row. The consequence is that the one layout Varar documents for multi-table examples cannot survive a markdown formatter, and does not render as written on GitHub — against the promise on Varar for Cucumber users:

Your specs live in ordinary Markdown, so they render on GitHub, in your docs site, in your editor.

I hit this porting a real 12-file Cucumber suite to .md, where every scenario has 2–4 tables.

Repro

Given the following users have been imported:

| email | name |
| ----- | ---- |
| a@b.c | Ada  |
And the following assets have been imported:

| name  |
| ----- |
| Moose |
Then the basket should contain the following line items:

Varar plans this as one example with three steps, which is what you want. Now run it through any GFM parser.

marked (GFM), abridged:

<tbody>
  <tr><td><a href="mailto:a@b.c">a@b.c</a></td><td>Ada</td></tr>
  <tr><td>And the following assets have been imported:</td><td></td></tr>
</tbody>

The step became a <td>. Same for the third step. On GitHub the reader sees two tables with a junk final row and no steps.

prettier --write does the corresponding thing to the source — it re-emits the step as a row, padding the column to fit:

-| owner@example.com  | Owen | Owner |
-| osiris@example.com | Osiris | Owner |
-And the following assets have been imported:
+| owner@example.com                            | Owen | Owner |
+| osiris@example.com                           | Osiris | Owner |
+| And the following assets have been imported: |

After that, the spec no longer means what it did. In my suite a 4-step example collapsed to its first step and the run failed:

❯ |varar| src/features/basket.md (1 test | 1 failed)
  × Given the following users have been imported:

That is the good outcome, because the steps that vanished were When/Then and the example failed loudly. Reorder so the surviving step is a Given and you get a passing example that asserts nothing — with no baseline recorded, the drift gate can't see it either (see #57 §10 for why a baseline is unreachable when the CLI can't host your steps).

So: running the formatter that the rest of the repo runs silently rewrites the spec.

There is currently no GFM-safe alternative

I tried all three candidate units the reference names ("a paragraph — or a list item, or a blockquote") on the same scenario:

Layout Valid GFM Varar result
Paragraph + hug ❌ steps parse as table rows ✅ 1 example, 4 steps
List item with indented blocks ❌ 4 separate examples — state not shared, all fail
Blockquote ❌ 1 example, but the nested tables are flattened into the example name and never parsed as tables

The blockquote case is the interesting one. Grouping already works — the whole quote became a single example. What's missing is that the scanner flattens the quote's contents to text instead of descending into its child blocks, so the tables are lost:

FAIL src/features/basket.md > Given the following users have been imported: | email | firstName | … | | ------ | … | | owner@example.com | Owen | … And the following assets have been imported: | merchantId | …

The list-item case is the mirror image: the blocks are parsed correctly but each paragraph starts a new example, so state from the Given never reaches the Then.

Suggested directions

Roughly in order of how much I'd want them:

  1. Make a list item a real container. One list item = one example; every block inside it (paragraphs, tables, fences) belongs to that example. This needs no new syntax, is the GFM-idiomatic way to group blocks, is prettier-stable, and renders correctly on GitHub. It also reads well — a scenario is a bulleted item whose steps are its paragraphs.

  2. Descend into blockquotes. Probably the smallest diff, since grouping already behaves. The scanner just needs to parse the quote's children as blocks rather than flattening them to a sentence.

  3. Let a blank line survive between an attached table and a continuing step paragraph. Keeps today's paragraph model and today's files working. The continuation rule would need care so two genuinely independent examples don't merge — "continue only when the previous block was a table/fence consumed by this example, and the next paragraph has a sentence that matches a step" is consistent with the existing no-keyword-sniffing design, but it does change the meaning of table-then-new-example documents.

Whatever the mechanism, I'd suggest the reference stop recommending the hug, since it can't survive contact with a formatter.

Workaround meanwhile

Add the spec globs to .prettierignore (and equivalents for markdownlint / fmt-on-save). That keeps the hug intact but leaves the GitHub rendering wrong, so it's a stopgap rather than a fix.


Environment: @varar/varar / @varar/vitest 0.6.1, marked 18.0.7, prettier 3.9.6.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions