Update to 15.3.0 #1

Merged
eroca merged 2 commits from update-15.3.0 into main 2026-02-05 09:58:23 +01:00
38 changed files with 510 additions and 253 deletions

View File

@@ -1,3 +1,16 @@
-------------------------------------------------------------------
Tue Jan 20 16:12:32 UTC 2026 - Elisei Roca <eroca@suse.com>
- 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 <eroca@suse.com>

View File

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

View File

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

View File

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

View File

@@ -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 / .

View File

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

View File

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

View File

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

View File

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

View File

@@ -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/<cloudId>/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()
}

View File

@@ -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)
}

View File

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

View File

@@ -151,7 +151,7 @@ func templates(api *confluence.API) (*template.Template, error) {
`<ac:structured-macro ac:name="jira">`,
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
`{{ if .Server }}`,
`<ac:parameter ac:name="server">{{ .Server }}</ac:parameter>`,
`<ac:parameter ac:name="server">{{ .Server }}</ac:parameter>`,
`{{ end }}`,
`</ac:structured-macro>`,
),
@@ -451,6 +451,15 @@ func templates(api *confluence.API) (*template.Template, error) {
`<ac:parameter ac:name="autoplay">{{ or .AutoPlay "false"}}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/view-file-macro-792499226.html */
`ac:view-file`: text(
`<ac:structured-macro ac:name="view-file">`,
`<ac:parameter ac:name="name">`,
`<ri:attachment ri:filename="{{ .Name | convertAttachment }}"/>`,
`</ac:parameter>`,
`<ac:parameter ac:name="height">{{ or .Height 250 }}</ac:parameter>`,
`</ac:structured-macro>`,
),
// TODO(seletskiy): more templates here
} {

View File

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

View File

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

View File

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

View File

@@ -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)
}

View File

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

View File

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

View File

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

View File

@@ -65,6 +65,7 @@ func buildCompletionCommand(appName string) *Command {
Action: func(ctx context.Context, cmd *Command) error {
return printShellCompletion(ctx, cmd, appName)
},
isCompletionCommand: true,
}
}

View File

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

View File

@@ -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])
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()
}

View File

@@ -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 ""
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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*$`)

View File

@@ -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)
}
}

8
vendor/modules.txt vendored
View File

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