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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>