Compare commits

..

No commits in common. "master" and "11.0.0" have entirely different histories.

101 changed files with 1393 additions and 2488 deletions

View File

@ -11,7 +11,7 @@ on:
- master
env:
GO_VERSION: "~1.24"
GO_VERSION: "~1.23.1"
jobs:
# Runs Golangci-lint on the source code
@ -29,7 +29,7 @@ jobs:
id: go
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v6
# Runs markdown-lint on the markdown files
ci-markdown-lint:
@ -39,12 +39,12 @@ jobs:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: markdownlint-cli2-action
uses: DavidAnson/markdownlint-cli2-action@v20
uses: DavidAnson/markdownlint-cli2-action@v16
# Executes Unit Tests
ci-unit-tests:
name: ci-unit-tests
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4

View File

@ -16,7 +16,7 @@ jobs:
- name: Set Up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
go-version: "1.23"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
/mark
/docker
/testdata
.idea/
/mark.test
/profile.cov

View File

@ -1,4 +1,4 @@
FROM golang:1.24.4 AS builder
FROM golang:1.23.1 AS builder
ENV GOPATH="/go"
WORKDIR /go/src/github.com/kovetskiy/mark
COPY / .
@ -7,7 +7,6 @@ RUN make get \
FROM chromedp/headless-shell:latest
RUN apt-get update \
&& apt-get upgrade -qq \
&& apt-get install --no-install-recommends -qq ca-certificates bash sed git dumb-init \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

View File

@ -19,7 +19,7 @@ build:
-gcflags "-trimpath $(GOPATH)/src"
test:
go test -race -coverprofile=profile.cov ./... -v
go test -race -coverprofile=profile.cov ./...
image:
@echo :: building image $(NAME):$(VERSION)

139
README.md
View File

