diff --git a/mark.changes b/mark.changes index 73f5381..950e285 100644 --- a/mark.changes +++ b/mark.changes @@ -1,3 +1,16 @@ +------------------------------------------------------------------- +Tue Jan 20 16:12:32 UTC 2026 - Elisei Roca + +- Update to version 15.3.0: + * parse "linenumbers" in code block + * add view-file template + * feat: extend link resolution to support blog posts + * feat: enhance Confluence link generation by utilizing base URL from API response + * feat: implement GenerateTinyLink function and associated tests for Confluence tiny link generation + * fix: resolve link space inheritance and enhance Confluence URL normalization tests + * feat: add normalizeConfluenceWebUIPath function and tests for URL rewriting + * Bump dependencies + ------------------------------------------------------------------- Thu Dec 11 14:27:10 UTC 2025 - Elisei Roca diff --git a/mark.spec b/mark.spec index 5cc5c82..6bb4749 100644 --- a/mark.spec +++ b/mark.spec @@ -17,7 +17,7 @@ # nodebuginfo Name: mark -Version: 15.2.0 +Version: 15.3.0 Release: 0 Summary: A tool for syncing your markdown documentation with Atlassian Confluence pages License: Apache-2.0 diff --git a/mark/.github/workflows/ci.yml b/mark/.github/workflows/ci.yml index 611c5d0..4b3fbae 100644 --- a/mark/.github/workflows/ci.yml +++ b/mark/.github/workflows/ci.yml @@ -11,7 +11,7 @@ on: - master env: - GO_VERSION: "~1.24" + GO_VERSION: "1.25.6" jobs: # Runs Golangci-lint on the source code @@ -39,7 +39,7 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v6 - name: markdownlint-cli2-action - uses: DavidAnson/markdownlint-cli2-action@v21 + uses: DavidAnson/markdownlint-cli2-action@v22 # Executes Unit Tests ci-unit-tests: diff --git a/mark/.github/workflows/goreleaser.yml b/mark/.github/workflows/goreleaser.yml index 03c8dc5..2aba9ec 100644 --- a/mark/.github/workflows/goreleaser.yml +++ b/mark/.github/workflows/goreleaser.yml @@ -16,7 +16,7 @@ jobs: - name: Set Up Go uses: actions/setup-go@v6 with: - go-version: "1.24" + go-version: "1.25.6" - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/mark/Dockerfile b/mark/Dockerfile index 3c02a95..be9bf26 100644 --- a/mark/Dockerfile +++ b/mark/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.25.5 AS builder +FROM golang:1.25.6 AS builder ENV GOPATH="/go" WORKDIR /go/src/github.com/kovetskiy/mark COPY / . diff --git a/mark/README.md b/mark/README.md index 059937b..785a1ed 100644 --- a/mark/README.md +++ b/mark/README.md @@ -237,48 +237,38 @@ Placeholder ### Code Blocks -If you have long code blocks, you can make them collapsible with the [Code Block Macro]: - -```bash collapse +````text +```bash ... some long bash code block ... ``` +```` -And you can also add a title: +| Parameter | Default | +| ------------------------------ | ------- | +| `collapse` | false | +| `title` | none | +| `linenumbers` | false | +| `1` (any number for firstline) | 1 | -```bash collapse title Some long long bash function -... -some long bash code block -... -``` +Example: -Or linenumbers, by giving the first number +* `bash collapse` + If you have long code blocks, you can make them collapsible. +* `bash collapse title Some long long bash function` + And you can also add a title. +* `bash linenumbers collapse title Some long long bash function` + And linenumbers. +* `bash 1 collapse title Some long long bash function` + Or directly give a number as firstline number. +* `bash 1 collapse midnight title Some long long bash function` + And even themes. +* `- 1 collapse midnight title Some long long code` + Please note that, if you want to have a code block without a language + use `-` as the first character, if you want to have the other goodies. -```bash 1 collapse title Some long long bash function -... -some long bash code block -... -``` - -And even themes - -```bash 1 collapse midnight title Some long long bash function -... -some long bash code block -... -``` - -Please note that, if you want to have a code block without a language -use `-` as the first character, if you want to have the other goodies - -``` - 1 collapse midnight title Some long long code -... -some long code block -... -``` - -[Code Block Macro]: https://confluence.atlassian.com/doc/code-block-macro-139390.html +More details at Confluence [Code Block Macro](https://confluence.atlassian.com/doc/code-block-macro-139390.html) doc. ### Block Quotes @@ -515,6 +505,10 @@ By default, mark provides several built-in templates and macros: * Width: Width of the video (optional) * AutoPlay: Start playing the file on page load (default: false) +* template `ac:view-file` + * Name: Name of the file + * Height: height of the view + * macro `@{...}` to mention user by name specified in the braces. ## Template & Macros Usecases @@ -815,23 +809,23 @@ USAGE: mark [global options] VERSION: - 14.0.2 + v15.1.0@b3a6f1efae97dfaa1400a3175cdd3377f8176e88 DESCRIPTION: Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark GLOBAL OPTIONS: --files string, -f string use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES] - --continue-on-error don't exit if an error occurs while processing a file, continue processing remaining files. (default: false) [$MARK_CONTINUE_ON_ERROR] - --compile-only show resulting HTML and don't update Confluence page content. (default: false) [$MARK_COMPILE_ONLY] - --dry-run resolve page and ancestry, show resulting HTML and exit. (default: false) [$MARK_DRY_RUN] - --edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. (default: false) [$MARK_EDIT_LOCK] - --drop-h1 don't include the first H1 heading in Confluence output. (default: false) [$MARK_DROP_H1] - --strip-linebreaks, -L remove linebreaks inside of tags, to accommodate non-standard Confluence behavior (default: false) [$MARK_STRIP_LINEBREAKS] - --title-from-h1 extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. (default: false) [$MARK_TITLE_FROM_H1] - --title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title (default: false) [$MARK_TITLE_APPEND_GENERATED_HASH] - --title-from-filename use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1. (default: false) [$MARK_TITLE_FROM_FILENAME] - --minor-edit don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT] + --continue-on-error don't exit if an error occurs while processing a file, continue processing remaining files. [$MARK_CONTINUE_ON_ERROR] + --compile-only show resulting HTML and don't update Confluence page content. [$MARK_COMPILE_ONLY] + --dry-run resolve page and ancestry, show resulting HTML and exit. [$MARK_DRY_RUN] + --edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. [$MARK_EDIT_LOCK] + --drop-h1 don't include the first H1 heading in Confluence output. [$MARK_DROP_H1] + --strip-linebreaks, -L remove linebreaks inside of tags, to accommodate non-standard Confluence behavior [$MARK_STRIP_LINEBREAKS] + --title-from-h1 extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. Mutually exclusive with --title-from-filename. [$MARK_TITLE_FROM_H1] + --title-from-filename use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1. [$MARK_TITLE_FROM_FILENAME] + --title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title [$MARK_TITLE_APPEND_GENERATED_HASH] + --minor-edit don't send notifications while updating Confluence page. [$MARK_MINOR_EDIT] --version-message string add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE] --color string display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR] --log-level string set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL] @@ -839,17 +833,17 @@ GLOBAL OPTIONS: --password string, -p string use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication. [$MARK_PASSWORD] --target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL] --base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL] - --config string, -c string use the specified configuration file. (default: $HOME/.config/mark.toml") [$MARK_CONFIG] - --ci run on CI mode. It won't fail if files are not found. (default: false) [$MARK_CI] + --config string, -c string use the specified configuration file. (default: "$HOME/.config/mark.toml") [$MARK_CONFIG] + --ci run on CI mode. It won't fail if files are not found. [$MARK_CI] --space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE] --parents string A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself. [$MARK_PARENTS] --parents-delimiter string The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER] --mermaid-scale float defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE] --include-path string Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH] - --changes-only Avoids re-uploading pages that haven't changed since the last run. (default: false) [$MARK_CHANGES_ONLY] + --changes-only Avoids re-uploading pages that haven't changed since the last run. [$MARK_CHANGES_ONLY] --d2-scale float defines the scaling factor for d2 renderings. (default: 1) [$MARK_D2_SCALE] - --features string [ --features string ] Enables optional features. Current features: d2, mermaid (default: "mermaid") [$MARK_FEATURES] - --insecure-skip-tls-verify Disables tls verification, useful for instances with self-signed certificates + --features string [ --features string ] Enables optional features. Current features: d2, mermaid, mkdocsadmonitions (default: "mermaid") [$MARK_FEATURES] + --insecure-skip-tls-verify skip TLS certificate verification (useful for self-signed certificates) [$MARK_INSECURE_SKIP_TLS_VERIFY] --help, -h show help --version, -v print the version ``` diff --git a/mark/confluence/api.go b/mark/confluence/api.go index cb1aeae..312015e 100644 --- a/mark/confluence/api.go +++ b/mark/confluence/api.go @@ -61,6 +61,7 @@ type PageInfo struct { Links struct { Full string `json:"webui"` + Base string `json:"-"` // Not from JSON; populated from response _links.base } `json:"_links"` } @@ -193,6 +194,9 @@ func (api *API) FindPage( ) (*PageInfo, error) { result := struct { Results []PageInfo `json:"results"` + Links struct { + Base string `json:"base"` + } `json:"_links"` }{} payload := map[string]string{ @@ -222,7 +226,13 @@ func (api *API) FindPage( return nil, nil } - return &result.Results[0], nil + page := &result.Results[0] + // Populate the base URL from the response _links.base + if result.Links.Base != "" { + page.Links.Base = result.Links.Base + } + + return page, nil } func (api *API) CreateAttachment( diff --git a/mark/go.mod b/mark/go.mod index 435b22d..3d674d3 100644 --- a/mark/go.mod +++ b/mark/go.mod @@ -5,7 +5,7 @@ go 1.24.0 toolchain go1.24.2 require ( - github.com/bmatcuk/doublestar/v4 v4.9.1 + github.com/bmatcuk/doublestar/v4 v4.9.2 github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d github.com/chromedp/chromedp v0.14.2 github.com/dreampuf/mermaid.go v0.0.39 @@ -17,9 +17,9 @@ require ( github.com/stefanfritsch/goldmark-admonitions v1.1.1 github.com/stretchr/testify v1.11.1 github.com/urfave/cli-altsrc/v3 v3.1.0 - github.com/urfave/cli/v3 v3.6.1 - github.com/yuin/goldmark v1.7.13 - golang.org/x/text v0.32.0 + github.com/urfave/cli/v3 v3.6.2 + github.com/yuin/goldmark v1.7.16 + golang.org/x/text v0.33.0 gopkg.in/yaml.v3 v3.0.1 oss.terrastruct.com/d2 v0.7.1 oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a diff --git a/mark/go.sum b/mark/go.sum index 4787356..102ae71 100644 --- a/mark/go.sum +++ b/mark/go.sum @@ -16,8 +16,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= -github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI= +github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU= github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k= github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM= @@ -90,13 +90,13 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/urfave/cli-altsrc/v3 v3.1.0 h1:6E5+kXeAWmRxXlPgdEVf9VqVoTJ2MJci0UMpUi/w/bA= github.com/urfave/cli-altsrc/v3 v3.1.0/go.mod h1:VcWVTGXcL3nrXUDJZagHAeUX702La3PKeWav7KpISqA= -github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= -github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= +github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= -github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 h1:75pcOSsb40+ub185cJI7g5uykl9Uu76rD5ONzK/4s40= github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -138,8 +138,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/mark/page/link.go b/mark/page/link.go index 4167faf..c4ceab2 100644 --- a/mark/page/link.go +++ b/mark/page/link.go @@ -2,12 +2,14 @@ package page import ( "bytes" + "encoding/base64" + "encoding/binary" "fmt" "net/http" - "net/url" "os" "path/filepath" "regexp" + "strconv" "strings" "github.com/kovetskiy/mark/confluence" @@ -40,6 +42,13 @@ func ResolveRelativeLinks( ) ([]LinkSubstitution, error) { matches := parseLinks(string(markdown)) + // If the user didn't provide --space, inherit the current document's space so + // relative links can be resolved within the same space. + spaceForLinks := spaceFromCli + if spaceForLinks == "" && meta != nil { + spaceForLinks = meta.Space + } + links := []LinkSubstitution{} for _, match := range matches { log.Tracef( @@ -49,7 +58,7 @@ func ResolveRelativeLinks( match.filename, match.hash, ) - resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, titleFromFilename, parents, titleAppendGeneratedHash) + resolved, err := resolveLink(api, base, match, spaceForLinks, titleFromH1, titleFromFilename, parents, titleAppendGeneratedHash) if err != nil { return nil, karma.Format(err, "resolve link: %q", match.full) } @@ -71,7 +80,7 @@ func resolveLink( api *confluence.API, base string, link markdownLink, - spaceFromCli string, + spaceForLinks string, titleFromH1 bool, titleFromFilename bool, parents []string, @@ -113,7 +122,7 @@ func resolveLink( // This helps to determine if found link points to file that's // not markdown or have mark required metadata - linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceFromCli, titleFromH1, titleFromFilename, filepath, parents, titleAppendGeneratedHash) + linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceForLinks, titleFromH1, titleFromFilename, filepath, parents, titleAppendGeneratedHash) if err != nil { log.Errorf( err, @@ -193,34 +202,98 @@ func parseLinks(markdown string) []markdownLink { return links } -// getConfluenceLink build (to be) link for Confluence, and tries to verify from -// API if there's real link available +// getConfluenceLink builds a stable Confluence tiny link for the given page or blog post. +// Tiny links use the format {baseURL}/x/{encodedPageID} and are immune to +// Cloud-specific URL variations like /ex/confluence//wiki/... func getConfluenceLink( api *confluence.API, space, title string, ) (string, error) { - link := fmt.Sprintf( - "%s/display/%s/%s", - api.BaseURL, - space, - url.QueryEscape(title), - ) - + // Try to find as a page first page, err := api.FindPage(space, title, "page") if err != nil { return "", karma.Format(err, "api: find page") } - if page != nil { - link = api.BaseURL + page.Links.Full + // If not found as a page, try to find as a blog post + if page == nil { + page, err = api.FindPage(space, title, "blogpost") + if err != nil { + return "", karma.Format(err, "api: find blogpost") + } } - linkUrl, err := url.Parse(link) - if err != nil { - return "", karma.Format(err, "parse URL: %s", link) + if page == nil { + return "", nil } - // Confluence supports relative links to reference other pages: - // https://confluence.atlassian.com/doc/links-776656293.html - linkPath := linkUrl.Path - return linkPath, nil + + // Prefer the base URL from the API response (_links.base) as it contains + // the canonical user-facing wiki URL (e.g., https://tenant.atlassian.net/wiki). + // Fall back to api.BaseURL if _links.base is not available. + baseURL := page.Links.Base + if baseURL == "" { + baseURL = api.BaseURL + } + + tiny, err := GenerateTinyLink(baseURL, page.ID) + if err != nil { + return "", karma.Format(err, "generate tiny link for page %s", page.ID) + } + + return tiny, nil +} + +// GenerateTinyLink generates a Confluence tiny link from a page ID. +// The algorithm converts the page ID to a little-endian 32-bit byte array, +// base64-encodes it, and applies URL-safe transformations. +// Format: {baseURL}/x/{encodedID} +// +// Reference: https://support.atlassian.com/confluence/kb/how-to-programmatically-generate-the-tiny-link-of-a-confluence-page +func GenerateTinyLink(baseURL string, pageID string) (string, error) { + id, err := strconv.ParseUint(pageID, 10, 64) + if err != nil { + return "", fmt.Errorf("invalid page ID %q: %w", pageID, err) + } + + encoded := encodeTinyLinkID(id) + baseURL = strings.TrimSuffix(baseURL, "/") + + return baseURL + "/x/" + encoded, nil +} + +// encodeTinyLinkID encodes a page ID into the Confluence tiny link format. +// This is the core algorithm extracted for testability. +func encodeTinyLinkID(id uint64) string { + // Pack as little-endian. Use 8 bytes to support large page IDs, + // but the base64 trimming will remove unnecessary trailing zeros. + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, id) + + // Trim trailing zero bytes (they become 'A' padding in base64) + for len(buf) > 1 && buf[len(buf)-1] == 0 { + buf = buf[:len(buf)-1] + } + + // Base64 encode + encoded := base64.StdEncoding.EncodeToString(buf) + + // Transform to URL-safe format: + // - Strip '=' padding + // - Replace '/' with '-' + // - Replace '+' with '_' + var result strings.Builder + for _, c := range encoded { + switch c { + case '=': + continue + case '/': + result.WriteByte('-') + case '+': + result.WriteByte('_') + default: + result.WriteRune(c) + } + } + + return result.String() } diff --git a/mark/page/link_test.go b/mark/page/link_test.go index 3f94ddf..7c50f66 100644 --- a/mark/page/link_test.go +++ b/mark/page/link_test.go @@ -1,6 +1,9 @@ package page import ( + "encoding/base64" + "encoding/binary" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -51,3 +54,153 @@ func TestParseLinks(t *testing.T) { assert.Equal(t, "example.md", links[7].full) assert.Equal(t, len(links), 8) } + +func TestEncodeTinyLinkID(t *testing.T) { + // Test cases for the tiny link encoding algorithm. + // The algorithm: little-endian bytes -> base64 -> URL-safe transform + tests := []struct { + name string + pageID uint64 + expected string + }{ + { + name: "small page ID", + pageID: 98319, + expected: "D4AB", + }, + { + name: "another small page ID", + pageID: 98320, + expected: "EIAB", + }, + { + name: "large page ID (Confluence Cloud)", + pageID: 5000000001, + expected: "AfIFKgE", + }, + { + name: "page ID 1", + pageID: 1, + expected: "AQ", + }, + { + name: "page ID 255", + pageID: 255, + expected: "-w", + }, + { + name: "page ID 256", + pageID: 256, + expected: "AAE", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := encodeTinyLinkID(tt.pageID) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestGenerateTinyLink(t *testing.T) { + tests := []struct { + name string + baseURL string + pageID string + expected string + wantErr bool + }{ + { + name: "cloud URL with trailing slash", + baseURL: "https://example.atlassian.net/wiki/", + pageID: "5000000001", + expected: "https://example.atlassian.net/wiki/x/AfIFKgE", + wantErr: false, + }, + { + name: "cloud URL without trailing slash", + baseURL: "https://example.atlassian.net/wiki", + pageID: "5000000001", + expected: "https://example.atlassian.net/wiki/x/AfIFKgE", + wantErr: false, + }, + { + name: "server URL", + baseURL: "https://confluence.example.com", + pageID: "98319", + expected: "https://confluence.example.com/x/D4AB", + wantErr: false, + }, + { + name: "invalid page ID", + baseURL: "https://example.atlassian.net/wiki", + pageID: "not-a-number", + expected: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := GenerateTinyLink(tt.baseURL, tt.pageID) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expected, result) + } + }) + } +} + +// encodeTinyLinkIDPerl32 implements the Perl algorithm from Atlassian docs +// using pack("L", $pageID) which is 32-bit little-endian. +// This is used to validate our implementation matches the documented algorithm. +func encodeTinyLinkIDPerl32(id uint32) string { + buf := make([]byte, 4) + binary.LittleEndian.PutUint32(buf, id) + encoded := base64.StdEncoding.EncodeToString(buf) + + var result strings.Builder + for _, c := range encoded { + switch c { + case '=': + continue + case '/': + result.WriteByte('-') + case '+': + result.WriteByte('_') + default: + result.WriteRune(c) + } + } + s := result.String() + // Perl strips trailing 'A' chars (which are base64 for zero bits) + s = strings.TrimRight(s, "A") + return s +} + +func TestEncodeTinyLinkIDMatchesPerl(t *testing.T) { + // Validate that our implementation matches the Perl algorithm from: + // https://support.atlassian.com/confluence/kb/how-to-programmatically-generate-the-tiny-link-of-a-confluence-page + testIDs := []uint32{1, 255, 256, 65535, 98319, 98320} + + for _, id := range testIDs { + goResult := encodeTinyLinkID(uint64(id)) + perlResult := encodeTinyLinkIDPerl32(id) + assert.Equal(t, perlResult, goResult, "ID %d should match Perl implementation", id) + } +} + +func TestEncodeTinyLinkIDLargeIDs(t *testing.T) { + // Test large page IDs (> 32-bit) which are common in Confluence Cloud + // These exceed Perl's pack("L") but our implementation handles them + largeID := uint64(5000000001) + result := encodeTinyLinkID(largeID) + assert.NotEmpty(t, result) + assert.Equal(t, "AfIFKgE", result) + + // Verify the result is a valid URL-safe base64-like string + assert.Regexp(t, `^[A-Za-z0-9_-]+$`, result) +} diff --git a/mark/renderer/fencedcodeblock.go b/mark/renderer/fencedcodeblock.go index bc89650..1e5e9b4 100644 --- a/mark/renderer/fencedcodeblock.go +++ b/mark/renderer/fencedcodeblock.go @@ -108,6 +108,10 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu collapse = false continue } + if option == "linenumbers" { + linenumbers = true + continue + } var i int if _, err := fmt.Sscanf(option, "%d", &i); err == nil { diff --git a/mark/stdlib/stdlib.go b/mark/stdlib/stdlib.go index 57b784d..28eeac8 100644 --- a/mark/stdlib/stdlib.go +++ b/mark/stdlib/stdlib.go @@ -151,7 +151,7 @@ func templates(api *confluence.API) (*template.Template, error) { ``, `{{ .Ticket }}`, `{{ if .Server }}`, - `{{ .Server }}`, + `{{ .Server }}`, `{{ end }}`, ``, ), @@ -451,6 +451,15 @@ func templates(api *confluence.API) (*template.Template, error) { `{{ or .AutoPlay "false"}}`, ``, ), + /* https://confluence.atlassian.com/conf59/view-file-macro-792499226.html */ + `ac:view-file`: text( + ``, + ``, + ``, + ``, + `{{ or .Height 250 }}`, + ``, + ), // TODO(seletskiy): more templates here } { diff --git a/vendor/github.com/bmatcuk/doublestar/v4/README.md b/vendor/github.com/bmatcuk/doublestar/v4/README.md index e4d1941..2476b41 100644 --- a/vendor/github.com/bmatcuk/doublestar/v4/README.md +++ b/vendor/github.com/bmatcuk/doublestar/v4/README.md @@ -391,6 +391,38 @@ Class | Meaning `[^class]` | matches any single character which does *not* match the class `[!class]` | same as `^`: negates the class +#### Globs Are Not Regular Expressions + +Occasionally I get bug reports that some regular-expression-style syntax +doesn't work, or feature requests to add some regular-expression-inspired +syntax. Globs are not regular expressions. However, if globs are not +sufficiently expressive for your filtering needs, I recommend a two stage +approach using `GlobWalk`. Something like the following will get you started: + +```go +var matches []string +err := doublestar.GlobWalk(fsys, pattern, func(p string, d fs.DirEntry) error { + if (customFilter(p, d)) { + matches = append(matches, p) + } else if (d.isDir()) { + return doublestar.SkipDir + } + return nil +}) +return matches, err +``` + +In this example, `pattern` should be a glob that does a first pass at fetching +the files you might be interested in; `customFilter` is a function that does a +second pass. This second pass could be anything, including regular expressions. +Try to fashion a `pattern` that reduces the number of files you need to +consider in your second pass `customFilter`. + +One final note: empty alternatives can be used to build some more complicated +globs. For example, `some{thing,}` will match both "something" and "some". +Alternatives can also be nested, like `some{thing{new,},}`, which would match +"somethingnew", "something", and "some". + ## Performance ``` diff --git a/vendor/github.com/bmatcuk/doublestar/v4/glob.go b/vendor/github.com/bmatcuk/doublestar/v4/glob.go index 3471bea..defe8e2 100644 --- a/vendor/github.com/bmatcuk/doublestar/v4/glob.go +++ b/vendor/github.com/bmatcuk/doublestar/v4/glob.go @@ -158,7 +158,7 @@ func (g *glob) globAlts(fsys fs.FS, pattern string, openingIdx, closingIdx int, nextIdx += patIdx } - alt := buildAlt(d, pattern, startIdx, openingIdx, patIdx, nextIdx, afterIdx) + alt := buildAlt(escapeMeta(d), pattern, startIdx, openingIdx, patIdx, nextIdx, afterIdx) matches, err = g.doGlob(fsys, alt, matches, firstSegment, beforeMeta) if err != nil { return diff --git a/vendor/github.com/bmatcuk/doublestar/v4/globwalk.go b/vendor/github.com/bmatcuk/doublestar/v4/globwalk.go index 16601b7..4c66018 100644 --- a/vendor/github.com/bmatcuk/doublestar/v4/globwalk.go +++ b/vendor/github.com/bmatcuk/doublestar/v4/globwalk.go @@ -205,7 +205,7 @@ func (g *glob) doGlobAltsWalk(fsys fs.FS, d, pattern string, startIdx, openingId nextIdx += patIdx } - alt := buildAlt(d, pattern, startIdx, openingIdx, patIdx, nextIdx, afterIdx) + alt := buildAlt(escapeMeta(d), pattern, startIdx, openingIdx, patIdx, nextIdx, afterIdx) err = g.doGlobWalk(fsys, alt, firstSegment, beforeMeta, func(p string, d fs.DirEntry) error { // insertion sort, ignoring dups insertIdx := matchesLen diff --git a/vendor/github.com/bmatcuk/doublestar/v4/utils.go b/vendor/github.com/bmatcuk/doublestar/v4/utils.go index 7831e5c..7f5cda7 100644 --- a/vendor/github.com/bmatcuk/doublestar/v4/utils.go +++ b/vendor/github.com/bmatcuk/doublestar/v4/utils.go @@ -152,9 +152,16 @@ func indexNextAlt(s string, allowEscaping bool) int { return -1 } -var metaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}") +var escapeMetaReplacer = strings.NewReplacer("*", "\\*", "?", "\\?", "[", "\\[", "]", "\\]", "{", "\\{", "}", "\\}") + +// Escapes meta characters (*?[]{}) +func escapeMeta(path string) string { + return escapeMetaReplacer.Replace(path) +} + +var unescapeMetaReplacer = strings.NewReplacer("\\*", "*", "\\?", "?", "\\[", "[", "\\]", "]", "\\{", "{", "\\}", "}") // Unescapes meta characters (*?[]{}) func unescapeMeta(pattern string) string { - return metaReplacer.Replace(pattern) + return unescapeMetaReplacer.Replace(pattern) } diff --git a/vendor/github.com/urfave/cli/v3/args.go b/vendor/github.com/urfave/cli/v3/args.go index e63e1f5..b3acfd5 100644 --- a/vendor/github.com/urfave/cli/v3/args.go +++ b/vendor/github.com/urfave/cli/v3/args.go @@ -180,12 +180,10 @@ func (a *ArgumentsBase[T, C, VC]) Usage() string { func (a *ArgumentsBase[T, C, VC]) Parse(s []string) ([]string, error) { tracef("calling arg%[1] parse with args %[2]", &a.Name, s) if a.Max == 0 { - fmt.Printf("WARNING args %s has max 0, not parsing argument\n", a.Name) - return s, nil + return s, fmt.Errorf("args %s has max 0, not parsing argument", a.Name) } if a.Max != -1 && a.Min > a.Max { - fmt.Printf("WARNING args %s has min[%d] > max[%d], not parsing argument\n", a.Name, a.Min, a.Max) - return s, nil + return s, fmt.Errorf("args %s has min[%d] > max[%d], not parsing argument", a.Name, a.Min, a.Max) } count := 0 diff --git a/vendor/github.com/urfave/cli/v3/command.go b/vendor/github.com/urfave/cli/v3/command.go index 2572c36..8f73fd2 100644 --- a/vendor/github.com/urfave/cli/v3/command.go +++ b/vendor/github.com/urfave/cli/v3/command.go @@ -161,6 +161,8 @@ type Command struct { globaHelpFlagAdded bool // whether global version flag was added globaVersionFlagAdded bool + // whether this is a completion command + isCompletionCommand bool } // FullName returns the full name of the command. @@ -334,14 +336,10 @@ func (cmd *Command) handleExitCoder(ctx context.Context, err error) error { } func (cmd *Command) argsWithDefaultCommand(oldArgs Args) Args { - if cmd.DefaultCommand != "" { - rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...) - newArgs := &stringSliceArgs{v: rawArgs} + rawArgs := append([]string{cmd.DefaultCommand}, oldArgs.Slice()...) + newArgs := &stringSliceArgs{v: rawArgs} - return newArgs - } - - return oldArgs + return newArgs } // Root returns the Command at the root of the graph diff --git a/vendor/github.com/urfave/cli/v3/command_run.go b/vendor/github.com/urfave/cli/v3/command_run.go index 578683d..e5cfff8 100644 --- a/vendor/github.com/urfave/cli/v3/command_run.go +++ b/vendor/github.com/urfave/cli/v3/command_run.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "io" - "reflect" "slices" "unicode" ) @@ -140,13 +139,20 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } var rargs Args = &stringSliceArgs{v: osArgs} + var args Args = &stringSliceArgs{rargs.Tail()} + + if cmd.isCompletionCommand || cmd.Name == helpName { + tracef("special command detected, skipping pre-parse (cmd=%[1]q)", cmd.Name) + cmd.parsedArgs = args + return ctx, cmd.Action(ctx, cmd) + } + for _, f := range cmd.allFlags() { if err := f.PreParse(); err != nil { return ctx, err } } - var args Args = &stringSliceArgs{rargs.Tail()} var err error if cmd.SkipFlagParsing { @@ -252,6 +258,7 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context if cmd.SuggestCommandFunc != nil && name != "--" { name = cmd.SuggestCommandFunc(cmd.Commands, name) + tracef("suggested command name=%1[q] (cmd=%[2]q)", name, cmd.Name) } subCmd = cmd.Command(name) if subCmd == nil { @@ -263,14 +270,13 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } if isFlagName || hasDefault { - argsWithDefault := cmd.argsWithDefaultCommand(args) + argsWithDefault := cmd.argsWithDefaultCommand(cmd.parsedArgs) tracef("using default command args=%[1]q (cmd=%[2]q)", argsWithDefault, cmd.Name) - if !reflect.DeepEqual(args, argsWithDefault) { - subCmd = cmd.Command(argsWithDefault.First()) - } + subCmd = cmd.Command(argsWithDefault.First()) + cmd.parsedArgs = argsWithDefault } } - } else if cmd.parent == nil && cmd.DefaultCommand != "" { + } else if cmd.DefaultCommand != "" { tracef("no positional args present; checking default command %[1]q (cmd=%[2]q)", cmd.DefaultCommand, cmd.Name) if dc := cmd.Command(cmd.DefaultCommand); dc != cmd { diff --git a/vendor/github.com/urfave/cli/v3/completion.go b/vendor/github.com/urfave/cli/v3/completion.go index d97ade6..609b204 100644 --- a/vendor/github.com/urfave/cli/v3/completion.go +++ b/vendor/github.com/urfave/cli/v3/completion.go @@ -65,6 +65,7 @@ func buildCompletionCommand(appName string) *Command { Action: func(ctx context.Context, cmd *Command) error { return printShellCompletion(ctx, cmd, appName) }, + isCompletionCommand: true, } } diff --git a/vendor/github.com/urfave/cli/v3/errors.go b/vendor/github.com/urfave/cli/v3/errors.go index a1188e7..f365a57 100644 --- a/vendor/github.com/urfave/cli/v3/errors.go +++ b/vendor/github.com/urfave/cli/v3/errors.go @@ -150,12 +150,10 @@ func HandleExitCoder(err error) { } if exitErr, ok := err.(ExitCoder); ok { - if err.Error() != "" { - if _, ok := exitErr.(ErrorFormatter); ok { - _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) - } else { - _, _ = fmt.Fprintln(ErrWriter, err) - } + if _, ok := exitErr.(ErrorFormatter); ok { + _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + _, _ = fmt.Fprintln(ErrWriter, err) } OsExiter(exitErr.ExitCode()) return diff --git a/vendor/github.com/urfave/cli/v3/flag.go b/vendor/github.com/urfave/cli/v3/flag.go index 6ff83f5..bfac8fa 100644 --- a/vendor/github.com/urfave/cli/v3/flag.go +++ b/vendor/github.com/urfave/cli/v3/flag.go @@ -77,11 +77,6 @@ func (f FlagsByName) Len() int { } func (f FlagsByName) Less(i, j int) bool { - if len(f[j].Names()) == 0 { - return false - } else if len(f[i].Names()) == 0 { - return true - } return lexicographicLess(f[i].Names()[0], f[j].Names()[0]) } diff --git a/vendor/github.com/urfave/cli/v3/flag_bool.go b/vendor/github.com/urfave/cli/v3/flag_bool.go index d576448..0f2af27 100644 --- a/vendor/github.com/urfave/cli/v3/flag_bool.go +++ b/vendor/github.com/urfave/cli/v3/flag_bool.go @@ -50,7 +50,8 @@ func (b boolValue) Create(val bool, p *bool, c BoolConfig) Value { // ToString formats the bool value func (b boolValue) ToString(value bool) string { - return strconv.FormatBool(value) + b.destination = &value + return b.String() } // Below functions are to satisfy the flag.Value interface @@ -75,7 +76,3 @@ func (b *boolValue) String() string { } func (b *boolValue) IsBoolFlag() bool { return true } - -func (b *boolValue) Count() int { - return *b.count -} diff --git a/vendor/github.com/urfave/cli/v3/flag_duration.go b/vendor/github.com/urfave/cli/v3/flag_duration.go index 37b4cb6..e14ff42 100644 --- a/vendor/github.com/urfave/cli/v3/flag_duration.go +++ b/vendor/github.com/urfave/cli/v3/flag_duration.go @@ -18,7 +18,8 @@ func (d durationValue) Create(val time.Duration, p *time.Duration, c NoConfig) V } func (d durationValue) ToString(val time.Duration) string { - return fmt.Sprintf("%v", val) + d = durationValue(val) + return d.String() } // Below functions are to satisfy the flag.Value interface @@ -34,7 +35,9 @@ func (d *durationValue) Set(s string) error { func (d *durationValue) Get() any { return time.Duration(*d) } -func (d *durationValue) String() string { return (*time.Duration)(d).String() } +func (d *durationValue) String() string { + return fmt.Sprintf("%v", time.Duration(*d)) +} func (cmd *Command) Duration(name string) time.Duration { if v, ok := cmd.Value(name).(time.Duration); ok { diff --git a/vendor/github.com/urfave/cli/v3/flag_float.go b/vendor/github.com/urfave/cli/v3/flag_float.go index 71aa0c2..6173e80 100644 --- a/vendor/github.com/urfave/cli/v3/flag_float.go +++ b/vendor/github.com/urfave/cli/v3/flag_float.go @@ -25,7 +25,8 @@ func (f floatValue[T]) Create(val T, p *T, c NoConfig) Value { } func (f floatValue[T]) ToString(b T) string { - return strconv.FormatFloat(float64(b), 'g', -1, int(unsafe.Sizeof(T(0))*8)) + f.val = &b + return f.String() } // Below functions are to satisfy the flag.Value interface diff --git a/vendor/github.com/urfave/cli/v3/flag_generic.go b/vendor/github.com/urfave/cli/v3/flag_generic.go index 9618409..5ee07c7 100644 --- a/vendor/github.com/urfave/cli/v3/flag_generic.go +++ b/vendor/github.com/urfave/cli/v3/flag_generic.go @@ -17,10 +17,8 @@ func (f genericValue) Create(val Value, p *Value, c NoConfig) Value { } func (f genericValue) ToString(b Value) string { - if b != nil { - return b.String() - } - return "" + f.val = b + return f.String() } // Below functions are to satisfy the flag.Value interface diff --git a/vendor/github.com/urfave/cli/v3/flag_int.go b/vendor/github.com/urfave/cli/v3/flag_int.go index 0e08222..8e5af17 100644 --- a/vendor/github.com/urfave/cli/v3/flag_int.go +++ b/vendor/github.com/urfave/cli/v3/flag_int.go @@ -36,11 +36,8 @@ func (i intValue[T]) Create(val T, p *T, c IntegerConfig) Value { } func (i intValue[T]) ToString(b T) string { - if i.base == 0 { - i.base = 10 - } - - return strconv.FormatInt(int64(b), i.base) + i.val = &b + return i.String() } // Below functions are to satisfy the flag.Value interface diff --git a/vendor/github.com/urfave/cli/v3/flag_slice_base.go b/vendor/github.com/urfave/cli/v3/flag_slice_base.go index f1c9d0b..0248d8f 100644 --- a/vendor/github.com/urfave/cli/v3/flag_slice_base.go +++ b/vendor/github.com/urfave/cli/v3/flag_slice_base.go @@ -82,12 +82,12 @@ func (i *SliceBase[T, C, VC]) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (i *SliceBase[T, C, VC]) String() string { - v := i.Value() - var t T - if reflect.TypeOf(t).Kind() == reflect.String { - return fmt.Sprintf("%v", v) + var defaultVals []string + var v VC + for _, s := range *i.slice { + defaultVals = append(defaultVals, v.ToString(s)) } - return fmt.Sprintf("%T{%s}", v, i.ToString(v)) + return strings.Join(defaultVals, ", ") } // Serialize allows SliceBase to fulfill Serializer @@ -110,10 +110,6 @@ func (i *SliceBase[T, C, VC]) Get() interface{} { } func (i SliceBase[T, C, VC]) ToString(t []T) string { - var defaultVals []string - var v VC - for _, s := range t { - defaultVals = append(defaultVals, v.ToString(s)) - } - return strings.Join(defaultVals, ", ") + i.slice = &t + return i.String() } diff --git a/vendor/github.com/urfave/cli/v3/flag_string.go b/vendor/github.com/urfave/cli/v3/flag_string.go index bdc1ec6..ca24b37 100644 --- a/vendor/github.com/urfave/cli/v3/flag_string.go +++ b/vendor/github.com/urfave/cli/v3/flag_string.go @@ -30,10 +30,8 @@ func (s stringValue) Create(val string, p *string, c StringConfig) Value { } func (s stringValue) ToString(val string) string { - if val == "" { - return val - } - return fmt.Sprintf("%q", val) + s.destination = &val + return s.String() } // Below functions are to satisfy the flag.Value interface @@ -49,8 +47,8 @@ func (s *stringValue) Set(val string) error { func (s *stringValue) Get() any { return *s.destination } func (s *stringValue) String() string { - if s.destination != nil { - return *s.destination + if s.destination != nil && *s.destination != "" { + return fmt.Sprintf("%q", *s.destination) } return "" } diff --git a/vendor/github.com/urfave/cli/v3/flag_timestamp.go b/vendor/github.com/urfave/cli/v3/flag_timestamp.go index 413a2f0..82b1cb5 100644 --- a/vendor/github.com/urfave/cli/v3/flag_timestamp.go +++ b/vendor/github.com/urfave/cli/v3/flag_timestamp.go @@ -44,7 +44,8 @@ func (t timestampValue) ToString(b time.Time) string { if b.IsZero() { return "" } - return fmt.Sprintf("%v", b) + t.timestamp = &b + return t.String() } // Below functions are to satisfy the Value interface @@ -122,7 +123,7 @@ func (t *timestampValue) Set(value string) error { // String returns a readable representation of this value (for usage defaults) func (t *timestampValue) String() string { - return fmt.Sprintf("%#v", t.timestamp) + return fmt.Sprintf("%v", t.timestamp) } // Get returns the flag structure diff --git a/vendor/github.com/urfave/cli/v3/flag_uint.go b/vendor/github.com/urfave/cli/v3/flag_uint.go index 64ee231..9818579 100644 --- a/vendor/github.com/urfave/cli/v3/flag_uint.go +++ b/vendor/github.com/urfave/cli/v3/flag_uint.go @@ -31,12 +31,8 @@ func (i uintValue[T]) Create(val T, p *T, c IntegerConfig) Value { } func (i uintValue[T]) ToString(b T) string { - base := i.base - if base == 0 { - base = 10 - } - - return strconv.FormatUint(uint64(b), base) + i.val = &b + return i.String() } // Below functions are to satisfy the flag.Value interface diff --git a/vendor/github.com/urfave/cli/v3/help.go b/vendor/github.com/urfave/cli/v3/help.go index 78686b1..37b1091 100644 --- a/vendor/github.com/urfave/cli/v3/help.go +++ b/vendor/github.com/urfave/cli/v3/help.go @@ -188,7 +188,7 @@ func printCommandSuggestions(commands []*Command, writer io.Writer) { if command.Hidden { continue } - if strings.HasSuffix(os.Getenv("SHELL"), "zsh") { + if strings.HasSuffix(os.Getenv("SHELL"), "zsh") && len(command.Usage) > 0 { _, _ = fmt.Fprintf(writer, "%s:%s\n", command.Name, command.Usage) } else { _, _ = fmt.Fprintf(writer, "%s\n", command.Name) diff --git a/vendor/github.com/urfave/cli/v3/mkdocs-requirements.txt b/vendor/github.com/urfave/cli/v3/mkdocs-requirements.txt index 5f3a0c3..3f4ed15 100644 --- a/vendor/github.com/urfave/cli/v3/mkdocs-requirements.txt +++ b/vendor/github.com/urfave/cli/v3/mkdocs-requirements.txt @@ -1,5 +1,5 @@ mkdocs-git-revision-date-localized-plugin==1.5.0 -mkdocs-material==9.6.23 +mkdocs-material==9.7.1 mkdocs==1.6.1 mkdocs-redirects==1.2.2 pygments==2.19.2 diff --git a/vendor/github.com/yuin/goldmark/README.md b/vendor/github.com/yuin/goldmark/README.md index 1922fd0..cbf1eba 100644 --- a/vendor/github.com/yuin/goldmark/README.md +++ b/vendor/github.com/yuin/goldmark/README.md @@ -497,6 +497,10 @@ Extensions - [goldmark-wiki-table](https://github.com/movsb/goldmark-wiki-table): Adds support for embedding Wiki Tables. - [goldmark-tgmd](https://github.com/Mad-Pixels/goldmark-tgmd): A Telegram markdown renderer that can be passed to `goldmark.WithRenderer()`. - [goldmark-treeblood](https://github.com/Wyatt915/goldmark-treeblood): Renders $\LaTeX$ expressions as MathML (pure Go, no external dependencies). +- [goldmark-subtext](https://github.com/zeozeozeo/goldmark-subtext): Support for Discord-style markdown subtexts +- [goldmark-customtag](https://github.com/tendstofortytwo/goldmark-customtag): Allows you to define custom block tags. +- [goldmark-cjk-friendly](https://github.com/tats-u/goldmark-cjk-friendly): Port of npm package [`remark-cjk-friendly` / `markdown-it-cjk-friendly`](https://github.com/tats-u/markdown-cjk-friendly) to goldmark. Similar to the [CJK extension](#cjk-extension) (`WithEscapedSpace`), but you do not need to explicitly add `\ ` around `*` and `**`. You can combine this with the [CJK extension](#cjk-extension). +- [goldmark-chart](https://github.com/TheGreatRambler/goldmark-chart): Generate static ChartJS charts using the simple [Markvis](https://markvis.js.org/#/) format. ### Loading extensions at runtime [goldmark-dynamic](https://github.com/yuin/goldmark-dynamic) allows you to write a goldmark extension in Lua and load it at runtime without re-compilation. diff --git a/vendor/github.com/yuin/goldmark/extension/table.go b/vendor/github.com/yuin/goldmark/extension/table.go index ee782a2..c3b48b1 100644 --- a/vendor/github.com/yuin/goldmark/extension/table.go +++ b/vendor/github.com/yuin/goldmark/extension/table.go @@ -125,12 +125,16 @@ func isTableDelim(bs []byte) bool { if w, _ := util.IndentWidth(bs, 0); w > 3 { return false } + allSep := true for _, b := range bs { + if b != '-' { + allSep = false + } if !(util.IsSpace(b) || b == '-' || b == '|' || b == ':') { return false } } - return true + return !allSep } var tableDelimLeft = regexp.MustCompile(`^\s*\:\-+\s*$`) diff --git a/vendor/github.com/yuin/goldmark/parser/atx_heading.go b/vendor/github.com/yuin/goldmark/parser/atx_heading.go index dae5e84..b5c6df0 100644 --- a/vendor/github.com/yuin/goldmark/parser/atx_heading.go +++ b/vendor/github.com/yuin/goldmark/parser/atx_heading.go @@ -13,7 +13,7 @@ type HeadingConfig struct { } // SetOption implements SetOptioner. -func (b *HeadingConfig) SetOption(name OptionName, _ interface{}) { +func (b *HeadingConfig) SetOption(name OptionName, _ any) { switch name { case optAutoHeadingID: b.AutoHeadingID = true @@ -98,69 +98,47 @@ func (b *atxHeadingParser) Open(parent ast.Node, reader text.Reader, pc Context) if l == 0 { return nil, NoChildren } - start := i + l - if start >= len(line) { - start = len(line) - 1 - } - origstart := start - stop := len(line) - util.TrimRightSpaceLength(line) + start := min(i+l, len(line)-1) node := ast.NewHeading(level) - parsed := false - if b.Attribute { // handles special case like ### heading ### {#id} - start-- - closureClose := -1 - closureOpen := -1 - for j := start; j < stop; { - c := line[j] - if util.IsEscapedPunctuation(line, j) { - j += 2 - } else if util.IsSpace(c) && j < stop-1 && line[j+1] == '#' { - closureOpen = j + 1 - k := j + 1 - for ; k < stop && line[k] == '#'; k++ { - } - closureClose = k - break - } else { - j++ - } - } - if closureClose > 0 { - reader.Advance(closureClose) - attrs, ok := ParseAttributes(reader) - rest, _ := reader.PeekLine() - parsed = ok && util.IsBlank(rest) - if parsed { - for _, attr := range attrs { - node.SetAttribute(attr.Name, attr.Value) - } - node.Lines().Append(text.NewSegment( - segment.Start+start+1-segment.Padding, - segment.Start+closureOpen-segment.Padding)) - } - } + hl := text.NewSegment( + segment.Start+start-segment.Padding, + segment.Start+len(line)-segment.Padding) + hl = hl.TrimRightSpace(reader.Source()) + if hl.Len() == 0 { + reader.AdvanceToEOL() + return node, NoChildren } - if !parsed { - start = origstart - stop := len(line) - util.TrimRightSpaceLength(line) - if stop <= start { // empty headings like '##[space]' - stop = start - } else { - i = stop - 1 - for ; line[i] == '#' && i >= start; i-- { - } - if i != stop-1 && !util.IsSpace(line[i]) { - i = stop - 1 - } - i++ - stop = i - } - if len(util.TrimRight(line[start:stop], []byte{'#'})) != 0 { // empty heading like '### ###' - node.Lines().Append(text.NewSegment(segment.Start+start-segment.Padding, segment.Start+stop-segment.Padding)) + if b.Attribute { + node.Lines().Append(hl) + parseLastLineAttributes(node, reader, pc) + hl = node.Lines().At(0) + node.Lines().Clear() + } + + // handle closing sequence of '#' characters + line = hl.Value(reader.Source()) + stop := len(line) + if stop == 0 { // empty headings like '##[space]' + stop = 0 + } else { + i = stop - 1 + for ; line[i] == '#' && i > 0; i-- { + } + if i == 0 && line[0] == '#' { // empty headings like '### ###' + reader.AdvanceToEOL() + return node, NoChildren + } + if i != stop-1 && util.IsSpace(line[i]) { + stop = i + stop -= util.TrimRightSpaceLength(line[0:stop]) } } + hl.Stop = hl.Start + stop + node.Lines().Append(hl) + reader.AdvanceToEOL() + return node, NoChildren } @@ -169,13 +147,6 @@ func (b *atxHeadingParser) Continue(node ast.Node, reader text.Reader, pc Contex } func (b *atxHeadingParser) Close(node ast.Node, reader text.Reader, pc Context) { - if b.Attribute { - _, ok := node.AttributeString("id") - if !ok { - parseLastLineAttributes(node, reader, pc) - } - } - if b.AutoHeadingID { id, ok := node.AttributeString("id") if !ok { @@ -205,7 +176,7 @@ func generateAutoHeadingID(node *ast.Heading, reader text.Reader, pc Context) { node.SetAttribute(attrNameID, headingID) } -func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) { +func parseLastLineAttributes(node ast.Node, reader text.Reader, _ Context) { lastIndex := node.Lines().Len() - 1 if lastIndex < 0 { // empty headings return @@ -213,36 +184,36 @@ func parseLastLineAttributes(node ast.Node, reader text.Reader, pc Context) { lastLine := node.Lines().At(lastIndex) line := lastLine.Value(reader.Source()) lr := text.NewReader(line) - var attrs Attributes - var ok bool var start text.Segment var sl int - var end text.Segment for { c := lr.Peek() - if c == text.EOF { + if c == text.EOF || c == '\n' { break } if c == '\\' { lr.Advance(1) - if lr.Peek() == '{' { + if util.IsPunct(lr.Peek()) { lr.Advance(1) } continue } if c == '{' { sl, start = lr.Position() - attrs, ok = ParseAttributes(lr) - _, end = lr.Position() + attrs, ok := ParseAttributes(lr) + if ok { + if nl, _ := lr.PeekLine(); nl == nil || util.IsBlank(nl) { + for _, attr := range attrs { + node.SetAttribute(attr.Name, attr.Value) + } + lastLine.Stop = lastLine.Start + start.Start + lastLine = lastLine.TrimRightSpace(reader.Source()) + node.Lines().Set(lastIndex, lastLine) + return + } + } lr.SetPosition(sl, start) } lr.Advance(1) } - if ok && util.IsBlank(line[end.Start:]) { - for _, attr := range attrs { - node.SetAttribute(attr.Name, attr.Value) - } - lastLine.Stop = lastLine.Start + start.Start - node.Lines().Set(lastIndex, lastLine) - } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 3da972b..22a5fe4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -20,7 +20,7 @@ github.com/andybalholm/brotli/matchfinder # github.com/andybalholm/cascadia v1.3.2 ## explicit; go 1.16 github.com/andybalholm/cascadia -# github.com/bmatcuk/doublestar/v4 v4.9.1 +# github.com/bmatcuk/doublestar/v4 v4.9.2 ## explicit; go 1.16 github.com/bmatcuk/doublestar/v4 # github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d @@ -185,10 +185,10 @@ github.com/stretchr/testify/assert/yaml ## explicit; go 1.23.2 github.com/urfave/cli-altsrc/v3 github.com/urfave/cli-altsrc/v3/toml -# github.com/urfave/cli/v3 v3.6.1 +# github.com/urfave/cli/v3 v3.6.2 ## explicit; go 1.22 github.com/urfave/cli/v3 -# github.com/yuin/goldmark v1.7.13 +# github.com/yuin/goldmark v1.7.16 ## explicit; go 1.22 github.com/yuin/goldmark github.com/yuin/goldmark/ast @@ -216,7 +216,7 @@ golang.org/x/net/html/atom # golang.org/x/sys v0.36.0 ## explicit; go 1.24.0 golang.org/x/sys/unix -# golang.org/x/text v0.32.0 +# golang.org/x/text v0.33.0 ## explicit; go 1.24.0 golang.org/x/text/cases golang.org/x/text/collate