737 Commits

Author SHA1 Message Date
Manuel Rüger
17436efd17 renderer: HTML-escape admonition title and guard against empty attachments
mkDocsAdmonition: escape the admonition title with html.EscapeString
before inserting it into the Confluence storage format XML. An unescaped
title containing '<', '>', '&', or '"' would break the XML structure.

image: add a len(attachments)==0 guard before accessing attachments[0]
in the local-attachment code path. ResolveLocalAttachments always returns
either an error or the requested attachments, so this is currently
unreachable, but the explicit check prevents a future silent panic if the
function's behaviour changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
2b62ffd822 confluence: fix NewAPI double-slash and DeletePageLabel missing status check
NewAPI: normalize baseURL by trimming the trailing slash before building
rest and json-rpc endpoints. Previously the TrimSuffix only applied to
api.BaseURL but rest/json URLs were already constructed with the raw
(potentially trailing-slash) baseURL, producing double slashes like
'http://example.com//rest/api'.

DeletePageLabel: add a non-200/non-204 status check before the type
assertion. Without this guard any error response (400, 403, 500) would
fall through to request.Response.(*LabelInfo) and either panic or return
garbage data.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
807d057f7b stdlib: remove duplicate err check in New() and add XML escaping to user-controlled template values
Remove the dead second 'if err != nil' block after the already-checked
lib.Templates assignment.

