Write down what the converter must be, before any of it is built
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
# Working in this repo
|
||||||
|
|
||||||
|
Decisions that a reader would otherwise relitigate. Everything about *using* the library is in
|
||||||
|
`README.md`; what is still to build, and what is still undecided, is in `todo.md`.
|
||||||
|
|
||||||
|
## 1. Two formats, never three
|
||||||
|
|
||||||
|
ADF and one markdown flavour. **No HTML** — not as an output, not as an intermediate, not as a
|
||||||
|
convenience export. A consumer that wants HTML renders the markdown itself, with its own escaping
|
||||||
|
and its own stylesheet; a consumer that wants neither shows the markdown verbatim, which is what the
|
||||||
|
first one does.
|
||||||
|
|
||||||
|
Three formats would mean six directions to keep lossless instead of two.
|
||||||
|
|
||||||
|
## 2. The round-trip is the product
|
||||||
|
|
||||||
|
`markdownToAdf(adfToMarkdown(doc))` must equal `doc`. Anything less and a consumer that lets someone
|
||||||
|
edit a ticket destroys what it could not represent — a panel, a mention, an attachment — in a
|
||||||
|
document it did not author.
|
||||||
|
|
||||||
|
That is why the flavour is *extended*: markdown has no syntax for most of what ADF holds, so the
|
||||||
|
flavour invents it. Designing that syntax is the first real task, and it is open (`todo.md`).
|
||||||
|
|
||||||
|
Two consequences to settle before any node is implemented, not after:
|
||||||
|
|
||||||
|
- **What happens to a node the library does not know.** The documented ADF node set is not the whole
|
||||||
|
schema, and Atlassian adds to it. Whether an unknown node is carried opaquely, refused, or dropped
|
||||||
|
is a correctness decision for the whole library, and it decides the return shape of both functions.
|
||||||
|
- **Whether a lossless document must stay readable to a plain markdown reader.** Anything the flavour
|
||||||
|
invents is noise to a reader that does not know it. How much noise is acceptable bounds the syntax.
|
||||||
|
|
||||||
|
Round-trip equality is a property to test over a corpus, not a claim to make in prose.
|
||||||
|
|
||||||
|
## 3. Zero runtime dependencies
|
||||||
|
|
||||||
|
Nothing in `dependencies`, ever. TypeScript and whatever the tests need are `devDependencies`, and
|
||||||
|
they never reach a consumer. A markdown parser is exactly the dependency this rule exists to refuse:
|
||||||
|
the flavour is not CommonMark, so a general parser would have to be extended into one anyway.
|
||||||
|
|
||||||
|
## 4. The package contract
|
||||||
|
|
||||||
|
- **ESM only.** No CommonJS build, no dual-package hazard.
|
||||||
|
- **Two entrypoints.** The built JavaScript for ordinary consumers, and the TypeScript source for
|
||||||
|
consumers that run TypeScript directly through Node's type stripping — the first consumer is one,
|
||||||
|
which is why this exists.
|
||||||
|
- **Types for both.** The JavaScript entrypoint ships `.d.ts` beside it; the TypeScript entrypoint is
|
||||||
|
its own types.
|
||||||
|
- **Published to public npmjs as `@larvit/atlassian-adf-converter`**, matching `@larvit/log`. Public
|
||||||
|
means the source is public: the Gitea repo starts private, and going public — with the LICENSE in
|
||||||
|
place — is a step before the first publish, not after it.
|
||||||
|
- **Exact versions.** `save-exact=true` in `.npmrc`, as in every other repo here.
|
||||||
|
|
||||||
|
## 5. Nothing about any consumer
|
||||||
|
|
||||||
|
No Jira, no HTTP, no REST response shapes, no plainpages, no issue keys. The library takes a document
|
||||||
|
tree and returns a string, or the reverse. A consumer's concern that leaks in here is a seam nobody
|
||||||
|
declared — and the reason this is a library at all rather than a file in the client that needed it.
|
||||||
|
|
||||||
|
## 6. Tests first, in Docker
|
||||||
|
|
||||||
|
Write the test for the behaviour wanted, then implement until it passes. `node --test`, beside the
|
||||||
|
code. Node, tsc and npm never run on the host — a compose service or a `docker run` against a
|
||||||
|
**full patch version** image tag (`node:24.19.0-alpine3.24`, never `node:24`), so the same commit
|
||||||
|
builds the same thing on a different day.
|
||||||
|
|
||||||
|
## 7. Style
|
||||||
|
|
||||||
|
Two-space indent, alphabetically sorted object keys, strict TypeScript. Failures are values, not
|
||||||
|
exceptions: a function that both returns a result and throws for some inputs has two error channels.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Larv IT AB
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# @larvit/atlassian-adf-converter
|
||||||
|
|
||||||
|
Lossless conversion between **Atlassian Document Format** (ADF) and an extended markdown flavour
|
||||||
|
that can carry the nodes plain markdown has no syntax for.
|
||||||
|
|
||||||
|
**Status: specification only. No code is implemented yet.** `todo.md` holds the plan and the design
|
||||||
|
questions still open; `AGENTS.md` holds the decisions already made.
|
||||||
|
|
||||||
|
## What it is for
|
||||||
|
|
||||||
|
Jira Cloud's REST v3 API hands out issue descriptions and comment bodies as ADF — a JSON node tree,
|
||||||
|
ProseMirror-shaped — and takes them back the same way. There is no Atlassian endpoint that converts
|
||||||
|
it: `pf-editor-service/convert` was decommissioned and
|
||||||
|
[JRACLOUD-77436](https://jira.atlassian.com/browse/JRACLOUD-77436) is still an open request. The npm
|
||||||
|
ecosystem covers one direction each, drops what markdown cannot express, and none of it round-trips.
|
||||||
|
|
||||||
|
A client that shows a ticket and lets someone edit it needs both directions, and needs them
|
||||||
|
lossless — otherwise saving an edit silently destroys the panels, mentions and attachments that were
|
||||||
|
in someone else's ticket. That is what this library is.
|
||||||
|
|
||||||
|
## The intended shape
|
||||||
|
|
||||||
|
Two pure functions and their types. No I/O, no network, no configuration:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
adfToMarkdown(document: AdfDocument): string
|
||||||
|
markdownToAdf(markdown: string): AdfDocument
|
||||||
|
```
|
||||||
|
|
||||||
|
The published package is ESM only, has **no runtime dependencies**, and offers two entrypoints — the
|
||||||
|
built JavaScript for ordinary consumers, and the TypeScript source for consumers that run TypeScript
|
||||||
|
directly (Node's type stripping), with exported types either way. `AGENTS.md` §4 has the contract.
|
||||||
|
|
||||||
|
## The first consumer
|
||||||
|
|
||||||
|
[`plainpages-plugin-fastjira`](https://gitea.larvit.se/larvit/plainpages-plugin-fastjira) — a
|
||||||
|
server-rendered Jira client. Its read-only ticket view shows the markdown this library produces
|
||||||
|
verbatim, with no HTML rendering anywhere; its later write paths post back what this library
|
||||||
|
converts the other way. **That view is blocked on `0.1.0`,** and it needs `adfToMarkdown` first.
|
||||||
|
|
||||||
|
The library knows nothing about that consumer. No Jira, no HTTP, no REST shapes, no plainpages — a
|
||||||
|
document tree in, a string out, and the reverse.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Todo
|
||||||
|
|
||||||
|
The plan, in order. Nothing here is built yet.
|
||||||
|
|
||||||
|
## Open design questions — settle these first
|
||||||
|
|
||||||
|
None of them have an answer yet, and each one changes what every later milestone implements.
|
||||||
|
|
||||||
|
- [ ] **The flavour's syntax.** Markdown has no syntax for most of ADF. Every node in the inventory
|
||||||
|
below that is not plain markdown needs one, and the set has to be internally consistent rather
|
||||||
|
than invented node by node. Prior art worth reading before choosing: CommonMark's generic
|
||||||
|
directives proposal, MDX, Obsidian's and Pandoc's extensions, and what Atlassian's own
|
||||||
|
`editor-markdown-transformer` does (it is lossy — read it for the failure modes, not the design).
|
||||||
|
- [ ] **The unknown-node policy** (AGENTS.md §2). Carried opaquely, refused, or dropped — it decides
|
||||||
|
the return shape of both functions, so it cannot be retrofitted.
|
||||||
|
- [ ] **How readable a converted document must stay** to a reader that does not know the flavour.
|
||||||
|
- [ ] **Whether plain CommonMark is valid input** to `markdownToAdf`. A human typing ordinary
|
||||||
|
markdown into a comment box is the second consumer's whole write path.
|
||||||
|
- [ ] **Table fidelity.** ADF tables carry column widths, colspan, rowspan, header rows and cell
|
||||||
|
background colours; markdown tables carry none of it.
|
||||||
|
- [ ] **Identity-bearing nodes.** `mention` holds an account id, `media` an attachment id, `emoji` a
|
||||||
|
shortcode plus an id. The rendered text is not enough to reconstruct them, so the syntax has to
|
||||||
|
carry the id — and then a document is only portable within the site it came from.
|
||||||
|
|
||||||
|
## Milestones
|
||||||
|
|
||||||
|
- [ ] **0 — Scaffold.** `package.json` with the §4 contract, `tsconfig.json`, `.npmrc`, LICENSE (MIT,
|
||||||
|
Larv IT AB), the Docker tooling setup, and `.gitea/workflows/ci.yml` gating branches. Mirror
|
||||||
|
`plainpages` for the workflow shape: `runs-on: docker-host`, actions pinned to semver tags.
|
||||||
|
- [ ] **1 — Settle the flavour.** Write the syntax down as this repo's specification before
|
||||||
|
implementing it, and make the round-trip corpus from it.
|
||||||
|
- [ ] **2 — `adfToMarkdown`.** The direction the first consumer needs. Ships `0.1.0`.
|
||||||
|
- [ ] **3 — `markdownToAdf`.**
|
||||||
|
- [ ] **4 — Round-trip property tests** over a corpus of real Jira documents, both ways. Not a
|
||||||
|
milestone that follows 2 and 3 so much as the thing that proves them.
|
||||||
|
- [ ] **5 — Release pipeline.** Tag-triggered publish to public npmjs, `NPM_TOKEN` secret, the repo
|
||||||
|
made public with the LICENSE in place first (AGENTS.md §4).
|
||||||
|
|
||||||
|
## The ADF inventory to cover
|
||||||
|
|
||||||
|
From Atlassian's [structure
|
||||||
|
reference](https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/). **It is
|
||||||
|
not the whole schema** — real payloads also carry `taskList`/`taskItem`, `decisionList`/`decisionItem`,
|
||||||
|
`layoutSection`/`layoutColumn`, `blockCard`/`embedCard`, `extension`/`bodiedExtension`/`inlineExtension`
|
||||||
|
and `placeholder`, none of which are documented there. Treat the documented set as the floor, not the
|
||||||
|
ceiling, and see the unknown-node policy above.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
| --- | --- |
|
||||||
|
| Top-level block | `blockquote` `bodiedSyncBlock` `bulletList` `codeBlock` `expand` `heading` `mediaGroup` `mediaSingle` `multiBodiedExtension` `orderedList` `panel` `paragraph` `rule` `syncBlock` `table` |
|
||||||
|
| Child block | `blockTaskItem` `extensionFrame` `listItem` `media` `nestedExpand` `tableCell` `tableHeader` `tableRow` |
|
||||||
|
| Inline | `date` `emoji` `hardBreak` `inlineCard` `mediaInline` `mention` `status` `text` |
|
||||||
|
| Marks | `border` `code` `em` `link` `strike` `strong` `subsup` `textColor` `underline` |
|
||||||
|
|
||||||
|
Plain markdown already covers `blockquote`, `bulletList`, `codeBlock`, `heading`, `orderedList`,
|
||||||
|
`paragraph`, `rule`, `listItem`, `hardBreak`, `text`, and the `code`, `em`, `link`, `strike` and
|
||||||
|
`strong` marks. Everything else is what the flavour is for.
|
||||||
Reference in New Issue
Block a user