@ -69,12 +69,6 @@ Also, optional following headers are supported:
Setting the sidebar creates a column on the right side. You're able to add any valid HTML content. Adding this property sets the layout to `article`.
```markdown
<!-- Emoji: 🚀 -->
```
You can set a page emoji icon by specifying the icon in the headers.
Mark supports Go templates, which can be included into article by using path
to the template relative to current working dir, e.g.:
@ -268,15 +262,7 @@ some long code block
Block Quotes are converted to Confluence Info/Warn/Note box when the following conditions are met
1. The BlockQuote is on the root level of the document (not nested)
1. The first line of the BlockQuote contains one of the following patterns `Info/Warn/Note` or [Github MD Alerts style](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) `[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]`
| Github Alerts | Confluence |
|---------------|------------|
| Tip (green lightbulb) | Tip (green checkmark in circle) |
| Note (blue I in circle) | Info (blue I in circle) |
| Important (purple exclamation mark in speech bubble) | Info (blue I in circle) |
| Warning (yellow exclamation mark in triangle) | Note (yellow exclamation mark in triangle) |
| Caution (red exclamation mark in hexagon) | Warning (red exclamation mark in hexagon) |
1. The first line of the BlockQuote contains one of the following patterns `Info/Warn/Note`
In any other case the default behaviour will be resumed and html `<blockquote>` tag will be used
@ -368,32 +354,32 @@ By default, mark provides several built-in templates and macros:
* template: `ac:children` to include Children Display macro
* Reverse (Reverse Sort): Use with the `Sort Children By` parameter. When set, the sort order changes from ascending to descending.
* `true`
* `false` (Default)
* `true`
* `false` (Default)
* Sort (Sort Children By):
* `creation` — to sort by content creation date
* `title` — to sort alphabetically on title
* `modified` — to sort of last modification date.
* If not specified, manual sorting is used if manually ordered, otherwise alphabetical.
* `creation` — to sort by content creation date
* `title` — to sort alphabetically on title
* `modified` — to sort of last modification date.
* If not specified, manual sorting is used if manually ordered, otherwise alphabetical.
* Style (Heading Style): Choose the style used to display descendants.
* from `h1` to `h6`
* If not specified, default style is applied.
* from `h1` to `h6`
* If not specified, default style is applied.
* Page (Parent Page):
* `/` — to list the top-level pages of the current space, i.e. those without parents.
* `pagename` — to list the children of the specified page.
* `spacekey:pagename` — to list the children of the specified page in the specified space.
* If not specified, the current page is used.
* `/` — to list the top-level pages of the current space, i.e. those without parents.
* `pagename` — to list the children of the specified page.
* `spacekey:pagename` — to list the children of the specified page in the specified space.
* If not specified, the current page is used.
* Excerpt (Include Excerpts): Allows you to include a short excerpt under each page in the list.
* `none` - no excerpt will be displayed. (Default)
* `simple` - displays the first line of text contained in an Excerpt macro any of the returned pages. If there is not an Excerpt macro on the page, nothing will be shown.
* `rich content` - displays the contents of an Excerpt macro, or if there is not an Excerpt macro on the page, the first part of the page content, including formatted text, images and some macros.
* `none` - no excerpt will be displayed. (Default)
* `simple` - displays the first line of text contained in an Excerpt macro any of the returned pages. If there is not an Excerpt macro on the page, nothing will be shown.
* `rich content` - displays the contents of an Excerpt macro, or if there is not an Excerpt macro on the page, the first part of the page content, including formatted text, images and some macros.
* First (Number of Children): Restrict the number of child pages that are displayed at the top level.
* If not specified, no limit is applied.
* If not specified, no limit is applied.
* Depth (Depth of Descendants): Enter a number to specify the depth of descendants to display. For example, if the value is 2, the macro will display 2 levels of child pages. This setting has no effect if `Show Descendants` is enabled.
* If not specified, no limit is applied.
* If not specified, no limit is applied.
* All (Show Descendants): Choose whether to display all the parent page's descendants.
* `true`
* `false` (Default)
* `true`
* `false` (Default)
See: <https://confluence.atlassian.com/doc/children-display-macro-139501.html>
@ -438,12 +424,10 @@ By default, mark provides several built-in templates and macros:
* template: `ac:excerpt-include` to include the excerpt from another page
* Page: the page the excerpt should be included from
* Name: The specific identifier for the excerpt, allowing multiple Excerpt macros on one page to be referenced individually. If not provided, the first excerpt from the page will be used (optional, cloud only)
* NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
* template: `ac:excerpt` to create an excerpt and include it in the page
* Excerpt: The text you want to include
* Name: Allows you to identify this macro so that you can add multiple Excerpt macros to one page and use a specific one on another page using the Excerpt Include macro (optional, cloud only)
* OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
* Hidden: Hide the excerpt content (optional, default: false)
@ -719,7 +703,7 @@ Currently this is not compatible with the automated upload of inline images.
### Render Mermaid Diagram
Confluence doesn't provide [mermaid.js](https://github.com/mermaid-js/mermaid) support natively. Mark provides a convenient way to enable the feature like [Github does](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/).
As long as you have a code block marked as "mermaid", mark will automatically render it as a PNG image and attach it to the page as a rendered version of the code block.
As long as you have a code block and are marked as "mermaid", the mark will automatically render it as a PNG image and insert into before the code block.
```mermaid title diagrams_example
graph TD;
@ -729,17 +713,7 @@ A-->B;
In order to properly render mermaid, you can choose between the following mermaid providers:
* "mermaid-go" via [mermaid.go](https://github.com/dreampuf/mermaid.go)
* "cloudscript" via [cloudscript-io-mermaid-addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon) (deprecated)
### Render D2 Diagram
Optionally you can enable [D2](https://github.com/terrastruct/d2) rendering via `--features="d2"`.
This will transform the d2 diagram into a png that will be attached to Confluence, similar to how mermaid-go support works.
All you need is a codeblock marked as "d2".
```d2
X -> Y
```
* "cloudscript" via [cloudscript-io-mermaid-addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon)
## Installation
@ -790,45 +764,41 @@ NAME:
mark - A tool for updating Atlassian Confluence pages from markdown.
USAGE:
mark [global options]
mark [global options]
VERSION:
13.0.0
11.0.0
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]
--minor-edit don't send notifications while updating Confluence page. (default: false) [$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]
--username string, -u string use specified username for updating Confluence page. [$MARK_USERNAME]
--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]
--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-provider string defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go. (default: "cloudscript") [$MARK_MERMAID_PROVIDER]
--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]
--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]
--help, -h show help
--version, -v print the version
--files value, -f value use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES]
--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, --h1_drop don't include the first H1 heading in Confluence output. (default: false) [$MARK_H1_DROP]
--strip-linebreaks, -L remove linebreaks inside of tags, to accomodate non-standard Confluence behavior (default: false) [$MARK_STRIP_LINEBREAK]
--title-from-h1, --h1_title 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_H1_TITLE]
--minor-edit don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT]
--version-message value add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
--color value display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
--debug enable debug logs. (default: false) [$MARK_DEBUG]
--trace enable trace logs. (default: false) [$MARK_TRACE]
--username value, -u value use specified username for updating Confluence page. [$MARK_USERNAME]
--password value, -p value 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 value, -l value edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
--base-url value, -b value, --base_url value base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
--config value, -c value use the specified configuration file. (default: System specific) [$MARK_CONFIG]
--ci run on CI mode. It won't fail if files are not found. (default: false) [$MARK_CI]
--space value use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
--parents value 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 value The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
--mermaid-provider value defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go. (default: "cloudscript") [$MARK_MERMAID_PROVIDER]
--mermaid-scale value defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
--include-path value Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
--help, -h show help
--version, -v print the version
```
You can store user credentials in the configuration file, which should be
@ -847,7 +817,7 @@ drop-h1 = true
**NOTE**: The system specific locations are described in here:
<https://pkg.go.dev/os#UserConfigDir>.
Currently, these are:
Currently these are:
On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib.
## Tricks
@ -888,7 +858,7 @@ done
The following directive tells the CI to run this particular job only if the changes are pushed into the
`main` branch. It means you can safely push your changes into feature branches without being afraid
that they have automatically shown in Confluence, then go through the reviewal process and automatically
that they automatically shown in Confluence, then go through the reviewal process and automatically
deploy them when PR got merged.
```yaml
@ -918,12 +888,12 @@ We recommend to lint your markdown files with [markdownlint-cli2](https://github
## Issues, Bugs & Contributions
I've started the project to solve my own problem and open sourced the solution so anyone who has a problem like me can solve it too.
I have no profits/sponsors from these projects which means I don't really prioritize working on this project in my free time.
I have no profits/sponsors from this projects which means I don't really prioritize working on this project in my free time.
I still check the issues and do code reviews for Pull Requests which means if you encounter a bug in
the program, you should not expect me to fix it as soon as possible, but I'll be very glad to
merge your own contributions into the project and release the new version.
I try to label all new issues, so it's easy to find a bug or a feature request to fix/implement, if
I try to label all new issues so it's easy to find a bug or a feature request to fix/implement, if
you are willing to help with the project, you can use the following labels to find issues, just make
sure to reply in the issue to let everyone know you took the issue:
@ -996,7 +966,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/recrtl"><img src="https://avatars.githubusercontent.com/u/14078835?v=4?s=100" width="100px;" alt="recrtl"/><br /><sub><b>recrtl</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=recrtl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/seletskiy"><img src="https://avatars.githubusercontent.com/u/674812?v=4?s=100" width="100px;" alt="Stanislav Seletskiy"/><br /><sub><b>Stanislav Seletskiy</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=seletskiy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nr18"><img src="https://avatars.githubusercontent.com/u/1660601?v=4?s=100" width="100px;" alt="Joris Conijn"/><br /><sub><b>Joris Conijn</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=nr18" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -1,4 +1,4 @@
package util
package main
import (
"errors"

107
d2/d2.go
View File

@ -1,107 +0,0 @@
package d2
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"strconv"
"time"
"github.com/chromedp/cdproto/dom"
"github.com/chromedp/chromedp"
"github.com/kovetskiy/mark/attachment"
"github.com/reconquest/pkg/log"
"oss.terrastruct.com/d2/d2graph"
"oss.terrastruct.com/d2/d2layouts/d2dagrelayout"
"oss.terrastruct.com/d2/d2lib"
"oss.terrastruct.com/d2/d2renderers/d2svg"
"oss.terrastruct.com/d2/d2themes/d2themescatalog"
d2log "oss.terrastruct.com/d2/lib/log"
"oss.terrastruct.com/d2/lib/textmeasure"
"oss.terrastruct.com/util-go/go2"
)
var renderTimeout = 120 * time.Second
func ProcessD2(title string, d2Diagram []byte, scale float64) (attachment.Attachment, error) {
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
ctx = d2log.WithDefault(ctx)
defer cancel()
ruler, err := textmeasure.NewRuler()
if err != nil {
return attachment.Attachment{}, err
}
layoutResolver := func(engine string) (d2graph.LayoutGraph, error) {
return d2dagrelayout.DefaultLayout, nil
}
renderOpts := &d2svg.RenderOpts{
Pad: go2.Pointer(int64(5)),
ThemeID: &d2themescatalog.GrapeSoda.ID,
}
compileOpts := &d2lib.CompileOptions{
LayoutResolver: layoutResolver,
Ruler: ruler,
}
diagram, _, err := d2lib.Compile(ctx, string(d2Diagram), compileOpts, renderOpts)
if err != nil {
return attachment.Attachment{}, err
}
out, err := d2svg.Render(diagram, renderOpts)
if err != nil {
return attachment.Attachment{}, err
}
log.Debugf(nil, "Rendering: %q", title)
pngBytes, boxModel, err := convertSVGtoPNG(ctx, out, scale)
if err != nil {
return attachment.Attachment{}, err
}
checkSum, err := attachment.GetChecksum(bytes.NewReader(d2Diagram))
log.Debugf(nil, "Checksum: %q -> %s", title, checkSum)
if err != nil {
return attachment.Attachment{}, err
}
if title == "" {
title = checkSum
}
fileName := title + ".png"
return attachment.Attachment{
ID: "",
Name: title,
Filename: fileName,
FileBytes: pngBytes,
Checksum: checkSum,
Replace: title,
Width: strconv.FormatInt(boxModel.Width, 10),
Height: strconv.FormatInt(boxModel.Height, 10),
}, nil
}
func convertSVGtoPNG(ctx context.Context, svg []byte, scale float64) (png []byte, m *dom.BoxModel, err error) {
var (
result []byte
model *dom.BoxModel
)
ctx, cancel := chromedp.NewContext(ctx)
defer cancel()
err = chromedp.Run(ctx,
chromedp.Navigate(fmt.Sprintf("data:image/svg+xml;base64,%s", base64.StdEncoding.EncodeToString(svg))),
chromedp.ScreenshotScale(`document.querySelector("svg > svg")`, scale, &result, chromedp.ByJSPath),
chromedp.Dimensions(`document.querySelector("svg > svg")`, &model, chromedp.ByJSPath),
)
if err != nil {
return nil, nil, err
}
return result, model, err
}

View File

@ -1,102 +0,0 @@
package d2
import (
"fmt"
"testing"
"github.com/kovetskiy/mark/attachment"
"github.com/stretchr/testify/assert"
)
var diagram string = `d2
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: {shape: hexagon}
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: {shape: page; style.multiple: true}
network.data processor -> api server
`
func TestExtractD2Image(t *testing.T) {
tests := []struct {
name string
markdown []byte
scale float64
want attachment.Attachment
wantErr assert.ErrorAssertionFunc
}{
{"example", []byte(diagram), 1.0, attachment.Attachment{
// This is only the PNG Magic Header
FileBytes: []byte{0x89, 0x50, 0x4e, 0x47, 0xd, 0xa, 0x1a, 0xa},
Filename: "example.png",
Name: "example",
Replace: "example",
Checksum: "58fa387384181445e2d8f90a8c7fda945cb75174f73e8b9853ff59b9e0103ddd",
ID: "",
Width: "198",
Height: "441",
},
assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessD2(tt.name, tt.markdown, tt.scale)
if !tt.wantErr(t, err, fmt.Sprintf("processD2(%v, %v)", tt.name, string(tt.markdown))) {
return
}
assert.Equal(t, tt.want.Filename, got.Filename, "processD2(%v, %v)", tt.name, string(tt.markdown))
// We only test for the header as png changes based on system png library
assert.Equal(t, tt.want.FileBytes, got.FileBytes[0:8], "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Name, got.Name, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Replace, got.Replace, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Checksum, got.Checksum, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.ID, got.ID, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Width, got.Width, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Height, got.Height, "processD2(%v, %v)", tt.name, string(tt.markdown))
})
}
}

54
go.mod
View File

@ -1,58 +1,40 @@
module github.com/kovetskiy/mark
go 1.24.0
toolchain go1.24.2
go 1.23
require (
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b
github.com/chromedp/chromedp v0.13.6
github.com/dreampuf/mermaid.go v0.0.27
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/dreampuf/mermaid.go v0.0.18
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4
github.com/reconquest/karma-go v1.5.0
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
github.com/stretchr/testify v1.10.0
github.com/urfave/cli-altsrc/v3 v3.0.1
github.com/urfave/cli/v3 v3.3.3
github.com/yuin/goldmark v1.7.12
golang.org/x/tools v0.34.0
github.com/stretchr/testify v1.9.0
github.com/urfave/cli/v2 v2.27.4
github.com/yuin/goldmark v1.7.4
golang.org/x/tools v0.25.0
gopkg.in/yaml.v3 v3.0.1
oss.terrastruct.com/d2 v0.7.0
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.0 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 // indirect
github.com/chromedp/chromedp v0.10.0 // indirect
github.com/chromedp/sysutil v1.0.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mazznoer/csscolorparser v0.1.5 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/image v0.20.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/plot v0.14.0 // indirect
golang.org/x/sys v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

162
go.sum
View File

@ -1,66 +1,31 @@
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg=
github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
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.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b h1:jJmiCljLNTaq/O1ju9Bzz2MPpFlmiTn0F7LwCoeDZVw=
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/chromedp/cdproto v0.0.0-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E=
github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE=
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 h1:Ux9RXuPQmTB4C1MKagNLme0krvq8ulewfor+ORO/QL4=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dreampuf/mermaid.go v0.0.27 h1:uriWHpcc4clTaAUdJqpyDzyGvAZumeLb61n2VBxc0ZQ=
github.com/dreampuf/mermaid.go v0.0.27/go.mod h1:13PeW5y49ouLGlP3RdZm6ke+lQIcz3z7rdVoqRkt5hY=
github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM=
github.com/go-fonts/liberation v0.3.1/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
github.com/go-pdf/fpdf v0.8.0 h1:IJKpdaagnWUeSkUFUjTcSzTppFxmv8ucGQyNPQWxYOQ=
github.com/go-pdf/fpdf v0.8.0/go.mod h1:gfqhcNwXrsd3XYKte9a7vM3smvU/jB4ZRDrmWSxpfdc=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/dreampuf/mermaid.go v0.0.18 h1:PTDbfWrsi9JRIs1ocq65ajj8/eJnD1oVRsUKDEOmwKk=
github.com/dreampuf/mermaid.go v0.0.18/go.mod h1:I8KXPKfpH96jHVi1BygPq3pRn1tk970UhnEYbgfRI5A=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 h1:dMHbguTqGtorivvHTaOnbYp+tFzrw5M9gjkU4lCplgg=
github.com/google/pprof v0.0.0-20240927180334-d43a67379298/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a h1:OPt6gCghZXQ/WZpT6EhGkA7v+YMAYzcCb8SPQWmsb/8=
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a/go.mod h1:gRW37oDEg9LzOHApv31YzxKBICcCmPtDogaImsxZ6xc=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0 h1:LVc416BwqYl2D6sxv76ElZ4ZAT4+VQk4a80Ki/cNse8=
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0/go.mod h1:dVsBLabGUkYCN1Zh9spGL2GYfAOpG2LPWZf9H0qG66k=
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4 h1:2eV8tF1u58dqRJMlFUD/Df26BxcIlGVy71rZHN+aNoI=
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4/go.mod h1:p1RuSvyflTF/G4ubeATGurCRKWkULOrN/4PUAEFRq0s=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -69,10 +34,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mazznoer/csscolorparser v0.1.5 h1:Wr4uNIE+pHWN3TqZn2SGpA2nLRG064gB7WdSfSS5cz4=
github.com/mazznoer/csscolorparser v0.1.5/go.mod h1:OQRVvgCyHDCAquR1YWfSwwaDcM0LhnSffGnlbOew/3I=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@ -86,73 +49,26 @@ github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64 h1:OBNLiZay5PYLmG
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64/go.mod h1:r1Z1JNh3in9xLWbhv5u7cdox9vvGFjlKp89VI10Jrdo=
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 h1:bcDXaTFC09IIg13Z8gfQHk4gSu001ET7ssW/wKRvPzg=
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli-altsrc/v3 v3.0.1 h1:v+gHk59syLk8ao9rYybZs43+D5ut/gzj0omqQ1XYl8k=
github.com/urfave/cli-altsrc/v3 v3.0.1/go.mod h1:8UtsKKcxFVzvaoySFPfvQOk413T+IXJhaCWyyoPW3yM=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
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=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
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=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -160,9 +76,3 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
oss.terrastruct.com/d2 v0.7.0 h1:nFTap/RgAQtm1aAmUOOJxO8vgSCj3SLILcOkStnyHeI=
oss.terrastruct.com/d2 v0.7.0/go.mod h1:QseS95MrwfSRDJcFmVpBBIKuPIr8/RUoR3526QQ3rVk=
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a h1:UXF/Z9i9tOx/wqGUOn/T12wZeez1Gg0sAVKKl7YUDwM=
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a/go.mod h1:eMWv0sOtD9T2RUl90DLWfuShZCYp4NrsqNpI8eqO6U4=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

608
main.go
View File

@ -1,33 +1,613 @@
package main
import (
"context"
"bytes"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"time"
"github.com/kovetskiy/mark/util"
"github.com/bmatcuk/doublestar/v4"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"github.com/urfave/cli/v3"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
const (
version = "13.0.0"
version = "11.0.0"
usage = "A tool for updating Atlassian Confluence pages from markdown."
description = `Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark`
)
var flags = []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{
Name: "files",
Aliases: []string{"f"},
Value: "",
Usage: "use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted).",
TakesFile: true,
EnvVars: []string{"MARK_FILES"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "compile-only",
Value: false,
Usage: "show resulting HTML and don't update Confluence page content.",
EnvVars: []string{"MARK_COMPILE_ONLY"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "dry-run",
Value: false,
Usage: "resolve page and ancestry, show resulting HTML and exit.",
EnvVars: []string{"MARK_DRY_RUN"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "edit-lock",
Value: false,
Aliases: []string{"k"},
Usage: "lock page editing to current user only to prevent accidental manual edits over Confluence Web UI.",
EnvVars: []string{"MARK_EDIT_LOCK"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "drop-h1",
Value: false,
Aliases: []string{"h1_drop"},
Usage: "don't include the first H1 heading in Confluence output.",
EnvVars: []string{"MARK_H1_DROP"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "strip-linebreaks",
Value: false,
Aliases: []string{"L"},
Usage: "remove linebreaks inside of tags, to accomodate non-standard Confluence behavior",
EnvVars: []string{"MARK_STRIP_LINEBREAK"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "title-from-h1",
Value: false,
Aliases: []string{"h1_title"},
Usage: "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.",
EnvVars: []string{"MARK_H1_TITLE"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "minor-edit",
Value: false,
Usage: "don't send notifications while updating Confluence page.",
EnvVars: []string{"MARK_MINOR_EDIT"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "version-message",
Value: "",
Usage: "add a message to the page version, to explain the edit (default: \"\")",
EnvVars: []string{"MARK_VERSION_MESSAGE"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "color",
Value: "auto",
Usage: "display logs in color. Possible values: auto, never.",
EnvVars: []string{"MARK_COLOR"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "debug",
Value: false,
Usage: "enable debug logs.",
EnvVars: []string{"MARK_DEBUG"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "trace",
Value: false,
Usage: "enable trace logs.",
EnvVars: []string{"MARK_TRACE"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Value: "",
Usage: "use specified username for updating Confluence page.",
EnvVars: []string{"MARK_USERNAME"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Value: "",
Usage: "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.",
EnvVars: []string{"MARK_PASSWORD"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "target-url",
Aliases: []string{"l"},
Value: "",
Usage: "edit specified Confluence page. If -l is not specified, file should contain metadata (see above).",
EnvVars: []string{"MARK_TARGET_URL"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "base-url",
Aliases: []string{"b", "base_url"},
Value: "",
Usage: "base URL for Confluence. Alternative option for base_url config field.",
EnvVars: []string{"MARK_BASE_URL"},
}),
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: configFilePath(),
Usage: "use the specified configuration file.",
TakesFile: true,
EnvVars: []string{"MARK_CONFIG"},
},
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "ci",
Value: false,
Usage: "run on CI mode. It won't fail if files are not found.",
EnvVars: []string{"MARK_CI"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "space",
Value: "",
Usage: "use specified space key. If the space key is not specified, it must be set in the page metadata.",
EnvVars: []string{"MARK_SPACE"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "parents",
Value: "",
Usage: "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.",
EnvVars: []string{"MARK_PARENTS"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "parents-delimiter",
Value: "/",
Usage: "The delimiter used for the parents list",
EnvVars: []string{"MARK_PARENTS_DELIMITER"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "mermaid-provider",
Value: "cloudscript",
Usage: "defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go.",
EnvVars: []string{"MARK_MERMAID_PROVIDER"},
}),
altsrc.NewFloat64Flag(&cli.Float64Flag{
Name: "mermaid-scale",
Value: 1.0,
Usage: "defines the scaling factor for mermaid renderings.",
EnvVars: []string{"MARK_MERMAID_SCALE"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: "include-path",
Value: "",
Usage: "Path for shared includes, used as a fallback if the include doesn't exist in the current directory.",
TakesFile: true,
EnvVars: []string{"MARK_INCLUDE_PATH"},
}),
}
func main() {
cmd := &cli.Command{
Name: "mark",
Usage: usage,
Description: description,
Version: version,
Flags: util.Flags,
EnableShellCompletion: true,
HideHelpCommand: true,
Action: util.RunMark,
app := &cli.App{
Name: "mark",
Usage: usage,
Description: description,
Version: version,
Flags: flags,
Before: altsrc.InitInputSourceWithContext(flags,
func(context *cli.Context) (altsrc.InputSourceContext, error) {
if context.IsSet("config") {
filePath := context.String("config")
return altsrc.NewTomlSourceFromFile(filePath)
} else {
// Fall back to default if config is unset and path exists
_, err := os.Stat(configFilePath())
if os.IsNotExist(err) {
return &altsrc.MapInputSource{}, nil
}
return altsrc.NewTomlSourceFromFile(configFilePath())
}
}),
EnableBashCompletion: true,
HideHelpCommand: true,
Action: RunMark,
}
if err := cmd.Run(context.TODO(), os.Args); err != nil {
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
func RunMark(cCtx *cli.Context) error {
if cCtx.Bool("debug") {
log.SetLevel(lorg.LevelDebug)
}
if cCtx.Bool("trace") {
log.SetLevel(lorg.LevelTrace)
}
if cCtx.String("color") == "never" {
log.GetLogger().SetFormat(
lorg.NewFormat(
`${time:2006-01-02 15:04:05.000} ${level:%s:left:true} ${prefix}%s`,
),
)
log.GetLogger().SetOutput(os.Stderr)
}
creds, err := GetCredentials(cCtx.String("username"), cCtx.String("password"), cCtx.String("target-url"), cCtx.String("base-url"), cCtx.Bool("compile-only"))
if err != nil {
return err
}
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
files, err := doublestar.FilepathGlob(cCtx.String("files"))
if err != nil {
return err
}
if len(files) == 0 {
msg := "No files matched"
if cCtx.Bool("ci") {
log.Warning(msg)
} else {
log.Fatal(msg)
}
}
log.Debug("config:")
for _, f := range cCtx.Command.Flags {
flag := f.Names()
if flag[0] == "password" {
log.Debugf(nil, "%20s: %v", flag[0], "******")
} else {
log.Debugf(nil, "%20s: %v", flag[0], cCtx.Value(flag[0]))
}
}
// Loop through files matched by glob pattern
for _, file := range files {
log.Infof(
nil,
"processing %s",
file,
)
target := processFile(file, api, cCtx, creds.PageID, creds.Username)
log.Infof(
nil,
"page successfully updated: %s",
creds.BaseURL+target.Links.Full,
)
fmt.Println(creds.BaseURL + target.Links.Full)
}
return nil
}
func processFile(
file string,
api *confluence.API,
cCtx *cli.Context,
pageID string,
username string,
) *confluence.PageInfo {
markdown, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
markdown = bytes.ReplaceAll(markdown, []byte("\r\n"), []byte("\n"))
parents := strings.Split(cCtx.String("parents"), cCtx.String("parents-delimiter"))
meta, markdown, err := mark.ExtractMeta(markdown, cCtx.String("space"), cCtx.Bool("title-from-h1"), parents)
if err != nil {
log.Fatal(err)
}
if pageID != "" && meta != nil {
log.Warning(
`specified file contains metadata, ` +
`but it will be ignored due specified command line URL`,
)
meta = nil
}
if pageID == "" && meta == nil {
log.Fatal(
`specified file doesn't contain metadata ` +
`and URL is not specified via command line ` +
`or doesn't contain pageId GET-parameter`,
)
}
if meta.Space == "" {
log.Fatal(
"space is not set ('Space' header is not set and '--space' option is not set)",
)
}
if meta.Title == "" {
log.Fatal(
`page title is not set ('Title' header is not set ` +
`and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file)`,
)
}
stdlib, err := stdlib.New(api)
if err != nil {
log.Fatal(err)
}
templates := stdlib.Templates
var recurse bool
for {
templates, markdown, recurse, err = includes.ProcessIncludes(
filepath.Dir(file),
cCtx.String("include-path"),
markdown,
templates,
)
if err != nil {
log.Fatal(err)
}
if !recurse {
break
}
}
macros, markdown, err := macro.ExtractMacros(
filepath.Dir(file),
cCtx.String("include-path"),
markdown,
templates,
)
if err != nil {
log.Fatal(err)
}
macros = append(macros, stdlib.Macros...)
for _, macro := range macros {
markdown, err = macro.Apply(markdown)
if err != nil {
log.Fatal(err)
}
}
links, err := mark.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cCtx.String("space"), cCtx.Bool("title-from-h1"), parents)
if err != nil {
log.Fatalf(err, "unable to resolve relative links")
}
markdown = mark.SubstituteLinks(markdown, links)
if cCtx.Bool("dry-run") {
_, _, err := mark.ResolvePage(cCtx.Bool("dry-run"), api, meta)
if err != nil {
log.Fatalf(err, "unable to resolve page location")
}
}
if cCtx.Bool("compile-only") || cCtx.Bool("dry-run") {
if cCtx.Bool("drop-h1") {
log.Info(
"the leading H1 heading will be excluded from the Confluence output",
)
}
html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks"))
fmt.Println(html)
os.Exit(0)
}
var target *confluence.PageInfo
if meta != nil {
parent, page, err := mark.ResolvePage(cCtx.Bool("dry-run"), api, meta)
if err != nil {
log.Fatalf(
karma.Describe("title", meta.Title).Reason(err),
"unable to resolve %s",
meta.Type,
)
}
if page == nil {
page, err = api.CreatePage(
meta.Space,
meta.Type,
parent,
meta.Title,
``,
)
if err != nil {
log.Fatalf(
err,
"can't create %s %q",
meta.Type,
meta.Title,
)
}
// (issues/139): A delay between the create and update call
// helps mitigate a 409 conflict that can occur when attempting
// to update a page just after it was created.
time.Sleep(1 * time.Second)
}
target = page
} else {
if pageID == "" {
log.Fatalf(nil, "URL should provide 'pageId' GET-parameter")
}
page, err := api.GetPageByID(pageID)
if err != nil {
log.Fatalf(err, "unable to retrieve page by id")
}
target = page
}
// Resolve attachments created from <!-- Attachment: --> directive
localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments)
if err != nil {
log.Fatalf(err, "unable to locate attachments")
}
attaches, err := attachment.ResolveAttachments(
api,
target,
localAttachments,
)
if err != nil {
log.Fatalf(err, "unable to create/update attachments")
}
markdown = attachment.CompileAttachmentLinks(markdown, attaches)
if cCtx.Bool("drop-h1") {
log.Info(
"the leading H1 heading will be excluded from the Confluence output",
)
}
html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks"))
// Resolve attachements detected from markdown
_, err = attachment.ResolveAttachments(
api,
target,
inlineAttachments,
)
if err != nil {
log.Fatalf(err, "unable to create/update attachments")
}
{
var buffer bytes.Buffer
err := stdlib.Templates.ExecuteTemplate(
&buffer,
"ac:layout",
struct {
Layout string
Sidebar string
Body string
}{
Layout: meta.Layout,
Sidebar: meta.Sidebar,
Body: html,
},
)
if err != nil {
log.Fatal(err)
}
html = buffer.String()
}
err = api.UpdatePage(target, html, cCtx.Bool("minor-edit"), cCtx.String("version-message"), meta.Labels, meta.ContentAppearance)
if err != nil {
log.Fatal(err)
}
updateLabels(api, target, meta)
if cCtx.Bool("edit-lock") {
log.Infof(
nil,
`edit locked on page %q by user %q to prevent manual edits`,
target.Title,
username,
)
err := api.RestrictPageUpdates(target, username)
if err != nil {
log.Fatal(err)
}
}
return target
}
func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *mark.Meta) {
labelInfo, err := api.GetPageLabels(target, "global")
if err != nil {
log.Fatal(err)
}
log.Debug("Page Labels:")
log.Debug(labelInfo.Labels)
log.Debug("Meta Labels:")
log.Debug(meta.Labels)
delLabels := determineLabelsToRemove(labelInfo, meta)
log.Debug("Del Labels:")
log.Debug(delLabels)
addLabels := determineLabelsToAdd(meta, labelInfo)
log.Debug("Add Labels:")
log.Debug(addLabels)
if len(addLabels) > 0 {
_, err = api.AddPageLabels(target, addLabels)
if err != nil {
log.Fatal(err)
}
}
for _, label := range delLabels {
_, err = api.DeletePageLabel(target, label)
if err != nil {
log.Fatal(err)
}
}
}
// Page has label but label not in Metadata
func determineLabelsToRemove(labelInfo *confluence.LabelInfo, meta *mark.Meta) []string {
var labels []string
for _, label := range labelInfo.Labels {
if !slices.ContainsFunc(meta.Labels, func(metaLabel string) bool {
return strings.EqualFold(metaLabel, label.Name)
}) {
labels = append(labels, label.Name)
}
}
return labels
}
// Metadata has label but Page does not have it
func determineLabelsToAdd(meta *mark.Meta, labelInfo *confluence.LabelInfo) []string {
var labels []string
for _, metaLabel := range meta.Labels {
if !slices.ContainsFunc(labelInfo.Labels, func(label confluence.Label) bool {
return strings.EqualFold(label.Name, metaLabel)
}) {
labels = append(labels, metaLabel)
}
}
return labels
}
func configFilePath() string {
fp, err := os.UserConfigDir()
if err != nil {
log.Fatal(err)
}
return filepath.Join(fp, "mark")
}

View File

@ -1,51 +0,0 @@
package main
import (
"testing"
"github.com/kovetskiy/mark/util"
"github.com/reconquest/pkg/log"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func Test_setLogLevel(t *testing.T) {
type args struct {
lvl string
}
tests := map[string]struct {
args args
want log.Level
expectedErr string
}{
"invalid": {args: args{lvl: "INVALID"}, want: log.LevelInfo, expectedErr: "unknown log level: INVALID"},
"empty": {args: args{lvl: ""}, want: log.LevelInfo, expectedErr: "unknown log level: "},
"info": {args: args{lvl: log.LevelInfo.String()}, want: log.LevelInfo},
"debug": {args: args{lvl: log.LevelDebug.String()}, want: log.LevelDebug},
"trace": {args: args{lvl: log.LevelTrace.String()}, want: log.LevelTrace},
"warning": {args: args{lvl: log.LevelWarning.String()}, want: log.LevelWarning},
"error": {args: args{lvl: log.LevelError.String()}, want: log.LevelError},
"fatal": {args: args{lvl: log.LevelFatal.String()}, want: log.LevelFatal},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
cmd := &cli.Command{
Name: "test",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "log-level",
Value: tt.args.lvl,
Usage: "set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL.",
},
},
}
err := util.SetLogLevel(cmd)
if tt.expectedErr != "" {
assert.EqualError(t, err, tt.expectedErr)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, log.GetLevel())
}
})
}
}

View File

