mirror of
https://github.com/kovetskiy/mark.git
synced 2025-12-13 12:17:36 +08:00
Compare commits
173 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
7206729968 | ||
|
|
7d05b6f286 | ||
|
|
1962ce7c25 | ||
|
|
4d77464f5e | ||
|
|
52d0cd94db | ||
|
|
0acd97b434 | ||
|
|
82e1879c57 | ||
|
|
bbcabbe419 | ||
|
|
4a058a0da9 | ||
|
|
4d241e069a | ||
|
|
060a4ee100 | ||
|
|
3d96781f47 | ||
|
|
649c20d4f2 | ||
|
|
876626098b | ||
|
|
e7a3877ded | ||
|
|
82aebec1eb | ||
|
|
0bdeb4de3d | ||
|
|
d6e932adf0 | ||
|
|
699370a677 | ||
|
|
9eb44f95fe | ||
|
|
b0f337c4a3 | ||
|
|
2af50c627f | ||
|
|
5d2adc8a23 | ||
|
|
4305957d47 | ||
|
|
33e5d1ff19 | ||
|
|
d5c41f6f1f | ||
|
|
dc8842106b |
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@v17
|
uses: DavidAnson/markdownlint-cli2-action@v21
|
||||||
|
|
||||||
# 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
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,5 @@
|
|||||||
/mark
|
/mark
|
||||||
/docker
|
/docker
|
||||||
/testdata
|
|
||||||
.idea/
|
.idea/
|
||||||
/mark.test
|
/mark.test
|
||||||
/profile.cov
|
/profile.cov
|
||||||
|
|||||||
@ -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.1 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)
|
||||||
|
|||||||
118
README.md
118
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) |
|
||||||
@ -432,10 +455,12 @@ By default, mark provides several built-in templates and macros:
|
|||||||
|
|
||||||
* template: `ac:excerpt-include` to include the excerpt from another page
|
* template: `ac:excerpt-include` to include the excerpt from another page
|
||||||
* Page: the page the excerpt should be included from
|
* Page: the page the excerpt should be included from
|
||||||
|
* Name: The specific identifier for the excerpt, allowing multiple Excerpt macros on one page to be referenced individually. If not provided, the first excerpt from the page will be used (optional, cloud only)
|
||||||
* NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
|
* NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
|
||||||
|
|
||||||
* template: `ac:excerpt` to create an excerpt and include it in the page
|
* template: `ac:excerpt` to create an excerpt and include it in the page
|
||||||
* Excerpt: The text you want to include
|
* Excerpt: The text you want to include
|
||||||
|
* Name: Allows you to identify this macro so that you can add multiple Excerpt macros to one page and use a specific one on another page using the Excerpt Include macro (optional, cloud only)
|
||||||
* OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
|
* OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
|
||||||
* Hidden: Hide the excerpt content (optional, default: false)
|
* Hidden: Hide the excerpt content (optional, default: false)
|
||||||
|
|
||||||
@ -711,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
|
||||||
|
|
||||||
@ -775,36 +815,41 @@ USAGE:
|
|||||||
mark [global options]
|
mark [global options]
|
||||||
|
|
||||||
VERSION:
|
VERSION:
|
||||||
11.1.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]
|
||||||
--minor-edit don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT]
|
--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]
|
||||||
--version-message value add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
|
--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]
|
||||||
--color value display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
|
--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]
|
||||||
--debug enable debug logs. (default: false) [$MARK_DEBUG]
|
--minor-edit don't send notifications while updating Confluence page. [$MARK_MINOR_EDIT]
|
||||||
--trace enable trace logs. (default: false) [$MARK_TRACE]
|
--version-message string add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
|
||||||
--username value, -u value use specified username for updating Confluence page. [$MARK_USERNAME]
|
--color string display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
|
||||||
--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]
|
--log-level string set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL]
|
||||||
--target-url value, -l value edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
|
--username string, -u string use specified username for updating Confluence page. [$MARK_USERNAME]
|
||||||
--base-url value, -b value, --base_url value base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_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]
|
||||||
--config value, -c value use the specified configuration file. (default: System specific) [$MARK_CONFIG]
|
--target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
|
||||||
--ci run on CI mode. It won't fail if files are not found. (default: false) [$MARK_CI]
|
--base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
|
||||||
--space value use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
|
--config string, -c string use the specified configuration file. (default: "$HOME/.config/mark.toml") [$MARK_CONFIG]
|
||||||
--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]
|
--ci run on CI mode. It won't fail if files are not found. [$MARK_CI]
|
||||||
--parents-delimiter value The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
|
--space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
|
||||||
--mermaid-provider value defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go. (default: "cloudscript") [$MARK_MERMAID_PROVIDER]
|
--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-scale value defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
|
--parents-delimiter string The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
|
||||||
--include-path value Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
|
--mermaid-scale float defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
|
||||||
|
--include-path string Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
|
||||||
|
--changes-only Avoids re-uploading pages that haven't changed since the last run. [$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
|
||||||
```
|
```
|
||||||
@ -825,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
|
||||||
@ -866,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
|
||||||
@ -896,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:
|
||||||
|
|
||||||
@ -974,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>
|
||||||
|
|||||||
@ -11,8 +11,8 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/vfs"
|
"github.com/kovetskiy/mark/vfs"
|
||||||
"github.com/reconquest/karma-go"
|
"github.com/reconquest/karma-go"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
)
|
)
|
||||||
@ -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.
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -599,8 +625,8 @@ func (api *API) AddPageLabels(page *PageInfo, newLabels []string) (*LabelInfo, e
|
|||||||
func (api *API) DeletePageLabel(page *PageInfo, label string) (*LabelInfo, error) {
|
func (api *API) DeletePageLabel(page *PageInfo, label string) (*LabelInfo, error) {
|
||||||
|
|
||||||
request, err := api.rest.Res(
|
request, err := api.rest.Res(
|
||||||
"content/"+page.ID+"/label/"+label, &LabelInfo{},
|
"content/"+page.ID+"/label", &LabelInfo{},
|
||||||
).Delete()
|
).SetQuery(map[string]string{"name": label}).Delete()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
58
go.mod
58
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.6.1
|
github.com/bmatcuk/doublestar/v4 v4.9.1
|
||||||
github.com/dreampuf/mermaid.go v0.0.18
|
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.9.0
|
github.com/stefanfritsch/goldmark-admonitions v1.1.1
|
||||||
github.com/urfave/cli/v2 v2.27.4
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/yuin/goldmark v1.7.4
|
github.com/urfave/cli-altsrc/v3 v3.1.0
|
||||||
golang.org/x/tools v0.25.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-20240810084448-b931b754e476 // indirect
|
github.com/PuerkitoBio/goquery v1.10.0 // indirect
|
||||||
github.com/chromedp/chromedp v0.10.0 // indirect
|
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||||
github.com/chromedp/sysutil v1.0.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||||
|
github.com/chromedp/sysutil v1.1.0 // 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.25.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
|
||||||
)
|
)
|
||||||
|
|||||||
164
go.sum
164
go.sum
@ -1,43 +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.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I=
|
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.6.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-20240801214329-3f85d328b335/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476 h1:VnjHsRXCRti7Av7E+j4DCha3kf68echfDzQ+wD11SBU=
|
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
|
||||||
github.com/chromedp/cdproto v0.0.0-20240810084448-b931b754e476/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg=
|
||||||
github.com/chromedp/chromedp v0.10.0 h1:bRclRYVpMm/UVD76+1HcRW9eV3l58rFfy7AdBvKab1E=
|
github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk=
|
||||||
github.com/chromedp/chromedp v0.10.0/go.mod h1:ei/1ncZIqXX1YnAYDkxhD4gzBgavMEUu7JCKvztdomE=
|
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||||
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
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/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
|
||||||
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.18 h1:PTDbfWrsi9JRIs1ocq65ajj8/eJnD1oVRsUKDEOmwKk=
|
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
|
||||||
github.com/dreampuf/mermaid.go v0.0.18/go.mod h1:I8KXPKfpH96jHVi1BygPq3pRn1tk970UhnEYbgfRI5A=
|
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=
|
||||||
@ -49,30 +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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stefanfritsch/goldmark-admonitions v1.1.1 h1:SncsICdQrIYYaq02Ta+zyc9gNmMfYqQH2qwLSCJYxA4=
|
||||||
github.com/stretchr/testify v1.9.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.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
|
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.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo=
|
||||||
github.com/yuin/goldmark v1.7.4/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.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
|
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=
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/includes"
|
"github.com/kovetskiy/mark/includes"
|
||||||
"github.com/reconquest/karma-go"
|
"github.com/reconquest/karma-go"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
"github.com/reconquest/regexputil-go"
|
"github.com/reconquest/regexputil-go"
|
||||||
605
main.go
605
main.go
@ -1,613 +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/pkg/confluence"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/includes"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/macro"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/vfs"
|
|
||||||
"github.com/reconquest/karma-go"
|
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/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.1.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_LINEBREAK"},
|
|
||||||
}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "title-from-h1",
|
|
||||||
Value: false,
|
|
||||||
Aliases: []string{"h1_title"},
|
|
||||||
Usage: "extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata.",
|
|
||||||
EnvVars: []string{"MARK_H1_TITLE"},
|
|
||||||
}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "minor-edit",
|
|
||||||
Value: false,
|
|
||||||
Usage: "don't send notifications while updating Confluence page.",
|
|
||||||
EnvVars: []string{"MARK_MINOR_EDIT"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "version-message",
|
|
||||||
Value: "",
|
|
||||||
Usage: "add a message to the page version, to explain the edit (default: \"\")",
|
|
||||||
EnvVars: []string{"MARK_VERSION_MESSAGE"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "color",
|
|
||||||
Value: "auto",
|
|
||||||
Usage: "display logs in color. Possible values: auto, never.",
|
|
||||||
EnvVars: []string{"MARK_COLOR"},
|
|
||||||
}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "debug",
|
|
||||||
Value: false,
|
|
||||||
Usage: "enable debug logs.",
|
|
||||||
EnvVars: []string{"MARK_DEBUG"},
|
|
||||||
}),
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "trace",
|
|
||||||
Value: false,
|
|
||||||
Usage: "enable trace logs.",
|
|
||||||
EnvVars: []string{"MARK_TRACE"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "username",
|
|
||||||
Aliases: []string{"u"},
|
|
||||||
Value: "",
|
|
||||||
Usage: "use specified username for updating Confluence page.",
|
|
||||||
EnvVars: []string{"MARK_USERNAME"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "password",
|
|
||||||
Aliases: []string{"p"},
|
|
||||||
Value: "",
|
|
||||||
Usage: "use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication.",
|
|
||||||
EnvVars: []string{"MARK_PASSWORD"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "target-url",
|
|
||||||
Aliases: []string{"l"},
|
|
||||||
Value: "",
|
|
||||||
Usage: "edit specified Confluence page. If -l is not specified, file should contain metadata (see above).",
|
|
||||||
EnvVars: []string{"MARK_TARGET_URL"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "base-url",
|
|
||||||
Aliases: []string{"b", "base_url"},
|
|
||||||
Value: "",
|
|
||||||
Usage: "base URL for Confluence. Alternative option for base_url config field.",
|
|
||||||
EnvVars: []string{"MARK_BASE_URL"},
|
|
||||||
}),
|
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "config",
|
|
||||||
Aliases: []string{"c"},
|
|
||||||
Value: configFilePath(),
|
|
||||||
Usage: "use the specified configuration file.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVars: []string{"MARK_CONFIG"},
|
|
||||||
},
|
|
||||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
|
||||||
Name: "ci",
|
|
||||||
Value: false,
|
|
||||||
Usage: "run on CI mode. It won't fail if files are not found.",
|
|
||||||
EnvVars: []string{"MARK_CI"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "space",
|
|
||||||
Value: "",
|
|
||||||
Usage: "use specified space key. If the space key is not specified, it must be set in the page metadata.",
|
|
||||||
EnvVars: []string{"MARK_SPACE"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "parents",
|
|
||||||
Value: "",
|
|
||||||
Usage: "A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself.",
|
|
||||||
EnvVars: []string{"MARK_PARENTS"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "parents-delimiter",
|
|
||||||
Value: "/",
|
|
||||||
Usage: "The delimiter used for the parents list",
|
|
||||||
EnvVars: []string{"MARK_PARENTS_DELIMITER"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "mermaid-provider",
|
|
||||||
Value: "cloudscript",
|
|
||||||
Usage: "defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go.",
|
|
||||||
EnvVars: []string{"MARK_MERMAID_PROVIDER"},
|
|
||||||
}),
|
|
||||||
altsrc.NewFloat64Flag(&cli.Float64Flag{
|
|
||||||
Name: "mermaid-scale",
|
|
||||||
Value: 1.0,
|
|
||||||
Usage: "defines the scaling factor for mermaid renderings.",
|
|
||||||
EnvVars: []string{"MARK_MERMAID_SCALE"},
|
|
||||||
}),
|
|
||||||
altsrc.NewStringFlag(&cli.StringFlag{
|
|
||||||
Name: "include-path",
|
|
||||||
Value: "",
|
|
||||||
Usage: "Path for shared includes, used as a fallback if the include doesn't exist in the current directory.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVars: []string{"MARK_INCLUDE_PATH"},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
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 := mark.ExtractMeta(markdown, cCtx.String("space"), cCtx.Bool("title-from-h1"), parents)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pageID != "" && meta != nil {
|
|
||||||
log.Warning(
|
|
||||||
`specified file contains metadata, ` +
|
|
||||||
`but it will be ignored due specified command line URL`,
|
|
||||||
)
|
|
||||||
|
|
||||||
meta = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if pageID == "" && meta == nil {
|
|
||||||
log.Fatal(
|
|
||||||
`specified file doesn't contain metadata ` +
|
|
||||||
`and URL is not specified via command line ` +
|
|
||||||
`or doesn't contain pageId GET-parameter`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.Space == "" {
|
|
||||||
log.Fatal(
|
|
||||||
"space is not set ('Space' header is not set and '--space' option is not set)",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if meta.Title == "" {
|
|
||||||
log.Fatal(
|
|
||||||
`page title is not set ('Title' header is not set ` +
|
|
||||||
`and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file)`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
stdlib, err := stdlib.New(api)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
templates := stdlib.Templates
|
|
||||||
|
|
||||||
var recurse bool
|
|
||||||
|
|
||||||
for {
|
|
||||||
templates, markdown, recurse, err = includes.ProcessIncludes(
|
|
||||||
filepath.Dir(file),
|
|
||||||
cCtx.String("include-path"),
|
|
||||||
markdown,
|
|
||||||
templates,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !recurse {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macros, markdown, err := macro.ExtractMacros(
|
|
||||||
filepath.Dir(file),
|
|
||||||
cCtx.String("include-path"),
|
|
||||||
markdown,
|
|
||||||
templates,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
macros = append(macros, stdlib.Macros...)
|
|
||||||
|
|
||||||
for _, macro := range macros {
|
|
||||||
markdown, err = macro.Apply(markdown)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
links, err := mark.ResolveRelativeLinks(api, meta, markdown, filepath.Dir(file), cCtx.String("space"), cCtx.Bool("title-from-h1"), parents)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to resolve relative links")
|
|
||||||
}
|
|
||||||
|
|
||||||
markdown = mark.SubstituteLinks(markdown, links)
|
|
||||||
|
|
||||||
if cCtx.Bool("dry-run") {
|
|
||||||
_, _, err := mark.ResolvePage(cCtx.Bool("dry-run"), api, meta)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to resolve page location")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cCtx.Bool("compile-only") || cCtx.Bool("dry-run") {
|
|
||||||
if cCtx.Bool("drop-h1") {
|
|
||||||
log.Info(
|
|
||||||
"the leading H1 heading will be excluded from the Confluence output",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
html, _ := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks"))
|
|
||||||
fmt.Println(html)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
var target *confluence.PageInfo
|
|
||||||
|
|
||||||
if meta != nil {
|
|
||||||
parent, page, err := mark.ResolvePage(cCtx.Bool("dry-run"), api, meta)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(
|
|
||||||
karma.Describe("title", meta.Title).Reason(err),
|
|
||||||
"unable to resolve %s",
|
|
||||||
meta.Type,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if page == nil {
|
|
||||||
page, err = api.CreatePage(
|
|
||||||
meta.Space,
|
|
||||||
meta.Type,
|
|
||||||
parent,
|
|
||||||
meta.Title,
|
|
||||||
``,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(
|
|
||||||
err,
|
|
||||||
"can't create %s %q",
|
|
||||||
meta.Type,
|
|
||||||
meta.Title,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// (issues/139): A delay between the create and update call
|
|
||||||
// helps mitigate a 409 conflict that can occur when attempting
|
|
||||||
// to update a page just after it was created.
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
target = page
|
|
||||||
} else {
|
|
||||||
if pageID == "" {
|
|
||||||
log.Fatalf(nil, "URL should provide 'pageId' GET-parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
page, err := api.GetPageByID(pageID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to retrieve page by id")
|
|
||||||
}
|
|
||||||
|
|
||||||
target = page
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve attachments created from <!-- Attachment: --> directive
|
|
||||||
localAttachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(file), meta.Attachments)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to locate attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
attaches, err := attachment.ResolveAttachments(
|
|
||||||
api,
|
|
||||||
target,
|
|
||||||
localAttachments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to create/update attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
markdown = attachment.CompileAttachmentLinks(markdown, attaches)
|
|
||||||
|
|
||||||
if cCtx.Bool("drop-h1") {
|
|
||||||
log.Info(
|
|
||||||
"the leading H1 heading will be excluded from the Confluence output",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
html, inlineAttachments := mark.CompileMarkdown(markdown, stdlib, file, cCtx.String("mermaid-provider"), cCtx.Float64("mermaid-scale"), cCtx.Bool("drop-h1"), cCtx.Bool("strip-linebreaks"))
|
|
||||||
|
|
||||||
// Resolve attachements detected from markdown
|
|
||||||
_, err = attachment.ResolveAttachments(
|
|
||||||
api,
|
|
||||||
target,
|
|
||||||
inlineAttachments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf(err, "unable to create/update attachments")
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
var buffer bytes.Buffer
|
|
||||||
|
|
||||||
err := stdlib.Templates.ExecuteTemplate(
|
|
||||||
&buffer,
|
|
||||||
"ac:layout",
|
|
||||||
struct {
|
|
||||||
Layout string
|
|
||||||
Sidebar string
|
|
||||||
Body string
|
|
||||||
}{
|
|
||||||
Layout: meta.Layout,
|
|
||||||
Sidebar: meta.Sidebar,
|
|
||||||
Body: html,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
html = buffer.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
err = api.UpdatePage(target, html, cCtx.Bool("minor-edit"), cCtx.String("version-message"), meta.Labels, meta.ContentAppearance)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateLabels(api, target, meta)
|
|
||||||
|
|
||||||
if cCtx.Bool("edit-lock") {
|
|
||||||
log.Infof(
|
|
||||||
nil,
|
|
||||||
`edit locked on page %q by user %q to prevent manual edits`,
|
|
||||||
target.Title,
|
|
||||||
username,
|
|
||||||
)
|
|
||||||
|
|
||||||
err := api.RestrictPageUpdates(target, username)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateLabels(api *confluence.API, target *confluence.PageInfo, meta *mark.Meta) {
|
|
||||||
|
|
||||||
labelInfo, err := api.GetPageLabels(target, "global")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("Page Labels:")
|
|
||||||
log.Debug(labelInfo.Labels)
|
|
||||||
|
|
||||||
log.Debug("Meta Labels:")
|
|
||||||
log.Debug(meta.Labels)
|
|
||||||
|
|
||||||
delLabels := determineLabelsToRemove(labelInfo, meta)
|
|
||||||
log.Debug("Del Labels:")
|
|
||||||
log.Debug(delLabels)
|
|
||||||
|
|
||||||
addLabels := determineLabelsToAdd(meta, labelInfo)
|
|
||||||
log.Debug("Add Labels:")
|
|
||||||
log.Debug(addLabels)
|
|
||||||
|
|
||||||
if len(addLabels) > 0 {
|
|
||||||
_, err = api.AddPageLabels(target, addLabels)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, label := range delLabels {
|
|
||||||
_, err = api.DeletePageLabel(target, label)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page has label but label not in Metadata
|
|
||||||
func determineLabelsToRemove(labelInfo *confluence.LabelInfo, meta *mark.Meta) []string {
|
|
||||||
var labels []string
|
|
||||||
for _, label := range labelInfo.Labels {
|
|
||||||
if !slices.ContainsFunc(meta.Labels, func(metaLabel string) bool {
|
|
||||||
return strings.EqualFold(metaLabel, label.Name)
|
|
||||||
}) {
|
|
||||||
labels = append(labels, label.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata has label but Page does not have it
|
|
||||||
func determineLabelsToAdd(meta *mark.Meta, labelInfo *confluence.LabelInfo) []string {
|
|
||||||
var labels []string
|
|
||||||
for _, metaLabel := range meta.Labels {
|
|
||||||
if !slices.ContainsFunc(labelInfo.Labels, func(label confluence.Label) bool {
|
|
||||||
return strings.EqualFold(label.Name, metaLabel)
|
|
||||||
}) {
|
|
||||||
labels = append(labels, metaLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return labels
|
|
||||||
}
|
|
||||||
|
|
||||||
func configFilePath() string {
|
|
||||||
fp, err := os.UserConfigDir()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return filepath.Join(fp, "mark")
|
|
||||||
}
|
|
||||||
|
|||||||
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,13 +2,15 @@ package mark
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"regexp"
|
"slices"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
cparser "github.com/kovetskiy/mark/pkg/mark/parser"
|
cparser "github.com/kovetskiy/mark/parser"
|
||||||
crenderer "github.com/kovetskiy/mark/pkg/mark/renderer"
|
crenderer "github.com/kovetskiy/mark/renderer"
|
||||||
"github.com/kovetskiy/mark/pkg/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"
|
||||||
@ -23,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{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,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.
|
||||||
@ -69,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(),
|
||||||
@ -92,8 +100,10 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidPr
|
|||||||
html.WithXHTML(),
|
html.WithXHTML(),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
ctx := parser.NewContext(parser.WithIDs(&cparser.ConfluenceIDs{Values: map[string]bool{}}))
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := converter.Convert(markdown, &buf)
|
err := converter.Convert(markdown, &buf, parser.WithContext(ctx))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@ -104,16 +114,4 @@ func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, mermaidPr
|
|||||||
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
|
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
|
||||||
|
|
||||||
return string(html), confluenceExtension.Attachments
|
return string(html), confluenceExtension.Attachments
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractDocumentLeadingH1 will extract leading H1 heading
|
|
||||||
func ExtractDocumentLeadingH1(markdown []byte) string {
|
|
||||||
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
|
|
||||||
groups := h1.FindSubmatch(markdown)
|
|
||||||
if groups == nil {
|
|
||||||
return ""
|
|
||||||
} else {
|
|
||||||
return string(groups[1])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
185
markdown/markdown_test.go
Normal file
185
markdown/markdown_test.go
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package mark_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
mark "github.com/kovetskiy/mark/markdown"
|
||||||
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
"github.com/kovetskiy/mark/types"
|
||||||
|
"github.com/kovetskiy/mark/util"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/urfave/cli/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
|
||||||
|
t.Helper()
|
||||||
|
basename := filepath.Base(filename)
|
||||||
|
testname := strings.TrimSuffix(basename, ".md")
|
||||||
|
htmlname := filepath.Join(filepath.Dir(filename), testname+variant+".html")
|
||||||
|
|
||||||
|
markdown, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
html, err := os.ReadFile(htmlname)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return markdown, htmlname, html
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileMarkdown(t *testing.T) {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := path.Join(path.Dir(filename), "..")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
test := assert.New(t)
|
||||||
|
|
||||||
|
testcases, err := filepath.Glob("testdata/*.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range testcases {
|
||||||
|
fmt.Printf("Testing: %v\n", filename)
|
||||||
|
lib, err := stdlib.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
markdown, htmlname, html := loadData(t, filename, "")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileMarkdownDropH1(t *testing.T) {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := path.Join(path.Dir(filename), "..")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
test := assert.New(t)
|
||||||
|
|
||||||
|
testcases, err := filepath.Glob("testdata/*.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range testcases {
|
||||||
|
lib, err := stdlib.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var variant string
|
||||||
|
switch filename {
|
||||||
|
case "testdata/quotes.md", "testdata/header.md", "testdata/admonitions.md":
|
||||||
|
variant = "-droph1"
|
||||||
|
default:
|
||||||
|
variant = ""
|
||||||
|
}
|
||||||
|
markdown, htmlname, html := loadData(t, filename, variant)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompileMarkdownStripNewlines(t *testing.T) {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := path.Join(path.Dir(filename), "..")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
test := assert.New(t)
|
||||||
|
|
||||||
|
testcases, err := filepath.Glob("testdata/*.md")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, filename := range testcases {
|
||||||
|
lib, err := stdlib.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var variant string
|
||||||
|
switch filename {
|
||||||
|
case "testdata/quotes.md", "testdata/codes.md", "testdata/newlines.md", "testdata/macro-include.md", "testdata/admonitions.md":
|
||||||
|
variant = "-stripnewlines"
|
||||||
|
default:
|
||||||
|
variant = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
markdown, htmlname, html := loadData(t, filename, variant)
|
||||||
|
|
||||||
|
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,15 +3,17 @@ package mermaid
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mermaid "github.com/dreampuf/mermaid.go"
|
mermaid "github.com/dreampuf/mermaid.go"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
"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 {
|
||||||
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,10 +22,10 @@ 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: "42",
|
Width: "87",
|
||||||
Height: "132",
|
Height: "174",
|
||||||
},
|
},
|
||||||
assert.NoError},
|
assert.NoError},
|
||||||
}
|
}
|
||||||
@ -1,12 +1,17 @@
|
|||||||
package mark
|
package metadata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"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 (
|
||||||
@ -15,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`
|
||||||
@ -29,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
|
||||||
@ -44,7 +51,7 @@ var (
|
|||||||
reHeaderPatternMacro = regexp.MustCompile(`<!-- Macro: .*`)
|
reHeaderPatternMacro = regexp.MustCompile(`<!-- Macro: .*`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []string) (*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
|
||||||
@ -77,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 {
|
||||||
@ -105,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)
|
||||||
|
|
||||||
@ -134,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{}
|
||||||
}
|
}
|
||||||
@ -150,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
|
||||||
}
|
}
|
||||||
@ -164,5 +176,40 @@ func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []s
|
|||||||
meta.Parents = append(parents, meta.Parents...)
|
meta.Parents = append(parents, meta.Parents...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deterministically generate a hash from the page's parents, space, and title
|
||||||
|
if titleAppendGeneratedHash {
|
||||||
|
path := strings.Join(append(meta.Parents, meta.Space, meta.Title), "/")
|
||||||
|
pathHash := sha256.Sum256([]byte(path))
|
||||||
|
// postfix is an 8-character hexadecimal string representation of the first 4 out of 32 bytes of the hash
|
||||||
|
meta.Title = fmt.Sprintf("%s - %x", meta.Title, pathHash[0:4])
|
||||||
|
log.Debugf(
|
||||||
|
nil,
|
||||||
|
"appended hash to page title: %s",
|
||||||
|
meta.Title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func ExtractDocumentLeadingH1(markdown []byte) string {
|
||||||
|
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
|
||||||
|
groups := h1.FindSubmatch(markdown)
|
||||||
|
if groups == nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(groups[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
62
metadata/metadata_test.go
Normal file
62
metadata/metadata_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package metadata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtractDocumentLeadingH1(t *testing.T) {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
dir := path.Join(path.Dir(filename), "..")
|
||||||
|
err := os.Chdir(dir)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filename = "testdata/header.md"
|
||||||
|
|
||||||
|
markdown, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := ExtractDocumentLeadingH1(markdown)
|
||||||
|
|
||||||
|
assert.Equal(t, "a", actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
package mark
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
"github.com/reconquest/karma-go"
|
"github.com/reconquest/karma-go"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
)
|
)
|
||||||
@ -1,17 +1,19 @@
|
|||||||
package mark
|
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/pkg/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
|
"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 {
|
||||||
@ -27,12 +29,14 @@ type markdownLink struct {
|
|||||||
|
|
||||||
func ResolveRelativeLinks(
|
func ResolveRelativeLinks(
|
||||||
api *confluence.API,
|
api *confluence.API,
|
||||||
meta *Meta,
|
meta *metadata.Meta,
|
||||||
markdown []byte,
|
markdown []byte,
|
||||||
base string,
|
base string,
|
||||||
spaceFromCli string,
|
spaceFromCli string,
|
||||||
titleFromH1 bool,
|
titleFromH1 bool,
|
||||||
|
titleFromFilename bool,
|
||||||
parents []string,
|
parents []string,
|
||||||
|
titleAppendGeneratedHash bool,
|
||||||
) ([]LinkSubstitution, error) {
|
) ([]LinkSubstitution, error) {
|
||||||
matches := parseLinks(string(markdown))
|
matches := parseLinks(string(markdown))
|
||||||
|
|
||||||
@ -45,7 +49,7 @@ func ResolveRelativeLinks(
|
|||||||
match.filename,
|
match.filename,
|
||||||
match.hash,
|
match.hash,
|
||||||
)
|
)
|
||||||
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, parents)
|
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)
|
||||||
}
|
}
|
||||||
@ -69,7 +73,9 @@ func resolveLink(
|
|||||||
link markdownLink,
|
link markdownLink,
|
||||||
spaceFromCli string,
|
spaceFromCli string,
|
||||||
titleFromH1 bool,
|
titleFromH1 bool,
|
||||||
|
titleFromFilename bool,
|
||||||
parents []string,
|
parents []string,
|
||||||
|
titleAppendGeneratedHash bool,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
var result string
|
var result string
|
||||||
|
|
||||||
@ -88,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,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 := ExtractMeta(linkContents, spaceFromCli, titleFromH1, parents)
|
linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceFromCli, titleFromH1, titleFromFilename, filepath, parents, titleAppendGeneratedHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(
|
log.Errorf(
|
||||||
err,
|
err,
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package mark
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -1,9 +1,10 @@
|
|||||||
package mark
|
package page
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
|
"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"
|
||||||
)
|
)
|
||||||
@ -11,8 +12,11 @@ import (
|
|||||||
func ResolvePage(
|
func ResolvePage(
|
||||||
dryRun bool,
|
dryRun bool,
|
||||||
api *confluence.API,
|
api *confluence.API,
|
||||||
meta *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(
|
||||||
55
parser/confluenceids.go
Normal file
55
parser/confluenceids.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/yuin/goldmark/ast"
|
||||||
|
"github.com/yuin/goldmark/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfluenceIDs struct {
|
||||||
|
Values map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/yuin/goldmark/blob/d9c03f07f08c2d36f23afe52dda865f05320ac86/parser/parser.go#L75
|
||||||
|
func (s *ConfluenceIDs) Generate(value []byte, kind ast.NodeKind) []byte {
|
||||||
|
value = util.TrimLeftSpace(value)
|
||||||
|
value = util.TrimRightSpace(value)
|
||||||
|
result := []byte{}
|
||||||
|
for i := 0; i < len(value); {
|
||||||
|
v := value[i]
|
||||||
|
l := util.UTF8Len(v)
|
||||||
|
i += int(l)
|
||||||
|
if l != 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if util.IsAlphaNumeric(v) || v == '/' || v == '_' || v == '.' {
|
||||||
|
result = append(result, v)
|
||||||
|
} else if util.IsSpace(v) || v == '-' {
|
||||||
|
result = append(result, '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
if kind == ast.KindHeading {
|
||||||
|
result = []byte("heading")
|
||||||
|
} else {
|
||||||
|
result = []byte("id")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := s.Values[util.BytesToReadOnlyString(result)]; !ok {
|
||||||
|
s.Values[util.BytesToReadOnlyString(result)] = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
for i := 1; ; i++ {
|
||||||
|
newResult := fmt.Sprintf("%s-%d", result, i)
|
||||||
|
if _, ok := s.Values[newResult]; !ok {
|
||||||
|
s.Values[newResult] = true
|
||||||
|
return []byte(newResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfluenceIDs) Put(value []byte) {
|
||||||
|
s.Values[util.BytesToReadOnlyString(value)] = true
|
||||||
|
}
|
||||||
@ -1,99 +0,0 @@
|
|||||||
package mark
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func loadData(t *testing.T, filename, variant string) ([]byte, string, []byte) {
|
|
||||||
t.Helper()
|
|
||||||
basename := filepath.Base(filename)
|
|
||||||
testname := strings.TrimSuffix(basename, ".md")
|
|
||||||
htmlname := filepath.Join(filepath.Dir(filename), testname+variant+".html")
|
|
||||||
|
|
||||||
markdown, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
html, err := os.ReadFile(htmlname)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return markdown, htmlname, html
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompileMarkdown(t *testing.T) {
|
|
||||||
test := assert.New(t)
|
|
||||||
|
|
||||||
testcases, err := filepath.Glob("testdata/*.md")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filename := range testcases {
|
|
||||||
lib, err := stdlib.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
markdown, htmlname, html := loadData(t, filename, "")
|
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, false)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompileMarkdownDropH1(t *testing.T) {
|
|
||||||
test := assert.New(t)
|
|
||||||
|
|
||||||
testcases, err := filepath.Glob("testdata/*.md")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filename := range testcases {
|
|
||||||
lib, err := stdlib.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
markdown, htmlname, html := loadData(t, filename, "-droph1")
|
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, true, false)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompileMarkdownStripNewlines(t *testing.T) {
|
|
||||||
test := assert.New(t)
|
|
||||||
|
|
||||||
testcases, err := filepath.Glob("testdata/*.md")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, filename := range testcases {
|
|
||||||
lib, err := stdlib.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
markdown, htmlname, html := loadData(t, filename, "-stripnewlines")
|
|
||||||
actual, _ := CompileMarkdown(markdown, lib, filename, "", 1.0, false, true)
|
|
||||||
test.EqualValues(string(html), actual, filename+" vs "+htmlname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExtractDocumentLeadingH1(t *testing.T) {
|
|
||||||
filename := "testdata/header.md"
|
|
||||||
|
|
||||||
markdown, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
actual := ExtractDocumentLeadingH1(markdown)
|
|
||||||
|
|
||||||
assert.Equal(t, "a", actual)
|
|
||||||
}
|
|
||||||
20
pkg/mark/testdata/codes-droph1.html
vendored
20
pkg/mark/testdata/codes-droph1.html
vendored
@ -1,20 +0,0 @@
|
|||||||
<p><code>inline</code></p>
|
|
||||||
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[some code]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[code bash]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[with a newline
|
|
||||||
]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">unknown</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[unknown code]]></ac:plain-text-body></ac:structured-macro><p>text
|
|
||||||
text 2</p>
|
|
||||||
<ac:structured-macro ac:name="code"><ac:parameter ac:name="language">unknown</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[unknown code 2]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">sh</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:parameter ac:name="title">A b c</ac:parameter><ac:plain-text-body><![CDATA[no-collapse-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">bash</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:parameter ac:name="title">A b c</ac:parameter><ac:plain-text-body><![CDATA[collapse-and-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">c</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:plain-text-body><![CDATA[collapse-no-title]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language">nested</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[code
|
|
||||||
``` more code ```
|
|
||||||
even more code]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="code"><ac:parameter ac:name="language"></ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[indented code block
|
|
||||||
with multiple lines]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">true</ac:parameter><ac:parameter ac:name="title">my mermaid graph</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;]]></ac:plain-text-body></ac:structured-macro><ac:structured-macro ac:name="cloudscript-confluence-mermaid"><ac:parameter ac:name="showSource">true</ac:parameter><ac:parameter ac:name="collapse">false</ac:parameter><ac:parameter ac:name="title">my mermaid graph</ac:parameter><ac:plain-text-body><![CDATA[graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;]]></ac:plain-text-body></ac:structured-macro>
|
|
||||||
67
pkg/mark/testdata/codes.md
vendored
67
pkg/mark/testdata/codes.md
vendored
@ -1,67 +0,0 @@
|
|||||||
`inline`
|
|
||||||
|
|
||||||
```
|
|
||||||
some code
|
|
||||||
```
|
|
||||||
```bash
|
|
||||||
code bash
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
with a newline
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
```unknown
|
|
||||||
unknown code
|
|
||||||
```
|
|
||||||
text
|
|
||||||
text 2
|
|
||||||
```unknown
|
|
||||||
unknown code 2
|
|
||||||
```
|
|
||||||
|
|
||||||
```sh title A b c
|
|
||||||
no-collapse-title
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash collapse title A b c
|
|
||||||
collapse-and-title
|
|
||||||
```
|
|
||||||
|
|
||||||
```c collapse
|
|
||||||
collapse-no-title
|
|
||||||
```
|
|
||||||
|
|
||||||
```nested
|
|
||||||
code
|
|
||||||
``` more code ```
|
|
||||||
even more code
|
|
||||||
```
|
|
||||||
|
|
||||||
indented code block
|
|
||||||
with multiple lines
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid collapse title my mermaid graph
|
|
||||||
graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;
|
|
||||||
```
|
|
||||||
|
|
||||||
```mermaid title my mermaid graph
|
|
||||||
graph TD;
|
|
||||||
A-->B;
|
|
||||||
A-->C;
|
|
||||||
B-->D;
|
|
||||||
C-->D;
|
|
||||||
```
|
|
||||||
7
pkg/mark/testdata/header-stripnewlines.html
vendored
7
pkg/mark/testdata/header-stripnewlines.html
vendored
@ -1,7 +0,0 @@
|
|||||||
<h1 id="a">a</h1>
|
|
||||||
<h2 id="b">b</h2>
|
|
||||||
<h3 id="c">c</h3>
|
|
||||||
<h4 id="d">d</h4>
|
|
||||||
<h5 id="e">e</h5>
|
|
||||||
<h1 id="f">f</h1>
|
|
||||||
<h2 id="g">g</h2>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p><a href="Page2#Page2-Releasev71-22-Feb-2018(intern)">v71</a></p>
|
|
||||||
@ -1 +0,0 @@
|
|||||||
<p><a href="Page2#Page2-Releasev71-22-Feb-2018(intern)">v71</a></p>
|
|
||||||
21
pkg/mark/testdata/links-droph1.html
vendored
21
pkg/mark/testdata/links-droph1.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<p>Use <a href="https://example.com">https://example.com</a></p>
|
|
||||||
<p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Page"/><ac:plain-text-link-body><![CDATA[page link]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="AnotherPage"/><ac:plain-text-link-body><![CDATA[AnotherPage]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p><ac:image ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
|
|
||||||
<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&key2=value2"/></ac:image></p>
|
|
||||||
<p><ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[My test_link]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p><ac:link><ri:page ri:content-title="test_link_link"/><ac:plain-text-link-body><![CDATA[Another [Link]]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
|
||||||
<p>Use <a href="foo">Link [Text]</a></p>
|
|
||||||
<div class="footnotes" role="doc-endnotes">
|
|
||||||
<hr />
|
|
||||||
<ol>
|
|
||||||
<li id="fn:1">
|
|
||||||
<p>a footnote link <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
21
pkg/mark/testdata/links-stripnewlines.html
vendored
21
pkg/mark/testdata/links-stripnewlines.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<p>Use <a href="https://example.com">https://example.com</a></p>
|
|
||||||
<p>Use <ac:rich-text-body>aaa</ac:rich-text-body></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Page"/><ac:plain-text-link-body><![CDATA[page link]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="AnotherPage"/><ac:plain-text-link-body><![CDATA[AnotherPage]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Another Page"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[Another Page]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use <ac:link><ri:page ri:content-title="Page With Space"/><ac:plain-text-link-body><![CDATA[page link with spaces]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p><ac:image ac:alt="My Image"><ri:attachment ri:filename="test.png"/></ac:image></p>
|
|
||||||
<p><ac:image ac:alt="My External Image"><ri:url ri:value="http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&key2=value2"/></ac:image></p>
|
|
||||||
<p><ac:link><ri:page ri:content-title="test_link"/><ac:plain-text-link-body><![CDATA[My test_link]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p><ac:link><ri:page ri:content-title="test_link_link"/><ac:plain-text-link-body><![CDATA[Another [Link]]]></ac:plain-text-link-body></ac:link></p>
|
|
||||||
<p>Use footnotes link <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
|
|
||||||
<p>Use <a href="foo">Link [Text]</a></p>
|
|
||||||
<div class="footnotes" role="doc-endnotes">
|
|
||||||
<hr />
|
|
||||||
<ol>
|
|
||||||
<li id="fn:1">
|
|
||||||
<p>a footnote link <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
21
pkg/mark/testdata/lists-droph1.html
vendored
21
pkg/mark/testdata/lists-droph1.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<ul>
|
|
||||||
<li>dash 1-1</li>
|
|
||||||
<li>dash 1-2</li>
|
|
||||||
<li>dash 1-3
|
|
||||||
<ul>
|
|
||||||
<li>dash 1-3-1</li>
|
|
||||||
<li>dash 1-3-2</li>
|
|
||||||
<li>dash 1-3-3
|
|
||||||
<ul>
|
|
||||||
<li>dash 1-3-3-1</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>text</p>
|
|
||||||
<ul>
|
|
||||||
<li>a</li>
|
|
||||||
<li>b</li>
|
|
||||||
<li>c</li>
|
|
||||||
</ul>
|
|
||||||
21
pkg/mark/testdata/lists-stripnewlines.html
vendored
21
pkg/mark/testdata/lists-stripnewlines.html
vendored
@ -1,21 +0,0 @@
|
|||||||
<ul>
|
|
||||||
<li>dash 1-1</li>
|
|
||||||
<li>dash 1-2</li>
|
|
||||||
<li>dash 1-3
|
|
||||||
<ul>
|
|
||||||
<li>dash 1-3-1</li>
|
|
||||||
<li>dash 1-3-2</li>
|
|
||||||
<li>dash 1-3-3
|
|
||||||
<ul>
|
|
||||||
<li>dash 1-3-3-1</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>text</p>
|
|
||||||
<ul>
|
|
||||||
<li>a</li>
|
|
||||||
<li>b</li>
|
|
||||||
<li>c</li>
|
|
||||||
</ul>
|
|
||||||
26
pkg/mark/testdata/macro-include-droph1.html
vendored
26
pkg/mark/testdata/macro-include-droph1.html
vendored
@ -1,26 +0,0 @@
|
|||||||
<foo>bar</foo>
|
|
||||||
<ac:structured-macro ac:name="info">
|
|
||||||
<ac:parameter ac:name="icon">true</ac:parameter>
|
|
||||||
<ac:parameter ac:name="title">Attention</ac:parameter>
|
|
||||||
<ac:rich-text-body>This is an info!</ac:rich-text-body>
|
|
||||||
</ac:structured-macro>
|
|
||||||
<ac:structured-macro ac:name="info">
|
|
||||||
<ac:parameter ac:name="icon">true</ac:parameter>
|
|
||||||
<ac:parameter ac:name="title">Attention</ac:parameter>
|
|
||||||
<ac:rich-text-body>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Header 1</th>
|
|
||||||
<th>Header 2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>Cell A</td>
|
|
||||||
<td>Cell B</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ac:rich-text-body>
|
|
||||||
</ac:structured-macro>
|
|
||||||
10
pkg/mark/testdata/newlines-droph1.html
vendored
10
pkg/mark/testdata/newlines-droph1.html
vendored
@ -1,10 +0,0 @@
|
|||||||
<p>one-1
|
|
||||||
one-2</p>
|
|
||||||
<p>two-1</p>
|
|
||||||
<p>two-2</p>
|
|
||||||
<p>three-1</p>
|
|
||||||
<p>three-2</p>
|
|
||||||
<p>space-1
|
|
||||||
space-2</p>
|
|
||||||
<p>2space-1<br />
|
|
||||||
2space-2</p>
|
|
||||||
18
pkg/mark/testdata/pagelayout-droph1.html
vendored
18
pkg/mark/testdata/pagelayout-droph1.html
vendored
@ -1,18 +0,0 @@
|
|||||||
<ac:layout>
|
|
||||||
<ac:layout-section ac:type="three_with_sidebars">
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>Even More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
</ac:layout-section>
|
|
||||||
<ac:layout-section ac:type="single">
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>Still More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
</ac:layout-section>
|
|
||||||
</ac:layout>
|
|
||||||
18
pkg/mark/testdata/pagelayout-stripnewlines.html
vendored
18
pkg/mark/testdata/pagelayout-stripnewlines.html
vendored
@ -1,18 +0,0 @@
|
|||||||
<ac:layout>
|
|
||||||
<ac:layout-section ac:type="three_with_sidebars">
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>Even More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
</ac:layout-section>
|
|
||||||
<ac:layout-section ac:type="single">
|
|
||||||
<ac:layout-cell>
|
|
||||||
<p>Still More Content</p>
|
|
||||||
</ac:layout-cell>
|
|
||||||
</ac:layout-section>
|
|
||||||
</ac:layout>
|
|
||||||
28
pkg/mark/testdata/table-droph1.html
vendored
28
pkg/mark/testdata/table-droph1.html
vendored
@ -1,28 +0,0 @@
|
|||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>HEADER1</th>
|
|
||||||
<th>HEADER2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>row1</td>
|
|
||||||
<td>row2</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="text-align:center">HEADER1</th>
|
|
||||||
<th style="text-align:right">HEADER2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align:center">row1</td>
|
|
||||||
<td style="text-align:right">row2</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
28
pkg/mark/testdata/table-stripnewlines.html
vendored
28
pkg/mark/testdata/table-stripnewlines.html
vendored
@ -1,28 +0,0 @@
|
|||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>HEADER1</th>
|
|
||||||
<th>HEADER2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>row1</td>
|
|
||||||
<td>row2</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th style="text-align:center">HEADER1</th>
|
|
||||||
<th style="text-align:right">HEADER2</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td style="text-align:center">row1</td>
|
|
||||||
<td style="text-align:right">row2</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
6
pkg/mark/testdata/tags-droph1.html
vendored
6
pkg/mark/testdata/tags-droph1.html
vendored
@ -1,6 +0,0 @@
|
|||||||
<b>bold</b>
|
|
||||||
<p><strong>bold</strong></p>
|
|
||||||
<i>vitalik</i>
|
|
||||||
<p><em>vitalik</em></p>
|
|
||||||
<s>strikethrough</s>
|
|
||||||
<p><del>strikethrough</del></p>
|
|
||||||
6
pkg/mark/testdata/tags-stripnewlines.html
vendored
6
pkg/mark/testdata/tags-stripnewlines.html
vendored
@ -1,6 +0,0 @@
|
|||||||
<b>bold</b>
|
|
||||||
<p><strong>bold</strong></p>
|
|
||||||
<i>vitalik</i>
|
|
||||||
<p><em>vitalik</em></p>
|
|
||||||
<s>strikethrough</s>
|
|
||||||
<p><del>strikethrough</del></p>
|
|
||||||
@ -108,7 +108,7 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
|
|||||||
if countParagraphs < 2 && entering {
|
if countParagraphs < 2 && entering {
|
||||||
if node.Kind() == ast.KindText {
|
if node.Kind() == ast.KindText {
|
||||||
n := node.(*ast.Text)
|
n := node.(*ast.Text)
|
||||||
t = legacyClassifier.ClassifyingBlockQuote(string(n.Text(source)))
|
t = legacyClassifier.ClassifyingBlockQuote(string(n.Value(source)))
|
||||||
// If the node is a text node but classification returned none do not give up!
|
// If the node is a text node but classification returned none do not give up!
|
||||||
// Find the next two sibling nodes midNode and rightNode,
|
// Find the next two sibling nodes midNode and rightNode,
|
||||||
// 1. If both are also a text node
|
// 1. If both are also a text node
|
||||||
@ -118,15 +118,14 @@ func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
|
|||||||
// Classifying the next text type node (midNode) will confirm that.
|
// Classifying the next text type node (midNode) will confirm that.
|
||||||
if t == None {
|
if t == None {
|
||||||
midNode := node.NextSibling()
|
midNode := node.NextSibling()
|
||||||
rightNode := midNode.NextSibling()
|
|
||||||
|
|
||||||
if midNode.Kind() == ast.KindText {
|
if midNode != nil && midNode.Kind() == ast.KindText {
|
||||||
|
rightNode := midNode.NextSibling()
|
||||||
midTextNode := midNode.(*ast.Text)
|
midTextNode := midNode.(*ast.Text)
|
||||||
if rightNode != nil && rightNode.Kind() == ast.KindText {
|
if rightNode != nil && rightNode.Kind() == ast.KindText {
|
||||||
rightTextNode := rightNode.(*ast.Text)
|
rightTextNode := rightNode.(*ast.Text)
|
||||||
|
if string(n.Value(source)) == "[" && string(rightTextNode.Value(source)) == "]" {
|
||||||
if string(n.Text(source)) == "[" && string(rightTextNode.Text(source)) == "]" {
|
t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Value(source)))
|
||||||
t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Text(source)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,7 +3,7 @@ package renderer
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/renderer"
|
"github.com/yuin/goldmark/renderer"
|
||||||
@ -3,11 +3,14 @@ package renderer
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/mermaid"
|
"github.com/kovetskiy/mark/d2"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
"github.com/kovetskiy/mark/mermaid"
|
||||||
|
"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
|
||||||
@ -3,7 +3,7 @@ package renderer
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/renderer"
|
"github.com/yuin/goldmark/renderer"
|
||||||
@ -5,9 +5,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/mark/attachment"
|
"github.com/kovetskiy/mark/attachment"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/stdlib"
|
"github.com/kovetskiy/mark/stdlib"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/vfs"
|
"github.com/kovetskiy/mark/vfs"
|
||||||
|
|
||||||
"github.com/yuin/goldmark/ast"
|
"github.com/yuin/goldmark/ast"
|
||||||
"github.com/yuin/goldmark/renderer"
|
"github.com/yuin/goldmark/renderer"
|
||||||
@ -107,9 +107,9 @@ func nodeToHTMLText(n ast.Node, source []byte) []byte {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
|
||||||
if s, ok := c.(*ast.String); ok && s.IsCode() {
|
if s, ok := c.(*ast.String); ok && s.IsCode() {
|
||||||
buf.Write(s.Text(source))
|
buf.Write(s.Value)
|
||||||
} else if !c.HasChildren() {
|
} else if t, ok := c.(*ast.Text); ok {
|
||||||
buf.Write(util.EscapeHTML(c.Text(source)))
|
buf.Write(util.EscapeHTML(t.Value(source)))
|
||||||
} else {
|
} else {
|
||||||
buf.Write(nodeToHTMLText(c, source))
|
buf.Write(nodeToHTMLText(c, source))
|
||||||
}
|
}
|
||||||
@ -26,14 +26,15 @@ 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 {
|
||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(n.Destination) < 4 {
|
if len(string(n.Destination)) < 4 {
|
||||||
|
//nolint:staticcheck
|
||||||
_, err := writer.Write(node.Text(source))
|
_, err := writer.Write(node.Text(source))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
@ -50,6 +51,7 @@ func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte
|
|||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:staticcheck
|
||||||
_, err = writer.Write(node.Text(source))
|
_, err = writer.Write(node.Text(source))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ast.WalkStop, err
|
return ast.WalkStop, err
|
||||||
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
|
||||||
|
}
|
||||||
@ -63,7 +63,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
|
|||||||
if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
|
if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
|
||||||
sibling := node.NextSibling()
|
sibling := node.NextSibling()
|
||||||
if sibling != nil && sibling.Kind() == ast.KindText {
|
if sibling != nil && sibling.Kind() == ast.KindText {
|
||||||
if siblingText := sibling.(*ast.Text).Text(source); len(siblingText) != 0 {
|
if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
|
||||||
thisLastRune := util.ToRune(value, len(value)-1)
|
thisLastRune := util.ToRune(value, len(value)-1)
|
||||||
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
|
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
|
||||||
// Inline the softLineBreak function as it's not public
|
// Inline the softLineBreak function as it's not public
|
||||||
@ -72,7 +72,7 @@ func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, nod
|
|||||||
case html.EastAsianLineBreaksNone:
|
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)
|
||||||
}
|
}
|
||||||
@ -4,8 +4,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/kovetskiy/mark/pkg/confluence"
|
"github.com/kovetskiy/mark/confluence"
|
||||||
"github.com/kovetskiy/mark/pkg/mark/macro"
|
"github.com/kovetskiy/mark/macro"
|
||||||
"github.com/reconquest/pkg/log"
|
"github.com/reconquest/pkg/log"
|
||||||
|
|
||||||
"github.com/reconquest/karma-go"
|
"github.com/reconquest/karma-go"
|
||||||
@ -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>`,
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -296,18 +298,22 @@ func templates(api *confluence.API) (*template.Template, error) {
|
|||||||
),
|
),
|
||||||
|
|
||||||
/* https://confluence.atlassian.com/conf59/excerpt-include-macro-792499101.html */
|
/* https://confluence.atlassian.com/conf59/excerpt-include-macro-792499101.html */
|
||||||
|
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-include-macro/ */
|
||||||
|
|
||||||
`ac:excerpt-include`: text(
|
`ac:excerpt-include`: text(
|
||||||
`<ac:macro ac:name="excerpt-include">`,
|
`<ac:macro ac:name="excerpt-include">`,
|
||||||
|
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
|
||||||
`<ac:parameter ac:name="nopanel">{{ if .NoPanel }}{{ .NoPanel }}{{ else }}false{{ end }}</ac:parameter>`,
|
`<ac:parameter ac:name="nopanel">{{ if .NoPanel }}{{ .NoPanel }}{{ else }}false{{ end }}</ac:parameter>`,
|
||||||
`<ac:default-parameter>{{ .Page }}</ac:default-parameter>`,
|
`<ac:default-parameter>{{ .Page }}</ac:default-parameter>`,
|
||||||
`</ac:macro>`,
|
`</ac:macro>`,
|
||||||
),
|
),
|
||||||
|
|
||||||
/* https://confluence.atlassian.com/conf59/excerpt-macro-792499102.html */
|
/* https://confluence.atlassian.com/conf59/excerpt-macro-792499102.html */
|
||||||
|
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-macro/ */
|
||||||
|
|
||||||
`ac:excerpt`: text(
|
`ac:excerpt`: text(
|
||||||
`<ac:structured-macro ac:name="excerpt">`,
|
`<ac:structured-macro ac:name="excerpt">`,
|
||||||
|
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
|
||||||
`<ac:parameter ac:name="hidden">{{ if .Hidden }}{{ .Hidden }}{{ else }}false{{ end }}</ac:parameter>`,
|
`<ac:parameter ac:name="hidden">{{ if .Hidden }}{{ .Hidden }}{{ else }}false{{ end }}</ac:parameter>`,
|
||||||
`<ac:parameter ac:name="atlassian-macro-output-type">{{ if .OutputType }}{{ .OutputType }}{{ else }}BLOCK{{ end }}</ac:parameter>`,
|
`<ac:parameter ac:name="atlassian-macro-output-type">{{ if .OutputType }}{{ .OutputType }}{{ else }}BLOCK{{ end }}</ac:parameter>`,
|
||||||
`<ac:rich-text-body>`,
|
`<ac:rich-text-body>`,
|
||||||
@ -382,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>`,
|
||||||
@ -440,7 +446,7 @@ func templates(api *confluence.API) (*template.Template, error) {
|
|||||||
`<ac:structured-macro ac:name="multimedia">`,
|
`<ac:structured-macro ac:name="multimedia">`,
|
||||||
`<ac:parameter ac:name="width">{{ or .Width 500 }}</ac:parameter>`,
|
`<ac:parameter ac:name="width">{{ or .Width 500 }}</ac:parameter>`,
|
||||||
`<ac:parameter ac:name="name">`,
|
`<ac:parameter ac:name="name">`,
|
||||||
`<ri:attachment ri:filename="{{ .Name }}/>`,
|
`<ri:attachment ri:filename="{{ .Name | convertAttachment }}"/>`,
|
||||||
`</ac:parameter>`,
|
`</ac:parameter>`,
|
||||||
`<ac:parameter ac:name="autoplay">{{ or .AutoPlay "false"}}</ac:parameter>`,
|
`<ac:parameter ac:name="autoplay">{{ or .AutoPlay "false"}}</ac:parameter>`,
|
||||||
`</ac:structured-macro>`,
|
`</ac:structured-macro>`,
|
||||||
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
|
||||||
@ -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>
|
||||||
@ -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>
|
||||||
121
testdata/codes.md
vendored
Normal file
121
testdata/codes.md
vendored
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
`inline`
|
||||||
|
|
||||||
|
```
|
||||||
|
some code
|
||||||
|
```
|
||||||
|
```bash
|
||||||
|
code bash
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
with a newline
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
```unknown
|
||||||
|
unknown code
|
||||||
|
```
|
||||||
|
text
|
||||||
|
text 2
|
||||||
|
```unknown
|
||||||
|
unknown code 2
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh title A b c
|
||||||
|
no-collapse-title
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash collapse title A b c
|
||||||
|
collapse-and-title
|
||||||
|
```
|
||||||
|
|
||||||
|
```c collapse
|
||||||
|
collapse-no-title
|
||||||
|
```
|
||||||
|
|
||||||
|
```nested
|
||||||
|
code
|
||||||
|
``` more code ```
|
||||||
|
even more code
|
||||||
|
```
|
||||||
|
|
||||||
|
indented code block
|
||||||
|
with multiple lines
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
A-->C;
|
||||||
|
B-->D;
|
||||||
|
C-->D;
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid collapse title my mermaid graph
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
A-->C;
|
||||||
|
B-->D;
|
||||||
|
C-->D;
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid title my mermaid graph
|
||||||
|
graph TD;
|
||||||
|
A-->B;
|
||||||
|
A-->C;
|
||||||
|
B-->D;
|
||||||
|
C-->D;
|
||||||
|
```
|
||||||
|
|
||||||
|
```d2
|
||||||
|
vars: {
|
||||||
|
d2-config: {
|
||||||
|
layout-engine: elk
|
||||||
|
# Terminal theme code
|
||||||
|
theme-id: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
network: {
|
||||||
|
cell tower: {
|
||||||
|
satellites: {
|
||||||
|
shape: stored_data
|
||||||
|
style.multiple: true
|
||||||
|
}
|
||||||
|
|
||||||
|
transmitter
|
||||||
|
|
||||||
|
satellites -> transmitter: send
|
||||||
|
satellites -> transmitter: send
|
||||||
|
satellites -> transmitter: send
|
||||||
|
}
|
||||||
|
|
||||||
|
online portal: {
|
||||||
|
ui: {shape: hexagon}
|
||||||
|
}
|
||||||
|
|
||||||
|
data processor: {
|
||||||
|
storage: {
|
||||||
|
shape: cylinder
|
||||||
|
style.multiple: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cell tower.transmitter -> data processor.storage: phone logs
|
||||||
|
}
|
||||||
|
|
||||||
|
user: {
|
||||||
|
shape: person
|
||||||
|
width: 130
|
||||||
|
}
|
||||||
|
|
||||||
|
user -> network.cell tower: make call
|
||||||
|
user -> network.online portal.ui: access {
|
||||||
|
style.stroke-dash: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
api server -> network.online portal.ui: display
|
||||||
|
api server -> logs: persist
|
||||||
|
logs: {shape: page; style.multiple: true}
|
||||||
|
|
||||||
|
network.data processor -> api server
|
||||||
|
```
|
||||||
@ -4,3 +4,4 @@
|
|||||||
<h5 id="e">e</h5>
|
<h5 id="e">e</h5>
|
||||||
<h1 id="f">f</h1>
|
<h1 id="f">f</h1>
|
||||||
<h2 id="g">g</h2>
|
<h2 id="g">g</h2>
|
||||||
|
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>
|
||||||
@ -5,3 +5,4 @@
|
|||||||
<h5 id="e">e</h5>
|
<h5 id="e">e</h5>
|
||||||
<h1 id="f">f</h1>
|
<h1 id="f">f</h1>
|
||||||
<h2 id="g">g</h2>
|
<h2 id="g">g</h2>
|
||||||
|
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>
|
||||||
@ -8,3 +8,5 @@ f
|
|||||||
=
|
=
|
||||||
g
|
g
|
||||||
-
|
-
|
||||||
|
|
||||||
|
# This/is some_Heading.yml
|
||||||
@ -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>
|
||||||
@ -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
|
||||||
|
[]()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
<h2 id="first-heading">First Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>NOTES:</strong></p>
|
<p><strong>NOTES:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -11,7 +11,7 @@ b</p>
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="second-heading">Second Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>Warn</strong></p>
|
<p><strong>Warn</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -23,12 +23,12 @@ b</p>
|
|||||||
<li>Regular list
|
<li>Regular list
|
||||||
that runs long</li>
|
that runs long</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 id="third-heading">Third Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<p>Test</p>
|
<p>Test</p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</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>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>TIP:</strong></p>
|
<p><strong>TIP:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -41,12 +41,12 @@ b</p>
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="simple-blockquote">Simple Blockquote</h2>
|
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>This paragraph is a simple blockquote</p>
|
<p>This paragraph is a simple blockquote</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
|
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
|
||||||
<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
|
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!NOTE]</p>
|
<p>[!NOTE]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -54,7 +54,7 @@ b</p>
|
|||||||
<li>Note bullet 2</li>
|
<li>Note bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!TIP]</p>
|
<p>[!TIP]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -62,7 +62,7 @@ b</p>
|
|||||||
<li>Tip bullet 2</li>
|
<li>Tip bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
|
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!WARNING]</p>
|
<p>[!WARNING]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -70,7 +70,7 @@ b</p>
|
|||||||
<li>Warning bullet 2</li>
|
<li>Warning bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!IMPORTANT]</p>
|
<p>[!IMPORTANT]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -85,7 +85,7 @@ b</p>
|
|||||||
<li>Important bullet 2</li>
|
<li>Important bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[[!NOTE]</p>
|
<p>[[!NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
@ -98,3 +98,7 @@ b</p>
|
|||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[NOTE]</p>
|
<p>[NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>TL;DR:</strong> Thingy!
|
||||||
|
More stuff</p>
|
||||||
|
</blockquote>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<h1 id="main-heading">Main Heading</h1>
|
<h1 id="Main-Heading">Main Heading</h1>
|
||||||
<h2 id="first-heading">First Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>NOTES:</strong></p>
|
<p><strong>NOTES:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -11,7 +11,7 @@
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="second-heading">Second Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>Warn</strong></p>
|
<p><strong>Warn</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -22,12 +22,12 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Regular list that runs long</li>
|
<li>Regular list that runs long</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 id="third-heading">Third Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<p>Test</p>
|
<p>Test</p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</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>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>TIP:</strong></p>
|
<p><strong>TIP:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -39,12 +39,12 @@
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="simple-blockquote">Simple Blockquote</h2>
|
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>This paragraph is a simple blockquote</p>
|
<p>This paragraph is a simple blockquote</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
|
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
|
||||||
<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
|
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!NOTE]</p>
|
<p>[!NOTE]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<li>Note bullet 2</li>
|
<li>Note bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!TIP]</p>
|
<p>[!TIP]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<li>Tip bullet 2</li>
|
<li>Tip bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
|
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!WARNING]</p>
|
<p>[!WARNING]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -68,7 +68,7 @@
|
|||||||
<li>Warning bullet 2</li>
|
<li>Warning bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!IMPORTANT]</p>
|
<p>[!IMPORTANT]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -83,7 +83,7 @@
|
|||||||
<li>Important bullet 2</li>
|
<li>Important bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[[!NOTE]</p>
|
<p>[[!NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
@ -96,3 +96,6 @@
|
|||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[NOTE]</p>
|
<p>[NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>TL;DR:</strong> Thingy! More stuff</p>
|
||||||
|
</blockquote>
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<h1 id="main-heading">Main Heading</h1>
|
<h1 id="Main-Heading">Main Heading</h1>
|
||||||
<h2 id="first-heading">First Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>NOTES:</strong></p>
|
<p><strong>NOTES:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -12,7 +12,7 @@ b</p>
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="second-heading">Second Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="warning"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>Warn</strong></p>
|
<p><strong>Warn</strong></p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -24,12 +24,12 @@ b</p>
|
|||||||
<li>Regular list
|
<li>Regular list
|
||||||
that runs long</li>
|
that runs long</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2 id="third-heading">Third Heading</h2>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<p>Test</p>
|
<p>Test</p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</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>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p><strong>TIP:</strong></p>
|
<p><strong>TIP:</strong></p>
|
||||||
<ol>
|
<ol>
|
||||||
@ -42,12 +42,12 @@ b</p>
|
|||||||
</blockquote>
|
</blockquote>
|
||||||
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
<p><strong>Warn (Should not be picked as blockquote type)</strong></p>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h2 id="simple-blockquote">Simple Blockquote</h2>
|
<h2 id="Simple-Blockquote">Simple Blockquote</h2>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>This paragraph is a simple blockquote</p>
|
<p>This paragraph is a simple blockquote</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<h2 id="gh-alerts-heading">GH Alerts Heading</h2>
|
<h2 id="GH-Alerts-Heading">GH Alerts Heading</h2>
|
||||||
<h3 id="note-type-alert-heading">Note Type Alert Heading</h3>
|
<h3 id="Note-Type-Alert-Heading">Note Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!NOTE]</p>
|
<p>[!NOTE]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -55,7 +55,7 @@ b</p>
|
|||||||
<li>Note bullet 2</li>
|
<li>Note bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="tip-type-alert-heading">Tip Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="tip"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!TIP]</p>
|
<p>[!TIP]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -63,7 +63,7 @@ b</p>
|
|||||||
<li>Tip bullet 2</li>
|
<li>Tip bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="warning-type-alert-heading">Warning Type Alert Heading</h3>
|
<h3 id="Warning-Type-Alert-Heading">Warning Type Alert Heading</h3>
|
||||||
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
<ac:structured-macro ac:name="note"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!WARNING]</p>
|
<p>[!WARNING]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -71,7 +71,7 @@ b</p>
|
|||||||
<li>Warning bullet 2</li>
|
<li>Warning bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="importantcaution-type-alert-heading">Important/Caution Type Alert Heading</h3>
|
<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>
|
<ac:structured-macro ac:name="info"><ac:parameter ac:name="icon">true</ac:parameter><ac:rich-text-body>
|
||||||
<p>[!IMPORTANT]</p>
|
<p>[!IMPORTANT]</p>
|
||||||
<ul>
|
<ul>
|
||||||
@ -86,7 +86,7 @@ b</p>
|
|||||||
<li>Important bullet 2</li>
|
<li>Important bullet 2</li>
|
||||||
</ul>
|
</ul>
|
||||||
</ac:rich-text-body></ac:structured-macro>
|
</ac:rich-text-body></ac:structured-macro>
|
||||||
<h3 id="should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
<h3 id="Should-not-be-picked-up-and-converted-into-blockquote-macro">Should not be picked up and converted into blockquote macro</h3>
|
||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[[!NOTE]</p>
|
<p>[[!NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
@ -99,3 +99,7 @@ b</p>
|
|||||||
<blockquote>
|
<blockquote>
|
||||||
<p>[NOTE]</p>
|
<p>[NOTE]</p>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
|
<blockquote>
|
||||||
|
<p><strong>TL;DR:</strong> Thingy!
|
||||||
|
More stuff</p>
|
||||||
|
</blockquote>
|
||||||
@ -88,3 +88,8 @@
|
|||||||
> [Hey !NOTE]
|
> [Hey !NOTE]
|
||||||
|
|
||||||
> [NOTE]
|
> [NOTE]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
> **TL;DR:** Thingy!
|
||||||
|
> More stuff
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
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))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user