mirror of
https://github.com/kovetskiy/mark.git
synced 2025-12-20 01:47:40 +08:00
Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c2a3c9b96 | ||
|
|
967efde9bd | ||
|
|
b3a6f1efae | ||
|
|
e82c425471 | ||
|
|
b36d7aa135 | ||
|
|
be4ff0d58a | ||
|
|
8fbf355e33 | ||
|
|
76850d1128 | ||
|
|
817c9684ce | ||
|
|
1c708414dd | ||
|
|
71668cac21 | ||
|
|
888d5de655 | ||
|
|
a7390d8b33 | ||
|
|
5a70073a01 | ||
|
|
0583aaa7ce | ||
|
|
5fd79b897e | ||
|
|
dd67c43fa6 | ||
|
|
09552af3fd | ||
|
|
8d08f1bfeb | ||
|
|
79d7f252f4 | ||
|
|
3517a26f85 | ||
|
|
aaa2da238d | ||
|
|
204d35e37b | ||
|
|
4eb24f33a6 | ||
|
|
1932cf0c29 | ||
|
|
283f827bd7 | ||
|
|
8f7d31e033 | ||
|
|
8f6c95f241 | ||
|
|
2fa2dfee87 | ||
|
|
5f1352c6f0 | ||
|
|
d0e302cccc | ||
|
|
f08991137e | ||
|
|
bb32afdf09 | ||
|
|
a299177a7e | ||
|
|
7f7494f26e | ||
|
|
781e30bbbe | ||
|
|
5516809c41 | ||
|
|
ff677a8690 | ||
|
|
6d81045bf0 | ||
|
|
2173fbcfcd | ||
|
|
de0e1b622a | ||
|
|
f6b63aab86 | ||
|
|
0f13d249f5 | ||
|
|
ae5347053a | ||
|
|
9c24b0e154 | ||
|
|
a15f6571d4 | ||
|
|
310cdf17c4 | ||
|
|
1e009259a4 | ||
|
|
b83bfebf82 | ||
|
|
2c66d7ad00 | ||
|
|
80c46f9d4e | ||
|
|
5e2b7b64e8 | ||
|
|
68f84bedbd | ||
|
|
e184568a77 | ||
|
|
7d6a63c7ab | ||
|
|
2260b24ab0 | ||
|
|
f9846c00f2 | ||
|
|
3ce4597004 | ||
|
|
7c7f7fa003 | ||
|
|
f5b3c64dff | ||
|
|
f14417a2e0 | ||
|
|
93f73b2c3e | ||
|
|
ccc596c4eb | ||
|
|
0841a6b370 | ||
|
|
4f1d68bfee | ||
|
|
01a6bc7af2 | ||
|
|
eae6fc0d90 | ||
|
|
c54d458dba | ||
|
|
32490b2c90 | ||
|
|
242cebb5ee | ||
|
|
c16386abb2 | ||
|
|
733b3222b3 | ||
|
|
8c061c49d4 | ||
|
|
7536e288b4 | ||
|
|
bb476d3901 | ||
|
|
779d1791b4 | ||
|
|
0618f1de60 | ||
|
|
f32dbbc04d | ||
|
|
bf542ab684 | ||
|
|
58cdd5608f | ||
|
|
6767d655c7 | ||
|
|
c57256cb7b | ||
|
|
926945f884 | ||
|
|
760ee5a2eb | ||
|
|
3cc39ffe79 | ||
|
|
d1aee4d571 | ||
|
|
b7ef416472 | ||
|
|
7562d0499e | ||
|
|
2d89511ac1 | ||
|
|
1d00316ae5 | ||
|
|
5649939297 | ||
|
|
4ac93b556c | ||
|
|
d9a96f3700 | ||
|
|
92634869e3 | ||
|
|
5cbd0fd6eb | ||
|
|
f8a3945f62 | ||
|
|
6c33afc866 | ||
|
|
ef09fd27f8 | ||
|
|
1fa01dff70 | ||
|
|
d789261c9a | ||
|
|
dda17fcb55 | ||
|
|
a77a538ab5 | ||
|
|
f24d8c8957 | ||
|
|
a0c6abfa6d | ||
|
|
b630876c22 | ||
|
|
ddc0ab9fbf | ||
|
|
87160e8dd6 | ||
|
|
d88b81a6b8 | ||
|
|
7f5144a1d1 | ||
|
|
7f5dfae904 | ||
|
|
024259e480 | ||
|
|
ff015e2c24 | ||
|
|
f3c5a77a85 | ||
|
|
0b8caa078b | ||
|
|
d820ee4bf4 | ||
|
|
203d4439ef | ||
|
|
f8229c8acb | ||
|
|
b30b0491a8 | ||
|
|
c87b6821d4 | ||
|
|
b2f0e80b12 | ||
|
|
f2b2a7a309 | ||
|
|
8d05975142 | ||
|
|
076165c137 | ||
|
|
611e8e9b94 | ||
|
|
15a3c10ed1 | ||
|
|
ec5ee6eb0a | ||
|
|
ea2bae39da | ||
|
|
1a0e452910 | ||
|
|
f0b4d460a9 | ||
|
|
f3e27aaa50 | ||
|
|
25c187f741 | ||
|
|
213088b960 | ||
|
|
5504fd4c11 | ||
|
|
9486f0bbcf | ||
|
|
f1c3b2afcd | ||
|
|
fbfd36a16c | ||
|
|
c5d0a8b8b7 | ||
|
|
5a245519fe | ||
|
|
ebe77984c6 | ||
|
|
5accce3b17 | ||
|
|
c63201159d | ||
|
|
f25d8876fc | ||
|
|
2ba35118bf | ||
|
|
959ddc2171 | ||
|
|
0bb85b672b | ||
|
|
9cc00551ca | ||
|
|
96db0f8f24 |
22
.github/workflows/ci.yml
vendored
22
.github/workflows/ci.yml
vendored
@ -11,7 +11,7 @@ on:
|
|||||||
- master
|
- master
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "~1.23.1"
|
GO_VERSION: "~1.24"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Runs Golangci-lint on the source code
|
# Runs Golangci-lint on the source code
|
||||||
@ -20,16 +20,16 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v9
|
||||||
|
|
||||||
# Runs markdown-lint on the markdown files
|
# Runs markdown-lint on the markdown files
|
||||||
ci-markdown-lint:
|
ci-markdown-lint:
|
||||||
@ -37,20 +37,20 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
- name: markdownlint-cli2-action
|
- name: markdownlint-cli2-action
|
||||||
uses: DavidAnson/markdownlint-cli2-action@v18
|
uses: DavidAnson/markdownlint-cli2-action@v22
|
||||||
|
|
||||||
# Executes Unit Tests
|
# Executes Unit Tests
|
||||||
ci-unit-tests:
|
ci-unit-tests:
|
||||||
name: ci-unit-tests
|
name: ci-unit-tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
id: go
|
id: go
|
||||||
@ -65,10 +65,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
id: go
|
id: go
|
||||||
|
|||||||
6
.github/workflows/goreleaser.yml
vendored
6
.github/workflows/goreleaser.yml
vendored
@ -10,13 +10,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set Up Go
|
- name: Set Up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: "1.24"
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v6
|
||||||
with:
|
with:
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
# This is an example goreleaser.yaml file with some sane defaults.
|
|
||||||
# Make sure to check the documentation at http://goreleaser.com
|
|
||||||
version: 2
|
version: 2
|
||||||
before:
|
before:
|
||||||
hooks:
|
hooks:
|
||||||
# You may remove this if you don't use go modules.
|
|
||||||
- go mod download
|
- go mod download
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- env:
|
||||||
@ -28,7 +25,7 @@ archives:
|
|||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Tag }}-next"
|
version_template: "{{ .Tag }}-next"
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
@ -38,8 +35,7 @@ changelog:
|
|||||||
|
|
||||||
# Publish on Homebrew Tap
|
# Publish on Homebrew Tap
|
||||||
brews:
|
brews:
|
||||||
-
|
- name: mark
|
||||||
name: mark
|
|
||||||
repository:
|
repository:
|
||||||
owner: kovetskiy
|
owner: kovetskiy
|
||||||
name: homebrew-mark
|
name: homebrew-mark
|
||||||
@ -57,5 +53,9 @@ brews:
|
|||||||
description: "Sync your markdown files with Confluence pages."
|
description: "Sync your markdown files with Confluence pages."
|
||||||
license: "Apache 2.0"
|
license: "Apache 2.0"
|
||||||
|
|
||||||
|
install: |
|
||||||
|
bin.install "mark"
|
||||||
|
generate_completions_from_executable(bin/"mark", "completion")
|
||||||
|
|
||||||
test: |
|
test: |
|
||||||
system "#{bin}/program", "version"
|
system "#{bin}/mark", "version"
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23.4 AS builder
|
FROM golang:1.25.5 AS builder
|
||||||
ENV GOPATH="/go"
|
ENV GOPATH="/go"
|
||||||
WORKDIR /go/src/github.com/kovetskiy/mark
|
WORKDIR /go/src/github.com/kovetskiy/mark
|
||||||
COPY / .
|
COPY / .
|
||||||
|
|||||||
6
Makefile
6
Makefile
@ -1,7 +1,7 @@
|
|||||||
NAME = $(notdir $(PWD))
|
NAME = $(notdir $(PWD))
|
||||||
|
|
||||||
VERSION = $(shell git describe --tags --abbrev=0)
|
VERSION = $(shell git describe --tags --abbrev=0)
|
||||||
|
COMMIT = $(shell git rev-parse HEAD)
|
||||||
GO111MODULE = on
|
GO111MODULE = on
|
||||||
|
|
||||||
REMOTE = kovetskiy
|
REMOTE = kovetskiy
|
||||||
@ -15,11 +15,11 @@ get:
|
|||||||
build:
|
build:
|
||||||
@echo :: building go binary $(VERSION)
|
@echo :: building go binary $(VERSION)
|
||||||
CGO_ENABLED=0 go build \
|
CGO_ENABLED=0 go build \
|
||||||
-ldflags "-X main.version=$(VERSION)" \
|
-ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT)" \
|
||||||
-gcflags "-trimpath $(GOPATH)/src"
|
-gcflags "-trimpath $(GOPATH)/src"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -race -coverprofile=profile.cov ./...
|
go test -race -coverprofile=profile.cov ./... -v
|
||||||
|
|
||||||
image:
|
image:
|
||||||
@echo :: building image $(NAME):$(VERSION)
|
@echo :: building image $(NAME):$(VERSION)
|
||||||
|
|||||||
117
README.md
117
README.md
@ -7,8 +7,6 @@
|
|||||||
Mark — a tool for syncing your markdown documentation with Atlassian Confluence
|
Mark — a tool for syncing your markdown documentation with Atlassian Confluence
|
||||||
pages.
|
pages.
|
||||||
|
|
||||||
Read the blog post discussing the tool — <https://samizdat.dev/use-markdown-for-confluence/>
|
|
||||||
|
|
||||||
This is very useful if you store documentation to your software in a Git
|
This is very useful if you store documentation to your software in a Git
|
||||||
repository and don't want to do an extra job of updating Confluence page using
|
repository and don't want to do an extra job of updating Confluence page using
|
||||||
a tinymce wysiwyg enterprise core editor which always breaks everything.
|
a tinymce wysiwyg enterprise core editor which always breaks everything.
|
||||||
@ -69,6 +67,12 @@ 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`.
|
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
|
Mark supports Go templates, which can be included into article by using path
|
||||||
to the template relative to current working dir, e.g.:
|
to the template relative to current working dir, e.g.:
|
||||||
|
|
||||||
@ -171,6 +175,25 @@ The key's value must be a string which defines the template's content.
|
|||||||
</tblbox>
|
</tblbox>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Automatic Page Title
|
||||||
|
|
||||||
|
If you don't want to specify the page title in the metadata of each file, `mark` provides two ways to set it automatically.
|
||||||
|
|
||||||
|
### From the first H1 heading
|
||||||
|
|
||||||
|
You can use the `--title-from-h1` flag to extract the page title from the first H1 heading in the markdown file. If no H1 heading is found, the title must be set in the page metadata.
|
||||||
|
|
||||||
|
### From the filename
|
||||||
|
|
||||||
|
You can use the `--title-from-filename` flag to use the filename (without the extension) as the page title. `mark` will automatically convert the filename to a more readable title by:
|
||||||
|
|
||||||
|
* Replacing underscores (`_`) and dashes (`-`) with spaces.
|
||||||
|
* Applying title case to the filename.
|
||||||
|
|
||||||
|
For example, a file named `my_awesome-page.md` will have the title "My Awesome Page".
|
||||||
|
|
||||||
|
These two options are mutually exclusive. If both flags are provided, `mark` will produce an error.
|
||||||
|
|
||||||
## Customizing the page layout
|
## Customizing the page layout
|
||||||
|
|
||||||
If you set the Layout to plain, the page layout can be customized using HTML comments inside the markdown:
|
If you set the Layout to plain, the page layout can be customized using HTML comments inside the markdown:
|
||||||
@ -265,7 +288,7 @@ Block Quotes are converted to Confluence Info/Warn/Note box when the following c
|
|||||||
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]`
|
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 |
|
| Github Alerts | Confluence |
|
||||||
|---------------|------------|
|
| --- | --- |
|
||||||
| Tip (green lightbulb) | Tip (green checkmark in circle) |
|
| Tip (green lightbulb) | Tip (green checkmark in circle) |
|
||||||
| Note (blue I in circle) | Info (blue I in circle) |
|
| Note (blue I in circle) | Info (blue I in circle) |
|
||||||
| Important (purple exclamation mark in speech bubble) | Info (blue I in circle) |
|
| Important (purple exclamation mark in speech bubble) | Info (blue I in circle) |
|
||||||
@ -713,17 +736,32 @@ Currently this is not compatible with the automated upload of inline images.
|
|||||||
### Render Mermaid Diagram
|
### 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/).
|
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 and are marked as "mermaid", the mark will automatically render it as a PNG image and insert into before the code block.
|
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.
|
||||||
|
|
||||||
```mermaid title diagrams_example
|
```mermaid title diagrams_example
|
||||||
graph TD;
|
graph TD;
|
||||||
A-->B;
|
A-->B;
|
||||||
```
|
```
|
||||||
|
|
||||||
In order to properly render mermaid, you can choose between the following mermaid providers:
|
### Render D2 Diagram
|
||||||
|
|
||||||
* "mermaid-go" via [mermaid.go](https://github.com/dreampuf/mermaid.go)
|
Optionally you can enable [D2](https://github.com/terrastruct/d2) rendering via `--features="d2"`.
|
||||||
* "cloudscript" via [cloudscript-io-mermaid-addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon)
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### MkDocs' Admonitions
|
||||||
|
|
||||||
|
Optionally you can enable mkdocs-style [Admonitions](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) via `--features="mkdocsadmonitions"`.
|
||||||
|
|
||||||
|
When enabled, this renders note, warning, tip, info admonitions as Confluence alerts.
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
!!! note
|
||||||
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -777,37 +815,41 @@ USAGE:
|
|||||||
mark [global options]
|
mark [global options]
|
||||||
|
|
||||||
VERSION:
|
VERSION:
|
||||||
11.3.0
|
v15.1.0@b3a6f1efae97dfaa1400a3175cdd3377f8176e88
|
||||||
|
|
||||||
DESCRIPTION:
|
DESCRIPTION:
|
||||||
Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark
|
Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark
|
||||||
|
|
||||||
GLOBAL OPTIONS:
|
GLOBAL OPTIONS:
|
||||||
--files value, -f value use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES]
|
--files string, -f string 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]
|
--continue-on-error don't exit if an error occurs while processing a file, continue processing remaining files. [$MARK_CONTINUE_ON_ERROR]
|
||||||
--dry-run resolve page and ancestry, show resulting HTML and exit. (default: false) [$MARK_DRY_RUN]
|
--compile-only show resulting HTML and don't update Confluence page content. [$MARK_COMPILE_ONLY]
|
||||||
--edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. (default: false) [$MARK_EDIT_LOCK]
|
--dry-run resolve page and ancestry, show resulting HTML and exit. [$MARK_DRY_RUN]
|
||||||
--drop-h1, --h1_drop don't include the first H1 heading in Confluence output. (default: false) [$MARK_H1_DROP]
|
--edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. [$MARK_EDIT_LOCK]
|
||||||
--strip-linebreaks, -L remove linebreaks inside of tags, to accomodate non-standard Confluence behavior (default: false) [$MARK_STRIP_LINEBREAK]
|
--drop-h1 don't include the first H1 heading in Confluence output. [$MARK_DROP_H1]
|
||||||
--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]
|
--strip-linebreaks, -L remove linebreaks inside of tags, to accommodate non-standard Confluence behavior [$MARK_STRIP_LINEBREAKS]
|
||||||
--title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title (default: false) [$MARK_TITLE_APPEND_GENERATED_HASH]
|
--title-from-h1 extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. Mutually exclusive with --title-from-filename. [$MARK_TITLE_FROM_H1]
|
||||||
--minor-edit don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT]
|
--title-from-filename use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1. [$MARK_TITLE_FROM_FILENAME]
|
||||||
--version-message value add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
|
--title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title [$MARK_TITLE_APPEND_GENERATED_HASH]
|
||||||
--color value display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
|
--minor-edit don't send notifications while updating Confluence page. [$MARK_MINOR_EDIT]
|
||||||
--debug enable debug logs. (default: false) [$MARK_DEBUG]
|
--version-message string add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
|
||||||
--trace enable trace logs. (default: false) [$MARK_TRACE]
|
--color string display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
|
||||||
--username value, -u value use specified username for updating Confluence page. [$MARK_USERNAME]
|
--log-level string set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL]
|
||||||
--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]
|
--username string, -u string use specified username for updating Confluence page. [$MARK_USERNAME]
|
||||||
--target-url value, -l value edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
|
--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]
|
||||||
--base-url value, -b value, --base_url value base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
|
--target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
|
||||||
--config value, -c value use the specified configuration file. (default: System specific) [$MARK_CONFIG]
|
--base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
|
||||||
--ci run on CI mode. It won't fail if files are not found. (default: false) [$MARK_CI]
|
--config string, -c string use the specified configuration file. (default: "$HOME/.config/mark.toml") [$MARK_CONFIG]
|
||||||
--space value use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
|
--ci run on CI mode. It won't fail if files are not found. [$MARK_CI]
|
||||||
--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]
|
--space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
|
||||||
--parents-delimiter value The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
|
--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]
|
||||||
--mermaid-provider value defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go. (default: "cloudscript") [$MARK_MERMAID_PROVIDER]
|
--parents-delimiter string The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
|
||||||
--mermaid-scale value defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
|
--mermaid-scale float 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]
|
--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. [$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, mkdocsadmonitions (default: "mermaid") [$MARK_FEATURES]
|
||||||
|
--insecure-skip-tls-verify skip TLS certificate verification (useful for self-signed certificates) [$MARK_INSECURE_SKIP_TLS_VERIFY]
|
||||||
--help, -h show help
|
--help, -h show help
|
||||||
--version, -v print the version
|
--version, -v print the version
|
||||||
```
|
```
|
||||||
@ -828,7 +870,7 @@ drop-h1 = true
|
|||||||
|
|
||||||
**NOTE**: The system specific locations are described in here:
|
**NOTE**: The system specific locations are described in here:
|
||||||
<https://pkg.go.dev/os#UserConfigDir>.
|
<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.
|
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
|
## Tricks
|
||||||
@ -869,7 +911,7 @@ done
|
|||||||
|
|
||||||
The following directive tells the CI to run this particular job only if the changes are pushed into the
|
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
|
`main` branch. It means you can safely push your changes into feature branches without being afraid
|
||||||
that they automatically shown in Confluence, then go through the reviewal process and automatically
|
that they have automatically shown in Confluence, then go through the reviewal process and automatically
|
||||||
deploy them when PR got merged.
|
deploy them when PR got merged.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@ -899,12 +941,12 @@ We recommend to lint your markdown files with [markdownlint-cli2](https://github
|
|||||||
## Issues, Bugs & Contributions
|
## 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'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 this projects which means I don't really prioritize working on this project in my free time.
|
I have no profits/sponsors from these 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
|
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
|
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.
|
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
|
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:
|
sure to reply in the issue to let everyone know you took the issue:
|
||||||
|
|
||||||
@ -977,6 +1019,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<tr>
|
<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/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/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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -201,7 +201,9 @@ func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath)
|
return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer func() {
|
||||||
|
_ = file.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
fileBytes, err := io.ReadAll(file)
|
fileBytes, err := io.ReadAll(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -45,7 +45,7 @@ func TestPrepareAttachmentsWithWorkDirBase(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attaches, err := prepareAttachments(testingOpener, ".", replacements)
|
attaches, err := prepareAttachments(testingOpener, ".", replacements)
|
||||||
t.Logf("attaches: %s", err)
|
t.Logf("attaches: %v", err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println(err.Error())
|
println(err.Error())
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package confluence
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -9,6 +10,7 @@ import (
|
|||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/kovetskiy/gopencils"
|
"github.com/kovetskiy/gopencils"
|
||||||
"github.com/kovetskiy/lorg"
|
"github.com/kovetskiy/lorg"
|
||||||
@ -49,6 +51,7 @@ type PageInfo struct {
|
|||||||
|
|
||||||
Version struct {
|
Version struct {
|
||||||
Number int64 `json:"number"`
|
Number int64 `json:"number"`
|
||||||
|
Message string `json:"message"`
|
||||||
} `json:"version"`
|
} `json:"version"`
|
||||||
|
|
||||||
Ancestors []struct {
|
Ancestors []struct {
|
||||||
@ -95,7 +98,7 @@ func (tracer *tracer) Printf(format string, args ...interface{}) {
|
|||||||
log.Tracef(nil, tracer.prefix+" "+format, args...)
|
log.Tracef(nil, tracer.prefix+" "+format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAPI(baseURL string, username string, password string) *API {
|
func NewAPI(baseURL string, username string, password string, insecureSkipVerify bool) *API {
|
||||||
var auth *gopencils.BasicAuth
|
var auth *gopencils.BasicAuth
|
||||||
if username != "" {
|
if username != "" {
|
||||||
auth = &gopencils.BasicAuth{
|
auth = &gopencils.BasicAuth{
|
||||||
@ -103,7 +106,19 @@ func NewAPI(baseURL string, username string, password string) *API {
|
|||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rest := gopencils.Api(baseURL+"/rest/api", auth)
|
|
||||||
|
var httpClient *http.Client
|
||||||
|
if insecureSkipVerify {
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rest := gopencils.Api(baseURL+"/rest/api", auth, httpClient, 3) // set option for 3 retries on failure
|
||||||
if username == "" {
|
if username == "" {
|
||||||
if rest.Headers == nil {
|
if rest.Headers == nil {
|
||||||
rest.Headers = http.Header{}
|
rest.Headers = http.Header{}
|
||||||
@ -111,10 +126,7 @@ func NewAPI(baseURL string, username string, password string) *API {
|
|||||||
rest.SetHeader("Authorization", fmt.Sprintf("Bearer %s", password))
|
rest.SetHeader("Authorization", fmt.Sprintf("Bearer %s", password))
|
||||||
}
|
}
|
||||||
|
|
||||||
json := gopencils.Api(
|
json := gopencils.Api(baseURL+"/rpc/json-rpc/confluenceservice-v2", auth, httpClient, 3)
|
||||||
baseURL+"/rpc/json-rpc/confluenceservice-v2",
|
|
||||||
auth,
|
|
||||||
)
|
|
||||||
|
|
||||||
if log.GetLevel() == lorg.LevelTrace {
|
if log.GetLevel() == lorg.LevelTrace {
|
||||||
rest.Logger = &tracer{"rest:"}
|
rest.Logger = &tracer{"rest:"}
|
||||||
@ -258,7 +270,7 @@ func (api *API) CreateAttachment(
|
|||||||
|
|
||||||
if len(result.Results) == 0 {
|
if len(result.Results) == 0 {
|
||||||
return info, errors.New(
|
return info, errors.New(
|
||||||
"Confluence REST API for creating attachments returned " +
|
"the Confluence REST API for creating attachments returned " +
|
||||||
"0 json objects, expected at least 1",
|
"0 json objects, expected at least 1",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -512,7 +524,7 @@ func (api *API) CreatePage(
|
|||||||
return request.Response.(*PageInfo), nil
|
return request.Response.(*PageInfo), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, versionMessage string, newLabels []string, appearance string) error {
|
func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, versionMessage string, newLabels []string, appearance string, emojiString string) error {
|
||||||
nextPageVersion := page.Version.Number + 1
|
nextPageVersion := page.Version.Number + 1
|
||||||
oldAncestors := []map[string]interface{}{}
|
oldAncestors := []map[string]interface{}{}
|
||||||
|
|
||||||
@ -523,6 +535,29 @@ 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{}{
|
payload := map[string]interface{}{
|
||||||
"id": page.ID,
|
"id": page.ID,
|
||||||
"type": page.Type,
|
"type": page.Type,
|
||||||
@ -540,16 +575,7 @@ func (api *API) UpdatePage(page *PageInfo, newContent string, minorEdit bool, ve
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"metadata": map[string]interface{}{
|
"metadata": map[string]interface{}{
|
||||||
// Fix to set full-width as has changed on Confluence APIs again.
|
"properties": properties,
|
||||||
// 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.
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,21 +804,23 @@ func (api *API) RestrictPageUpdates(
|
|||||||
func newErrorStatusNotOK(request *gopencils.Resource) error {
|
func newErrorStatusNotOK(request *gopencils.Resource) error {
|
||||||
if request.Raw.StatusCode == http.StatusUnauthorized {
|
if request.Raw.StatusCode == http.StatusUnauthorized {
|
||||||
return errors.New(
|
return errors.New(
|
||||||
"Confluence API returned unexpected status: 401 (Unauthorized)",
|
"the Confluence API returned unexpected status: 401 (Unauthorized)",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.Raw.StatusCode == http.StatusNotFound {
|
if request.Raw.StatusCode == http.StatusNotFound {
|
||||||
return errors.New(
|
return errors.New(
|
||||||
"Confluence API returned unexpected status: 404 (Not Found)",
|
"the Confluence API returned unexpected status: 404 (Not Found)",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
output, _ := io.ReadAll(request.Raw.Body)
|
output, _ := io.ReadAll(request.Raw.Body)
|
||||||
defer request.Raw.Body.Close()
|
defer func() {
|
||||||
|
_ = request.Raw.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"Confluence API returned unexpected status: %v, "+
|
"the Confluence API returned unexpected status: %v, "+
|
||||||
"output: %q",
|
"output: %q",
|
||||||
request.Raw.Status, output,
|
request.Raw.Status, output,
|
||||||
)
|
)
|
||||||
|
|||||||
116
d2/d2.go
Normal file
116
d2/d2.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package d2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
scaleAsBytes := make([]byte, 8)
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint64(scaleAsBytes, math.Float64bits(scale))
|
||||||
|
|
||||||
|
d2Bytes := append(d2Diagram, scaleAsBytes...)
|
||||||
|
|
||||||
|
checkSum, err := attachment.GetChecksum(bytes.NewReader(d2Bytes))
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
102
d2/d2_test.go
Normal file
102
d2/d2_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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: "40e75f93e09da9242d4b1ab8e2892665ec7d5bd1ac78a4b65210ee219cf62297",
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
56
go.mod
56
go.mod
@ -1,40 +1,60 @@
|
|||||||
module github.com/kovetskiy/mark
|
module github.com/kovetskiy/mark
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1
|
github.com/bmatcuk/doublestar/v4 v4.9.1
|
||||||
github.com/dreampuf/mermaid.go v0.0.21
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
|
||||||
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0
|
github.com/chromedp/chromedp v0.14.2
|
||||||
|
github.com/dreampuf/mermaid.go v0.0.39
|
||||||
|
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a
|
||||||
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4
|
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4
|
||||||
github.com/reconquest/karma-go v1.5.0
|
github.com/reconquest/karma-go v1.5.0
|
||||||
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64
|
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64
|
||||||
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
|
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stefanfritsch/goldmark-admonitions v1.1.1
|
||||||
github.com/urfave/cli/v2 v2.27.5
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yuin/goldmark v1.7.8
|
github.com/urfave/cli-altsrc/v3 v3.1.0
|
||||||
golang.org/x/tools v0.28.0
|
github.com/urfave/cli/v3 v3.6.1
|
||||||
|
github.com/yuin/goldmark v1.7.13
|
||||||
|
golang.org/x/text v0.32.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
oss.terrastruct.com/d2 v0.7.1
|
||||||
|
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b // indirect
|
github.com/PuerkitoBio/goquery v1.10.0 // indirect
|
||||||
github.com/chromedp/chromedp v0.11.2 // indirect
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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-20250725192818-e39067aee2d2 // indirect
|
||||||
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/gobwas/httphead v0.1.0 // indirect
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
github.com/gobwas/pool v0.2.1 // indirect
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
github.com/gobwas/ws v1.4.0 // indirect
|
github.com/gobwas/ws v1.4.0 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
|
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/mailru/easyjson v0.7.7 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mazznoer/csscolorparser v0.1.5 // indirect
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb // indirect
|
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||||
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 // indirect
|
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
golang.org/x/image v0.20.0 // indirect
|
||||||
|
golang.org/x/net v0.44.0 // indirect
|
||||||
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
|
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
158
go.sum
158
go.sum
@ -1,42 +1,73 @@
|
|||||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b h1:md1Gk5jkNE91SZxFDCMHmKqX0/GsEr1/VTejht0sCbY=
|
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||||
github.com/chromedp/cdproto v0.0.0-20241110205750-a72e6703cd9b/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
|
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||||
github.com/chromedp/chromedp v0.11.2 h1:ZRHTh7DjbNTlfIv3NFTbB7eVeu5XCNkgrpcGSpn2oX0=
|
github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg=
|
||||||
github.com/chromedp/chromedp v0.11.2/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
|
github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk=
|
||||||
|
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/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||||
|
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
|
||||||
|
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
|
||||||
|
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
|
||||||
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
|
||||||
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dreampuf/mermaid.go v0.0.21 h1:6Lqa1XRYOg0ycJAK85QkMA/lPmz31kupK2Gj6a8yXTs=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dreampuf/mermaid.go v0.0.21/go.mod h1:xT7lC6+LL334PeARmvQ6+2xgRRhVAA78qOm3zC5s2Y8=
|
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.39 h1:K7R+FaAOxKd32/yic9SVz0u9bedS5nV/6nUgGnKdJuY=
|
||||||
|
github.com/dreampuf/mermaid.go v0.0.39/go.mod h1:xBmQWWnPFQl7HIfEz+KnZ+BpXPJl9qXe9aISIPJGsAM=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
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/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
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/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 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
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 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0 h1:LVc416BwqYl2D6sxv76ElZ4ZAT4+VQk4a80Ki/cNse8=
|
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 h1:dMHbguTqGtorivvHTaOnbYp+tFzrw5M9gjkU4lCplgg=
|
||||||
github.com/kovetskiy/gopencils v0.0.0-20240830111426-6b65e95c9cb0/go.mod h1:dVsBLabGUkYCN1Zh9spGL2GYfAOpG2LPWZf9H0qG66k=
|
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/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4 h1:2eV8tF1u58dqRJMlFUD/Df26BxcIlGVy71rZHN+aNoI=
|
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/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4/go.mod h1:p1RuSvyflTF/G4ubeATGurCRKWkULOrN/4PUAEFRq0s=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
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 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
github.com/mazznoer/csscolorparser v0.1.5 h1:Wr4uNIE+pHWN3TqZn2SGpA2nLRG064gB7WdSfSS5cz4=
|
||||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
github.com/mazznoer/csscolorparser v0.1.5/go.mod h1:OQRVvgCyHDCAquR1YWfSwwaDcM0LhnSffGnlbOew/3I=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@ -48,29 +79,82 @@ 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/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 h1:bcDXaTFC09IIg13Z8gfQHk4gSu001ET7ssW/wKRvPzg=
|
||||||
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA=
|
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
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/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stefanfritsch/goldmark-admonitions v1.1.1 h1:SncsICdQrIYYaq02Ta+zyc9gNmMfYqQH2qwLSCJYxA4=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stefanfritsch/goldmark-admonitions v1.1.1/go.mod h1:cOZK5O0gE6eWfpxTdjGUmeONW2IL9j3Zujv3KlZWlLo=
|
||||||
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/urfave/cli-altsrc/v3 v3.1.0 h1:6E5+kXeAWmRxXlPgdEVf9VqVoTJ2MJci0UMpUi/w/bA=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/urfave/cli-altsrc/v3 v3.1.0/go.mod h1:VcWVTGXcL3nrXUDJZagHAeUX702La3PKeWav7KpISqA=
|
||||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
|
||||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
|
||||||
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||||
|
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 h1:75pcOSsb40+ub185cJI7g5uykl9Uu76rD5ONzK/4s40=
|
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=
|
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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
|
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
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/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=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
oss.terrastruct.com/d2 v0.7.1 h1:LafTW1UoXJGODvKDZ8obyBfGcc2k2vHZ3EzrabMqEVE=
|
||||||
|
oss.terrastruct.com/d2 v0.7.1/go.mod h1:aT0PwLaxBZGgsWrIT8oSFYm5xoYX08BaOHewi5qLE2E=
|
||||||
|
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=
|
||||||
|
|||||||
613
main.go
613
main.go
@ -1,621 +1,40 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bmatcuk/doublestar/v4"
|
"github.com/kovetskiy/mark/util"
|
||||||
"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/vfs"
|
|
||||||
"github.com/reconquest/karma-go"
|
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v3"
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var (
|
||||||
|
version = "dev"
|
||||||
|
commit = "none"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
version = "11.3.0"
|
|
||||||
usage = "A tool for updating Atlassian Confluence pages from markdown."
|
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`
|
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_LINEBREAKS"},
|
|
||||||
}),
|
|
||||||
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: "title-append-generated-hash",
|
|
||||||
Value: false,
|
|
||||||
Usage: "appends a short hash generated from the path of the page (space, parents, and title) to the title",
|
|
||||||
EnvVars: []string{"MARK_TITLE_APPEND_GENERATED_HASH"},
|
|
||||||
}),
|
|
||||||
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() {
|
func main() {
|
||||||
app := &cli.App{
|
cmd := &cli.Command{
|
||||||
Name: "mark",
|
Name: "mark",
|
||||||
Usage: usage,
|
Usage: usage,
|
||||||
Description: description,
|
Description: description,
|
||||||
Version: version,
|
Version: fmt.Sprintf("%s@%s", version, commit),
|
||||||
Flags: flags,
|
Flags: util.Flags,
|
||||||
Before: altsrc.InitInputSourceWithContext(flags,
|
EnableShellCompletion: true,
|
||||||
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,
|
HideHelpCommand: true,
|
||||||
Action: RunMark,
|
Before: util.CheckMutuallyExclusiveTitleFlags,
|
||||||
|
Action: util.RunMark,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
if err := cmd.Run(context.TODO(), os.Args); err != nil {
|
||||||
log.Fatal(err)
|
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 := metadata.ExtractMeta(markdown, cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash"))
|
|
||||||
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 := page.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cCtx.String("space"), cCtx.Bool("title-from-h1"), parents, cCtx.Bool("title-append-generated-hash"))
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to resolve relative links")
|
|
||||||
}
|
|
||||||
|
|
||||||
markdown = page.SubstituteLinks(markdown, links)
|
|
||||||
|
|
||||||
if cCtx.Bool("dry-run") {
|
|
||||||
_, _, err := page.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 := page.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 *metadata.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 *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")
|
|
||||||
}
|
|
||||||
|
|||||||
51
main_test.go
Normal file
51
main_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,12 +2,15 @@ package mark
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
cparser "github.com/kovetskiy/mark/parser"
|
cparser "github.com/kovetskiy/mark/parser"
|
||||||
crenderer "github.com/kovetskiy/mark/renderer"
|
crenderer "github.com/kovetskiy/mark/renderer"
|
||||||
"github.com/kovetskiy/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
"github.com/kovetskiy/mark/types"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
|
mkDocsParser "github.com/stefanfritsch/goldmark-admonitions"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
@ -22,23 +25,17 @@ type ConfluenceExtension struct {
|
|||||||
html.Config
|
html.Config
|
||||||
Stdlib *stdlib.Lib
|
Stdlib *stdlib.Lib
|
||||||
Path string
|
Path string
|
||||||
MermaidProvider string
|
MarkConfig types.MarkConfig
|
||||||
MermaidScale float64
|
|
||||||
DropFirstH1 bool
|
|
||||||
StripNewlines bool
|
|
||||||
Attachments []attachment.Attachment
|
Attachments []attachment.Attachment
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
|
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
|
||||||
func NewConfluenceExtension(stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool, stripNewlines bool) *ConfluenceExtension {
|
func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceExtension {
|
||||||
return &ConfluenceExtension{
|
return &ConfluenceExtension{
|
||||||
Config: html.NewConfig(),
|
Config: html.NewConfig(),
|
||||||
Stdlib: stdlib,
|
Stdlib: stdlib,
|
||||||
Path: path,
|
Path: path,
|
||||||
MermaidProvider: mermaidProvider,
|
MarkConfig: cfg,
|
||||||
MermaidScale: mermaidScale,
|
|
||||||
DropFirstH1: dropFirstH1,
|
|
||||||
StripNewlines: stripNewlines,
|
|
||||||
Attachments: []attachment.Attachment{},
|
Attachments: []attachment.Attachment{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,17 +47,29 @@ func (c *ConfluenceExtension) Attach(a attachment.Attachment) {
|
|||||||
func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
|
func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
|
||||||
|
|
||||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
util.Prioritized(crenderer.NewConfluenceTextRenderer(c.StripNewlines), 100),
|
util.Prioritized(crenderer.NewConfluenceTextRenderer(c.MarkConfig.StripNewlines), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
|
util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
|
util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MermaidProvider, c.MermaidScale), 100),
|
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
|
util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.DropFirstH1), 100),
|
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.MarkConfig.DropFirstH1), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path), 100),
|
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
|
util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
|
||||||
util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
|
util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
if slices.Contains(c.MarkConfig.Features, "mkdocsadmonitions") {
|
||||||
|
m.Parser().AddOptions(
|
||||||
|
parser.WithBlockParsers(
|
||||||
|
util.Prioritized(mkDocsParser.NewAdmonitionParser(), 100),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||||
|
util.Prioritized(crenderer.NewConfluenceMkDocsAdmonitionRenderer(), 100),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
m.Parser().AddOptions(parser.WithInlineParsers(
|
m.Parser().AddOptions(parser.WithInlineParsers(
|
||||||
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
|
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
|
||||||
// the <ac:*/> tags.
|
// the <ac:*/> tags.
|
||||||
@ -68,20 +77,20 @@ func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidProvider string, mermaidScale float64, dropFirstH1 bool, stripNewlines bool) (string, []attachment.Attachment) {
|
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment) {
|
||||||
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
|
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
|
||||||
|
|
||||||
confluenceExtension := NewConfluenceExtension(stdlib, path, mermaidProvider, mermaidScale, dropFirstH1, stripNewlines)
|
confluenceExtension := NewConfluenceExtension(stdlib, path, cfg)
|
||||||
|
|
||||||
converter := goldmark.New(
|
converter := goldmark.New(
|
||||||
goldmark.WithExtensions(
|
goldmark.WithExtensions(
|
||||||
extension.GFM,
|
|
||||||
extension.Footnote,
|
extension.Footnote,
|
||||||
extension.DefinitionList,
|
extension.DefinitionList,
|
||||||
extension.NewTable(
|
extension.NewTable(
|
||||||
extension.WithTableCellAlignMethod(extension.TableCellAlignStyle),
|
extension.WithTableCellAlignMethod(extension.TableCellAlignStyle),
|
||||||
),
|
),
|
||||||
confluenceExtension,
|
confluenceExtension,
|
||||||
|
extension.GFM,
|
||||||
),
|
),
|
||||||
goldmark.WithParserOptions(
|
goldmark.WithParserOptions(
|
||||||
parser.WithAutoHeadingID(),
|
parser.WithAutoHeadingID(),
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
package mark
|
package mark_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -8,8 +10,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
mark "github.com/kovetskiy/mark/markdown"
|
||||||
"github.com/kovetskiy/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
"github.com/kovetskiy/mark/types"
|
||||||
|
"github.com/kovetskiy/mark/util"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
|
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
|
||||||
@ -46,13 +52,23 @@ func TestCompileMarkdown(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, filename := range testcases {
|
for _, filename := range testcases {
|
||||||
|
fmt.Printf("Testing: %v\n", filename)
|
||||||
lib, err := stdlib.New(nil)
|
lib, err := stdlib.New(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
markdown, htmlname, html := loadData(t, filename, "")
|
markdown, htmlname, html := loadData(t, filename, "")
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, false)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
cfg := types.MarkConfig{
|
||||||
|
MermaidScale: 1.0,
|
||||||
|
D2Scale: 1.0,
|
||||||
|
DropFirstH1: false,
|
||||||
|
StripNewlines: false,
|
||||||
|
Features: []string{"mkdocsadmonitions"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg)
|
||||||
|
test.EqualValues(strings.TrimSuffix(string(html), "\n"), strings.TrimSuffix(actual, "\n"), filename+" vs "+htmlname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,14 +94,24 @@ func TestCompileMarkdownDropH1(t *testing.T) {
|
|||||||
}
|
}
|
||||||
var variant string
|
var variant string
|
||||||
switch filename {
|
switch filename {
|
||||||
case "testdata/quotes.md", "testdata/header.md":
|
case "testdata/quotes.md", "testdata/header.md", "testdata/admonitions.md":
|
||||||
variant = "-droph1"
|
variant = "-droph1"
|
||||||
default:
|
default:
|
||||||
variant = ""
|
variant = ""
|
||||||
}
|
}
|
||||||
markdown, htmlname, html := loadData(t, filename, variant)
|
markdown, htmlname, html := loadData(t, filename, variant)
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, true, false)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
cfg := types.MarkConfig{
|
||||||
|
MermaidScale: 1.0,
|
||||||
|
D2Scale: 1.0,
|
||||||
|
DropFirstH1: true,
|
||||||
|
StripNewlines: false,
|
||||||
|
Features: []string{"mkdocsadmonitions"},
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, _ := mark.CompileMarkdown(markdown, lib, filename, cfg)
|
||||||
|
test.EqualValues(strings.TrimSuffix(string(html), "\n"), strings.TrimSuffix(actual, "\n"), filename+" vs "+htmlname)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,14 +137,49 @@ func TestCompileMarkdownStripNewlines(t *testing.T) {
|
|||||||
}
|
}
|
||||||
var variant string
|
var variant string
|
||||||
switch filename {
|
switch filename {
|
||||||
case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md":
|
case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md", "testdata/admonitions.md":
|
||||||
variant = "-stripnewlines"
|
variant = "-stripnewlines"
|
||||||
default:
|
default:
|
||||||
variant = ""
|
variant = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
markdown, htmlname, html := loadData(t, filename, variant)
|
markdown, htmlname, html := loadData(t, filename, variant)
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, true)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
cfg := types.MarkConfig{
|
||||||
|
MermaidScale: 1.0,
|
||||||
|
D2Scale: 1.0,
|
||||||
|
DropFirstH1: false,
|
||||||
|
StripNewlines: true,
|
||||||
|
Features: []string{"mkdocsadmonitions"},
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,8 @@ package mermaid
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -11,7 +13,7 @@ import (
|
|||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var renderTimeout = 90 * time.Second
|
var renderTimeout = 120 * time.Second
|
||||||
|
|
||||||
func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachment.Attachment, error) {
|
func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachment.Attachment, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
|
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
|
||||||
@ -30,7 +32,13 @@ func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (
|
|||||||
return attachment.Attachment{}, err
|
return attachment.Attachment{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
checkSum, err := attachment.GetChecksum(bytes.NewReader(mermaidDiagram))
|
scaleAsBytes := make([]byte, 8)
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint64(scaleAsBytes, math.Float64bits(scale))
|
||||||
|
|
||||||
|
mermaidBytes := append(mermaidDiagram, scaleAsBytes...)
|
||||||
|
|
||||||
|
checkSum, err := attachment.GetChecksum(bytes.NewReader(mermaidBytes))
|
||||||
log.Debugf(nil, "Checksum: %q -> %s", title, checkSum)
|
log.Debugf(nil, "Checksum: %q -> %s", title, checkSum)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ func TestExtractMermaidImage(t *testing.T) {
|
|||||||
Filename: "example.png",
|
Filename: "example.png",
|
||||||
Name: "example",
|
Name: "example",
|
||||||
Replace: "example",
|
Replace: "example",
|
||||||
Checksum: "1743a4f31ab66244591f06c8056e08053b8e0a554eb9a38709af6e9d145ac84f",
|
Checksum: "26296b73c960c25850b37bc9dd77cb24fce1a78db83b37755a25af7f8a48cc96",
|
||||||
ID: "",
|
ID: "",
|
||||||
Width: "87",
|
Width: "87",
|
||||||
Height: "174",
|
Height: "174",
|
||||||
|
|||||||
@ -5,10 +5,13 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
|
"golang.org/x/text/cases"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -17,6 +20,7 @@ const (
|
|||||||
HeaderType = `Type`
|
HeaderType = `Type`
|
||||||
HeaderTitle = `Title`
|
HeaderTitle = `Title`
|
||||||
HeaderLayout = `Layout`
|
HeaderLayout = `Layout`
|
||||||
|
HeaderEmoji = `Emoji`
|
||||||
HeaderAttachment = `Attachment`
|
HeaderAttachment = `Attachment`
|
||||||
HeaderLabel = `Label`
|
HeaderLabel = `Label`
|
||||||
HeaderInclude = `Include`
|
HeaderInclude = `Include`
|
||||||
@ -31,6 +35,7 @@ type Meta struct {
|
|||||||
Title string
|
Title string
|
||||||
Layout string
|
Layout string
|
||||||
Sidebar string
|
Sidebar string
|
||||||
|
Emoji string
|
||||||
Attachments []string
|
Attachments []string
|
||||||
Labels []string
|
Labels []string
|
||||||
ContentAppearance string
|
ContentAppearance string
|
||||||
@ -46,7 +51,7 @@ var (
|
|||||||
reHeaderPatternMacro = regexp.MustCompile(`<!-- Macro: .*`)
|
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, titleFromFilename bool, filename string, parents []string, titleAppendGeneratedHash bool) (*Meta, []byte, error) {
|
||||||
var (
|
var (
|
||||||
meta *Meta
|
meta *Meta
|
||||||
offset int
|
offset int
|
||||||
@ -79,8 +84,7 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
meta.ContentAppearance = FullWidthContentAppearance // Default to full-width for backwards compatibility
|
meta.ContentAppearance = FullWidthContentAppearance // Default to full-width for backwards compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:staticcheck
|
header := cases.Title(language.English).String(matches[1])
|
||||||
header := strings.Title(matches[1])
|
|
||||||
|
|
||||||
var value string
|
var value string
|
||||||
if len(matches) > 1 {
|
if len(matches) > 1 {
|
||||||
@ -107,6 +111,9 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
meta.Layout = "article"
|
meta.Layout = "article"
|
||||||
meta.Sidebar = strings.TrimSpace(value)
|
meta.Sidebar = strings.TrimSpace(value)
|
||||||
|
|
||||||
|
case HeaderEmoji:
|
||||||
|
meta.Emoji = strings.TrimSpace(value)
|
||||||
|
|
||||||
case HeaderAttachment:
|
case HeaderAttachment:
|
||||||
meta.Attachments = append(meta.Attachments, value)
|
meta.Attachments = append(meta.Attachments, value)
|
||||||
|
|
||||||
@ -136,7 +143,7 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if titleFromH1 || spaceFromCli != "" {
|
if titleFromH1 || titleFromFilename || spaceFromCli != "" {
|
||||||
if meta == nil {
|
if meta == nil {
|
||||||
meta = &Meta{}
|
meta = &Meta{}
|
||||||
}
|
}
|
||||||
@ -152,6 +159,9 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
if titleFromH1 && meta.Title == "" {
|
if titleFromH1 && meta.Title == "" {
|
||||||
meta.Title = ExtractDocumentLeadingH1(data)
|
meta.Title = ExtractDocumentLeadingH1(data)
|
||||||
}
|
}
|
||||||
|
if titleFromFilename && meta.Title == "" && filename != "" {
|
||||||
|
setTitleFromFilename(meta, filename)
|
||||||
|
}
|
||||||
if spaceFromCli != "" && meta.Space == "" {
|
if spaceFromCli != "" && meta.Space == "" {
|
||||||
meta.Space = spaceFromCli
|
meta.Space = spaceFromCli
|
||||||
}
|
}
|
||||||
@ -179,9 +189,20 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove trailing spaces from title
|
||||||
|
meta.Title = strings.Trim(meta.Title, " ")
|
||||||
|
meta.Space = strings.Trim(meta.Space, " ")
|
||||||
return meta, data[offset:], nil
|
return meta, data[offset:], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setTitleFromFilename(meta *Meta, filename string) {
|
||||||
|
base := filepath.Base(filename)
|
||||||
|
title := strings.TrimSuffix(base, filepath.Ext(base))
|
||||||
|
title = strings.ReplaceAll(title, "_", " ")
|
||||||
|
title = strings.ReplaceAll(title, "-", " ")
|
||||||
|
meta.Title = cases.Title(language.English).String(title)
|
||||||
|
}
|
||||||
|
|
||||||
// ExtractDocumentLeadingH1 will extract leading H1 heading
|
// ExtractDocumentLeadingH1 will extract leading H1 heading
|
||||||
func ExtractDocumentLeadingH1(markdown []byte) string {
|
func ExtractDocumentLeadingH1(markdown []byte) string {
|
||||||
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
|
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
|
||||||
|
|||||||
@ -28,3 +28,35 @@ func TestExtractDocumentLeadingH1(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "a", actual)
|
assert.Equal(t, "a", actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetTitleFromFilename(t *testing.T) {
|
||||||
|
t.Run("set title from filename", func(t *testing.T) {
|
||||||
|
meta := &Meta{Title: ""}
|
||||||
|
setTitleFromFilename(meta, "/path/to/test.md")
|
||||||
|
assert.Equal(t, "Test", meta.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replace underscores with spaces", func(t *testing.T) {
|
||||||
|
meta := &Meta{Title: ""}
|
||||||
|
setTitleFromFilename(meta, "/path/to/test_with_underscores.md")
|
||||||
|
assert.Equal(t, "Test With Underscores", meta.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("replace dashes with spaces", func(t *testing.T) {
|
||||||
|
meta := &Meta{Title: ""}
|
||||||
|
setTitleFromFilename(meta, "/path/to/test-with-dashes.md")
|
||||||
|
assert.Equal(t, "Test With Dashes", meta.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mixed underscores and dashes", func(t *testing.T) {
|
||||||
|
meta := &Meta{Title: ""}
|
||||||
|
setTitleFromFilename(meta, "/path/to/test_with-mixed_separators.md")
|
||||||
|
assert.Equal(t, "Test With Mixed Separators", meta.Title)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("already title cased", func(t *testing.T) {
|
||||||
|
meta := &Meta{Title: ""}
|
||||||
|
setTitleFromFilename(meta, "/path/to/Already-Title-Cased.md")
|
||||||
|
assert.Equal(t, "Already Title Cased", meta.Title)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
14
page/link.go
14
page/link.go
@ -3,16 +3,17 @@ package page
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
"github.com/kovetskiy/mark/metadata"
|
"github.com/kovetskiy/mark/metadata"
|
||||||
"github.com/reconquest/karma-go"
|
"github.com/reconquest/karma-go"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
"golang.org/x/tools/godoc/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LinkSubstitution struct {
|
type LinkSubstitution struct {
|
||||||
@ -33,6 +34,7 @@ func ResolveRelativeLinks(
|
|||||||
base string,
|
base string,
|
||||||
spaceFromCli string,
|
spaceFromCli string,
|
||||||
titleFromH1 bool,
|
titleFromH1 bool,
|
||||||
|
titleFromFilename bool,
|
||||||
parents []string,
|
parents []string,
|
||||||
titleAppendGeneratedHash bool,
|
titleAppendGeneratedHash bool,
|
||||||
) ([]LinkSubstitution, error) {
|
) ([]LinkSubstitution, error) {
|
||||||
@ -47,7 +49,7 @@ func ResolveRelativeLinks(
|
|||||||
match.filename,
|
match.filename,
|
||||||
match.hash,
|
match.hash,
|
||||||
)
|
)
|
||||||
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, parents, titleAppendGeneratedHash)
|
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, titleFromFilename, parents, titleAppendGeneratedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, karma.Format(err, "resolve link: %q", match.full)
|
return nil, karma.Format(err, "resolve link: %q", match.full)
|
||||||
}
|
}
|
||||||
@ -71,6 +73,7 @@ func resolveLink(
|
|||||||
link markdownLink,
|
link markdownLink,
|
||||||
spaceFromCli string,
|
spaceFromCli string,
|
||||||
titleFromH1 bool,
|
titleFromH1 bool,
|
||||||
|
titleFromFilename bool,
|
||||||
parents []string,
|
parents []string,
|
||||||
titleAppendGeneratedHash bool,
|
titleAppendGeneratedHash bool,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
@ -91,7 +94,10 @@ func resolveLink(
|
|||||||
|
|
||||||
linkContents, err := os.ReadFile(filepath)
|
linkContents, err := os.ReadFile(filepath)
|
||||||
|
|
||||||
if !util.IsText(linkContents) {
|
contentType := http.DetectContentType(linkContents)
|
||||||
|
// Check if the MIME type starts with "text/"
|
||||||
|
if !strings.HasPrefix(contentType, "text/") {
|
||||||
|
log.Debugf(nil, "Ignoring link to file %q: detected content type %v", filepath, contentType)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,7 +113,7 @@ func resolveLink(
|
|||||||
|
|
||||||
// This helps to determine if found link points to file that's
|
// This helps to determine if found link points to file that's
|
||||||
// not markdown or have mark required metadata
|
// not markdown or have mark required metadata
|
||||||
linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceFromCli, titleFromH1, parents, titleAppendGeneratedHash)
|
linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceFromCli, titleFromH1, titleFromFilename, filepath, parents, titleAppendGeneratedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
err,
|
err,
|
||||||
|
|||||||
@ -14,6 +14,9 @@ func ResolvePage(
|
|||||||
api *confluence.API,
|
api *confluence.API,
|
||||||
meta *metadata.Meta,
|
meta *metadata.Meta,
|
||||||
) (*confluence.PageInfo, *confluence.PageInfo, error) {
|
) (*confluence.PageInfo, *confluence.PageInfo, error) {
|
||||||
|
if meta == nil {
|
||||||
|
return nil, nil, karma.Format(nil, "metadata is empty")
|
||||||
|
}
|
||||||
page, err := api.FindPage(meta.Space, meta.Title, meta.Type)
|
page, err := api.FindPage(meta.Space, meta.Title, meta.Type)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, karma.Format(
|
return nil, nil, karma.Format(
|
||||||
|
|||||||
@ -11,7 +11,6 @@ type ConfluenceIDs struct {
|
|||||||
Values map[string]bool
|
Values map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// https://github.com/yuin/goldmark/blob/d9c03f07f08c2d36f23afe52dda865f05320ac86/parser/parser.go#L75
|
// https://github.com/yuin/goldmark/blob/d9c03f07f08c2d36f23afe52dda865f05320ac86/parser/parser.go#L75
|
||||||
func (s *ConfluenceIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
func (s *ConfluenceIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
||||||
value = util.TrimLeftSpace(value)
|
value = util.TrimLeftSpace(value)
|
||||||
|
|||||||
@ -3,11 +3,14 @@ package renderer
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
|
"github.com/kovetskiy/mark/d2"
|
||||||
"github.com/kovetskiy/mark/mermaid"
|
"github.com/kovetskiy/mark/mermaid"
|
||||||
"github.com/kovetskiy/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
"github.com/kovetskiy/mark/types"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
@ -19,8 +22,7 @@ import (
|
|||||||
type ConfluenceFencedCodeBlockRenderer struct {
|
type ConfluenceFencedCodeBlockRenderer struct {
|
||||||
html.Config
|
html.Config
|
||||||
Stdlib *stdlib.Lib
|
Stdlib *stdlib.Lib
|
||||||
MermaidProvider string
|
MarkConfig types.MarkConfig
|
||||||
MermaidScale float64
|
|
||||||
Attachments attachment.Attacher
|
Attachments attachment.Attacher
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,12 +33,11 @@ var reBlockDetails = regexp.MustCompile(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
|
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
|
||||||
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, mermaidProvider string, mermaidScale float64, opts ...html.Option) renderer.NodeRenderer {
|
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, cfg types.MarkConfig, opts ...html.Option) renderer.NodeRenderer {
|
||||||
return &ConfluenceFencedCodeBlockRenderer{
|
return &ConfluenceFencedCodeBlockRenderer{
|
||||||
Config: html.NewConfig(),
|
Config: html.NewConfig(),
|
||||||
Stdlib: stdlib,
|
Stdlib: stdlib,
|
||||||
MermaidProvider: mermaidProvider,
|
MarkConfig: cfg,
|
||||||
MermaidScale: mermaidScale,
|
|
||||||
Attachments: attachments,
|
Attachments: attachments,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,6 +108,7 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
|
|||||||
collapse = false
|
collapse = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var i int
|
var i int
|
||||||
if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
|
if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
|
||||||
linenumbers = i > 0
|
linenumbers = i > 0
|
||||||
@ -126,8 +128,39 @@ func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.Bu
|
|||||||
lval = append(lval, line.Value(source)...)
|
lval = append(lval, line.Value(source)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if lang == "mermaid" && r.MermaidProvider == "mermaid-go" {
|
if lang == "d2" && slices.Contains(r.MarkConfig.Features, "d2") {
|
||||||
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MermaidScale)
|
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") {
|
||||||
|
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MarkConfig.MermaidScale)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf(nil, "error: %v", err)
|
log.Debugf(nil, "error: %v", err)
|
||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
|
|||||||
@ -26,7 +26,7 @@ func (r *ConfluenceLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegi
|
|||||||
// renderLink renders links specifically for confluence
|
// renderLink renders links specifically for confluence
|
||||||
func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
n := node.(*ast.Link)
|
n := node.(*ast.Link)
|
||||||
if string(n.Destination[0:3]) == "ac:" {
|
if len(n.Destination) >= 3 && string(n.Destination[0:3]) == "ac:" {
|
||||||
if entering {
|
if entering {
|
||||||
_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
|
_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
153
renderer/mkDocsAdmonition.go
Normal file
153
renderer/mkDocsAdmonition.go
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
package renderer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
parser "github.com/stefanfritsch/goldmark-admonitions"
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/renderer"
|
||||||
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HeadingAttributeFilter defines attribute names which heading elements can have
|
||||||
|
var MkDocsAdmonitionAttributeFilter = html.GlobalAttributeFilter
|
||||||
|
|
||||||
|
// A Renderer struct is an implementation of renderer.NodeRenderer that renders
|
||||||
|
// nodes as (X)HTML.
|
||||||
|
type ConfluenceMkDocsAdmonitionRenderer struct {
|
||||||
|
html.Config
|
||||||
|
LevelMap MkDocsAdmonitionLevelMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
|
||||||
|
func NewConfluenceMkDocsAdmonitionRenderer(opts ...html.Option) renderer.NodeRenderer {
|
||||||
|
return &ConfluenceMkDocsAdmonitionRenderer{
|
||||||
|
Config: html.NewConfig(),
|
||||||
|
LevelMap: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
|
||||||
|
func (r *ConfluenceMkDocsAdmonitionRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||||
|
reg.Register(parser.KindAdmonition, r.renderMkDocsAdmonition)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define MkDocsAdmonitionType enum
|
||||||
|
type MkDocsAdmonitionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AInfo MkDocsAdmonitionType = iota
|
||||||
|
ANote
|
||||||
|
AWarn
|
||||||
|
ATip
|
||||||
|
ANone
|
||||||
|
)
|
||||||
|
|
||||||
|
func (t MkDocsAdmonitionType) String() string {
|
||||||
|
return []string{"info", "note", "warning", "tip", "none"}[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
type MkDocsAdmonitionLevelMap map[ast.Node]int
|
||||||
|
|
||||||
|
func (m MkDocsAdmonitionLevelMap) Level(node ast.Node) int {
|
||||||
|
return m[node]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMkDocsAdmonitionType(node ast.Node) MkDocsAdmonitionType {
|
||||||
|
n, ok := node.(*parser.Admonition)
|
||||||
|
if !ok {
|
||||||
|
return ANone
|
||||||
|
}
|
||||||
|
|
||||||
|
switch string(n.AdmonitionClass) {
|
||||||
|
case "info":
|
||||||
|
return AInfo
|
||||||
|
case "note":
|
||||||
|
return ANote
|
||||||
|
case "warning":
|
||||||
|
return AWarn
|
||||||
|
case "tip":
|
||||||
|
return ATip
|
||||||
|
default:
|
||||||
|
return ANone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateMkDocsAdmonitionLevel walks a given node and returns a map of blockquote levels
|
||||||
|
func GenerateMkDocsAdmonitionLevel(someNode ast.Node) MkDocsAdmonitionLevelMap {
|
||||||
|
|
||||||
|
// We define state variable that tracks BlockQuote level while we walk the tree
|
||||||
|
admonitionLevel := 0
|
||||||
|
AdmonitionLevelMap := make(map[ast.Node]int)
|
||||||
|
|
||||||
|
rootNode := someNode
|
||||||
|
for rootNode.Parent() != nil {
|
||||||
|
rootNode = rootNode.Parent()
|
||||||
|
}
|
||||||
|
_ = ast.Walk(rootNode, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
if node.Kind() == ast.KindBlockquote && entering {
|
||||||
|
AdmonitionLevelMap[node] = admonitionLevel
|
||||||
|
admonitionLevel += 1
|
||||||
|
}
|
||||||
|
if node.Kind() == ast.KindBlockquote && !entering {
|
||||||
|
admonitionLevel -= 1
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
})
|
||||||
|
return AdmonitionLevelMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// renderBlockQuote will render a BlockQuote
|
||||||
|
func (r *ConfluenceMkDocsAdmonitionRenderer) renderMkDocsAdmonition(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
// Initialize BlockQuote level map
|
||||||
|
n := node.(*parser.Admonition)
|
||||||
|
if r.LevelMap == nil {
|
||||||
|
r.LevelMap = GenerateMkDocsAdmonitionLevel(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
admonitionType := ParseMkDocsAdmonitionType(node)
|
||||||
|
admonitionLevel := r.LevelMap.Level(node)
|
||||||
|
|
||||||
|
if admonitionLevel == 0 && entering && admonitionType != ANone {
|
||||||
|
prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", admonitionType)
|
||||||
|
if _, err := writer.Write([]byte(prefix)); err != nil {
|
||||||
|
return ast.WalkStop, err
|
||||||
|
}
|
||||||
|
|
||||||
|
title, _ := strconv.Unquote(string(n.Title))
|
||||||
|
if title != "" {
|
||||||
|
titleHTML := fmt.Sprintf("<p><strong>%s</strong></p>\n", title)
|
||||||
|
if _, err := writer.Write([]byte(titleHTML)); err != nil {
|
||||||
|
return ast.WalkStop, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
if admonitionLevel == 0 && !entering && admonitionType != ANone {
|
||||||
|
suffix := "</ac:rich-text-body></ac:structured-macro>\n"
|
||||||
|
if _, err := writer.Write([]byte(suffix)); err != nil {
|
||||||
|
return ast.WalkStop, err
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
|
return r.renderMkDocsAdmon(writer, source, node, entering)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ConfluenceMkDocsAdmonitionRenderer) renderMkDocsAdmon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||||
|
n := node.(*parser.Admonition)
|
||||||
|
if entering {
|
||||||
|
if n.Attributes() != nil {
|
||||||
|
_, _ = w.WriteString("<blockquote")
|
||||||
|
html.RenderAttributes(w, n, MkDocsAdmonitionAttributeFilter)
|
||||||
|
_ = w.WriteByte('>')
|
||||||
|
} else {
|
||||||
|
_, _ = w.WriteString("<blockquote>\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, _ = w.WriteString("</blockquote>\n")
|
||||||
|
}
|
||||||
|
return ast.WalkContinue, nil
|
||||||
|
}
|
||||||
@ -72,7 +72,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
|
|||||||
case html.EastAsianLineBreaksNone:
|
case html.EastAsianLineBreaksNone:
|
||||||
writeLineBreak = false
|
writeLineBreak = false
|
||||||
case html.EastAsianLineBreaksSimple:
|
case html.EastAsianLineBreaksSimple:
|
||||||
writeLineBreak = !(util.IsEastAsianWideRune(thisLastRune) && util.IsEastAsianWideRune(siblingFirstRune))
|
writeLineBreak = !util.IsEastAsianWideRune(thisLastRune) || !util.IsEastAsianWideRune(siblingFirstRune)
|
||||||
case html.EastAsianLineBreaksCSS3Draft:
|
case html.EastAsianLineBreaksCSS3Draft:
|
||||||
writeLineBreak = eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
|
writeLineBreak = eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,9 +114,8 @@ func templates(api *confluence.API) (*template.Template, error) {
|
|||||||
|
|
||||||
// This template is used for rendering code in ```
|
// This template is used for rendering code in ```
|
||||||
`ac:code`: text(
|
`ac:code`: text(
|
||||||
`<ac:structured-macro ac:name="{{ if eq .Language "mermaid" }}cloudscript-confluence-mermaid{{ else }}code{{ end }}">`,
|
`<ac:structured-macro ac:name="code">`,
|
||||||
/**/ `{{ if eq .Language "mermaid" }}<ac:parameter ac:name="showSource">true</ac:parameter>{{ else }}`,
|
/**/ `<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>`,
|
||||||
/**/ `<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>{{ end }}`,
|
|
||||||
/**/ `<ac:parameter ac:name="collapse">{{ .Collapse }}</ac:parameter>`,
|
/**/ `<ac:parameter ac:name="collapse">{{ .Collapse }}</ac:parameter>`,
|
||||||
/**/ `{{ if .Theme }}<ac:parameter ac:name="theme">{{ .Theme }}</ac:parameter>{{ end }}`,
|
/**/ `{{ if .Theme }}<ac:parameter ac:name="theme">{{ .Theme }}</ac:parameter>{{ end }}`,
|
||||||
/**/ `{{ if .Linenumbers }}<ac:parameter ac:name="linenumbers">{{ .Linenumbers }}</ac:parameter>{{ end }}`,
|
/**/ `{{ if .Linenumbers }}<ac:parameter ac:name="linenumbers">{{ .Linenumbers }}</ac:parameter>{{ end }}`,
|
||||||
@ -151,6 +150,9 @@ func templates(api *confluence.API) (*template.Template, error) {
|
|||||||
`ac:jira:ticket`: text(
|
`ac:jira:ticket`: text(
|
||||||
`<ac:structured-macro ac:name="jira">`,
|
`<ac:structured-macro ac:name="jira">`,
|
||||||
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
|
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
|
||||||
|
`{{ if .Server }}`,
|
||||||
|
`<ac:parameter ac:name="server">{{ .Server }}</ac:parameter>`,
|
||||||
|
`{{ end }}`,
|
||||||
`</ac:structured-macro>`,
|
`</ac:structured-macro>`,
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -386,7 +388,7 @@ func templates(api *confluence.API) (*template.Template, error) {
|
|||||||
`<ac:structured-macro ac:name="pagetree" ac:schema-version="1">`,
|
`<ac:structured-macro ac:name="pagetree" ac:schema-version="1">`,
|
||||||
`<ac:parameter ac:name="root">`,
|
`<ac:parameter ac:name="root">`,
|
||||||
`<ac:link>`,
|
`<ac:link>`,
|
||||||
`<ri:page ri:content-title="@self"{{ or .Title "" }}/>`,
|
`<ri:page ri:content-title="{{ or .Title "@self" }}"/>`,
|
||||||
`</ac:link>`,
|
`</ac:link>`,
|
||||||
`</ac:parameter>`,
|
`</ac:parameter>`,
|
||||||
`<ac:parameter ac:name="sort">{{ or .Sort "" }}</ac:parameter>`,
|
`<ac:parameter ac:name="sort">{{ or .Sort "" }}</ac:parameter>`,
|
||||||
|
|||||||
83
testdata/admonitions-droph1.html
vendored
Normal file
83
testdata/admonitions-droph1.html
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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>
|
||||||
|
<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="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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><strong>[!IMPORTANT]</strong></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><strong>[!CAUTION]</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Important bullet 1</li>
|
||||||
|
<li>Important bullet 2</li>
|
||||||
|
</ul>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
83
testdata/admonitions-stripnewlines.html
vendored
Normal file
83
testdata/admonitions-stripnewlines.html
vendored
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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>
|
||||||
|
<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="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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><strong>[!IMPORTANT]</strong></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><strong>[!CAUTION]</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Important bullet 1</li>
|
||||||
|
<li>Important bullet 2</li>
|
||||||
|
</ul>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
84
testdata/admonitions.html
vendored
Normal file
84
testdata/admonitions.html
vendored
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<p>a<br />
|
||||||
|
b</p>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
|
<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="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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>
|
||||||
|
<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="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
|
<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><strong>[!IMPORTANT]</strong></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><strong>[!CAUTION]</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Important bullet 1</li>
|
||||||
|
<li>Important bullet 2</li>
|
||||||
|
</ul>
|
||||||
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
74
testdata/admonitions.md
vendored
Normal file
74
testdata/admonitions.md
vendored
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Main Heading
|
||||||
|
|
||||||
|
## First Heading
|
||||||
|
|
||||||
|
!!! note "NOTES:"
|
||||||
|
1. Note number one
|
||||||
|
1. Note number two
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
a
|
||||||
|
b
|
||||||
|
|
||||||
|
**Warn (Should not be picked as blockquote type)**
|
||||||
|
|
||||||
|
## Second Heading
|
||||||
|
|
||||||
|
!!! warning "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 "TIP:"
|
||||||
|
1. Note number one
|
||||||
|
1. Note number two
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
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
|
||||||
|
|
||||||
|
!!! info "[!IMPORTANT]"
|
||||||
|
* Important bullet 1
|
||||||
|
* Important bullet 2
|
||||||
|
|
||||||
|
!!! warning "[!CAUTION]"
|
||||||
|
* Important bullet 1
|
||||||
|
* Important bullet 2
|
||||||
|
|
||||||
6
testdata/batch-tests/bad-test.md
vendored
Normal file
6
testdata/batch-tests/bad-test.md
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
## Foo
|
||||||
|
|
||||||
|
> **TL;DR:** Thingy!
|
||||||
|
> More stuff
|
||||||
|
|
||||||
|
Foo
|
||||||
6
testdata/batch-tests/errord-test.md
vendored
Normal file
6
testdata/batch-tests/errord-test.md
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
## Foo
|
||||||
|
|
||||||
|
> **TL;DR:** Thingy!
|
||||||
|
> More stuff
|
||||||
|
|
||||||
|
Foo
|
||||||
10
testdata/batch-tests/good-test.md
vendored
Normal file
10
testdata/batch-tests/good-test.md
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<!-- Space: BatchTests -->
|
||||||
|
<!-- Title: Hello World -->
|
||||||
|
<!-- Title: Good Test -->
|
||||||
|
|
||||||
|
## Foo
|
||||||
|
|
||||||
|
> **TL;DR:** Thingy!
|
||||||
|
> More stuff
|
||||||
|
|
||||||
|
Foo
|
||||||
15
testdata/batch-tests/invalid-test.md
vendored
Normal file
15
testdata/batch-tests/invalid-test.md
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# a
|
||||||
|
|
||||||
|
## b
|
||||||
|
|
||||||
|
### c
|
||||||
|
|
||||||
|
#### d
|
||||||
|
|
||||||
|
##### e
|
||||||
|
|
||||||
|
# f
|
||||||
|
|
||||||
|
## g
|
||||||
|
|
||||||
|
# This/is some_Heading.yml
|
||||||
19
testdata/batch-tests/valid-test.md
vendored
Normal file
19
testdata/batch-tests/valid-test.md
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!-- Space: BatchTests -->
|
||||||
|
<!-- Title: Hello World -->
|
||||||
|
<!-- Title: Working Test -->
|
||||||
|
|
||||||
|
# a
|
||||||
|
|
||||||
|
## b
|
||||||
|
|
||||||
|
### c
|
||||||
|
|
||||||
|
#### d
|
||||||
|
|
||||||
|
##### e
|
||||||
|
|
||||||
|
# f
|
||||||
|
|
||||||
|
## g
|
||||||
|
|
||||||
|
# This/is some_Heading.yml
|
||||||
57
testdata/codes-stripnewlines.html
vendored
57
testdata/codes-stripnewlines.html
vendored
@ -4,16 +4,65 @@
|
|||||||
<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
|
<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 ```
|
``` 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
|
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;
|
with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
|
||||||
A-->B;
|
A-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
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;
|
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</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-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
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;
|
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</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-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
B-->D;
|
||||||
C-->D;]]></ac:plain-text-body></ac:structured-macro>
|
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>
|
||||||
|
|||||||
57
testdata/codes.html
vendored
57
testdata/codes.html
vendored
@ -5,16 +5,65 @@ 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
|
<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 ```
|
``` 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
|
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;
|
with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
|
||||||
A-->B;
|
A-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
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;
|
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</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-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
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;
|
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">mermaid</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-->B;
|
||||||
A-->C;
|
A-->C;
|
||||||
B-->D;
|
B-->D;
|
||||||
C-->D;]]></ac:plain-text-body></ac:structured-macro>
|
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>
|
||||||
|
|||||||
54
testdata/codes.md
vendored
54
testdata/codes.md
vendored
@ -42,6 +42,7 @@ even more code
|
|||||||
indented code block
|
indented code block
|
||||||
with multiple lines
|
with multiple lines
|
||||||
|
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
graph TD;
|
graph TD;
|
||||||
A-->B;
|
A-->B;
|
||||||
@ -65,3 +66,56 @@ graph TD;
|
|||||||
B-->D;
|
B-->D;
|
||||||
C-->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
|
||||||
|
```
|
||||||
|
|||||||
2
testdata/links.html
vendored
2
testdata/links.html
vendored
@ -11,6 +11,8 @@
|
|||||||
<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><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 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>
|
<p>Use <a href="foo">Link [Text]</a></p>
|
||||||
|
<h2 id="Empty-link">Empty link</h2>
|
||||||
|
<p><a href=""></a></p>
|
||||||
<div class="footnotes" role="doc-endnotes">
|
<div class="footnotes" role="doc-endnotes">
|
||||||
<hr />
|
<hr />
|
||||||
<ol>
|
<ol>
|
||||||
|
|||||||
3
testdata/links.md
vendored
3
testdata/links.md
vendored
@ -24,3 +24,6 @@ Use footnotes link [^1]
|
|||||||
[^1]: a footnote link
|
[^1]: a footnote link
|
||||||
|
|
||||||
Use [Link [Text]](foo)
|
Use [Link [Text]](foo)
|
||||||
|
|
||||||
|
## Empty link
|
||||||
|
[]()
|
||||||
|
|||||||
9
types/types.go
Normal file
9
types/types.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type MarkConfig struct {
|
||||||
|
MermaidScale float64
|
||||||
|
D2Scale float64
|
||||||
|
DropFirstH1 bool
|
||||||
|
StripNewlines bool
|
||||||
|
Features []string
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -61,14 +61,14 @@ func GetCredentials(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if url.Host == "" {
|
if url.Host == "" && baseURL == "" {
|
||||||
if baseURL == "" {
|
|
||||||
return nil, errors.New(
|
return nil, errors.New(
|
||||||
"confluence base URL should be specified using -l " +
|
"confluence base URL should be specified using -l " +
|
||||||
"flag or be stored in configuration file",
|
"flag or be stored in configuration file",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
if baseURL == "" {
|
||||||
baseURL = url.Scheme + "://" + url.Host
|
baseURL = url.Scheme + "://" + url.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
514
util/cli.go
Normal file
514
util/cli.go
Normal file
@ -0,0 +1,514 @@
|
|||||||
|
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, cmd.Bool("insecure-skip-tls-verify"))
|
||||||
|
|
||||||
|
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"), cmd.Bool("title-from-filename"), file, 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: use the 'Title' header, or the --title-from-h1 / --title-from-filename flags")
|
||||||
|
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"), cmd.Bool("title-from-filename"), 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{
|
||||||
|
MermaidScale: cmd.Float("mermaid-scale"),
|
||||||
|
D2Scale: cmd.Float("d2-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{
|
||||||
|
MermaidScale: cmd.Float("mermaid-scale"),
|
||||||
|
D2Scale: cmd.Float("d2-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))
|
||||||
|
}
|
||||||
52
util/cli_test.go
Normal file
52
util/cli_test.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runWithArgs(args []string) error {
|
||||||
|
cmd := &cli.Command{
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{Name: "title-from-h1"},
|
||||||
|
&cli.BoolFlag{Name: "title-from-filename"},
|
||||||
|
},
|
||||||
|
Before: CheckMutuallyExclusiveTitleFlags,
|
||||||
|
Action: func(ctx context.Context, cmd *cli.Command) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd.Run(context.Background(), args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckMutuallyExclusiveTitleFlags(t *testing.T) {
|
||||||
|
t.Run("neither flag set", func(t *testing.T) {
|
||||||
|
err := runWithArgs([]string{"cmd"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("only title-from-h1 set", func(t *testing.T) {
|
||||||
|
err := runWithArgs([]string{"cmd", "--title-from-h1"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("only title-from-filename set", func(t *testing.T) {
|
||||||
|
err := runWithArgs([]string{"cmd", "--title-from-filename"})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("both flags set", func(t *testing.T) {
|
||||||
|
err := runWithArgs([]string{"cmd", "--title-from-h1", "--title-from-filename"})
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error, got nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
34
util/error_handler.go
Normal file
34
util/error_handler.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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...)
|
||||||
|
}
|
||||||
213
util/flags.go
Normal file
213
util/flags.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
altsrc "github.com/urfave/cli-altsrc/v3"
|
||||||
|
altsrctoml "github.com/urfave/cli-altsrc/v3/toml"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var filename string
|
||||||
|
|
||||||
|
var Flags = []cli.Flag{
|
||||||
|
&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,
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_FILES"), altsrctoml.TOML("files", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "continue-on-error",
|
||||||
|
Value: false,
|
||||||
|
Usage: "don't exit if an error occurs while processing a file, continue processing remaining files.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CONTINUE_ON_ERROR"), altsrctoml.TOML("continue-on-error", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "compile-only",
|
||||||
|
Value: false,
|
||||||
|
Usage: "show resulting HTML and don't update Confluence page content.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_COMPILE_ONLY"), altsrctoml.TOML("compile-only", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "dry-run",
|
||||||
|
Value: false,
|
||||||
|
Usage: "resolve page and ancestry, show resulting HTML and exit.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_DRY_RUN"), altsrctoml.TOML("dry-run", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_EDIT_LOCK"), altsrctoml.TOML("edit-lock", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "drop-h1",
|
||||||
|
Value: false,
|
||||||
|
Usage: "don't include the first H1 heading in Confluence output.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_DROP_H1"), altsrctoml.TOML("drop-h1", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "strip-linebreaks",
|
||||||
|
Value: false,
|
||||||
|
Aliases: []string{"L"},
|
||||||
|
Usage: "remove linebreaks inside of tags, to accommodate non-standard Confluence behavior",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_STRIP_LINEBREAKS"), altsrctoml.TOML("strip-linebreaks", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "title-from-h1",
|
||||||
|
Value: false,
|
||||||
|
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. Mutually exclusive with --title-from-filename.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TITLE_FROM_H1"), altsrctoml.TOML("title-from-h1", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "title-from-filename",
|
||||||
|
Value: false,
|
||||||
|
Usage: "use the filename (without extension) as the Confluence page title if no explicit page title is set in the metadata. Mutually exclusive with --title-from-h1.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TITLE_FROM_FILENAME"), altsrctoml.TOML("title-from-filename", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "title-append-generated-hash",
|
||||||
|
Value: false,
|
||||||
|
Usage: "appends a short hash generated from the path of the page (space, parents, and title) to the title",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TITLE_APPEND_GENERATED_HASH"), altsrctoml.TOML("title-append-generated-hash", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "minor-edit",
|
||||||
|
Value: false,
|
||||||
|
Usage: "don't send notifications while updating Confluence page.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_MINOR_EDIT"), altsrctoml.TOML("minor-edit", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "version-message",
|
||||||
|
Value: "",
|
||||||
|
Usage: "add a message to the page version, to explain the edit (default: \"\")",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_VERSION_MESSAGE"), altsrctoml.TOML("version-message", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "color",
|
||||||
|
Value: "auto",
|
||||||
|
Usage: "display logs in color. Possible values: auto, never.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_COLOR"),
|
||||||
|
altsrctoml.TOML("color", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "log-level",
|
||||||
|
Value: "info",
|
||||||
|
Usage: "set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_LOG_LEVEL"), altsrctoml.TOML("log-level", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "username",
|
||||||
|
Aliases: []string{"u"},
|
||||||
|
Value: "",
|
||||||
|
Usage: "use specified username for updating Confluence page.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_USERNAME"),
|
||||||
|
altsrctoml.TOML("username", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PASSWORD"), altsrctoml.TOML("password", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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).",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TARGET_URL"), altsrctoml.TOML("target-url", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "base-url",
|
||||||
|
Aliases: []string{"b"},
|
||||||
|
Value: "",
|
||||||
|
Usage: "base URL for Confluence. Alternative option for base_url config field.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_BASE_URL"),
|
||||||
|
altsrctoml.TOML("base-url", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "config",
|
||||||
|
Aliases: []string{"c"},
|
||||||
|
Value: ConfigFilePath(),
|
||||||
|
Usage: "use the specified configuration file.",
|
||||||
|
TakesFile: true,
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CONFIG")),
|
||||||
|
Destination: &filename,
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "ci",
|
||||||
|
Value: false,
|
||||||
|
Usage: "run on CI mode. It won't fail if files are not found.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CI"), altsrctoml.TOML("ci", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_SPACE"), altsrctoml.TOML("space", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PARENTS"), altsrctoml.TOML("parents", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "parents-delimiter",
|
||||||
|
Value: "/",
|
||||||
|
Usage: "The delimiter used for the parents list",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PARENTS_DELIMITER"), altsrctoml.TOML("parents-delimiter", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.FloatFlag{
|
||||||
|
Name: "mermaid-scale",
|
||||||
|
Value: 1.0,
|
||||||
|
Usage: "defines the scaling factor for mermaid renderings.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_MERMAID_SCALE"), altsrctoml.TOML("mermaid-scale", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&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,
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_INCLUDE_PATH"), altsrctoml.TOML("include-path", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "changes-only",
|
||||||
|
Value: false,
|
||||||
|
Usage: "Avoids re-uploading pages that haven't changed since the last run.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CHANGES_ONLY"), altsrctoml.TOML("changes-only", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.FloatFlag{
|
||||||
|
Name: "d2-scale",
|
||||||
|
Value: 1.0,
|
||||||
|
Usage: "defines the scaling factor for d2 renderings.",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_D2_SCALE"), altsrctoml.TOML("d2-scale", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
|
||||||
|
&cli.StringSliceFlag{
|
||||||
|
Name: "features",
|
||||||
|
Value: []string{"mermaid"},
|
||||||
|
Usage: "Enables optional features. Current features: d2, mermaid, mkdocsadmonitions",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_FEATURES"), altsrctoml.TOML("features", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "insecure-skip-tls-verify",
|
||||||
|
Value: false,
|
||||||
|
Usage: "skip TLS certificate verification (useful for self-signed certificates)",
|
||||||
|
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_INSECURE_SKIP_TLS_VERIFY"), altsrctoml.TOML("insecure-skip-tls-verify", altsrc.NewStringPtrSourcer(&filename))),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckMutuallyExclusiveTitleFlags checks if both title-from-h1 and title-from-filename are set
|
||||||
|
func CheckMutuallyExclusiveTitleFlags(context context.Context, command *cli.Command) (context.Context, error) {
|
||||||
|
if command.Bool("title-from-h1") && command.Bool("title-from-filename") {
|
||||||
|
return context, errors.New("flags --title-from-h1 and --title-from-filename are mutually exclusive. Please specify only one")
|
||||||
|
}
|
||||||
|
return context, nil
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user