Skip to content

Add inline <adf> tag support#9

Open
merphx wants to merge 20 commits into
jamsinclair:mainfrom
merphx:feat/inline-adf-tag
Open

Add inline <adf> tag support#9
merphx wants to merge 20 commits into
jamsinclair:mainfrom
merphx:feat/inline-adf-tag

Conversation

@merphx
Copy link
Copy Markdown
Contributor

@merphx merphx commented Apr 13, 2026

Summary

Extends the existing <adf> passthrough feature to support inline placement — inside paragraphs, table cells, headings, and anywhere else marked processes inline content. Previously, <adf> tags only worked when surrounded by blank lines (block-level).

I've done some real-world testing of this, and it's working quite well. I've also done a few static analysis passes with a coding agent, and I think all the use cases are covered.

Changes

Inline <adf> tag support

  • <adf>…</adf> can now appear within any content, embedding the ADF node(s) into the surrounding paragraph / list item / table cell / etc. content array
  • Delegates to the block-level parseAdfTag under the hood:
    • Supports both single-object and JSON array forms, consistent with block-level behaviour
    • Error handling is identical to block-level: throws on invalid JSON, missing type, empty tag, non-object array
      items
    • No validation is performed on whether the embedded node type is contextually valid in ADF (inline vs block). This is consistent with block-level behaviour and the library's passthrough philosophy.
      • This is especially important for table cells where Confluence supports block-level content whereas Markdown table cells do not.
  • Placing an <adf> tag inside a Markdown link (e.g. [text <adf>...</adf>](https://example.com)) is not supported because it's not valid ADF (link is a mark, not a node, so it can't act as a container for other ADF nodes). I wasn't really sure how the library should handle this, so I just threw my hands up and declared it unsupported 😅

Reviewer notes

  • The inline extension uses marked's level: 'inline' extension API to intercept <adf>…</adf> as a single token before marked's default inline lexer can split it into fragments. This is the only reliable approach — post-processing fragmented tokens would be brittle.

  • The extension is registered on a private new Marked() instance rather than via marked.use() on the global singleton. marked.use() was the natural first approach for adding an extension, but it has a global side-effect: importing marklassian would silently register the extension in the consumer app which might not be desirable. A private instance avoids this entirely.

  • When content appears by itself on a line without any formatting markers, marked automatically treats that as a paragraph node. This results in a subtle difference between how markdownToAdf() handles block-level <adf> nodes and inline <adf> nodes that appear by themselves. I think the behaviour as implemented is correct, but it may be a little bit unintuitive, so I've added a unit test explicitly documenting it (lib/test/adf-passthrough.test.ts:420):

    • Block level (i.e. <adf>\n{...}\n</adf>) is added verbatim to the document.
    • Inline without any other block-level markers (i.e. <adf>{...}</adf>) is stuffed into a paragraph node (due to how marked works).
    • Inline with block-level markers (e.g. # <adf>{...}</adf>) does what you'd expect (e.g. puts the ADF inside a heading node).

    Compare this with how regular text is handled, and it might also make more sense (e.g. Kia ora te ao! gets parsed as a paragraph vs # Kia ora te ao! is parsed as a heading — exactly the same behaviour if we replace the text with an inline <adf> node).

merphx and others added 19 commits April 13, 2026 14:46
- Documents use cases, architecture, token shape, and affected files
- Captures scope decisions (passthrough, array support, error parity)
- Includes test plan covering happy path, errors, and non-regression

Co-Authored-By: opencode <noreply@opencode.ai>
- Reorder tasks: tests first (Task 1), implementation second (Task 2)
- Consolidate happy-path and error-case tests into a single task
- Add AdfInlineToken type to eliminate 'as any' cast
- Add code comments for extension purpose and renderer requirement
- Use varied ADF node types (mention, emoji, date, status) in test fixtures
- Add multiple-inline-tags-in-same-paragraph test case
- Remove superseded non-regression test rather than replacing it

Co-Authored-By: opencode <noreply@opencode.ai>
- Add inline array item missing-type error case
- Add inline non-object/array JSON error case
- Achieves parity with block-level error test suite

Co-Authored-By: opencode <noreply@opencode.ai>
- Register a marked inline extension that captures <adf>…</adf> as a
  single adf_inline token before the default lexer can fragment it
- Add AdfInlineToken type for safe casting in inlineToAdf
- Handle adf_inline in inlineToAdf by delegating to parseAdfTag,
  keeping validation and error behaviour identical to block-level

Co-Authored-By: opencode <noreply@opencode.ai>
- Remove test documenting that inline <adf> tags were unsupported
- Four happy-path inline tests added in the prior commit fully cover this ground with stronger assertions

Co-Authored-By: opencode <noreply@opencode.ai>
- Replace block-only caveat with note that both block and inline placement are supported
- Add markdown examples showing inline usage in sentences and table cells

Co-Authored-By: opencode <noreply@opencode.ai>
- Implementation is complete; design spec and plan served their purpose
- Deleting to keep the repo clean post-implementation

Co-Authored-By: opencode <noreply@opencode.ai>
- Use src.search(/<adf>/i) in start() to match the tokenizer regex,
  with a comment explaining the consistency rationale
- Add JSDoc note on marked.use() warning about singleton mutation
  and its effect on consumers using marked for HTML rendering
- Add matching note to README ⚠️ section
- Add block-level and inline tests for uppercase <ADF> tag handling

Co-Authored-By: opencode <noreply@opencode.ai>
- Replace global marked.use() with a module-scoped 'new Marked()'
  so the adf_inline extension is registered on a private instance only
- Importing marklassian no longer affects consumers using marked directly
- Add regression test confirming marked.parse() is unaffected after import
- Remove the README warning (issue is now resolved)

Co-Authored-By: opencode <noreply@opencode.ai>
- Replace length > 0 and includes() checks with a precise t.is() against
  the exact HTML marked produces for a literal <adf> tag on a clean singleton
- Makes the intent clear: a mutated singleton swallows content to '<p></p>\n',
  a clean one renders it as escaped literal HTML

Co-Authored-By: opencode <noreply@opencode.ai>
- Collapse t.throws() call arguments to match biome's preferred style
- Normalise single quotes to double quotes on string literal

Co-Authored-By: opencode <noreply@opencode.ai>
- Handle adf_inline child tokens in em, strong, and del cases of inlineToAdf
- Use flatMap with an adf_inline branch instead of map, so ADF nodes are
  emitted directly rather than passed through getSafeText (which drops them)
- Add three failing tests covering inline ADF inside bold, italic, and
  mixed bold+inline content before fixing

Co-Authored-By: opencode <noreply@opencode.ai>
- Extract resolveInlineToken(token, marks) helper before inlineToAdf
- Replace duplicated adf_inline branches in em, strong, del cases
- Replace standalone adf_inline case with single resolveInlineToken call
- No behaviour change; all 37 tests pass

Co-Authored-By: opencode <noreply@opencode.ai>
- Wrap long markdownToAdf call onto multiple lines

Co-Authored-By: opencode <noreply@opencode.ai>
- Extend resolveInlineToken to recurse into em/strong/del children,
  merging each token's mark into an inherited accumulator
- Make marks parameter optional (default []) — only supplied during recursion
- Simplify em/strong/del cases in inlineToAdf to single resolveInlineToken calls
- Add 6 tests covering nested bold/italic/strike combos and the intersection
  of nested emphasis with inline ADF nodes

Co-Authored-By: opencode <noreply@opencode.ai>
- Relocate two intersection tests (nested emphasis containing inline ADF
  nodes) from core-markdown.test.ts to adf-passthrough.test.ts where they
  better reflect the feature under test
- Retain the four pure nested-emphasis tests in core-markdown.test.ts as
  they cover cases not present in pre-existing fixture-based tests

Co-Authored-By: opencode <noreply@opencode.ai>
- ADF does not permit non-text inline nodes (e.g. mention, date) to carry
  a link mark, so there is no valid ADF representation for this construct
- Behaviour is undefined; document it explicitly under the adf tag caveats

Co-Authored-By: opencode <noreply@opencode.ai>
@merphx merphx marked this pull request as ready for review April 14, 2026 05:45
- Add test showing block <adf> emits a top-level doc child while bare inline defaults to paragraph via marked context
- Extend test with heading counter-example to clarify the paragraph comes from marked, not the handler

Co-Authored-By: opencode <noreply@opencode.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant