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
7 changes: 4 additions & 3 deletions CLAUDE.md

Large diffs are not rendered by default.

106 changes: 95 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ markfluence children --help
markfluence export --help
```

Validating markdown locally (no network, no credentials):

```sh
markfluence check --help
```

Manipulating Confluence page attachments:

```sh
Expand Down Expand Up @@ -338,6 +344,57 @@ markfluence fix docs/*.md
markfluence fix docs/foo.md --dry-run
```

### `check`

```
Usage: markfluence check FILE... [flags]
```

Validate one or more Markdown files against the converter and frontmatter
rules — offline: no network access, no credentials, and no writes to
Confluence or to disk. This is the primitive a CI job, a pre-commit hook, or
an agent editing docs wants: validate every change instantly, with no risk of
publishing anything. Each file is processed independently; the command exits
non-zero if any file is broken or fails outright.

It reports the same `Broken`/`Warnings` a real `update`/`create` would
produce — a missing or escaping image/link, an unpublished sibling link, a
`#fragment` matching no heading — each prefixed with the source line it came
from, plus three frontmatter checks: an unparseable/unterminated frontmatter
block, an invalid `page_width`, and a present-but-non-numeric `page_id`.
Deliberately not checked: whether `page_id`/`space`/`parent` are set at all —
`check` can't know whether you're about to `create` or `update`, and a false
positive there would be worse than a miss. A **Broken** result fails
(`update`/`create` would publish literal `LINK BROKEN: …`/`IMAGE BROKEN: …`
text); a **Warning** alone does not — an unpublished sibling link is the
normal state of a tree that hasn't been created yet, not a defect.

`link not resolved: TARGET` — the most common warning — means `TARGET` is a
sibling `.md` file that exists under the documentation root but has no
`page_id` yet. A same-page anchor (`#heading`) hits this same warning when
the *current* file itself has no `page_id` yet, since it's internally
treated as a link to itself — which can read as though the file names
itself as missing; it doesn't, that's just this file before its first
publish (see [Links to sibling `.md` files](#body)). Other message shapes:
[`IMAGE BROKEN`/`LINK BROKEN`](#body), and `anchor not found: TARGET` for a
`#fragment` that matches no heading.

```console
$ markfluence check docs/*.md
✗ [docs/broken-links.md] line 12: LINK BROKEN: typo-target.md (not found)
[docs/guide.md] clean
✗ 1 of 2 file(s) failed.
```

`--show-html` additionally prints the converted storage HTML (indented by
nesting depth) and the attachment list, for debugging what a file would
actually publish without publishing it:

```sh
markfluence check docs/*.md
markfluence check --show-html docs/one-page.md
```

### `info`

```
Expand Down Expand Up @@ -763,15 +820,25 @@ Notes on the schema:
to speak of.
- **Status verbs** are per-command: `published`/`skipped` (`update`),
`created`/`not_created` (`create`), `changed`/`consistent` (`fix`),
`clean`/`warnings`/`broken` (`check`),
`created`/`updated`/`skipped` (`attachment-upload`),
`downloaded`/`skipped` (`attachment-download`), plus `failed`. `info`, `read`,
and `attachment-list` results carry data only (no status verb).
- **One result per target**, and the target is per-command: the page for
`info`/`read`/`export` (always one), the file for `update`/`create`/`fix`, and
the attachment for the three `attachment-*` commands — so
`info`/`read`/`export` (always one), the file for `update`/`create`/`fix`/`check`,
and the attachment for the three `attachment-*` commands — so
`.results[] | .filename` works and `summary.total` is the attachment count.
`export` nests the files it wrote in an `attachments` array on its page
result, the way `update`/`create` do.
- **`check`'s `broken` status is `ok: false` with no `error`/`code`** — unlike
every other failure, its `broken`/`warnings` arrays already say everything
there is to say, so there's no separate operational error to attach. Only
its `failed` status (a file that never reached the converter at all) sets
them, the same as every other command's failure. `check --show-html` adds a
`debug: { html, attachments } | null` field, populated only for a file that
reached the converter; `html` stays exactly what the converter produced
(unindented), since it's meant to match what `update`/`create` would
literally publish.
- **Compound values are objects**, never display strings — `version`,
`page_width`, and the `created`/`updated` author stamps on `info`.
- **`create`'s two-phase abort** (a validation failure means nothing is created)
Expand Down Expand Up @@ -1040,8 +1107,8 @@ Example:
```

**Images** — `![alt](./path.png)` uploads a local file as an attachment (or
references a remote URL); a missing/unsupported image becomes `IMAGE BROKEN: …`
text.
references a remote URL); a missing/unsupported image becomes
`line N: IMAGE BROKEN: …` text (`N` is the line it's on in the file).

Image paths resolve relative to the Markdown file, the same way they do when you
view the file on GitHub, so a page in a subdirectory can share an asset
Expand Down Expand Up @@ -1073,8 +1140,8 @@ That layout needs a [documentation root](#the-documentation-root) declared at

Every image is bounded by the [documentation root](#the-documentation-root):
one resolving outside it (`../../secrets/x.png`) is reported as
`IMAGE BROKEN: … (outside the documentation root)` rather than uploaded, and a
symlink is refused even when it resolves inside the root.
`line N: IMAGE BROKEN: … (outside the documentation root)` rather than
uploaded, and a symlink is refused even when it resolves inside the root.

Confluence attachment names cannot contain `/`, so the path — relative to the
root, not to the page — is percent-encoded into the attachment name:
Expand Down Expand Up @@ -1117,11 +1184,28 @@ space is written `[see](my%20doc.md)` (or `[see](<my doc.md>)`), and a bare
`[see](my doc.md)` is not a link at all. The same applies to the fragment, so a
non-ASCII heading anchor may arrive as `#caf%C3%A9-section`. Both are decoded
before markfluence matches them against files and headings on disk, so either
spelling resolves. A link it cannot resolve — a target with no `page_id`, or a
file that isn't there — is left exactly as written and published as-is, which on
Confluence is a dead relative link. A `.md` link shaped like a same-tree
reference gets a warning when this happens; a mention, an attachment link, or
an external URL was never meant to resolve here and stays silent.
spelling resolves.

Whether an unresolved link is reported — and how badly — depends on why:

* A target that **doesn't exist at all**, or **resolves outside the
documentation root**, is Broken: the whole link element is replaced with
literal `line N: LINK BROKEN: … (not found)` or
`line N: LINK BROKEN: … (outside the documentation root)` text, the same
way a broken image already is.
* A target that **exists but has no `page_id` yet** — the normal state of
every page in a tree that hasn't been published — is a Warning
(`link not resolved: …`); the href still renders exactly as written. A
**same-page anchor** (`#heading`) is internally treated as a link to the
*current* file, so it hits this exact warning too when the current file
itself has no `page_id` yet — which reads as though the file names itself
as missing; it doesn't, that's just this file before its first publish.
* A `#fragment` that **matches no heading** on an otherwise-resolvable target
is also a Warning (`anchor not found: …`); the link still works, it just
lands at the top of the page instead of the named heading.

A mention, an attachment link, or an external URL was never meant to resolve
here and stays silent either way.

**Comment directives:**
- `<!-- confluence-toc -->` — replaced with Confluence table-of-contents macro.
Expand Down
Loading