Add html.EscapeString as 'xmlesc' template function and apply it to
user-controlled string parameters in ac:code, ac:status, and ac:box
templates. Values like .Title, .Color, .Language, and .Theme can contain
XML special characters (<, >, &, ") when supplied by users, which would
break Confluence storage format XML structure.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
0d7caab5d8 fix: close response body on all paths in newErrorStatusNotOK
The 401 and 404 early-return paths returned without closing the HTTP
response body, leaking the underlying connection. Move the
defer body.Close() to the top of the function so it runs regardless
of which code path is taken.

fix: add HTTP status check to GetCurrentUser

GetCurrentUser did not validate the HTTP response status code. A
401/403/500 response was silently ignored and returned a zero-value
User pointer, causing callers (e.g. RestrictPageUpdatesCloud fallback)
to use an empty accountId.

fix: return nil on HTTP 204 from DeletePageLabel instead of panicking

DeletePageLabel accepted both 200 OK and 204 No Content as success, but
then unconditionally did request.Response.(*LabelInfo). On a 204 the
response body is empty so request.Response is nil; the type assertion
panics. Return (nil, nil) for 204 responses.

fix: paginate GetPageLabels to handle pages with >50 labels

A single request with the default page size silently truncated label
lists longer than the API default (~50). Add a pagination loop
matching the pattern used by GetAttachments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
14219aea59 fix: return error from Run() when ContinueOnError files fail
When --continue-on-error was set and one or more files failed to
process, Run() logged each failure but returned nil, making it
impossible for callers or CI systems to detect partial failures.

Track whether any file failed with a hasErrors flag and return a
descriptive error after all files have been attempted.

Update TestContinueOnError to reflect the corrected behaviour: the
test now asserts that an error IS returned (partial failure is
surfaced) while still verifying that all files in the batch are
attempted (not just the first one).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
7d4d0458ca fix: narrow vfs.Opener interface from ReadWriteCloser to ReadCloser
All callers only read from the opened file (io.ReadAll + Close). Using
io.ReadWriteCloser in the interface was misleading and violated the
principle of interface segregation. os.Open returns a read-only file
that satisfies io.ReadCloser but not the write contract implied by
io.ReadWriteCloser. Narrow both the interface and the implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
d6b37affd3 fix: use index-based loop in ResolveLocalAttachments to persist checksums
The range loop 'for _, attachment := range attachments' copied each
element by value. Assigning attachment.Checksum inside the loop only
modified the local copy; the original slice element was never updated.
All returned attachments had empty Checksum fields, causing every
attachment to be treated as changed on every run (the checksum
comparison would never match).

Switch to an index-based loop so the checksum is written directly to
the slice element.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
ed6ae15500 fix: add HTTP status checks to GetUserByName; remove redundant FindHomePage check
GetUserByName made two REST requests without checking the HTTP status
codes. A 401/403/500 response would silently be treated as an empty
result set and return 'user not found' instead of the real error.
Add a status check after each request.

FindHomePage had 'StatusNotFound || != StatusOK' — the first clause
is always a subset of the second, making it dead code. Simplified to
just '!= StatusOK'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
0d735203dd fix: trim whitespace from ParseTitle result
ParseTitle returned lang[start:] without trimming, so inputs like
'python title  My Title' returned '  My Title' with leading spaces.
The extra whitespace propagated into the rendered Confluence title
element. Add strings.TrimSpace to remove leading/trailing whitespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
d714bc9d2b fix: move scanner.Err() check after loop and fix matches bounds check
Two bugs in ExtractMeta:

1. scanner.Err() was checked inside the Scan() loop. According to the
   bufio.Scanner docs, Err() returns nil during a successful scan; it
   only reflects an error after Scan() returns false. Moving the check
   after the loop ensures real I/O errors are surfaced instead of
   silently ignored.

2. The len(matches) guard was 'len(matches) > 1' but the code
   accessed matches[2] (requires len >= 3). reHeaderPatternV2 always
   produces a 3-element slice when it matches, but the condition was
   misleading and could panic if the regex were ever changed to make
   the second capture group optional. Changed to 'len(matches) > 2'.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
cf0699c088 fix: remove dead level-map infrastructure in mkDocsAdmonitionRenderer
GenerateMkDocsAdmonitionLevel walked the AST looking for
ast.KindBlockquote nodes to build a nesting-level map, but the
renderer is registered for parser.KindAdmonition nodes. Because
admonition nodes were never added to the map, LevelMap.Level()
always returned 0 for every admonition, making the level check
in renderMkDocsAdmonition a no-op.

The intended behaviour (all admonitions rendered as Confluence
structured macros regardless of nesting) was accidentally working
because of this bug. Remove the dead MkDocsAdmonitionLevelMap type,
GenerateMkDocsAdmonitionLevel function, and LevelMap field, and
simplify renderMkDocsAdmonition to directly render the Confluence
macro for all known admonition types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
b7c9229da4 fix: RestrictPageUpdatesCloud now resolves allowedUser by name
The allowedUser parameter was completely ignored; the function always
restricted edits to the currently authenticated API user via
GetCurrentUser(). Resolve the specified user via GetUserByName first
and fall back to the current user only if that lookup fails, matching
the behaviour of RestrictPageUpdatesServer which uses the parameter
directly.

fix: paginate GetAttachments to handle pages with >100 attachments

The previous implementation fetched a single page of up to 1000
attachments. Pages with more than 1000 attachments would silently
miss some, causing attachment sync to skip or re-upload them.
Replace with a pagination loop (100 per page) that follows the
_links.next cursor until all attachments are retrieved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
fef39dc1e0 fix: preserve include directive in output on error in ProcessIncludes
All error paths in the ReplaceAllFunc callback returned nil, which
ReplaceAllFunc treats as an empty replacement, silently erasing the
guard (if err != nil) fired, every subsequent include in the file
was also erased. Return spec (the original directive bytes) instead
so failed includes are preserved and subsequent ones are not lost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
4c81c81fb3 fix: return original match on error in macro Apply()
Two bugs in the ReplaceAllFunc callback:
1. After yaml.Unmarshal failure, execution continued into Execute(),
   which could succeed and overwrite err with nil, silently swallowing
   the unmarshal error and producing output with default (empty) config.
2. After any error, the callback returned buffer.Bytes() (empty or
   partial) instead of the original match, corrupting the document.

Return the original match bytes unchanged on either error so the
directive is preserved in output and the error is not lost.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
3e71d65f61 fix: remove unused newLabels parameter from UpdatePage
The newLabels parameter was accepted but never used in the function
body; labels are synced through the separate updateLabels/AddPageLabels
/DeletePageLabel calls. The dead parameter misled callers into thinking
labels were being set during the page update.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
5b87945f23 fix: use ancestry slice for warning log, not meta.Parents
ancestry grows to len(meta.Parents)+1. The subsequent warning log
indexed meta.Parents[len(ancestry)-1], which is one past the end of
meta.Parents, causing an out-of-bounds panic. Use ancestry[len(ancestry)-1]
to always reference the last element of the actual slice being validated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
566fd74efe fix: validate emoji rune from utf8.DecodeRuneInString
DecodeRuneInString returns utf8.RuneError for invalid UTF-8, which was
silently converted to the hex string "fffd" and sent to Confluence.
Return an error instead so the caller gets a clear diagnostic rather
than storing a replacement character as the page emoji.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
2a505a24a8 fix: parseLinks regex misses links at position 0 of string
The previous pattern [^\!]\[...\] required exactly one non-! character
before the opening bracket, so a markdown link at the very start of
anchors to start-of-string or a non-! character without consuming
a required preceding character.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
594c1e4fa0 fix: guard FirstChild() nil in paragraph renderer
A paragraph node with no children causes FirstChild() to return nil,
making both the entering and leaving Kind() checks panic. Cache the
result once and treat nil the same as a non-RawHTML child (emit <p>).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
9184e91268 fix: defer body.Close() before ReadAll to ensure it runs on read error
The defer was placed after io.ReadAll, so if ReadAll returned an
error the body would not be closed. Move the defer before the read.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
a4ef890247 fix: trim whitespace from stdin password
When password is read from stdin (e.g. echo password | mark ...),
the trailing newline was included in the password string, causing
authentication to fail. Use strings.TrimSpace to strip it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
a481424f7b fix: return error instead of panic from CompileMarkdown
Markdown conversion failures called panic(err), crashing the process
rather than allowing graceful error handling. Change the return type
to (string, []attachment.Attachment, error) and propagate the error.
Update all callers (mark.go, markdown_test.go) accordingly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
32577c91f4 fix: return error instead of panic in ResolveAttachments; fix wrong slice index in log
Two issues in attachment handling:
1. GetAttachments failure called panic(err) instead of returning an
   error, crashing the process on any API failure.
2. The 'keeping unmodified' log loop indexed into the original
   attachments slice using the range of existing, causing wrong names
   to be logged and a potential out-of-bounds panic when existing is
   longer than attachments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
9e7f2cf9d5 fix: check os.ReadFile error before using file contents
http.DetectContentType was called with potentially nil/empty contents
when ReadFile failed; the error was only checked afterward. Move the
error check immediately after ReadFile so bad data is never used.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 19:18:29 +01:00
Manuel Rüger
8205794e7b Update Makefile
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
664a99cd00 test: restore global log level after each SetLogLevel subtest
SetLogLevel mutates a process-global logger, leaking state into
subsequent tests and causing order-dependent failures. Save the
current level before each subtest and restore it via t.Cleanup.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
5e1d40d910 fix: remove no-op log.GetLevel() call in SetLogLevel
The return value was unused and had no effect on logger state,
misleading readers into thinking it was needed for initialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
d68f8c3bb3 fix: use api.BaseURL instead of config.BaseURL for page URL output
confluence.NewAPI trims trailing slashes from the base URL into
api.BaseURL. Using config.BaseURL directly could produce double
slashes in the logged/printed URL when the caller passes a
trailing-slash BaseURL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
1fc553f102 style: fix indentation in Test_setLogLevel
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
aa16e7ae26 fix: check error return from fmt.Fprintln
errcheck lint requires all error return values to be handled.
Propagate write errors from both Fprintln call sites.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
66120b937e fix: handle nil meta in dry-run mode when PageID is set
page.ResolvePage requires non-nil metadata and would error immediately
when called with meta == nil (e.g. when --page-id is used and the file
has no metadata header, or when metadata is intentionally suppressed).

Guard the call: when meta != nil use ResolvePage as before; when meta
is nil but PageID is provided, validate the page exists via
api.GetPageByID instead; when neither is set the earlier mandatory-
field check already returns an error, so no further action is needed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
4e3b90c03c fix: route result output through Config.Output, not os.Stdout
mark.Run and ProcessFile were writing directly to os.Stdout via
fmt.Println, which is a surprising side-effect for library callers.

Add Config.Output io.Writer for callers to provide their own sink.
When nil the helper falls back to io.Discard, so library embedders
that do not set Output receive no implicit stdout writes. The CLI
layer sets Output: os.Stdout to preserve existing behaviour.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
06c3afef25 fix: skip label sync when metadata is absent
When PageID mode is used (meta == nil), labels is nil and calling
updateLabels unconditionally treats that as an empty desired set,
silently removing all existing global labels from the page. Guard
the call so label syncing only runs when metadata is present.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
e68a9f64ff Refactor CLI to be a thin adapter over the library
- util/cli.go: RunMark() now maps CLI flags into mark.Config and
  delegates to mark.Run(); all core processing logic removed
- util/cli_test.go: absorb Test_setLogLevel from deleted main_test.go
- main.go, main_test.go: removed (entry point is now cmd/mark/main.go)
- Makefile: update build target to ./cmd/mark with -o mark

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
Manuel Rüger
9e4c4bde30 Add root library package with Config, Run and ProcessFile
Expose the core mark functionality as an importable Go library.
Library users can now import github.com/kovetskiy/mark and call:

  err := mark.Run(mark.Config{
      BaseURL:  "https://confluence.example.com",
      Username: "user",
      Password: "token",
      Files:    "docs/**/*.md",
      Features: []string{"mermaid", "mention"},
  })

The new package provides:
- Config struct: all options decoupled from the CLI framework
- Run(config Config) error: process all files matching Config.Files
- ProcessFile(file, api, config): process a single markdown file

Also moves the CLI entry point to cmd/mark/main.go following standard
Go convention for projects that serve as both a library and a binary.

Fixes a pre-existing nil-pointer dereference on meta.Attachments,
meta.Layout and related fields when using --target-url with a pageId
(meta was nil in that code path).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-13 01:05:32 +01:00
dependabot[bot]
2ae4601a02 build(deps): bump golang.org/x/text from 0.34.0 to 0.35.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.34.0 to 0.35.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.34.0...v0.35.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-version: 0.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
v15.4.0
2026-03-11 19:38:34 +01:00
Manuel Rüger
76c4dfa16b Bump to go 1.26.1 2026-03-11 19:35:02 +01:00
Manuel Rüger
de71225164 fix d2 test
Co-Authored-By: mnowotnik <michal@mnowotnik.com>
2026-03-11 19:28:48 +01:00
mnowotnik
b8decd954b fix mermaid test 2026-03-11 18:43:18 +01:00
Johan Fagerberg
ab2c456851 chore: fix comment referring to wrong class 2026-03-11 12:49:22 +01:00
Johan Fagerberg
8e613c5e70 style: run gofmt on image_test.go 2026-03-11 12:49:22 +01:00
Johan Fagerberg
d264648916 test: update expected testdata output 2026-03-11 12:49:22 +01:00
Johan Fagerberg
9795f74f0f feat: only attempt to parse image attachments 2026-03-11 12:49:22 +01:00
Johan Fagerberg
6f18a47ed0 feat: don't add redundant 'height' to images 2026-03-11 12:49:22 +01:00
Johan Fagerberg
ec6a718c76 feat: throw errors instead of passing unknown aligns through 2026-03-11 12:49:22 +01:00
Johan Fagerberg
99dbcd9383 chore: clean up README changes a little bit 2026-03-11 12:49:22 +01:00
Johan Fagerberg
a0e9594f50 chore: clean up over-explanations slightly 2026-03-11 12:49:22 +01:00
Johan Fagerberg
9c58c36b46 feat: use 'center' for image widths 760<x<1800 2026-03-11 12:49:22 +01:00
Johan Fagerberg
cbc7400f92 feat: align image rendering with Confluence default 2026-03-11 12:49:22 +01:00
Johan Fagerberg
4d887bde74 feat: add support for image dimensions 2026-03-11 12:49:22 +01:00