@ -1,183 +0,0 @@
package mark_test
import (
"context"
"os"
"path"
"path/filepath"
"runtime"
"strings"
"testing"
mark "github.com/kovetskiy/mark/markdown"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/kovetskiy/mark/util"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v3"
)
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
t.Helper()
basename := filepath.Base(filename)
testname := strings.TrimSuffix(basename, ".md")
htmlname := filepath.Join(filepath.Dir(filename), testname+variant+".html")
markdown, err := os.ReadFile(filename)
if err != nil {
panic(err)
}
html, err := os.ReadFile(htmlname)
if err != nil {
panic(err)
}
return markdown, htmlname, html
}
func TestCompileMarkdown(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
markdown, htmlname, html := loadData(t, filename, "")
cfg := types.MarkConfig{
MermaidProvider: "",
MermaidScale: 1.0,
DropFirstH1: false,
StripNewlines: false,
Features: []string{},
}
actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg)
test.EqualValues(strings.TrimSuffix(string(html), "\n"), strings.TrimSuffix(actual, "\n"), filename+" vs "+htmlname)
}
}
func TestCompileMarkdownDropH1(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
var variant string
switch filename {
case "testdata/quotes.md", "testdata/header.md":
variant = "-droph1"
default:
variant = ""
}
markdown, htmlname, html := loadData(t, filename, variant)
cfg := types.MarkConfig{
MermaidProvider: "",
MermaidScale: 1.0,
DropFirstH1: true,
StripNewlines: false,
Features: []string{},
}
actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg)
test.EqualValues(strings.TrimSuffix(string(html), "\n"), strings.TrimSuffix(actual, "\n"), filename+" vs "+htmlname)
}
}
func TestCompileMarkdownStripNewlines(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
var variant string
switch filename {
case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md":
variant = "-stripnewlines"
default:
variant = ""
}
markdown, htmlname, html := loadData(t, filename, variant)
cfg := types.MarkConfig{
MermaidProvider: "",
MermaidScale: 1.0,
DropFirstH1: false,
StripNewlines: true,
Features: []string{},
}
actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg)
test.EqualValues(strings.TrimSuffix(string(html), "\n"), strings.TrimSuffix(actual, "\n"), filename+" vs "+htmlname)
}
}
func TestContinueOnError(t *testing.T) {
cmd := &cli.Command{
Name: "temp-mark",
Usage: "test usage",
Description: "mark unit tests",
Version: "TEST-VERSION",
Flags: util.Flags,
EnableShellCompletion: true,
HideHelpCommand: true,
Action: util.RunMark,
}
filePath := filepath.Join("testdata", "batch-tests", "*.md")
argList := []string{
"",
"--log-level", "INFO",
"--compile-only",
"--continue-on-error",
"--files", filePath,
}
err := cmd.Run(context.TODO(), argList)
assert.NoError(t, err, "App should run without errors when continue-on-error is enabled")
}

View File

@ -1,30 +0,0 @@
package metadata
import (
"os"
"path"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExtractDocumentLeadingH1(t *testing.T) {
_, filename, _, _ := runtime.Caller(0)
dir := path.Join(path.Dir(filename), "..")
err := os.Chdir(dir)
if err != nil {
panic(err)
}
filename = "testdata/header.md"
markdown, err := os.ReadFile(filename)
if err != nil {
panic(err)
}
actual := ExtractDocumentLeadingH1(markdown)
assert.Equal(t, "a", actual)
}

View File

@ -1,55 +0,0 @@
package parser
import (
"fmt"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/util"
)
type ConfluenceIDs struct {
Values map[string]bool
}
// https://github.com/yuin/goldmark/blob/d9c03f07f08c2d36f23afe52dda865f05320ac86/parser/parser.go#L75
func (s *ConfluenceIDs) Generate(value []byte, kind ast.NodeKind) []byte {
value = util.TrimLeftSpace(value)
value = util.TrimRightSpace(value)
result := []byte{}
for i := 0; i < len(value); {
v := value[i]
l := util.UTF8Len(v)
i += int(l)
if l != 1 {
continue
}
if util.IsAlphaNumeric(v) || v == '/' || v == '_' || v == '.' {
result = append(result, v)
} else if util.IsSpace(v) || v == '-' {
result = append(result, '-')
}
}
if len(result) == 0 {
if kind == ast.KindHeading {
result = []byte("heading")
} else {
result = []byte("id")
}
}
if _, ok := s.Values[util.BytesToReadOnlyString(result)]; !ok {
s.Values[util.BytesToReadOnlyString(result)] = true
return result
}
for i := 1; ; i++ {
newResult := fmt.Sprintf("%s-%d", result, i)
if _, ok := s.Values[newResult]; !ok {
s.Values[newResult] = true
return []byte(newResult)
}
}
}
func (s *ConfluenceIDs) Put(value []byte) {
s.Values[util.BytesToReadOnlyString(value)] = true
}

View File

@ -9,7 +9,6 @@ import (
"mime/multipart"
"net/http"
"strings"
"unicode/utf8"
"github.com/kovetskiy/gopencils"
"github.com/kovetskiy/lorg"
@ -49,8 +48,7 @@ type PageInfo struct {
Type string `json:"type"`
Version struct {
Number int64 `json:"number"`
Message string `json:"message"`
Number int64 `json:"number"`
} `json:"version"`
Ancestors []struct {
@ -105,7 +103,7 @@ func NewAPI(baseURL string, username string, password string) *API {
Password: password,
}
}
rest := gopencils.Api(baseURL+"/rest/api", auth, 3) // set option for 3 retries on failure
rest := gopencils.Api(baseURL+"/rest/api", auth)
if username == "" {
if rest.Headers == nil {
rest.Headers = http.Header{}
@ -113,7 +111,10 @@ func NewAPI(baseURL string, username string, password string) *API {
rest.SetHeader("Authorization", fmt.Sprintf("Bearer %s", password))
}
json := gopencils.Api(baseURL+"/rpc/json-rpc/confluenceservice-v2", auth, 3)
json := gopencils.Api(
baseURL+"/rpc/json-rpc/confluenceservice-v2",
auth,
)
if log.GetLevel() == lorg.LevelTrace {
rest.Logger = &tracer{"rest:"}
@ -257,7 +258,7 @@ func (api *API) CreateAttachment(
if len(result.Results) == 0 {
return info, errors.New(
"the Confluence REST API for creating attachments returned " +
"Confluence REST API for creating attachments returned " +
"0 json objects, expected at least 1",
)
}
@ -511,7 +512,7 @@ func (api *API) CreatePage(
return request.Response.(*PageInfo), nil
}
func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, versionMessage string, newLabels []string, appearance string, emojiString string) error {
func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, versionMessage string, newLabels []string, appearance string) error {
nextPageVersion := page.Version.Number + 1
oldAncestors := []map[string]interface{}{}
@ -522,29 +523,6 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ve
}
}
properties := map[string]interface{}{
// Fix to set full-width as has changed on Confluence APIs again.
// https://jira.atlassian.com/browse/CONFCLOUD-65447
//
"content-appearance-published": map[string]interface{}{
"value": appearance,
},
// content-appearance-draft should not be set as this is impacted by
// the user editor default configurations - which caused the sporadic published widths.
}
if emojiString != "" {
r, _ := utf8.DecodeRuneInString(emojiString)
unicodeHex := fmt.Sprintf("%x", r)
properties["emoji-title-draft"] = map[string]interface{}{
"value": unicodeHex,
}
properties["emoji-title-published"] = map[string]interface{}{
"value": unicodeHex,
}
}
payload := map[string]interface{}{
"id": page.ID,
"type": page.Type,
@ -562,7 +540,16 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ve
},
},
"metadata": map[string]interface{}{
"properties": properties,
// Fix to set full-width as has changed on Confluence APIs again.
// https://jira.atlassian.com/browse/CONFCLOUD-65447
//
"properties": map[string]interface{}{
"content-appearance-published": map[string]interface{}{
"value": appearance,
},
},
// content-appearance-draft should not be set as this is impacted by
// the user editor default configurations - which caused the sporadic published widths.
},
}
@ -612,8 +599,8 @@ func (api *API) AddPageLabels(page *PageInfo, newLabels []string) (*LabelInfo, e
func (api *API) DeletePageLabel(page *PageInfo, label string) (*LabelInfo, error) {
request, err := api.rest.Res(
"content/"+page.ID+"/label", &LabelInfo{},
).SetQuery(map[string]string{"name": label}).Delete()
"content/"+page.ID+"/label/"+label, &LabelInfo{},
).Delete()
if err != nil {
return nil, err
}
@ -791,23 +778,21 @@ func (api *API) RestrictPageUpdates(
func newErrorStatusNotOK(request *gopencils.Resource) error {
if request.Raw.StatusCode == http.StatusUnauthorized {
return errors.New(
"the Confluence API returned unexpected status: 401 (Unauthorized)",
"Confluence API returned unexpected status: 401 (Unauthorized)",
)
}
if request.Raw.StatusCode == http.StatusNotFound {
return errors.New(
"the Confluence API returned unexpected status: 404 (Not Found)",
"Confluence API returned unexpected status: 404 (Not Found)",
)
}
output, _ := io.ReadAll(request.Raw.Body)
defer func() {
_ = request.Raw.Body.Close()
}()
defer request.Raw.Body.Close()
return fmt.Errorf(
"the Confluence API returned unexpected status: %v, "+
"Confluence API returned unexpected status: %v, "+
"output: %q",
request.Raw.Status, output,
)

View File

@ -1,10 +1,10 @@
package page
package mark
import (
"fmt"
"strings"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)

View File

@ -11,8 +11,8 @@ import (
"sort"
"strings"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/vfs"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
@ -201,9 +201,7 @@ func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error)
if err != nil {
return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath)
}
defer func() {
_ = file.Close()
}()
defer file.Close()
fileBytes, err := io.ReadAll(file)
if err != nil {

View File

@ -45,7 +45,7 @@ func TestPrepareAttachmentsWithWorkDirBase(t *testing.T) {
}
attaches, err := prepareAttachments(testingOpener, ".", replacements)
t.Logf("attaches: %v", err)
t.Logf("attaches: %s", err)
if err != nil {
println(err.Error())
t.Fatal(err)

View File

@ -1,4 +1,4 @@
package page
package mark
import (
"bytes"
@ -8,8 +8,7 @@ import (
"path/filepath"
"regexp"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/metadata"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"golang.org/x/tools/godoc/util"
@ -28,13 +27,12 @@ type markdownLink struct {
func ResolveRelativeLinks(
api *confluence.API,
meta *metadata.Meta,
meta *Meta,
markdown []byte,
base string,
spaceFromCli string,
titleFromH1 bool,
parents []string,
titleAppendGeneratedHash bool,
) ([]LinkSubstitution, error) {
matches := parseLinks(string(markdown))
@ -47,7 +45,7 @@ func ResolveRelativeLinks(
match.filename,
match.hash,
)
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, parents, titleAppendGeneratedHash)
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, parents)
if err != nil {
return nil, karma.Format(err, "resolve link: %q", match.full)
}
@ -72,7 +70,6 @@ func resolveLink(
spaceFromCli string,
titleFromH1 bool,
parents []string,
titleAppendGeneratedHash bool,
) (string, error) {
var result string
@ -107,7 +104,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, parents, titleAppendGeneratedHash)
linkMeta, _, err := ExtractMeta(linkContents, spaceFromCli, titleFromH1, parents)
if err != nil {
log.Errorf(
err,

View File

@ -1,4 +1,4 @@
package page
package mark
import (
"testing"

View File

@ -7,7 +7,7 @@ import (
"strings"
"text/template"
"github.com/kovetskiy/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"github.com/reconquest/regexputil-go"

View File

@ -1,10 +1,9 @@
package page
package mark
import (
"strings"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/metadata"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
@ -12,7 +11,7 @@ import (
func ResolvePage(
dryRun bool,
api *confluence.API,
meta *metadata.Meta,
meta *Meta,
) (*confluence.PageInfo, *confluence.PageInfo, error) {
page, err := api.FindPage(meta.Space, meta.Title, meta.Type)
if err != nil {

View File

@ -2,12 +2,12 @@ package mark
import (
"bytes"
"regexp"
"github.com/kovetskiy/mark/attachment"
cparser "github.com/kovetskiy/mark/parser"
crenderer "github.com/kovetskiy/mark/renderer"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/kovetskiy/mark/pkg/mark/attachment"
cparser "github.com/kovetskiy/mark/pkg/mark/parser"
crenderer "github.com/kovetskiy/mark/pkg/mark/renderer"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log"
"github.com/yuin/goldmark"
@ -21,20 +21,26 @@ import (
// Renderer renders anchor [Node]s.
type ConfluenceExtension struct {
html.Config
Stdlib *stdlib.Lib
Path string
MarkConfig types.MarkConfig
Attachments []attachment.Attachment
Stdlib *stdlib.Lib
Path string
MermaidProvider string
MermaidScale float64
DropFirstH1 bool
StripNewlines bool
Attachments []attachment.Attachment
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceExtension {
func NewConfluenceExtension(stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool, stripNewlines bool) *ConfluenceExtension {
return &ConfluenceExtension{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
MarkConfig: cfg,
Attachments: []attachment.Attachment{},
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
MermaidProvider: mermaidProvider,
MermaidScale: mermaidScale,
DropFirstH1: dropFirstH1,
StripNewlines: stripNewlines,
Attachments: []attachment.Attachment{},
}
}
@ -45,12 +51,12 @@ func (c *ConfluenceExtension) Attach(a attachment.Attachment) {
func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(crenderer.NewConfluenceTextRenderer(c.MarkConfig.StripNewlines), 100),
util.Prioritized(crenderer.NewConfluenceTextRenderer(c.StripNewlines), 100),
util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MermaidProvider, c.MermaidScale), 100),
util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.MarkConfig.DropFirstH1), 100),
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.DropFirstH1), 100),
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
@ -63,20 +69,20 @@ func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
))
}
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment) {
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool, stripNewlines bool) (string, []attachment.Attachment) {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
confluenceExtension := NewConfluenceExtension(stdlib, path, cfg)
confluenceExtension := NewConfluenceExtension(stdlib, path, mermaidProvider, mermaidScale, dropFirstH1, stripNewlines)
converter := goldmark.New(
goldmark.WithExtensions(
extension.GFM,
extension.Footnote,
extension.DefinitionList,
extension.NewTable(
extension.WithTableCellAlignMethod(extension.TableCellAlignStyle),
),
confluenceExtension,
extension.GFM,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
@ -86,10 +92,8 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types
html.WithXHTML(),
))
ctx := parser.NewContext(parser.WithIDs(&cparser.ConfluenceIDs{Values: map[string]bool{}}))
var buf bytes.Buffer
err := converter.Convert(markdown, &buf, parser.WithContext(ctx))
err := converter.Convert(markdown, &buf)
if err != nil {
panic(err)
@ -100,4 +104,16 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
return string(html), confluenceExtension.Attachments
}
// ExtractDocumentLeadingH1 will extract leading H1 heading
func ExtractDocumentLeadingH1(markdown []byte) string {
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
groups := h1.FindSubmatch(markdown)
if groups == nil {
return ""
} else {
return string(groups[1])
}
}

99
pkg/mark/markdown_test.go Normal file
View File

@ -0,0 +1,99 @@
package mark
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/stretchr/testify/assert"
)
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
t.Helper()
basename := filepath.Base(filename)
testname := strings.TrimSuffix(basename, ".md")
htmlname := filepath.Join(filepath.Dir(filename), testname+variant+".html")
markdown, err := os.ReadFile(filename)
if err != nil {
panic(err)
}
html, err := os.ReadFile(htmlname)
if err != nil {
panic(err)
}
return markdown, htmlname, html
}
func TestCompileMarkdown(t *testing.T) {
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
markdown, htmlname, html := loadData(t, filename, "")
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, false)
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
}
}
func TestCompileMarkdownDropH1(t *testing.T) {
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
markdown, htmlname, html := loadData(t, filename, "-droph1")
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, true, false)
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
}
}
func TestCompileMarkdownStripNewlines(t *testing.T) {
test := assert.New(t)
testcases, err := filepath.Glob("testdata/*.md")
if err != nil {
panic(err)
}
for _, filename := range testcases {
lib, err := stdlib.New(nil)
if err != nil {
panic(err)
}
markdown, htmlname, html := loadData(t, filename, "-stripnewlines")
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, true)
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
}
}
func TestExtractDocumentLeadingH1(t *testing.T) {
filename := "testdata/header.md"
markdown, err := os.ReadFile(filename)
if err != nil {
panic(err)
}
actual := ExtractDocumentLeadingH1(markdown)
assert.Equal(t, "a", actual)
}

View File

@ -7,11 +7,11 @@ import (
"time"
mermaid "github.com/dreampuf/mermaid.go"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/reconquest/pkg/log"
)
var renderTimeout = 120 * time.Second
var renderTimeout = 90 * time.Second
func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachment.Attachment, error) {
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/stretchr/testify/assert"
)
@ -24,8 +24,8 @@ func TestExtractMermaidImage(t *testing.T) {
Replace: "example",
Checksum: "1743a4f31ab66244591f06c8056e08053b8e0a554eb9a38709af6e9d145ac84f",
ID: "",
Width: "87",
Height: "174",
Width: "42",
Height: "132",
},
assert.NoError},
}

View File

@ -1,10 +1,8 @@
package metadata
package mark
import (
"bufio"
"bytes"
"crypto/sha256"
"fmt"
"regexp"
"strings"
@ -17,7 +15,6 @@ const (
HeaderType = `Type`
HeaderTitle = `Title`
HeaderLayout = `Layout`
HeaderEmoji = `Emoji`
HeaderAttachment = `Attachment`
HeaderLabel = `Label`
HeaderInclude = `Include`
@ -32,7 +29,6 @@ type Meta struct {
Title string
Layout string
Sidebar string
Emoji string
Attachments []string
Labels []string
ContentAppearance string
@ -48,7 +44,7 @@ var (
reHeaderPatternMacro = regexp.MustCompile(`<!-- Macro: .*`)
)
func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []string, titleAppendGeneratedHash bool) (*Meta, []byte, error) {
func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []string) (*Meta, []byte, error) {
var (
meta *Meta
offset int
@ -109,9 +105,6 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
meta.Layout = "article"
meta.Sidebar = strings.TrimSpace(value)
case HeaderEmoji:
meta.Emoji = strings.TrimSpace(value)
case HeaderAttachment:
meta.Attachments = append(meta.Attachments, value)
@ -171,29 +164,5 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
meta.Parents = append(parents, meta.Parents...)
}
// deterministically generate a hash from the page's parents, space, and title
if titleAppendGeneratedHash {
path := strings.Join(append(meta.Parents, meta.Space, meta.Title), "/")
pathHash := sha256.Sum256([]byte(path))
// postfix is an 8-character hexadecimal string representation of the first 4 out of 32 bytes of the hash
meta.Title = fmt.Sprintf("%s - %x", meta.Title, pathHash[0:4])
log.Debugf(
nil,
"appended hash to page title: %s",
meta.Title,
)
}
return meta, data[offset:], nil
}
// ExtractDocumentLeadingH1 will extract leading H1 heading
func ExtractDocumentLeadingH1(markdown []byte) string {
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
groups := h1.FindSubmatch(markdown)
if groups == nil {
return ""
} else {
return string(groups[1])
}
}

View File

@ -35,12 +35,11 @@ const (
Info BlockQuoteType = iota
Note
Warn
Tip
None
)
func (t BlockQuoteType) String() string {
return []string{"info", "note", "warning", "tip", "none"}[t]
return []string{"info", "note", "warn", "none"}[t]
}
type BlockQuoteLevelMap map[ast.Node]int
@ -49,45 +48,20 @@ func (m BlockQuoteLevelMap) Level(node ast.Node) int {
return m[node]
}
type BlockQuoteClassifier struct {
patternMap map[string]*regexp.Regexp
}
func LegacyBlockQuoteClassifier() BlockQuoteClassifier {
return BlockQuoteClassifier{
patternMap: map[string]*regexp.Regexp{
"info": regexp.MustCompile(`(?i)info`),
"note": regexp.MustCompile(`(?i)note`),
"warn": regexp.MustCompile(`(?i)warn`),
"tip": regexp.MustCompile(`(?i)tip`),
},
}
}
func GHAlertsBlockQuoteClassifier() BlockQuoteClassifier {
return BlockQuoteClassifier{
patternMap: map[string]*regexp.Regexp{
"info": regexp.MustCompile(`(?i)^\!(note|important)`),
"note": regexp.MustCompile(`(?i)^\!warning`),
"warn": regexp.MustCompile(`(?i)^\!caution`),
"tip": regexp.MustCompile(`(?i)^\!tip`),
},
}
}
// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) BlockQuoteType {
func ClassifyingBlockQuote(literal string) BlockQuoteType {
infoPattern := regexp.MustCompile(`info|Info|INFO`)
notePattern := regexp.MustCompile(`note|Note|NOTE`)
warnPattern := regexp.MustCompile(`warn|Warn|WARN`)
var t = None
switch {
case classifier.patternMap["info"].MatchString(literal):
case infoPattern.MatchString(literal):
t = Info
case classifier.patternMap["note"].MatchString(literal):
case notePattern.MatchString(literal):
t = Note
case classifier.patternMap["warn"].MatchString(literal):
case warnPattern.MatchString(literal):
t = Warn
case classifier.patternMap["tip"].MatchString(literal):
t = Tip
}
return t
}
@ -95,8 +69,6 @@ func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) Blo
// ParseBlockQuoteType parses the first line of a blockquote and returns its type
func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
var t = None
var legacyClassifier = LegacyBlockQuoteClassifier()
var ghAlertsClassifier = GHAlertsBlockQuoteClassifier()
countParagraphs := 0
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
@ -108,28 +80,7 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
if countParagraphs < 2 && entering {
if node.Kind() == ast.KindText {
n := node.(*ast.Text)
t = legacyClassifier.ClassifyingBlockQuote(string(n.Value(source)))
// If the node is a text node but classification returned none do not give up!
// Find the next two sibling nodes midNode and rightNode,
// 1. If both are also a text node
// 2. and the original node (node) text value is '['
// 3. and the rightNode text value is ']'
// It means with high degree of confidence that the original md doc contains a Github alert type of blockquote
// Classifying the next text type node (midNode) will confirm that.
if t == None {
midNode := node.NextSibling()
if midNode != nil && midNode.Kind() == ast.KindText {
rightNode := midNode.NextSibling()
midTextNode := midNode.(*ast.Text)
if rightNode != nil && rightNode.Kind() == ast.KindText {
rightTextNode := rightNode.(*ast.Text)
if string(n.Value(source)) == "[" && string(rightTextNode.Value(source)) == "]" {
t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Value(source)))
}
}
}
}
t = ClassifyingBlockQuote(string(n.Text(source)))
countParagraphs += 1
}
if node.Kind() == ast.KindHTMLBlock {
@ -137,7 +88,7 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
n := node.(*ast.HTMLBlock)
for i := 0; i < n.BaseBlock.Lines().Len(); i++ {
line := n.BaseBlock.Lines().At(i)
t = legacyClassifier.ClassifyingBlockQuote(string(line.Value(source)))
t = ClassifyingBlockQuote(string(line.Value(source)))
if t != None {
break
}

View File

@ -3,7 +3,7 @@ package renderer
import (
"strings"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"

View File

@ -3,14 +3,11 @@ package renderer
import (
"fmt"
"regexp"
"slices"
"strings"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/d2"
"github.com/kovetskiy/mark/mermaid"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/mermaid"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log"
"github.com/yuin/goldmark/ast"
@ -21,9 +18,10 @@ import (
type ConfluenceFencedCodeBlockRenderer struct {
html.Config
Stdlib *stdlib.Lib
MarkConfig types.MarkConfig
Attachments attachment.Attacher
Stdlib *stdlib.Lib
MermaidProvider string
MermaidScale float64
Attachments attachment.Attacher
}
var reBlockDetails = regexp.MustCompile(
@ -33,12 +31,13 @@ var reBlockDetails = regexp.MustCompile(
)
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, cfg types.MarkConfig, opts ...html.Option) renderer.NodeRenderer {
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, mermaidProvider string, mermaidScale float64, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceFencedCodeBlockRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
MarkConfig: cfg,
Attachments: attachments,
Config: html.NewConfig(),
Stdlib: stdlib,
MermaidProvider: mermaidProvider,
MermaidScale: mermaidScale,
Attachments: attachments,
}
}
@ -127,39 +126,8 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
lval = append(lval, line.Value(source)...)
}
if lang == "d2" && slices.Contains(r.MarkConfig.Features, "d2") {
attachment, err := d2.ProcessD2(title, lval, r.MarkConfig.D2Scale)
if err != nil {
log.Debugf(nil, "error: %v", err)
return ast.WalkStop, err
}
r.Attachments.Attach(attachment)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
attachment.Width,
attachment.Height,
attachment.Name,
"",
attachment.Filename,
"",
},
)
if err != nil {
return ast.WalkStop, err
}
} else if lang == "mermaid" && slices.Contains(r.MarkConfig.Features, "mermaid") && r.MarkConfig.MermaidProvider == "mermaid-go" {
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MarkConfig.MermaidScale)
if lang == "mermaid" && r.MermaidProvider == "mermaid-go" {
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MermaidScale)
if err != nil {
log.Debugf(nil, "error: %v", err)
return ast.WalkStop, err

View File

@ -3,7 +3,7 @@ package renderer
import (
"strings"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"

View File

@ -5,9 +5,9 @@ import (
"path/filepath"
"strings"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/vfs"
"github.com/kovetskiy/mark/pkg/mark/attachment"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/kovetskiy/mark/pkg/mark/vfs"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
@ -107,9 +107,9 @@ func nodeToHTMLText(n ast.Node, source []byte) []byte {
var buf bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok && s.IsCode() {
buf.Write(s.Value)
} else if t, ok := c.(*ast.Text); ok {
buf.Write(util.EscapeHTML(t.Value(source)))
buf.Write(s.Text(source))
} else if !c.HasChildren() {
buf.Write(util.EscapeHTML(c.Text(source)))
} else {
buf.Write(nodeToHTMLText(c, source))
}

View File

@ -33,8 +33,7 @@ func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte
return ast.WalkStop, err
}
if len(string(n.Destination)) < 4 {
//nolint:staticcheck
if len(n.Destination) < 4 {
_, err := writer.Write(node.Text(source))
if err != nil {
return ast.WalkStop, err
@ -51,7 +50,6 @@ func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte
return ast.WalkStop, err
}
//nolint:staticcheck
_, err = writer.Write(node.Text(source))
if err != nil {
return ast.WalkStop, err

View File

@ -63,7 +63,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
sibling := node.NextSibling()
if sibling != nil && sibling.Kind() == ast.KindText {
if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
if siblingText := sibling.(*ast.Text).Text(source); len(siblingText) != 0 {
thisLastRune := util.ToRune(value, len(value)-1)
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
// Inline the softLineBreak function as it's not public
@ -72,7 +72,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
case html.EastAsianLineBreaksNone:
writeLineBreak = false
case html.EastAsianLineBreaksSimple:
writeLineBreak = !util.IsEastAsianWideRune(thisLastRune) || !util.IsEastAsianWideRune(siblingFirstRune)
writeLineBreak = !(util.IsEastAsianWideRune(thisLastRune) && util.IsEastAsianWideRune(siblingFirstRune))
case html.EastAsianLineBreaksCSS3Draft:
writeLineBreak = eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
}

View File

@ -4,8 +4,8 @@ import (
"strings"
"text/template"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/macro"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/reconquest/pkg/log"
"github.com/reconquest/karma-go"
@ -296,22 +296,18 @@ func templates(api *confluence.API) (*template.Template, error) {
),
/* https://confluence.atlassian.com/conf59/excerpt-include-macro-792499101.html */
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-include-macro/ */
`ac:excerpt-include`: text(
`<ac:macro ac:name="excerpt-include">`,
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
`<ac:parameter ac:name="nopanel">{{ if .NoPanel }}{{ .NoPanel }}{{ else }}false{{ end }}</ac:parameter>`,
`<ac:default-parameter>{{ .Page }}</ac:default-parameter>`,
`</ac:macro>`,
),
/* https://confluence.atlassian.com/conf59/excerpt-macro-792499102.html */
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-macro/ */
`ac:excerpt`: text(
`<ac:structured-macro ac:name="excerpt">`,
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
`<ac:parameter ac:name="hidden">{{ if .Hidden }}{{ .Hidden }}{{ else }}false{{ end }}</ac:parameter>`,
`<ac:parameter ac:name="atlassian-macro-output-type">{{ if .OutputType }}{{ .OutputType }}{{ else }}BLOCK{{ end }}</ac:parameter>`,
`<ac:rich-text-body>`,
@ -386,7 +382,7 @@ func templates(api *confluence.API) (*template.Template, error) {
`<ac:structured-macro ac:name="pagetree" ac:schema-version="1">`,
`<ac:parameter ac:name="root">`,
`<ac:link>`,
`<ri:page ri:content-title="{{ or .Title "@self" }}"/>`,
`<ri:page ri:content-title="@self"{{ or .Title "" }}/>`,
`</ac:link>`,
`</ac:parameter>`,
`<ac:parameter ac:name="sort">{{ or .Sort "" }}</ac:parameter>`,
@ -444,9 +440,9 @@ func templates(api *confluence.API) (*template.Template, error) {
`<ac:structured-macro ac:name="multimedia">`,
`<ac:parameter ac:name="width">{{ or .Width 500 }}</ac:parameter>`,
`<ac:parameter ac:name="name">`,
`<ri:attachment ri:filename="{{ .Name | convertAttachment }}"/>`,
`<ri:attachment ri:filename="{{ .Name }}/>`,
`</ac:parameter>`,
`<ac:parameter ac:name="autoplay">{{ or .AutoPlay "false"}}</ac:parameter>`,
`<ac:parameter ac:name="autoplay">{{ .AutoPlay "false"}}</ac:parameter>`,
`</ac:structured-macro>`,
),

20
pkg/mark/testdata/codes-droph1.html vendored Normal file
View File

@ -0,0 +1,20 @@
<p><code>inline</code></p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[some code]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[code bash]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[with a newline
]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">unknown</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body></ac:structured-macro><p>text
text 2</p>
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">unknown</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[unknown code 2]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">sh</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:parameter ac:name="title">A b c</ac:parameter><ac:plain-text-body><![CDATA[no-collapse-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:parameter ac:name="title">A b c</ac:parameter><ac:plain-text-body><![CDATA[collapse-and-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">c</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:plain-text-body><![CDATA[collapse-no-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">nested</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[code
``` more code ```
even more code]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[indented code block
with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
A-->B;
A-->C;
B-->D;
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:parameter ac:name="title">my mermaid graph</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
A-->B;
A-->C;
B-->D;
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:parameter ac:name="title">my mermaid graph</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
A-->B;
A-->C;
B-->D;
C-->D;]]></ac:plain-text-body></ac:structured-macro>

View File

@ -16,53 +16,4 @@ with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-
A-->B;
A-->C;
B-->D;
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">d2</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: {shape: hexagon}
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: {shape: page; style.multiple: true}
network.data processor -> api server]]></ac:plain-text-body></ac:structured-macro>
C-->D;]]></ac:plain-text-body></ac:structured-macro>

View File

@ -17,53 +17,4 @@ with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-
A-->B;
A-->C;
B-->D;
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">d2</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: {shape: hexagon}
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: {shape: page; style.multiple: true}
network.data processor -> api server]]></ac:plain-text-body></ac:structured-macro>
C-->D;]]></ac:plain-text-body></ac:structured-macro>

67
pkg/mark/testdata/codes.md vendored Normal file
View File

@ -0,0 +1,67 @@
`inline`
```
some code
```
```bash
code bash
```
```bash
with a newline
```
```unknown
unknown code
```
text
text 2
```unknown
unknown code 2
```
```sh title A b c
no-collapse-title
```
```bash collapse title A b c
collapse-and-title
```
```c collapse
collapse-no-title
```
```nested
code
``` more code ```
even more code
```
indented code block
with multiple lines
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```mermaid collapse title my mermaid graph
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```mermaid title my mermaid graph
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```

View File

@ -4,4 +4,3 @@
<h5 id="e">e</h5>
<h1 id="f">f</h1>
<h2 id="g">g</h2>
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>

View File

@ -0,0 +1,7 @@
<h1 id="a">a</h1>
<h2 id="b">b</h2>
<h3 id="c">c</h3>
<h4 id="d">d</h4>
<h5 id="e">e</h5>
<h1 id="f">f</h1>
<h2 id="g">g</h2>

View File

@ -5,4 +5,3 @@
<h5 id="e">e</h5>
<h1 id="f">f</h1>
<h2 id="g">g</h2>
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>

View File

@ -8,5 +8,3 @@ f
=
g
-
# This/is some_Heading.yml

View File

@ -0,0 +1 @@
<p><a href="Page2#Page2-Releasev71-22-Feb-2018(intern)">v71</a></p>

View File

@ -0,0 +1 @@
<p><a href="Page2#Page2-Releasev71-22-Feb-2018(intern)">v71</a></p>

View File

@ -0,0 +1,21 @@
<p>Use <a href="https://example.com">https://example.com</a></p>
<p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
<p>Use <ac:link><ri:page ri:content-title="Page"/><ac:plain-text-link-body><![CDATA[page link]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="AnotherPage"/><ac:plain-text-link-body><![CDATA[AnotherPage]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
<p><ac:image ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&amp;key2=value2"/></ac:image></p>
<p><ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[My test_link]]></ac:plain-text-link-body></ac:link></p>
<p><ac:link><ri:page ri:content-title="test_link_link"/><ac:plain-text-link-body><![CDATA[Another [Link]]]></ac:plain-text-link-body></ac:link></p>
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Use <a href="foo">Link [Text]</a></p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:1">
<p>a footnote link&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

21
pkg/mark/testdata/links.html vendored Normal file
View File

@ -0,0 +1,21 @@
<p>Use <a href="https://example.com">https://example.com</a></p>
<p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
<p>Use <ac:link><ri:page ri:content-title="Page"/><ac:plain-text-link-body><![CDATA[page link]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="AnotherPage"/><ac:plain-text-link-body><![CDATA[AnotherPage]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
<p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
<p><ac:image ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&amp;key2=value2"/></ac:image></p>
<p><ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[My test_link]]></ac:plain-text-link-body></ac:link></p>
<p><ac:link><ri:page ri:content-title="test_link_link"/><ac:plain-text-link-body><![CDATA[Another [Link]]]></ac:plain-text-link-body></ac:link></p>
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>Use <a href="foo">Link [Text]</a></p>
<div class="footnotes" role="doc-endnotes">
<hr />
<ol>
<li id="fn:1">
<p>a footnote link&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>

View File

@ -0,0 +1,21 @@
<ul>
<li>dash 1-1</li>
<li>dash 1-2</li>
<li>dash 1-3
<ul>
<li>dash 1-3-1</li>
<li>dash 1-3-2</li>
<li>dash 1-3-3
<ul>
<li>dash 1-3-3-1</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>text</p>
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>

21
pkg/mark/testdata/lists.html vendored Normal file
View File

@ -0,0 +1,21 @@
<ul>
<li>dash 1-1</li>
<li>dash 1-2</li>
<li>dash 1-3
<ul>
<li>dash 1-3-1</li>
<li>dash 1-3-2</li>
<li>dash 1-3-3
<ul>
<li>dash 1-3-3-1</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>text</p>
<ul>
<li>a</li>
<li>b</li>
<li>c</li>
</ul>

26
pkg/mark/testdata/macro-include.html vendored Normal file
View File

@ -0,0 +1,26 @@
<foo>bar</foo>
<ac:structured-macro ac:name="info">
<ac:parameter ac:name="icon">true</ac:parameter>
<ac:parameter ac:name="title">Attention</ac:parameter>
<ac:rich-text-body>This is an info!</ac:rich-text-body>
</ac:structured-macro>
<ac:structured-macro ac:name="info">
<ac:parameter ac:name="icon">true</ac:parameter>
<ac:parameter ac:name="title">Attention</ac:parameter>
<ac:rich-text-body>
<table>
<thead>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>Cell A</td>
<td>Cell B</td>
</tr>
</tbody>
</table>
</ac:rich-text-body>
</ac:structured-macro>

10
pkg/mark/testdata/newlines.html vendored Normal file
View File

@ -0,0 +1,10 @@
<p>one-1
one-2</p>
<p>two-1</p>
<p>two-2</p>
<p>three-1</p>
<p>three-2</p>
<p>space-1
space-2</p>
<p>2space-1<br />
2space-2</p>

View File

@ -0,0 +1,18 @@
<ac:layout>
<ac:layout-section ac:type="three_with_sidebars">
<ac:layout-cell>
<p>More Content</p>
</ac:layout-cell>
<ac:layout-cell>
<p>More Content</p>
</ac:layout-cell>
<ac:layout-cell>
<p>Even More Content</p>
</ac:layout-cell>
</ac:layout-section>
<ac:layout-section ac:type="single">
<ac:layout-cell>
<p>Still More Content</p>
</ac:layout-cell>
</ac:layout-section>
</ac:layout>

18
pkg/mark/testdata/pagelayout.html vendored Normal file
View File

@ -0,0 +1,18 @@
<ac:layout>
<ac:layout-section ac:type="three_with_sidebars">
<ac:layout-cell>
<p>More Content</p>
</ac:layout-cell>
<ac:layout-cell>
<p>More Content</p>
</ac:layout-cell>
<ac:layout-cell>
<p>Even More Content</p>
</ac:layout-cell>
</ac:layout-section>
<ac:layout-section ac:type="single">
<ac:layout-cell>
<p>Still More Content</p>
</ac:layout-cell>
</ac:layout-section>
</ac:layout>

34
pkg/mark/testdata/quotes-droph1.html vendored Normal file
View File

@ -0,0 +1,34 @@
<h2 id="first-heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="second-heading">Second Heading</h2>
<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list
that runs long</li>
</ul>
<h2 id="third-heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="simple-blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>

View File

@ -0,0 +1,33 @@
<h1 id="main-heading">Main Heading</h1>
<h2 id="first-heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="second-heading">Second Heading</h2>
<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list that runs long</li>
</ul>
<h2 id="third-heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="simple-blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>

35
pkg/mark/testdata/quotes.html vendored Normal file
View File

@ -0,0 +1,35 @@
<h1 id="main-heading">Main Heading</h1>
<h2 id="first-heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="second-heading">Second Heading</h2>
<ac:structured-macro ac:name="warn"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list
that runs long</li>
</ul>
<h2 id="third-heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="simple-blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>

31
pkg/mark/testdata/quotes.md vendored Normal file
View File

@ -0,0 +1,31 @@
# Main Heading
## First Heading
> **NOTES:**
>
> 1. Note number one
> 1. Note number two
>
>> a
>> b
>
> **Warn (Should not be picked as blockquote type)**
## Second Heading
> **Warn**
>
> * Warn bullet 1
> * Warn bullet 2
* Regular list
that runs long
## Third Heading
> <!-- Info -->
> Test
## Simple Blockquote
> This paragraph is a simple blockquote

View File

@ -0,0 +1,28 @@
<table>
<thead>
<tr>
<th>HEADER1</th>
<th>HEADER2</th>
</tr>
</thead>
<tbody>
<tr>
<td>row1</td>
<td>row2</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th style="text-align:center">HEADER1</th>
<th style="text-align:right">HEADER2</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">row1</td>
<td style="text-align:right">row2</td>
</tr>
</tbody>
</table>

28
pkg/mark/testdata/table.html vendored Normal file
View File

@ -0,0 +1,28 @@
<table>
<thead>
<tr>
<th>HEADER1</th>
<th>HEADER2</th>
</tr>
</thead>
<tbody>
<tr>
<td>row1</td>
<td>row2</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr>
<th style="text-align:center">HEADER1</th>
<th style="text-align:right">HEADER2</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:center">row1</td>
<td style="text-align:right">row2</td>
</tr>
</tbody>
</table>

View File

@ -0,0 +1,6 @@
<b>bold</b>
<p><strong>bold</strong></p>
<i>vitalik</i>
<p><em>vitalik</em></p>
<s>strikethrough</s>
<p><del>strikethrough</del></p>

6
pkg/mark/testdata/tags.html vendored Normal file
View File

@ -0,0 +1,6 @@
<b>bold</b>
<p><strong>bold</strong></p>
<i>vitalik</i>
<p><em>vitalik</em></p>
<s>strikethrough</s>
<p><del>strikethrough</del></p>

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -1,6 +0,0 @@
## Foo
> **TL;DR:** Thingy!
> More stuff
Foo

View File

@ -1,6 +0,0 @@
## Foo
> **TL;DR:** Thingy!
> More stuff
Foo

View File

@ -1,10 +0,0 @@
<!-- Space: BatchTests -->
<!-- Title: Hello World -->
<!-- Title: Good Test -->
## Foo
> **TL;DR:** Thingy!
> More stuff
Foo

View File

@ -1,15 +0,0 @@
# a
## b
### c
#### d
##### e
# f
## g
# This/is some_Heading.yml

View File

@ -1,19 +0,0 @@
<!-- Space: BatchTests -->
<!-- Title: Hello World -->
<!-- Title: Working Test -->
# a
## b
### c
#### d
##### e
# f
## g
# This/is some_Heading.yml

120
testdata/codes.md vendored
View File

@ -1,120 +0,0 @@
`inline`
```
some code
```
```bash
code bash
```
```bash
with a newline
```
```unknown
unknown code
```
text
text 2
```unknown
unknown code 2
```
```sh title A b c
no-collapse-title
```
```bash collapse title A b c
collapse-and-title
```
```c collapse
collapse-no-title
```
```nested
code
``` more code ```
even more code
```
indented code block
with multiple lines
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```mermaid collapse title my mermaid graph
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```mermaid title my mermaid graph
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
```d2
vars: {
d2-config: {
layout-engine: elk
# Terminal theme code
theme-id: 300
}
}
network: {
cell tower: {
satellites: {
shape: stored_data
style.multiple: true
}
transmitter
satellites -> transmitter: send
satellites -> transmitter: send
satellites -> transmitter: send
}
online portal: {
ui: {shape: hexagon}
}
data processor: {
storage: {
shape: cylinder
style.multiple: true
}
}
cell tower.transmitter -> data processor.storage: phone logs
}
user: {
shape: person
width: 130
}
user -> network.cell tower: make call
user -> network.online portal.ui: access {
style.stroke-dash: 3
}
api server -> network.online portal.ui: display
api server -> logs: persist
logs: {shape: page; style.multiple: true}
network.data processor -> api server
```

View File

@ -1,104 +0,0 @@
<h2 id="First-Heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Second-Heading">Second Heading</h2>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list
that runs long</li>
</ul>
<h2 id="Third-Heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Fourth-Heading---Warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>TIP:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!NOTE]</p>
<ul>
<li>Note bullet 1</li>
<li>Note bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Tip-Type-Alert-Heading">Tip Type Alert Heading</h3>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!TIP]</p>
<ul>
<li>Tip bullet 1</li>
<li>Tip bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!WARNING]</p>
<ul>
<li>Warning bullet 1</li>
<li>Warning bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Important/Caution-Type-Alert-Heading">Important/Caution Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!IMPORTANT]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!CAUTION]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
<blockquote>
<p>[[!NOTE]</p>
</blockquote>
<blockquote>
<p>[!NOTE</p>
</blockquote>
<blockquote>
<p>[Hey !NOTE]</p>
</blockquote>
<blockquote>
<p>[NOTE]</p>
</blockquote>
<blockquote>
<p><strong>TL;DR:</strong> Thingy!
More stuff</p>
</blockquote>

View File

@ -1,101 +0,0 @@
<h1 id="Main-Heading">Main Heading</h1>
<h2 id="First-Heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Second-Heading">Second Heading</h2>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list that runs long</li>
</ul>
<h2 id="Third-Heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Fourth-Heading---Warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>TIP:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!NOTE]</p>
<ul>
<li>Note bullet 1</li>
<li>Note bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Tip-Type-Alert-Heading">Tip Type Alert Heading</h3>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!TIP]</p>
<ul>
<li>Tip bullet 1</li>
<li>Tip bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!WARNING]</p>
<ul>
<li>Warning bullet 1</li>
<li>Warning bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Important/Caution-Type-Alert-Heading">Important/Caution Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!IMPORTANT]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!CAUTION]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
<blockquote>
<p>[[!NOTE]</p>
</blockquote>
<blockquote>
<p>[!NOTE</p>
</blockquote>
<blockquote>
<p>[Hey !NOTE]</p>
</blockquote>
<blockquote>
<p>[NOTE]</p>
</blockquote>
<blockquote>
<p><strong>TL;DR:</strong> Thingy! More stuff</p>
</blockquote>

105
testdata/quotes.html vendored
View File

@ -1,105 +0,0 @@
<h1 id="Main-Heading">Main Heading</h1>
<h2 id="First-Heading">First Heading</h2>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>NOTES:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Second-Heading">Second Heading</h2>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>Warn</strong></p>
<ul>
<li>Warn bullet 1</li>
<li>Warn bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ul>
<li>Regular list
that runs long</li>
</ul>
<h2 id="Third-Heading">Third Heading</h2>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<!-- Info -->
<p>Test</p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Fourth-Heading---Warn-should-not-get-picked-as-block-quote">Fourth Heading - Warn should not get picked as block quote</h2>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p><strong>TIP:</strong></p>
<ol>
<li>Note number one</li>
<li>Note number two</li>
</ol>
<blockquote>
<p>a
b</p>
</blockquote>
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
</ac:rich-text-body></ac:structured-macro>
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
<blockquote>
<p>This paragraph is a simple blockquote</p>
</blockquote>
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!NOTE]</p>
<ul>
<li>Note bullet 1</li>
<li>Note bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Tip-Type-Alert-Heading">Tip Type Alert Heading</h3>
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!TIP]</p>
<ul>
<li>Tip bullet 1</li>
<li>Tip bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!WARNING]</p>
<ul>
<li>Warning bullet 1</li>
<li>Warning bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Important/Caution-Type-Alert-Heading">Important/Caution Type Alert Heading</h3>
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!IMPORTANT]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
<p>[!CAUTION]</p>
<ul>
<li>Important bullet 1</li>
<li>Important bullet 2</li>
</ul>
</ac:rich-text-body></ac:structured-macro>
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
<blockquote>
<p>[[!NOTE]</p>
</blockquote>
<blockquote>
<p>[!NOTE</p>
</blockquote>
<blockquote>
<p>[Hey !NOTE]</p>
</blockquote>
<blockquote>
<p>[NOTE]</p>
</blockquote>
<blockquote>
<p><strong>TL;DR:</strong> Thingy!
More stuff</p>
</blockquote>

95
testdata/quotes.md vendored
View File

@ -1,95 +0,0 @@
# Main Heading
## First Heading
> **NOTES:**
>
> 1. Note number one
> 1. Note number two
>
>> a
>> b
>
> **Warn (Should not be picked as blockquote type)**
## Second Heading
> **Warn**
>
> * Warn bullet 1
> * Warn bullet 2
* Regular list
that runs long
## Third Heading
> <!-- Info -->
> Test
## Fourth Heading - Warn should not get picked as block quote
> **TIP:**
>
> 1. Note number one
> 1. Note number two
>
>> a
>> b
>
> **Warn (Should not be picked as blockquote type)**
## Simple Blockquote
> This paragraph is a simple blockquote
## GH Alerts Heading
### Note Type Alert Heading
> [!NOTE]
>
> * Note bullet 1
> * Note bullet 2
### Tip Type Alert Heading
> [!TIP]
>
> * Tip bullet 1
> * Tip bullet 2
### Warning Type Alert Heading
> [!WARNING]
>
> * Warning bullet 1
> * Warning bullet 2
### Important/Caution Type Alert Heading
> [!IMPORTANT]
>
> * Important bullet 1
> * Important bullet 2
> [!CAUTION]
>
> * Important bullet 1
> * Important bullet 2
### Should not be picked up and converted into blockquote macro
> [[!NOTE]
> [!NOTE
> [Hey !NOTE]
> [NOTE]
> **TL;DR:** Thingy!
> More stuff

View File

@ -1,10 +0,0 @@
package types
type MarkConfig struct {
MermaidProvider string
MermaidScale float64
D2Scale float64
DropFirstH1 bool
StripNewlines bool
Features []string
}

View File

@ -1,514 +0,0 @@
package util
import (
"bytes"
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"time"
"github.com/bmatcuk/doublestar/v4"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/includes"
"github.com/kovetskiy/mark/macro"
mark "github.com/kovetskiy/mark/markdown"
"github.com/kovetskiy/mark/metadata"
"github.com/kovetskiy/mark/page"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/kovetskiy/mark/vfs"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"github.com/urfave/cli/v3"
)
func RunMark(ctx context.Context, cmd *cli.Command) error {
if err := SetLogLevel(cmd); err != nil {
return err
}
if cmd.String("color") == "never" {
log.GetLogger().SetFormat(
lorg.NewFormat(
`${time:2006-01-02 15:04:05.000} ${level:%s:left:true} ${prefix}%s`,
),
)
log.GetLogger().SetOutput(os.Stderr)
}
creds, err := GetCredentials(cmd.String("username"), cmd.String("password"), cmd.String("target-url"), cmd.String("base-url"), cmd.Bool("compile-only"))
if err != nil {
return err
}
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
files, err := doublestar.FilepathGlob(cmd.String("files"))
if err != nil {
return err
}
if len(files) == 0 {
msg := "No files matched"
if cmd.Bool("ci") {
log.Warning(msg)
} else {
log.Fatal(msg)
}
}
log.Debug("config:")
for _, f := range cmd.Flags {
flag := f.Names()
if flag[0] == "password" {
log.Debugf(nil, "%20s: %v", flag[0], "******")
} else {
log.Debugf(nil, "%20s: %v", flag[0], cmd.Value(flag[0]))
}
}
fatalErrorHandler := NewErrorHandler(cmd.Bool("continue-on-error"))
// Loop through files matched by glob pattern
for _, file := range files {
log.Infof(
nil,
"processing %s",
file,
)
target := processFile(file, api, cmd, creds.PageID, creds.Username, fatalErrorHandler)
if target != nil { // on dry-run or compile-only, the target is nil
log.Infof(
nil,
"page successfully updated: %s",
creds.BaseURL+target.Links.Full,
)
fmt.Println(creds.BaseURL + target.Links.Full)
}
}
return nil
}
func processFile(
file string,
api *confluence.API,
cmd *cli.Command,
pageID string,
username string,
fatalErrorHandler *FatalErrorHandler,
) *confluence.PageInfo {
markdown, err := os.ReadFile(file)
if err != nil {
fatalErrorHandler.Handle(err, "unable to read file %q", file)
return nil
}
markdown = bytes.ReplaceAll(markdown, []byte("\r\n"), []byte("\n"))
parents := strings.Split(cmd.String("parents"), cmd.String("parents-delimiter"))
meta, markdown, err := metadata.ExtractMeta(markdown, cmd.String("space"), cmd.Bool("title-from-h1"), parents, cmd.Bool("title-append-generated-hash"))
if err != nil {
fatalErrorHandler.Handle(err, "unable to extract metadata from file %q", file)
return nil
}
if pageID != "" && meta != nil {
log.Warning(
`specified file contains metadata, ` +
`but it will be ignored due specified command line URL`,
)
meta = nil
}
if pageID == "" && meta == nil {
fatalErrorHandler.Handle(nil, "specified file doesn't contain metadata and URL is not specified via command line or doesn't contain pageId GET-parameter")
return nil
}
if meta != nil {
if meta.Space == "" {
fatalErrorHandler.Handle(nil, "space is not set ('Space' header is not set and '--space' option is not set)")
return nil
}
if meta.Title == "" {
fatalErrorHandler.Handle(nil, "page title is not set ('Title' header is not set and '--title-from-h1' option and 'h1-title' config is not set or there is no H1 in the file)")
return nil
}
}
stdlib, err := stdlib.New(api)
if err != nil {
fatalErrorHandler.Handle(err, "unable to retrieve standard library")
return nil
}
templates := stdlib.Templates
var recurse bool
for {
templates, markdown, recurse, err = includes.ProcessIncludes(
filepath.Dir(file),
cmd.String("include-path"),
markdown,
templates,
)
if err != nil {
fatalErrorHandler.Handle(err, "unable to process includes")
return nil
}
if !recurse {
break
}
}
macros, markdown, err := macro.ExtractMacros(
filepath.Dir(file),
cmd.String("include-path"),
markdown,
templates,
)
if err != nil {
fatalErrorHandler.Handle(err, "unable to extract macros")
return nil
}
macros = append(macros, stdlib.Macros...)
for _, macro := range macros {
markdown, err = macro.Apply(markdown)
if err != nil {
fatalErrorHandler.Handle(err, "unable to apply macro")
return nil
}
}
links, err := page.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cmd.String("space"), cmd.Bool("title-from-h1"), parents, cmd.Bool("title-append-generated-hash"))
if err != nil {
fatalErrorHandler.Handle(err, "unable to resolve relative links")
return nil
}
markdown = page.SubstituteLinks(markdown, links)
if cmd.Bool("dry-run") {
_, _, err := page.ResolvePage(cmd.Bool("dry-run"), api, meta)
if err != nil {
fatalErrorHandler.Handle(err, "unable to resolve page location")
return nil
}
}
if cmd.Bool("compile-only") || cmd.Bool("dry-run") {
if cmd.Bool("drop-h1") {
log.Info(
"the leading H1 heading will be excluded from the Confluence output",
)
}
cfg := types.MarkConfig{
MermaidProvider: cmd.String("mermaid-provider"),
MermaidScale: cmd.Float("mermaid-scale"),
DropFirstH1: cmd.Bool("drop-h1"),
StripNewlines: cmd.Bool("strip-linebreaks"),
Features: cmd.StringSlice("features"),
}
html, _ := mark.CompileMarkdown(markdown, stdlib, file, cfg)
fmt.Println(html)
return nil
}
var target *confluence.PageInfo
if meta != nil {
parent, page, err := page.ResolvePage(cmd.Bool("dry-run"), api, meta)
if err != nil {
fatalErrorHandler.Handle(karma.Describe("title", meta.Title).Reason(err), "unable to resolve %s", meta.Type)
return nil
}
if page == nil {
page, err = api.CreatePage(
meta.Space,
meta.Type,
parent,
meta.Title,
``,
)
if err != nil {
fatalErrorHandler.Handle(err, "can't create %s %q", meta.Type, meta.Title)
return nil
}
// (issues/139): A delay between the create and update call
// helps mitigate a 409 conflict that can occur when attempting
// to update a page just after it was created.
time.Sleep(1 * time.Second)
}
target = page
} else {
if pageID == "" {
fatalErrorHandler.Handle(nil, "URL should provide 'pageId' GET-parameter")
return nil
}
page, err := api.GetPageByID(pageID)
if err != nil {
fatalErrorHandler.Handle(err, "unable to retrieve page by id")
return nil
}
target = page
}
// Resolve attachments created from <!-- Attachment: --> directive
localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments)
if err != nil {
fatalErrorHandler.Handle(err, "unable to locate attachments")
return nil
}
attaches, err := attachment.ResolveAttachments(
api,
target,
localAttachments,
)
if err != nil {
fatalErrorHandler.Handle(err, "unable to create/update attachments")
return nil
}
markdown = attachment.CompileAttachmentLinks(markdown, attaches)
if cmd.Bool("drop-h1") {
log.Info(
"the leading H1 heading will be excluded from the Confluence output",
)
}
cfg := types.MarkConfig{
MermaidProvider: cmd.String("mermaid-provider"),
MermaidScale: cmd.Float("mermaid-scale"),
DropFirstH1: cmd.Bool("drop-h1"),
StripNewlines: cmd.Bool("strip-linebreaks"),
Features: cmd.StringSlice("features"),
}
html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cfg)
// Resolve attachements detected from markdown
_, err = attachment.ResolveAttachments(
api,
target,
inlineAttachments,
)
if err != nil {
fatalErrorHandler.Handle(err, "unable to create/update attachments")
return nil
}
{
var buffer bytes.Buffer
err := stdlib.Templates.ExecuteTemplate(
&buffer,
"ac:layout",
struct {
Layout string
Sidebar string
Body string
}{
Layout: meta.Layout,
Sidebar: meta.Sidebar,
Body: html,
},
)
if err != nil {
fatalErrorHandler.Handle(err, "unable to execute layout template")
return nil
}
html = buffer.String()
}
var finalVersionMessage string
var shouldUpdatePage = true
if cmd.Bool("changes-only") {
contentHash := getSHA1Hash(html)
log.Debugf(
nil,
"content hash: %s",
contentHash,
)
versionPattern := `\[v([a-f0-9]{40})]$`
re := regexp.MustCompile(versionPattern)
matches := re.FindStringSubmatch(target.Version.Message)
if len(matches) > 1 {
log.Debugf(
nil,
"previous content hash: %s",
matches[1],
)
if matches[1] == contentHash {
log.Infof(
nil,
"page %q is already up to date",
target.Title,
)
shouldUpdatePage = false
}
}
finalVersionMessage = fmt.Sprintf("%s [v%s]", cmd.String("version-message"), contentHash)
} else {
finalVersionMessage = cmd.String("version-message")
}
if shouldUpdatePage {
err = api.UpdatePage(target, html, cmd.Bool("minor-edit"), finalVersionMessage, meta.Labels, meta.ContentAppearance, meta.Emoji)
if err != nil {
fatalErrorHandler.Handle(err, "unable to update page")
return nil
}
}
if !updateLabels(api, target, meta, fatalErrorHandler) { // on error updating labels, return nil
return nil
}
if cmd.Bool("edit-lock") {
log.Infof(
nil,
`edit locked on page %q by user %q to prevent manual edits`,
target.Title,
username,
)
err := api.RestrictPageUpdates(target, username)
if err != nil {
fatalErrorHandler.Handle(err, "unable to restrict page updates")
return nil
}
}
return target
}
func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *metadata.Meta, fatalErrorHandler *FatalErrorHandler) bool {
labelInfo, err := api.GetPageLabels(target, "global")
if err != nil {
log.Fatal(err)
}
log.Debug("Page Labels:")
log.Debug(labelInfo.Labels)
log.Debug("Meta Labels:")
log.Debug(meta.Labels)
delLabels := determineLabelsToRemove(labelInfo, meta)
log.Debug("Del Labels:")
log.Debug(delLabels)
addLabels := determineLabelsToAdd(meta, labelInfo)
log.Debug("Add Labels:")
log.Debug(addLabels)
if len(addLabels) > 0 {
_, err = api.AddPageLabels(target, addLabels)
if err != nil {
fatalErrorHandler.Handle(err, "error adding labels")
return false
}
}
for _, label := range delLabels {
_, err = api.DeletePageLabel(target, label)
if err != nil {
fatalErrorHandler.Handle(err, "error deleting labels")
return false
}
}
return true
}
// Page has label but label not in Metadata
func determineLabelsToRemove(labelInfo *confluence.LabelInfo, meta *metadata.Meta) []string {
var labels []string
for _, label := range labelInfo.Labels {
if !slices.ContainsFunc(meta.Labels, func(metaLabel string) bool {
return strings.EqualFold(metaLabel, label.Name)
}) {
labels = append(labels, label.Name)
}
}
return labels
}
// Metadata has label but Page does not have it
func determineLabelsToAdd(meta *metadata.Meta, labelInfo *confluence.LabelInfo) []string {
var labels []string
for _, metaLabel := range meta.Labels {
if !slices.ContainsFunc(labelInfo.Labels, func(label confluence.Label) bool {
return strings.EqualFold(label.Name, metaLabel)
}) {
labels = append(labels, metaLabel)
}
}
return labels
}
func ConfigFilePath() string {
fp, err := os.UserConfigDir()
if err != nil {
log.Fatal(err)
}
return filepath.Join(fp, "mark.toml")
}
func SetLogLevel(cmd *cli.Command) error {
logLevel := cmd.String("log-level")
switch strings.ToUpper(logLevel) {
case lorg.LevelTrace.String():
log.SetLevel(lorg.LevelTrace)
case lorg.LevelDebug.String():
log.SetLevel(lorg.LevelDebug)
case lorg.LevelInfo.String():
log.SetLevel(lorg.LevelInfo)
case lorg.LevelWarning.String():
log.SetLevel(lorg.LevelWarning)
case lorg.LevelError.String():
log.SetLevel(lorg.LevelError)
case lorg.LevelFatal.String():
log.SetLevel(lorg.LevelFatal)
default:
return fmt.Errorf("unknown log level: %s", logLevel)
}
log.GetLevel()
return nil
}
func getSHA1Hash(input string) string {
hash := sha1.New()
hash.Write([]byte(input))
return hex.EncodeToString(hash.Sum(nil))
}

View File

@ -1,34 +0,0 @@
package util
import (
"fmt"
"github.com/reconquest/pkg/log"
)
type FatalErrorHandler struct {
ContinueOnError bool
}
func NewErrorHandler(continueOnError bool) *FatalErrorHandler {
return &FatalErrorHandler{
ContinueOnError: continueOnError,
}
}
func (h *FatalErrorHandler) Handle(err error, format string, args ...interface{}) {
if err == nil {
if h.ContinueOnError {
log.Error(fmt.Sprintf(format, args...))
return
}
log.Fatal(fmt.Sprintf(format, args...))
}
if h.ContinueOnError {
log.Errorf(err, format, args...)
return
}
log.Fatalf(err, format, args...)
}

Some files were not shown because too many files have changed in this diff Show More