Compare commits

...

484 Commits

Author SHA1 Message Date
Manuel Rüger
bf542ab684 fix: Config loading from file 2025-06-06 13:54:34 +02:00
dependabot[bot]
58cdd5608f Bump golang.org/x/tools from 0.33.0 to 0.34.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.33.0 to 0.34.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.33.0...v0.34.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.34.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 13:42:07 +02:00
dependabot[bot]
6767d655c7 Bump golang from 1.24.3 to 1.24.4
Bumps golang from 1.24.3 to 1.24.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 13:41:58 +02:00
Manuel Rüger
c57256cb7b Only use hyphenated versions of config keys 2025-06-06 02:31:17 +02:00
Manuel Rüger
926945f884 Version bump to 13.0.0 2025-05-31 20:41:06 +02:00
dependabot[bot]
760ee5a2eb Bump github.com/chromedp/chromedp from 0.13.3 to 0.13.6
Bumps [github.com/chromedp/chromedp](https://github.com/chromedp/chromedp) from 0.13.3 to 0.13.6.
- [Release notes](https://github.com/chromedp/chromedp/releases)
- [Commits](https://github.com/chromedp/chromedp/compare/v0.13.3...v0.13.6)

---
updated-dependencies:
- dependency-name: github.com/chromedp/chromedp
  dependency-version: 0.13.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-31 20:25:57 +02:00
Manuel Rüger
3cc39ffe79 Add support for d2lang 2025-05-30 23:37:15 +02:00
Manuel Rüger
d1aee4d571 Migrate to urfave/cli/v3 2025-05-28 21:53:45 +02:00
Manuel Rüger
b7ef416472 Fix #594 2025-05-18 22:10:04 +02:00
dependabot[bot]
7562d0499e Bump github.com/yuin/goldmark from 1.7.11 to 1.7.12
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.11 to 1.7.12.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.11...v1.7.12)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.7.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 22:01:37 +02:00
dependabot[bot]
2d89511ac1 Bump DavidAnson/markdownlint-cli2-action from 19 to 20
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 19 to 20.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v19...v20)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: '20'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-18 22:01:23 +02:00
dependabot[bot]
1d00316ae5 Bump golang.org/x/tools from 0.32.0 to 0.33.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.32.0 to 0.33.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.32.0...v0.33.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.33.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 19:28:25 +02:00
dependabot[bot]
5649939297 Bump golang from 1.24.2 to 1.24.3
Bumps golang from 1.24.2 to 1.24.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-version: 1.24.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-12 19:28:12 +02:00
dependabot[bot]
4ac93b556c Bump golangci/golangci-lint-action from 7 to 8
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 7 to 8.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v7...v8)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-05-08 00:14:34 +02:00
dependabot[bot]
d9a96f3700 Bump github.com/yuin/goldmark from 1.7.10 to 1.7.11
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.10 to 1.7.11.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.10...v1.7.11)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.7.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 23:53:55 +02:00
Manuel Rüger
92634869e3 Bump to go 1.24 2025-04-15 20:36:21 +02:00
dependabot[bot]
5cbd0fd6eb Bump github.com/yuin/goldmark from 1.7.9 to 1.7.10
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.9 to 1.7.10.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.9...v1.7.10)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.7.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-15 20:34:05 +02:00
dependabot[bot]
f8a3945f62 Bump github.com/yuin/goldmark from 1.7.8 to 1.7.9
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.8 to 1.7.9.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.8...v1.7.9)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-version: 1.7.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-15 15:11:48 +02:00
Manuel Rüger
6c33afc866 Bump version to 12.2.0 2025-04-13 00:23:10 +02:00
dependabot[bot]
ef09fd27f8 Bump golang.org/x/tools from 0.31.0 to 0.32.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.31.0 to 0.32.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.31.0...v0.32.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-version: 0.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-13 00:21:45 +02:00
dependabot[bot]
1fa01dff70 Bump golang.org/x/tools from 0.30.0 to 0.31.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.30.0 to 0.31.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.30.0...v0.31.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-04 22:53:59 +02:00
iyz
d789261c9a feat: use gopencils retrial option, upgrade version 2025-04-04 22:13:57 +02:00
Manuel Rüger
dda17fcb55 Use go 1.23.8 to build 2025-04-02 22:47:56 +02:00
dependabot[bot]
a77a538ab5 Bump golangci/golangci-lint-action from 6 to 7
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 6 to 7.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 16:49:43 +02:00
Manuel Rüger
f24d8c8957 Fix lint issues detected by golangci v2 2025-04-02 16:42:32 +02:00
Rich Scott
a0c6abfa6d Fix data path specification on TestContinueOnError
Signed-off-by: Rich Scott <richscott@sent.com>
2025-04-02 14:37:48 +02:00
Rich Scott
b630876c22 More code fixes for unit-testing '--continue-on-error'
Signed-off-by: Rich Scott <richscott@sent.com>
2025-04-02 14:37:48 +02:00
Rich Scott
ddc0ab9fbf Restructure code for more testaability
Move a number of funcs/files in the top-level `main` package into a new
`util` package, so test logic can directly invoke functions like
RunMark(), etc.  The main.go has been trimmed down to minimal sizing,
with former supporting funcs moved into `util` package, so they
can be run by unit tests.

Signed-off-by: Rich Scott <richscott@sent.com>
2025-04-02 14:37:48 +02:00
iyz
87160e8dd6 change COE flag testing logic to use internal functions 2025-04-02 14:37:48 +02:00
iyz
d88b81a6b8 add batch test, test function for continue-on-error flag 2025-04-02 14:37:48 +02:00
iyz
7f5144a1d1 add batch tests in sub directory under /testdata 2025-04-02 14:37:48 +02:00
iyz
7f5dfae904 fix: correct Errorf/Fatalf argument handling
- Fix incorrect argument passing to log.Errorf and log.Fatalf functions
- Remove debugging comments, temporary tests, and print statements
- Address linter warnings related to error logging
2025-04-02 14:37:48 +02:00
iyz
024259e480 add code & testing files for continue-on-error flag
- add continue-on-error flag as a command line option
    - if set, doesnt exit on error and continues
        processing other files that were passed in
- add fatalErrorHandler to handle fatal errors
    - if continue-on-error flag is set, does not exit
- add temporary tests for continue-on-error flag
    - add tests in batch-tests subdirectory
2025-04-02 14:37:48 +02:00
dependabot[bot]
ff015e2c24 Bump github.com/dreampuf/mermaid.go from 0.0.25 to 0.0.27
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.25 to 0.0.27.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.25...v0.0.27)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-02 13:40:43 +02:00
dependabot[bot]
f3c5a77a85 Bump github.com/dreampuf/mermaid.go from 0.0.24 to 0.0.25
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.24 to 0.0.25.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.24...v0.0.25)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-10 11:36:53 +01:00
dependabot[bot]
0b8caa078b Bump github.com/urfave/cli/v2 from 2.27.5 to 2.27.6
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.5 to 2.27.6.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.5...v2.27.6)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-03-06 23:06:42 +01:00
dependabot[bot]
d820ee4bf4 Bump github.com/dreampuf/mermaid.go from 0.0.23 to 0.0.24
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.23 to 0.0.24.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.23...v0.0.24)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-24 09:58:26 +01:00
Manuel Rüger
203d4439ef Bump to 12.1.2 2025-02-21 16:41:04 +01:00
Manuel Rüger
f8229c8acb Revert back to go 1.23 2025-02-21 16:40:39 +01:00
Manuel Rüger
b30b0491a8 Bump to 12.1.1 2025-02-19 10:57:19 +01:00
Manuel Rüger
c87b6821d4 Use go 1.24 2025-02-19 10:56:52 +01:00
Manuel Rüger
b2f0e80b12 Bump version to 12.1.0 2025-02-19 10:55:40 +01:00
iyz
f2b2a7a309 add check for dry-run and compile-only flags 2025-02-18 23:56:45 +01:00
iyz
8d05975142 remove extra newline/space 2025-02-18 23:56:45 +01:00
iyz
076165c137 return nil instead of exiting, check for nil in loop 2025-02-18 23:56:45 +01:00
dependabot[bot]
611e8e9b94 Bump golang from 1.23.6 to 1.24.0
Bumps golang from 1.23.6 to 1.24.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-18 23:21:06 +01:00
Joris Conijn
15a3c10ed1 docs: describe how to use the emoji header 2025-02-17 17:31:04 +01:00
Joris Conijn
ec5ee6eb0a fix: profile picture 2025-02-17 17:31:04 +01:00
Joris Conijn
ea2bae39da style: typos 2025-02-17 17:31:04 +01:00
Joris Conijn
1a0e452910 feat: support emojis on pages
Define an emoji in the markdown files and get them published as page
emoji icons.
2025-02-17 17:31:04 +01:00
tiimo
f0b4d460a9 docs: fix indentation for ac:children macro arguments description 2025-02-13 09:11:08 +01:00
dependabot[bot]
f3e27aaa50 Bump golang.org/x/tools from 0.29.0 to 0.30.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.29.0 to 0.30.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-12 23:17:10 +01:00
dependabot[bot]
25c187f741 Bump golang from 1.23.5 to 1.23.6
Bumps golang from 1.23.5 to 1.23.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-10 18:15:14 +01:00
dependabot[bot]
213088b960 Bump github.com/bmatcuk/doublestar/v4 from 4.8.0 to 4.8.1
Some checks failed
continuous-integration / ci-markdown-lint (push) Has been cancelled
continuous-integration / ci-unit-tests (push) Has been cancelled
continuous-integration / ci-build (push) Has been cancelled
continuous-integration / ci-docker-build (push) Has been cancelled
continuous-integration / ci-go-lint (push) Has been cancelled
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.8.0...v4.8.1)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 12:32:38 +01:00
dependabot[bot]
5504fd4c11 Bump github.com/dreampuf/mermaid.go from 0.0.22 to 0.0.23
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.22 to 0.0.23.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.22...v0.0.23)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 12:29:50 +01:00
dependabot[bot]
9486f0bbcf Bump golang from 1.23.4 to 1.23.5
Some checks failed
continuous-integration / ci-go-lint (push) Failing after 9m44s
continuous-integration / ci-markdown-lint (push) Failing after 16m50s
continuous-integration / ci-unit-tests (push) Failing after 7m22s
continuous-integration / ci-build (push) Failing after 7m22s
continuous-integration / ci-docker-build (push) Failing after 1m35s
Bumps golang from 1.23.4 to 1.23.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:32:47 +01:00
dependabot[bot]
f1c3b2afcd Bump github.com/bmatcuk/doublestar/v4 from 4.7.1 to 4.8.0
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.7.1 to 4.8.0.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.7.1...v4.8.0)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:32:37 +01:00
Shawn Falkner-Horine
fbfd36a16c PageTree macro: Fix Title parameter usage. (fixes #550)
Some checks failed
continuous-integration / ci-go-lint (push) Failing after 9m30s
continuous-integration / ci-markdown-lint (push) Successful in 8s
continuous-integration / ci-unit-tests (push) Failing after 7m16s
continuous-integration / ci-build (push) Failing after 7m18s
continuous-integration / ci-docker-build (push) Failing after 1m35s
2025-01-14 22:53:56 +01:00
Manuel Rüger
c5d0a8b8b7 Bump version to v12.0.0
Some checks failed
continuous-integration / ci-go-lint (push) Failing after 9m31s
continuous-integration / ci-markdown-lint (push) Successful in 11s
continuous-integration / ci-unit-tests (push) Failing after 7m18s
continuous-integration / ci-build (push) Failing after 6m29s
continuous-integration / ci-docker-build (push) Failing after 10m15s
2025-01-13 19:09:29 +01:00
Kassem Sandarusi
5a245519fe Update main.go
Co-authored-by: Manuel Rüger <manuel@rueg.eu>
2025-01-13 19:05:29 +01:00
Kassem Sandarusi
ebe77984c6 fix spacing 2025-01-13 19:05:29 +01:00
Kassem Sandarusi
5accce3b17 add flag for updating on change 2025-01-13 19:05:29 +01:00
Sotirios Mantziaris
c63201159d CR fixes
Some checks failed
continuous-integration / ci-go-lint (push) Failing after 10m18s
continuous-integration / ci-markdown-lint (push) Failing after 4m20s
continuous-integration / ci-unit-tests (push) Failing after 7m22s
continuous-integration / ci-build (push) Failing after 7m18s
continuous-integration / ci-docker-build (push) Failing after 11m9s
2025-01-09 20:04:54 +01:00
Sotirios Mantziaris
f25d8876fc Log levels support 2025-01-09 20:04:54 +01:00
dependabot[bot]
2ba35118bf Bump github.com/dreampuf/mermaid.go from 0.0.21 to 0.0.22
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.21 to 0.0.22.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.21...v0.0.22)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-09 17:59:34 +01:00
dependabot[bot]
959ddc2171 Bump DavidAnson/markdownlint-cli2-action from 18 to 19
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 18 to 19.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v18...v19)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-09 16:14:31 +01:00
dependabot[bot]
0bb85b672b Bump golang.org/x/tools from 0.28.0 to 0.29.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.28.0 to 0.29.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.28.0...v0.29.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-09 16:11:26 +01:00
Manuel Rüger
9cc00551ca .github: Run tests on ubuntu 22.04 2025-01-09 15:58:54 +01:00
Manuel Rüger
96db0f8f24 Bump version to 11.3.1 2025-01-09 15:58:54 +01:00
Manuel Rüger
7206729968 Fix test 2024-12-10 21:45:09 +01:00
dependabot[bot]
7d05b6f286 Bump github.com/dreampuf/mermaid.go from 0.0.20 to 0.0.21
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.20 to 0.0.21.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.20...v0.0.21)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 21:45:09 +01:00
dependabot[bot]
1962ce7c25 Bump golang from 1.23.3 to 1.23.4
Bumps golang from 1.23.3 to 1.23.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-10 16:12:50 +01:00
dependabot[bot]
4d77464f5e Bump golang.org/x/tools from 0.27.0 to 0.28.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.27.0...v0.28.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 17:40:46 +01:00
Manuel Rüger
52d0cd94db Fix env var 2024-12-04 17:25:00 +01:00
dependabot[bot]
0acd97b434 Bump github.com/dreampuf/mermaid.go from 0.0.19 to 0.0.20
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.19 to 0.0.20.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.19...v0.0.20)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 18:17:58 +01:00
dependabot[bot]
82e1879c57 Bump github.com/stretchr/testify from 1.9.0 to 1.10.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-25 17:15:40 +01:00
dependabot[bot]
bbcabbe419 Bump DavidAnson/markdownlint-cli2-action from 17 to 18
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 17 to 18.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v17...v18)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-18 18:13:45 +01:00
dependabot[bot]
4a058a0da9 Bump golang.org/x/tools from 0.26.0 to 0.27.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.26.0 to 0.27.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.26.0...v0.27.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 18:14:31 +01:00
dependabot[bot]
4d241e069a Bump golang from 1.23.2 to 1.23.3
Bumps golang from 1.23.2 to 1.23.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-11-11 17:30:36 +01:00
Yuriy Myronov
060a4ee100 Fixed attachment name handling in ac:multimedia macro 2024-11-07 18:27:18 +01:00
Yurii Myronov
3d96781f47 Support named excerpts
- Resolves feature request #316
2024-11-05 12:04:11 +01:00
Manuel Rüger
649c20d4f2 Version bump to 11.3.0 2024-10-22 11:18:05 +02:00
Manuel Rüger
876626098b Feat: Use custom heading anchors
Confluence Anchors are case-sensitive.
2024-10-22 11:15:33 +02:00
Manuel Rüger
e7a3877ded Bump goldmark to 1.7.8 2024-10-21 14:39:40 +02:00
Smaug123
82aebec1eb Use query param for labels 2024-10-21 13:11:42 +02:00
dependabot[bot]
0bdeb4de3d Bump github.com/urfave/cli/v2 from 2.27.4 to 2.27.5
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.4 to 2.27.5.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.4...v2.27.5)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 11:53:39 +02:00
dependabot[bot]
d6e932adf0 Bump github.com/bmatcuk/doublestar/v4 from 4.6.1 to 4.7.1
Bumps [github.com/bmatcuk/doublestar/v4](https://github.com/bmatcuk/doublestar) from 4.6.1 to 4.7.1.
- [Release notes](https://github.com/bmatcuk/doublestar/releases)
- [Commits](https://github.com/bmatcuk/doublestar/compare/v4.6.1...v4.7.1)

---
updated-dependencies:
- dependency-name: github.com/bmatcuk/doublestar/v4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 11:49:51 +02:00
dependabot[bot]
699370a677 Bump github.com/yuin/goldmark from 1.7.4 to 1.7.6
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.4 to 1.7.6.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.4...v1.7.6)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-14 11:49:34 +02:00
Manuel Rüger
9eb44f95fe Bump version to 11.2.0 2024-10-09 10:08:13 +02:00
Peter Landoll
b0f337c4a3 feat: add flag to append hash to pages to ensure unique titles 2024-10-09 00:28:45 +02:00
dependabot[bot]
2af50c627f Bump github.com/dreampuf/mermaid.go from 0.0.18 to 0.0.19
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.18 to 0.0.19.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.18...v0.0.19)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-08 21:15:59 +02:00
dependabot[bot]
5d2adc8a23 Bump golang from 1.23.1 to 1.23.2
Bumps golang from 1.23.1 to 1.23.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 21:49:37 +02:00
dependabot[bot]
4305957d47 Bump golang.org/x/tools from 0.25.0 to 0.26.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.25.0 to 0.26.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.25.0...v0.26.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-10-07 18:14:43 +02:00
Manuel Rüger
33e5d1ff19 blockquote: Fix invalid access 2024-10-06 00:10:46 +02:00
Manuel Rüger
d5c41f6f1f Remove superfluous testcases 2024-09-29 00:13:04 +02:00
Manuel Rüger
dc8842106b *: Reorganize code 2024-09-29 00:13:04 +02:00
Manuel Rüger
2c71b50438 Bump version to 11.1.0 2024-09-26 09:58:35 +02:00
Manuel Rüger
a93b54d784 Dockerfile: Run apt-get upgrade 2024-09-26 09:46:05 +02:00
Noam Asor
035db7b7b3 To add support for github md alerts 2024-09-26 08:21:02 +02:00
dependabot[bot]
c001ad98cf Bump DavidAnson/markdownlint-cli2-action from 16 to 17
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 16 to 17.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v16...v17)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-16 19:49:13 +02:00
Manuel Rüger
0e4d5507b0 Version bump to 11.0.1 2024-09-13 20:04:16 +02:00
Manuel Rüger
091ee8c0b9 Fix multimedia macro 2024-09-13 20:03:23 +02:00
Manuel Rüger
b7f17bde8b Bump to version 11.0.0 2024-09-09 21:56:38 +02:00
monti-python
e5398b4ee1 Use relative links for objects within the same Confluence instance 2024-09-09 21:54:59 +02:00
Manuel Rüger
f7dda910c5 Build with go 1.23 2024-09-09 21:34:29 +02:00
Manuel Rüger
1e91fe184f stdlib: Add multimedia macro 2024-09-09 20:40:56 +02:00
dependabot[bot]
63ebfb853c Bump golang from 1.23.0 to 1.23.1
Bumps golang from 1.23.0 to 1.23.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-09 11:28:29 +02:00
Rodrigue Cloutier
24438a2a27 Fix: Early exit when deleting label
Allow status code 204 to be valid and not error on label delete
2024-09-06 11:28:12 +02:00
Egor Kovetskiy
0e8ce186ca strip common clause license condition v1.0
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2024-09-06 10:42:29 +02:00
Manuel Rüger
ca6df8ec90 go.mod: Update dependencies 2024-09-02 20:59:27 +02:00
dependabot[bot]
02c7543fbd Bump golang from 1.22.6 to 1.23.0
Bumps golang from 1.22.6 to 1.23.0.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-02 20:56:55 +02:00
Manuel Rüger
ed9d64c663 Bump to 10.0.1 2024-08-12 15:32:58 +02:00
Manuel Rüger
eb39e035f0 Fix paragraphs 2024-08-12 14:22:09 +02:00
Manuel Rüger
b7f4b5c6a2 Dockerfile: Build with 1.22.6 2024-08-12 14:16:13 +02:00
dependabot[bot]
39f47c74cf Bump github.com/urfave/cli/v2 from 2.27.3 to 2.27.4
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.3 to 2.27.4.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.3...v2.27.4)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 14:14:47 +02:00
dependabot[bot]
54efab4fe5 Bump golang.org/x/tools from 0.23.0 to 0.24.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.23.0 to 0.24.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.23.0...v0.24.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 13:43:21 +02:00
dependabot[bot]
6a8ed8f441 Bump github.com/reconquest/karma-go from 1.4.0 to 1.5.0
Bumps [github.com/reconquest/karma-go](https://github.com/reconquest/karma-go) from 1.4.0 to 1.5.0.
- [Commits](https://github.com/reconquest/karma-go/compare/v1.4.0...v1.5.0)

---
updated-dependencies:
- dependency-name: github.com/reconquest/karma-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 13:43:09 +02:00
Manuel Rüger
fad9866b41 Include firstColumn parameter 2024-07-26 16:17:03 +02:00
dependabot[bot]
a48678fd0a Bump github.com/dreampuf/mermaid.go from 0.0.16 to 0.0.17
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.16 to 0.0.17.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.16...v0.0.17)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-26 01:08:35 +02:00
Manuel Rüger
1eb1b2454c Build with go 1.22 2024-07-26 00:43:03 +02:00
Manuel Rüger
349224d2aa Fix markdown lint 2024-07-26 00:01:40 +02:00
Manuel Rüger
9c25ab1e88 Bump to version 10.0.0 2024-07-25 23:57:22 +02:00
Manuel Rüger
2d9039c581 Document placeholders 2024-07-25 23:56:23 +02:00
Manuel Rüger
7991fe4c10 Drop legacy pattern check 2024-07-25 23:51:13 +02:00
Manuel Rüger
f727c860b4 Fix links containing an underscore 2024-07-25 23:42:20 +02:00
Manuel Rüger
29c4e3020e Fix Regex for links that contain square brackets in the text 2024-07-25 23:18:32 +02:00
dependabot[bot]
f54cceac83 Bump goreleaser/goreleaser-action from 5 to 6
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 5 to 6.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-25 23:18:23 +02:00
Manuel Rüger
c7e8b13b10 Prepare for goreleaser v2 2024-07-25 23:18:07 +02:00
Manuel Rüger
2dbae733f1 Do not render paragraph tags 2024-07-25 23:11:40 +02:00
Manuel Rüger
9e2f0143e9 Revert "Make references to pages within the same Confluence relative"
This reverts commit ecd563e0957c23573bc8b37ba398396b8144c0d0.
2024-07-25 23:10:49 +02:00
dependabot[bot]
313247f41a Bump github.com/urfave/cli/v2 from 2.27.2 to 2.27.3
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.2 to 2.27.3.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.2...v2.27.3)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-25 21:54:30 +02:00
Manuel Rüger
b48110a706 Bump to 9.13.1 2024-07-23 11:55:06 +02:00
Manuel Rüger
0ba9ff0653 Fix quoting 2024-07-23 10:46:03 +02:00
Manuel Rüger
ecf47cba38 .gorelease.yml: Replace deprecated fields 2024-07-19 17:22:24 +02:00
Manuel Rüger
a4722fe53e Bump to v9.13.0 2024-07-19 17:16:33 +02:00
Camiel de Vleeschauwer
8355d98969 Convert for strings.ToLower to strings.EqualFold as per linter 2024-07-19 17:14:38 +02:00
Camiel de Vleeschauwer
49ab5abad5 Change label management to rely on independent rest calls 2024-07-19 17:14:38 +02:00
Camiel de Vleeschauwer
4a3a69b997 Change label management to rely on independent rest calls 2024-07-19 17:14:38 +02:00
lukasriegel
d168ec8b14 feat: add placeholder in plain layout 2024-07-19 17:14:27 +02:00
dependabot[bot]
cdee2c4e0c Bump golang.org/x/tools from 0.22.0 to 0.23.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.22.0 to 0.23.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.22.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-12 23:02:55 +02:00
dependabot[bot]
796b1e5adb Bump golang from 1.22.4 to 1.22.5
Bumps golang from 1.22.4 to 1.22.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-12 23:02:28 +02:00
Angel Montana
ecd563e095 Make references to pages within the same Confluence relative 2024-07-02 15:04:57 +02:00
dependabot[bot]
44649c682b Bump github.com/yuin/goldmark from 1.7.3 to 1.7.4
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.3 to 1.7.4.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.3...v1.7.4)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-01 12:40:57 +02:00
dependabot[bot]
ae87b63cd6 Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 22:26:48 +02:00
dependabot[bot]
1f89187083 Bump github.com/yuin/goldmark from 1.7.2 to 1.7.3
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.2 to 1.7.3.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.2...v1.7.3)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-24 22:25:50 +02:00
dependabot[bot]
7063332234 Bump github.com/yuin/goldmark from 1.7.1 to 1.7.2
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.1 to 1.7.2.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.1...v1.7.2)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-19 23:15:59 +02:00
dependabot[bot]
b6d1d4e899 Bump golang from 1.22.3 to 1.22.4
Bumps golang from 1.22.3 to 1.22.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 14:30:05 +02:00
dependabot[bot]
818c9821e1 Bump golang.org/x/tools from 0.21.0 to 0.22.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.21.0...v0.22.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-11 14:29:26 +02:00
ChrisRu82
5199fff49a fix: ac:jiraissues to use correct format for the urls 2024-05-31 14:09:31 +02:00
dependabot[bot]
94a00c5e22 Bump golangci/golangci-lint-action from 5 to 6
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:36:23 +02:00
dependabot[bot]
cb9d9f0298 Bump golang.org/x/tools from 0.20.0 to 0.21.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:36:08 +02:00
dependabot[bot]
4daee45079 Bump golang from 1.22.2 to 1.22.3
Bumps golang from 1.22.2 to 1.22.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-14 16:35:52 +02:00
dependabot[bot]
75c09e78e0 Bump golangci/golangci-lint-action from 4 to 5
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 22:12:09 +02:00
dependabot[bot]
c62541d5ae Bump github.com/urfave/cli/v2 from 2.27.1 to 2.27.2
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.27.1 to 2.27.2.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.27.1...v2.27.2)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-29 22:11:56 +02:00
Manuel Rüger
73629a2838 Bump to 9.12.0 2024-04-11 19:39:11 +02:00
Stephen Paulger
9b91ee3344 Add option to pass a version message 2024-04-11 19:31:32 +02:00
dependabot[bot]
b7cb1824ae Bump golang.org/x/tools from 0.19.0 to 0.20.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 19:29:11 +02:00
dependabot[bot]
6b70249e7f Bump DavidAnson/markdownlint-cli2-action from 15 to 16
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 15 to 16.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v15...v16)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 18:01:34 +02:00
dependabot[bot]
a324dae511 Bump github.com/yuin/goldmark from 1.7.0 to 1.7.1
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 17:59:36 +02:00
dependabot[bot]
14e6efd2cb Bump golang from 1.22.1 to 1.22.2
Bumps golang from 1.22.1 to 1.22.2.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 17:58:37 +02:00
dependabot[bot]
c1fbd12b5b Bump golang from 1.21.6 to 1.22.1
Bumps golang from 1.21.6 to 1.22.1.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 17:27:53 +01:00
dependabot[bot]
ea60ad3170 Bump golang.org/x/tools from 0.18.0 to 0.19.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.18.0 to 0.19.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.18.0...v0.19.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 12:35:35 +01:00
dependabot[bot]
e3d713939b Bump github.com/dreampuf/mermaid.go from 0.0.15 to 0.0.16
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.15 to 0.0.16.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.15...v0.0.16)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-06 11:13:36 +01:00
dependabot[bot]
b7d64ce03a Bump github.com/stretchr/testify from 1.8.4 to 1.9.0
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.8.4 to 1.9.0.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.8.4...v1.9.0)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 18:11:31 +01:00
dependabot[bot]
093d62e6d1 Bump golang.org/x/tools from 0.17.0 to 0.18.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.17.0 to 0.18.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.17.0...v0.18.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-19 21:06:14 +01:00
dependabot[bot]
d5758e21e8 Bump golangci/golangci-lint-action from 3 to 4
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 3 to 4.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-12 18:24:05 +01:00
dependabot[bot]
dc99e34373 Bump github.com/yuin/goldmark from 1.6.0 to 1.7.0
Bumps [github.com/yuin/goldmark](https://github.com/yuin/goldmark) from 1.6.0 to 1.7.0.
- [Release notes](https://github.com/yuin/goldmark/releases)
- [Commits](https://github.com/yuin/goldmark/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: github.com/yuin/goldmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-09 00:19:22 +01:00
Stephen Paulger
41358cf9b8 Build multi-arch image for amd64 and arm64 2024-02-09 00:18:30 +01:00
dependabot[bot]
3bbf5e277a Bump DavidAnson/markdownlint-cli2-action from 14 to 15
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 14 to 15.
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v14...v15)

---
updated-dependencies:
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 17:24:04 +01:00
Manuel Rüger
a706499803 Add markdown lint 2024-01-26 16:27:51 +01:00
Manuel Rüger
78f7666697 Document column macro 2024-01-26 16:18:14 +01:00
Guilherme Paiva
8dd83bdf62 add support to column macro 2024-01-26 16:09:44 +01:00
Manuel Rüger
a04f41161f Bump goldmark 1.6.0 and go 1.21 2024-01-26 16:03:25 +01:00
Manuel Rüger
ac8ff7f0b7 Bump to 9.11.1 2024-01-15 12:38:56 +01:00
dependabot[bot]
0073834e85 Bump github.com/dreampuf/mermaid.go from 0.0.14 to 0.0.15
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.14 to 0.0.15.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.14...v0.0.15)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 11:30:37 +01:00
dependabot[bot]
53e367cb06 Bump golang from 1.21.5 to 1.21.6
Bumps golang from 1.21.5 to 1.21.6.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 11:30:22 +01:00
dependabot[bot]
3ef2a244d9 Bump golang.org/x/tools from 0.16.1 to 0.17.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.16.1 to 0.17.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.16.1...v0.17.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-15 11:29:59 +01:00
dependabot[bot]
cfc76d87ce Bump github.com/urfave/cli/v2 from 2.26.0 to 2.27.1
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.26.0 to 2.27.1.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.26.0...v2.27.1)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 11:24:40 +01:00
Stephen Paulger
aaae918478
Fix layout section attribute tags (#401)
* Fix attribute tags

* Update test date to expect attribute with ac: prefix
2023-12-19 13:57:52 +01:00
dependabot[bot]
21d76ec25a
Bump golang.org/x/tools from 0.16.0 to 0.16.1 (#402)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.16.0...v0.16.1)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-18 18:28:52 +01:00
dependabot[bot]
165226a491
Bump golang from 1.21.4 to 1.21.5 (#399)
Bumps golang from 1.21.4 to 1.21.5.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 14:27:45 +01:00
dependabot[bot]
834896b2f0
Bump actions/setup-go from 4 to 5 (#398)
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 4 to 5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 14:27:30 +01:00
dependabot[bot]
a8d762ab83
Bump github.com/reconquest/karma-go from 1.3.1 to 1.4.0 (#400)
Bumps [github.com/reconquest/karma-go](https://github.com/reconquest/karma-go) from 1.3.1 to 1.4.0.
- [Commits](https://github.com/reconquest/karma-go/compare/v1.3.1...v1.4.0)

---
updated-dependencies:
- dependency-name: github.com/reconquest/karma-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-11 14:27:12 +01:00
Manuel Rüger
10dc8c0a62 Version 9.11.0 2023-12-04 10:20:50 +01:00
dependabot[bot]
d64bece68d
Bump golang.org/x/tools from 0.15.0 to 0.16.0 (#396)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.15.0 to 0.16.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-03 20:08:08 +01:00
dependabot[bot]
da25cf19d2
Bump github.com/urfave/cli/v2 from 2.25.7 to 2.26.0 (#395)
Bumps [github.com/urfave/cli/v2](https://github.com/urfave/cli) from 2.25.7 to 2.26.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v2.25.7...v2.26.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-03 20:07:57 +01:00
Oliver Radwell
ab1b3c2e60
feat: Support for double star (**) globbing (#392)
* feat: Support for double star (**) globbing

* fixup: use FilepathGlob instead of Glob
2023-12-01 13:31:09 +01:00
Oliver Radwell
9c0941edd4 docs: Fix typo: preprended 2023-11-22 23:07:53 +01:00
Oliver Radwell
38d974bde3 docs: Fix typo: preprended 2023-11-22 23:07:53 +01:00
dependabot[bot]
b752586ff3 Bump github.com/reconquest/karma-go from 1.2.0 to 1.3.1
Bumps [github.com/reconquest/karma-go](https://github.com/reconquest/karma-go) from 1.2.0 to 1.3.1.
- [Commits](https://github.com/reconquest/karma-go/compare/v1.2.0...v1.3.1)

---
updated-dependencies:
- dependency-name: github.com/reconquest/karma-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-20 17:52:15 +01:00
dependabot[bot]
f63b499887 Bump golang from 1.21.3 to 1.21.4
Bumps golang from 1.21.3 to 1.21.4.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 18:32:20 +01:00
dependabot[bot]
15d9295781 Bump golang.org/x/tools from 0.14.0 to 0.15.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.14.0 to 0.15.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.14.0...v0.15.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-13 18:32:02 +01:00
dependabot[bot]
4c865b4546 Bump github.com/dreampuf/mermaid.go from 0.0.13 to 0.0.14
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.13 to 0.0.14.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.13...v0.0.14)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-06 17:36:51 +01:00
Manuel Rüger
118575684d Bump version to 9.10.1 2023-10-23 21:12:37 +02:00
Manuel Rüger
292bd0d713 Bump mermaid render timeout to 90s 2023-10-23 21:12:37 +02:00
Manuel Rüger
04bf20c0a3 change: Move back to XHTML 2023-10-23 21:12:37 +02:00
dependabot[bot]
40015ed86f Bump golang from 1.20.5 to 1.21.3
Bumps golang from 1.20.5 to 1.21.3.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 19:35:45 +02:00
Manuel Rüger
8f1772455e .github: Update Issue template 2023-10-23 19:32:29 +02:00
Manuel Rüger
28d2f31a60 dependabot: Add Docker config 2023-10-23 19:32:14 +02:00
dependabot[bot]
df9166c513 Bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-23 18:36:29 +02:00
Manuel Rüger
523ec6dbff Version bump to 9.10.0 2023-10-19 13:34:21 +02:00
Manuel Rüger
d269369911 feat: Add fallback directory to load includes from 2023-10-19 13:32:39 +02:00
Manuel Rüger
8e16f6f29c change: Move from XHTML to HTML5 2023-10-18 15:29:16 +02:00
dependabot[bot]
afd15a20db Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:45:23 +02:00
dependabot[bot]
d0904f6a47 Bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:43:36 +02:00
dependabot[bot]
521486148d Bump goreleaser/goreleaser-action from 4 to 5
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 4 to 5.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:43:22 +02:00
dependabot[bot]
b0124f74a1 Bump golang.org/x/tools from 0.12.0 to 0.14.0
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.12.0 to 0.14.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.12.0...v0.14.0)

---
updated-dependencies:
- dependency-name: golang.org/x/tools
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:43:03 +02:00
dependabot[bot]
c5134d926a Bump github.com/dreampuf/mermaid.go from 0.0.12 to 0.0.13
Bumps [github.com/dreampuf/mermaid.go](https://github.com/dreampuf/mermaid.go) from 0.0.12 to 0.0.13.
- [Release notes](https://github.com/dreampuf/mermaid.go/releases)
- [Commits](https://github.com/dreampuf/mermaid.go/compare/v0.0.12...v0.0.13)

---
updated-dependencies:
- dependency-name: github.com/dreampuf/mermaid.go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:42:47 +02:00
dependabot[bot]
dc65135fa8 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:42:34 +02:00
dependabot[bot]
26fbd62a03 Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-16 19:42:08 +02:00
Manuel Rüger
9d03f096aa Add dependabot integration 2023-10-16 19:39:48 +02:00
Judson Lester
3157a8c4d9 Rebase fixup 2023-09-11 12:02:39 +02:00
Judson Lester
da3b31b3a9 Fixes for Attachments 2023-09-11 12:02:39 +02:00
Judson Lester
f0d7bfdb75 Adding comments, removing stray imports 2023-09-07 21:30:52 +02:00
Judson Lester
20c65ca373 Adding linebreak stripping 2023-09-07 21:30:52 +02:00
Stephan Hradek
11896e43ec describe config directory locations 2023-09-07 18:12:49 +02:00
Stephan Hradek
de50b62875 switch from $HOME to UserConfigDir 2023-09-07 18:12:49 +02:00
Manuel Rüger
295d17e6f3 Sync confluencetag parser with upstream
Adds changes from
254b9f8f77
2023-09-04 22:37:55 +02:00
Manuel Rüger
f57b4245f9 chore: Migrate to a goldmark extension 2023-09-04 17:16:34 +02:00
Manuel Rüger
10d0778adf Bump to 9.9.0 2023-08-30 15:15:13 +02:00
Manuel Rüger
737b9239f8 feat: Allow scaling of mermaid rendering 2023-08-30 15:15:13 +02:00
Manuel Rüger
b426d5c6b1 Define parent pages from CLI 2023-08-10 08:02:38 +02:00
Manuel Rüger
8b72bc8e20 Add pagetreesearch macro 2023-08-09 15:43:06 +02:00
Manuel Rüger
4c81bbe7f9 Bump to 9.7.1 2023-07-31 10:06:31 +02:00
Manuel Rüger
3c7512b7e1 Bump dependencies 2023-07-31 10:06:31 +02:00
Manuel Rüger
d147aac98b .goreleaser: Update deprecated syntax 2023-07-05 17:40:55 +02:00
Manuel Rüger
cbe0b05dab Bump version to 9.7.0 2023-07-05 17:33:33 +02:00
Manuel Rüger
bf16d55d68 Add recently-updated macro 2023-07-05 17:32:55 +02:00
Manuel Rüger
ac982f3077 Add panel macro 2023-07-05 15:48:11 +02:00
Manuel Rüger
179700f3e4 .github: Reorder steps to use module caching 2023-07-03 15:39:50 +02:00
gmarraff
e18b30b38d doc: clarify links usage of page title with spaces
The word space can be misleading in this context since it can be confused as 'Confluence Space'.
2023-07-03 15:33:41 +02:00
Manuel Rüger
bc5c7ae2e7 Bump to v9.6.2 2023-06-21 18:43:14 +02:00
Manuel Rüger
494ddce3d1 go.mod: Update dependencies 2023-06-21 18:43:14 +02:00
Manuel Rüger
9fb9d893b2 Bump version to 9.6.1 2023-06-14 15:57:31 +02:00
Manuel Rüger
4591586b01 Disable typographer extension
This caused issues with confluence macros
2023-06-14 15:56:40 +02:00
Manuel Rüger
857bc871e0 Fix flaky mermaid png test
Only check for the header as bytes might changed depending on system png
library
2023-06-14 15:54:53 +02:00
Manuel Rüger
d07e8ec291 Bump version to 9.6.0 2023-06-08 22:54:15 +02:00
Ryan Kitchen
bec6d38aed Update README.md
Co-authored-by: Manuel Rüger <manuel@rueg.eu>
2023-06-08 22:52:46 +02:00
Ryan Kitchen
383e4ae2d0 Update pkg/mark/stdlib/stdlib.go
Co-authored-by: Manuel Rüger <manuel@rueg.eu>
2023-06-08 22:52:46 +02:00
Ryan
e41572c20f readme 2023-06-08 22:52:46 +02:00
Ryan
d8109c6e82 readme 2023-06-08 22:52:46 +02:00
Ryan
063b6e7e1b page-tree 2023-06-08 22:52:46 +02:00
Manuel Rüger
22bf46026b Fix user mentioning 2023-06-08 22:49:15 +02:00
Manuel Rüger
278488ed88 Update mermaid to v10.2.0 2023-05-26 11:26:44 +02:00
Zackery Griesinger
80d3be268d Add Jira Filter View Macro 2023-05-25 18:05:31 +02:00
Manuel Rüger
0988a7aba1 Add Page properties macro 2023-05-25 07:21:10 +02:00
Manuel Rüger
28ee195a77 Add Page Properties Report macro 2023-05-25 07:21:10 +02:00
Manuel Rüger
3307f329d9 Add contentbylabel macro 2023-05-25 07:21:10 +02:00
Manuel Rüger
dbb5237338 Bump version to 9.5.2 2023-05-23 12:36:16 +02:00
Manuel Rüger
0b6dd09013 Drop newlines completely from macros 2023-05-23 12:30:46 +02:00
Manuel Rüger
dbadf47954 Bump version to 9.5.1 2023-05-23 11:53:43 +02:00
Manuel Rüger
9a37fc4713 Drop newlines within macros
Newlines will cause trouble when rendering and the macro is used inside
a table.
2023-05-23 11:49:04 +02:00
Manuel Rüger
6d8d70a415 Version bump to 9.5.0 2023-05-23 08:43:06 +02:00
Manuel Rüger
cb5e4aa28a Bump dependencies 2023-05-23 07:33:11 +02:00
Manuel Rüger
6a63913a1e Add user profile macro 2023-05-23 07:33:11 +02:00
Manuel Rüger
d67cc63871 Fallback to old user search 2023-05-23 07:33:11 +02:00
Manuel Rüger
08281712cc Add expand macro 2023-05-23 07:33:11 +02:00
Manuel Rüger
484f988f32 Move drop-h1 to the renderer
This is more reliable than a regular expression.
2023-05-22 07:55:04 +02:00
Ryan Kitchen
5602297459 Update README.md
updated per mrueg suggestions
2023-05-12 19:45:33 +02:00
Ryan Kitchen
679eb7fb48 Update README.md
tiny changes, big impact
2023-05-12 19:45:33 +02:00
Ryan Kitchen
f0bb70651e Update README.md
Co-authored-by: Manuel Rüger <manuel@rueg.eu>
2023-05-10 19:06:40 +02:00
Ryan Kitchen
ae5163d470 Update README.md 2023-05-10 19:06:40 +02:00
Manuel Rüger
3271218a9a Bump version to 9.4.0 2023-05-05 14:52:52 +02:00
Manuel Rüger
2aa1606d11 Fix usage of strings.ReplaceAll 2023-05-05 14:51:51 +02:00
Rémi Courtel
eba2396010 fix 305: external images URLs are not properly escaped 2023-05-05 14:41:59 +02:00
Manuel Rüger
622be6e25f Support customizable layouts 2023-05-05 10:52:04 +02:00
Manuel Rüger
21d0a7d8bc Bump version to 9.3.1 2023-05-03 21:32:42 +02:00
Manuel Rüger
13c594981e Fix mermaid-go rendering 2023-05-03 21:31:45 +02:00
Manuel Rüger
af221d50c7 .goreleaser.yml: Support arm64 builds 2023-05-03 21:26:41 +02:00
Manuel Rüger
72cfc04ab6 Bump version to 9.3.0 2023-05-03 18:47:50 +02:00
Manuel Rüger
fd97ee70f9 Support inline images 2023-05-03 18:47:20 +02:00
Manuel Rüger
2b756daf37 Add debug output for config 2023-05-03 18:30:47 +02:00
Manuel Rüger
51accf008f Bump version to 9.2.1 2023-04-26 12:15:14 +02:00
Manuel Rüger
73f2693421 Dockerfile: Use dumb-init and overwrite entrypoint 2023-04-26 12:14:49 +02:00
Manuel Rüger
abd0c8bad6 Bump version to 9.2.0 2023-04-26 08:03:04 +02:00
Dreampuf
d9d560eda0 Add support for mermaid via library
Implementation is largely based on: https://github.com/kovetskiy/mark/pull/167

Co-Authored-By: Manuel Rueger <manuel@rueg.eu>
2023-04-25 23:59:17 +02:00
Manuel Rüger
88c070f524 Add issue templates 2023-04-25 22:58:00 +02:00
Manuel Rüger
4c3d417725 Ignore error if default config file does not exist 2023-04-25 22:47:40 +02:00
Manuel Rüger
2f44bcb6be Make config backwards-compatible 2023-04-20 14:34:55 +02:00
Manuel Rüger
262853f6c0 Simplify config handling
* Switch to urfave/cli/v2
* Add more environment variables
2023-04-20 14:34:55 +02:00
Manuel Rüger
d1f69bc704 Bump version to 9.1.4 2023-04-03 14:26:09 +02:00
Manuel Rüger
2773e15056 .github: Quote go version 2023-04-03 14:26:09 +02:00
Manuel Rüger
1c1f82a55e Bump version to 9.1.3 2023-04-03 14:06:23 +02:00
Manuel Rüger
2e52ae8847 .github: Fix goreleaser build 2023-04-03 14:06:23 +02:00
Manuel Rüger
c6d01eb3de Bump version to 9.1.2 2023-04-03 13:56:49 +02:00
Manuel Rüger
f75924b1a9 .github: Fix tag 2023-04-03 13:56:49 +02:00
Manuel Rüger
20afbc534a Bump version to 9.1.1 2023-04-03 13:42:38 +02:00
Manuel Rüger
768b470322 .github: Fix release workflow 2023-04-03 13:42:38 +02:00
Manuel Rüger
657d4f5c61 Bump version to 9.1.0 2023-04-03 13:32:51 +02:00
Manuel Rüger
1b6ab893ef
Merge pull request #279 from mrueg/build-fixes
go 1.20 / docker image push automation
2023-04-03 13:32:20 +02:00
Manuel Rüger
429bbf9bff Push Docker image on tag 2023-04-03 13:30:48 +02:00
Manuel Rüger
7969bcbfdd
Merge pull request #278 from mrueg/add-anchor
feat: Support anchor macro
2023-04-03 13:30:10 +02:00
Manuel Rüger
eade059cad Update to go 1.20 2023-04-03 13:21:40 +02:00
Manuel Rüger
9bb9f45326 feat: Support anchor macro 2023-04-03 13:11:35 +02:00
Manuel Rüger
a29feb1e96
Merge pull request #277 from mrueg/support-ac-include
feat: Support page include, excerpt and excerpt-include macro
2023-04-03 10:23:00 +02:00
Manuel Rüger
deb7cd9cbc feat: Add Excerpt and ExcerptInclude macros 2023-03-31 17:35:18 +02:00
Manuel Rüger
1285947ab3 feat: Support page inclusion macro
Also generalize the ac_tag_parser a bit and support <ri:* /> tags as
well
2023-03-31 16:47:24 +02:00
Manuel Rüger
943a356508 Bump version to 9.0.3 2023-03-31 11:40:29 +02:00
Manuel Rüger
8193f2d466
Merge pull request #273 from bernd/fix/issue-272
Implement a custom parser for `<ac:*/>` tags
2023-03-31 11:35:42 +02:00
Bernd Ahlers
80d906417c
Fix custom link renderer and add tests for Confluence links
Since we now have a custom parser for <ac:*/> tags, the custom link
renderer added an additional </a> tag at the end of each internal
Confluence link.

Add tests for internal links and add an example for internal links with
spaces in page titles to the README file.
2023-03-31 11:09:58 +02:00
Bernd Ahlers
6e4a912b11
Implement a custom parser for <ac:*/> tags
This replaces the workaround to replace colons in <ac:*/> tags with a
magic string with a custom parser for these tags to parse them as
ast.KindRawHtml.

The custom parser is a stripped down version of goldmark's rawHTMLParser.
2023-03-31 10:51:50 +02:00
Manuel Rüger
e7e61ba234 Bump version to 9.0.2 2023-03-30 15:02:05 +02:00
Manuel Rüger
a60dd52442
Merge pull request #270 from bernd/fix/ac-tag-escaping
Fix "<ac:*>" tag rendering
2023-03-30 15:01:03 +02:00
Bernd Ahlers
974de93ef1
Rename variable 2023-03-30 14:34:19 +02:00
Bernd Ahlers
238ae29b59
Fix "<ac:*>" tag rendering
Extend the existing workaround to avoid HTML element escaping to handle
more variants.
2023-03-30 14:30:15 +02:00
Manuel Rüger
98e15ed7ff Bump version to 9.0.1 2023-03-29 15:57:13 +02:00
Manuel Rüger
fca934f90c
Merge pull request #268 from mrueg/fix-codeblock
Render codeblocks properly
2023-03-29 15:56:23 +02:00
Manuel Rüger
6265c7ff81 Render CodeBlocks properly 2023-03-29 15:45:11 +02:00
Manuel Rüger
fef66925d1 Bump version to 9.0 2023-03-29 11:01:07 +02:00
Manuel Rüger
93218f1e69
Merge pull request #260 from mrueg/switch-to-goldmark
Replace blackfriday with goldmark
2023-03-28 16:54:04 +02:00
Manuel Rüger
a3dc8bed05 Replace blackfriday with goldmark
This change replaces the unmaintained blackfriday markdown renderer with the
goldmark renderer.
The goal is to have wider support for more markdown extensions and ideally
have a better solution for mermaid transformation via extensions as well.
2023-03-27 20:36:43 +02:00
Manuel Rüger
3dd6d0ab99 Bump version to 8.10 2023-03-27 20:16:21 +02:00
Manuel Rüger
700cbc139a
Merge pull request #265 from lucasoskorep/fix/relative-links
Fix: Relative links extractmetadata files for filetypes other than markdown.
2023-03-23 08:01:46 +01:00
Lucas Oskorep
30b0785fae feat: moving to IsTextFile for file type detection 2023-03-22 19:14:34 -04:00
Lucas Oskorep
67c9b248dd fix: extract metadata fails for file types which are not .md 2023-03-22 15:45:14 -04:00
Manuel Rüger
d00d94afa3 Bump to version 8.9 2023-03-21 11:09:47 +01:00
Manuel Rüger
530ff5cc3c
Merge pull request #262 from mrueg/fix-relative-links
fix: Support relative links with titleFromH1
2023-03-21 11:07:59 +01:00
Manuel Rüger
9840c01e8a Include space parameter as well 2023-03-20 22:54:11 +01:00
Manuel Rüger
21cf51efd7 Clean up logic a bit 2023-03-20 19:56:02 +01:00
Manuel Rüger
20d3d1f05c fix: Support relative links with titleFromH1 2023-03-20 19:43:01 +01:00
Manuel Rüger
eab5655456
Merge pull request #258 from bernd/add/content-appearance-option
Add Content-Appearance header
2023-03-06 15:00:18 +01:00
Bernd Ahlers
9e656ece15
Add Content-Appearance header
Allows switching between the "full-width" and "fixed" page layouts.

The "fixed" layout renders the page in a narrow column. (Confluence
default)

If not configured, it defaults to "full-width".
2023-03-06 10:09:28 +01:00
Manuel Rüger
4b5e9c23ec
Merge pull request #256 from mrueg/bump-8.8
Bump version to 8.8
2023-02-21 15:25:34 +01:00
Manuel Rüger
c0bc7dcd35 Bump version to 8.8 2023-02-21 14:56:37 +01:00
Manuel Rüger
cea051057a
Merge pull request #255 from mrueg/build-image
Build image
2023-02-21 14:49:22 +01:00
Manuel Rüger
2a9f830817 .github: Add build image step 2023-02-21 14:46:46 +01:00
Manuel Rüger
ca2dbe5fc4 Dockerfile: Freeze builder image versions 2023-02-21 14:46:42 +01:00
Manuel Rüger
e754bf17a0
Merge pull request #250 from Fethbita/master
ac:image macro
2023-02-13 15:50:17 +01:00
Fethbita
df3e95f5fd
Merge branch 'kovetskiy:master' into master 2023-02-13 16:46:45 +02:00
Fethbita
24aa73e243
Add width keyword for html comment 2023-02-13 16:46:06 +02:00
Manuel Rüger
fb8e589bc4
Merge pull request #252 from mrueg/add-blog-posts-macro
Add blog-posts macro
2023-02-07 15:43:48 +01:00
Manuel Rüger
20ba4dd8fe
Merge pull request #251 from jay-stillman/issue/173
bugfix: Remove forced default width, forced full-width
2023-02-07 13:50:12 +01:00
Manuel Rüger
738400c292 Add blog-posts macro 2023-02-06 18:16:39 +01:00
Jay Stillman
4d16a1f606 bugfix: Remove forced default width, forced full-width without editor config #175 2023-02-06 01:18:34 -05:00
Fethbita
e0222ae6bd
Add ac:image width and edited README.md 2023-02-04 15:20:21 +02:00
Sylvain Malnuit
8debc23476
ac:image 2023-02-04 15:15:47 +02:00
Manuel Rüger
b82e18f8b8
Merge pull request #248 from prokod/bugfix/prokod/#247
To add fallback to blockquote when type is none
2023-01-31 14:18:48 +01:00
Noam Asor
ce9573a86e To add fallack to blockquote when type is none 2023-01-28 21:15:31 +01:00
Manuel Rüger
cd08a70ba9 .github: Build with go 1.19 2023-01-27 11:40:46 +01:00
Manuel Rüger
d09ee6ea22 Bump version to 8.7 2023-01-27 11:38:15 +01:00
Manuel Rüger
adee0189bd
Merge pull request #244 from xiu/fix/214
fix: Title not being extracted when not on the first line
2023-01-27 11:37:00 +01:00
Guillaume Hérail
1b3c7b4127 fix: Properly handle macro when extracting Metadata
When a macro is set in the header, only the first line will be read and then
discarded. This makes sure we keep the macro in and stop processing metadata
when we hit a macro.

Co-authored-by: Manuel Rüger <manuel@rueg.eu>
2023-01-26 17:01:47 +01:00
Manuel Rüger
8a1bd88577
Merge pull request #246 from mrueg/bump-deps
go.mod: Update dependencies
2023-01-26 10:44:42 +01:00
Manuel Rüger
fb8213bc0d go.mod: Update dependencies 2023-01-26 09:49:40 +01:00
Guillaume Hérail
667e7be221 fix: Title not being extracted when not on the first line
While a page with a macro is processed, having a macro leaves an unclean buffer
for further processing steps, including title extraction.  The problem seems to
come from `ExtractMeta`: as it runs through the metadata, it hits the first
line of the macro but since it doesn't look like a complete Metadata (e.g.
matching either `\[\]:\s*#\s*\(([^:]+):\s*(.*)\)` or
`<!--\s*([^:]+):\s*(.*)\s*-->` in
https://github.com/kovetskiy/mark/blob/master/pkg/mark/meta.go#L37:L38, it will
break the loop in
https://github.com/kovetskiy/mark/blob/master/pkg/mark/meta.go#L61 and return
the final document as:
```
  Template: ac:children
  Style: h2
  Excerpt: none
  Page:
  TestSpace:Test
  Reverse: false
  All: true -->

:children:
```

This then goes into `ExtractDocumentLeadingH1` which doesn't find a match for
the regex `^#[^#]\s*(.*)\s*\n`, returning an empty title since the title is not
on the first line. This commit, while it doesn't fix the unclean document,
fixes the regex to properly find the title.

Closes #214
2023-01-25 11:51:56 +01:00
Manuel Rüger
5ff8daf5e1
Merge pull request #243 from mrueg/fix-lint
Lint: Disable staticcheck for strings.Title
2023-01-19 11:35:06 +01:00
Manuel Rüger
7204ef21f6 Lint: Disable staticcheck for strings.Title
strings.Title is deprecated, but we need to keep using it since there is
no 1:1 replacement and otherwise this might change page names on
confluence.
2023-01-18 19:46:27 +01:00
Manuel Rüger
1a4ae1afb5
Merge pull request #242 from lucasoskorep/master
fix: rendermode typo broke builds
2023-01-18 19:31:02 +01:00
Lucas Oskorep
6083ddcc88 fix: rendermode typo broke builds 2023-01-17 12:46:49 -05:00
Manuel Rüger
2f39cd80e7
Merge pull request #234 from Nr18/feat/jiraissues
feat: support ac:jiraissues macro
2023-01-16 15:23:18 +01:00
Manuel Rüger
abc91203bb
.github: Move CI into the right folder (#239) 2023-01-06 17:34:49 +00:00
Manuel Rüger
4ef1d47b5c
Merge pull request #238 from mrueg/ci
Add a CI pipeline that runs on PRs
2023-01-05 23:05:36 +01:00
Manuel Rüger
8deecfd67a Lint 2023-01-05 11:37:42 +01:00
Manuel Rüger
bde4b70242 .github: Add CI pipeline 2023-01-05 11:26:50 +01:00
Manuel Rüger
721dd1b642 Makefile: Add test target 2023-01-05 11:25:14 +01:00
Egor Kovetskiy
7eb7ccc68e no funding needed
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2023-01-04 09:09:37 +00:00
Egor Kovetskiy
843089aea0 all-contributors: generate thank you list
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2023-01-04 09:08:15 +00:00
Egor Kovetskiy
ab5407091b bump version
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2023-01-04 08:59:03 +00:00
Noam Asor
8c02497b5f
Feature/prokod/#194 (#237)
* Initial commit (w/ debug)

* To remove debug
2023-01-04 08:54:15 +00:00
Egor Kovetskiy
5920dbf67a bump version
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2023-01-03 17:58:28 +00:00
Zackery Griesinger
90d8729a7d
Fix for jira.com hosted sites (#231) 2023-01-03 17:54:30 +00:00
Manuel Rüger
3e558ac2e3
Replace deprecated io/ioutils (#230) 2023-01-03 17:54:04 +00:00
Stephan Hradek
f4bbbb19ca
Feature/themed blocks (#223)
* Allow themed blocks

* add macro code for themed blocks
2023-01-03 17:53:41 +00:00
Stephan Hradek
0b745c25e4
This should fix #171 by allowing a markdown link to start with ac: (#222) 2023-01-03 17:53:20 +00:00
Manuel Rüger
55b58bd59b
Allow Title via h1 without any other metadata (#229) 2023-01-03 17:52:48 +00:00
Joris Conijn
5f3238b48a
feat: support ac:jiraissues macro
Add support for the `ac:jiraissues` macro. By adding the template to the stdlib. Not sure if there are other places where you need to add stuff for macros. Or how I could test this locally.
2022-12-13 11:56:27 +01:00
Egor Kovetskiy
ef71aa1792 bump version
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2022-10-19 20:44:04 +06:00
Egor Kovetskiy
1ebb29eba0 small style fixes related to templates 2022-10-19 20:43:40 +06:00
Manuel Rüger
78345736d5
go.mod: Update dependencies (#211)
Also switch to yaml.v3
2022-10-19 20:43:12 +06:00
Toru Kawaguchi
d276b7e78a
Correct attachment name to include path on update (#215) 2022-10-19 20:42:32 +06:00
recrtl
49c25c2c29
fix 178: update breaks on more than 50 attachments (#220) 2022-10-19 20:38:49 +06:00
Egor Kovetskiy
e1d8eccfff fix ancestry search
Signed-off-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2022-06-28 16:58:44 +06:00
Will Gorman
5d2c1b0d84
Fix builds on go 1.18 (#206)
Avoids errors like "../../go/pkg/mod/golang.org/x/sys@v0.0.0-20200116001909-b77594299b42/unix/syscall_darwin.1_13.go:25:3: //go:linkname must refer to declared function or variable"
2022-06-21 10:30:22 +06:00
Stephan Hradek
c70eb550fe
Do not require password and url when compileonly (#193) 2022-06-10 10:13:32 +06:00
Stephan Hradek
abfd01cc74
fix issue 177 (#200) 2022-06-10 10:13:22 +06:00
datsickkunt
e52d40c922
update install instructions with latest go standards (#202) 2022-06-10 10:13:05 +06:00
Stephan Hradek
39dfdec099
fix 142 redone for current master (#199) 2022-06-07 14:16:24 +06:00
Stephan Hradek
ac2132b9c6
Feature/docker4build (#192)
* Help developers with docker-compose configuration

* unrequire Buillderfile

* GOOS and GOARCH from environment
2022-06-07 10:27:18 +06:00
Stephan Hradek
9ec7728d08
introduce inline templates (#198) 2022-06-07 10:26:15 +06:00
Stephan Hradek
4c812741ac
Feature/h1 title (#196)
* h1_title config

* introduce h1_title in config

* add h1_drop config setting

* allsow allow h1_drop in config
2022-06-06 12:38:23 +06:00
Stephan Hradek
3c7bd6133f
allow templates and macros to include blanks in path (#191) 2022-06-06 12:35:48 +06:00
Egor Kovetskiy
5428cc6833 bump to 8.1 2022-04-22 15:26:25 +06:00
cc-chris
a6ac341799
fix for nil metadata (issue #161) (#162) 2022-04-22 15:18:59 +06:00
guoweis-outreach
9ae3e658d2
fix the README (#172)
this change allows the `box` macro to have http link inside
2022-04-22 15:17:59 +06:00
Joel Andritsch
7b359b19f7
bugfix: Add delay between create and update calls (#174)
This helps mitigate a 409 conflict response from Confluence when
creating a new page and then updating it right away. This may not be the
cleanest solution, but it seems to do the trick.

Resolves #139
2022-04-22 15:17:48 +06:00
Joel Andritsch
5ad1a4abe5
bugfix: Force pages to default width. (#175)
This change is to compensate for an API bug in Confluence where newly
create pages end up getting a random width of either "fixed" or "full".

Fixes #173
2022-04-22 15:16:46 +06:00
Philipp
f95835469c
fix: remove double collapse for codeboxes (#179)
This fixes a bug where if you used the code macro the renderer would create
an additional expand macro if the collapse option was set. This led to
the code section being collapsiable twice.

Co-authored-by: Philipp Reinke <philipp.reinke@scandio.de>
2022-04-22 15:16:04 +06:00
Egor Kovetskiy
2f34e93973 do not build for windows: it is broken 2022-02-02 16:06:19 +06:00
Egor Kovetskiy
c57d8e63e7 bump to 8.0 2022-02-02 16:03:06 +06:00
Dreampuf
9a4f9ff32d
Add subdirectories related templates (#158) 2022-02-02 16:02:01 +06:00
Dreampuf
2251e8ad8a
Add footnotes support (#156) 2022-02-02 16:01:38 +06:00
Pommier Vincent
c8709eecd1
feat: Add support of confluence server. (#154) 2022-02-02 16:01:26 +06:00
Egor Kovetskiy
49eb97b434 add open_collective to FUNDING 2022-01-18 12:16:26 +06:00
Egor Kovetskiy
b15f1f8e70 bump to 7.0 2022-01-18 12:10:46 +06:00
klysunkin
9a7146c7d7
Fix for subdirectories related attachments (issue #31) (#151) 2022-01-18 12:07:36 +06:00
klysunkin
8d58ff26a3
Space from config and title from h1 (#153)
* Add option to define Space.
Add option to use H1 heading to define page title.

* Update readme
2022-01-18 12:05:26 +06:00
Joey Freeland
851a8047f3
feat: update to go 1.17 (#147) 2021-12-27 16:35:22 +06:00
Egor Kovetskiy
2cbb942bc4 add .dockerignore 2021-12-09 11:48:56 +06:00
Egor Kovetskiy
9cde833a18 fix updating attachments when confluence returns a short response object 2021-12-09 11:46:57 +06:00
Egor Kovetskiy
f1e861c2fa bump to 6.6 2021-12-09 11:14:34 +06:00
guoweis-outreach
08f37fe2b9
feat: add iframe support (#143)
* feat: add iframe support

* add comment
2021-12-09 11:14:01 +06:00
Mateus Miranda
ac8133a8b9
Change log level when files are not found (#138)
* Change log level when files are not found

* Adding new flag CI
2021-12-02 14:38:47 +06:00
Egor Kovetskiy
f3ff1dc098 Add @mmiranda as a contributor 2021-11-30 15:35:10 +06:00
Egor Kovetskiy
d017d31f61 Add @jevfok as a contributor 2021-11-30 15:34:29 +06:00
Mateus Miranda
50f78ee6a1
Homebrew tap (#137)
* Add brew step on goreleaser

* Add brew install doc

* fix typo
2021-11-30 15:18:57 +06:00
Egor Kovetskiy
f0e00468e9 use AutoLink rule to all links except ac: namespace 2021-11-11 14:45:08 +06:00
Egor Kovetskiy
07926611cf bump to 6.5 2021-11-08 20:16:13 +06:00
Egor Kovetskiy
1bd022b065 replace \r\n with \n, fix #42 2021-11-08 20:15:59 +06:00
jevfok
f3ba7c57a3
Children display macro (#133)
Added Children Display macro
2021-11-08 20:12:13 +06:00
Florent Monbillard
cc98590e9d
Update golang crypto library to fix CVE-2020-29652 (#131) 2021-11-03 11:41:51 +06:00
Egor Kovetskiy
4626544585 bump to 6.4, 6.3 was mistakenly released without patching files 2021-10-25 15:55:28 +06:00
Leandro Carneiro
3797116d97
feat: youtube widget macro (#127) 2021-10-25 15:52:04 +06:00
Leandro Carneiro
e635d1f945
feat: support mermaid graphs (#128) 2021-10-25 15:51:56 +06:00
Egor Kovetskiy
ee0a6e23e7 fix #114 do not crash when resolving links on directories 2021-09-11 14:37:45 +03:00
Egor Kovetskiy
3fcfcde298 add table test 2021-09-09 13:12:58 +03:00
Egor Kovetskiy
4e47ae14a7 bump to 6.2 2021-08-23 13:20:37 +06:00
Egor Kovetskiy
b1eea83d49 add config parameter 2021-08-23 13:20:18 +06:00
Egor Kovetskiy
a5221cbedd bump to 6.1 2021-08-23 12:51:03 +06:00
Egor Kovetskiy
c64d6e77dc Add @eitchugo as a contributor 2021-08-23 12:45:36 +06:00
Hugo Cisneiros
97313e59e1
allow space home update and its children pages (#101)
* allow space home update and its children pages

* fix coding styling

* fix coding styling

* codestyle: fix checking isHomepage

* codestyle: fix checking skipHomeAncestry

Co-authored-by: Hugo Cisneiros (Eitch) <hugo.cisneiros@ifood.com.br>
Co-authored-by: Egor Kovetskiy <e.kovetskiy@gmail.com>
2021-08-23 12:42:31 +06:00
Egor Kovetskiy
11e7e82671 do not skip tls cert verification by default 2021-08-11 10:16:37 +03:00
Egor Kovetskiy
ccee37ce22 bump to 5.9 2021-07-14 12:46:10 +03:00
Egor Kovetskiy
2ea132f84d add a note about issues, bugs, and feature requests 2021-07-14 12:45:38 +03:00
Egor Kovetskiy
5d65fddd43 Add @Taldrain as a contributor 2021-07-12 09:01:00 +03:00
Egor Kovetskiy
b68abf9c0f
Merge pull request #93 from Taldrain/update-panel-format
update the panel format
2021-07-12 08:59:56 +03:00
Thomas 'Taldrain' Mariaux
8d0ad4d298 update the panel format 2021-07-10 23:01:43 +02:00
Egor Kovetskiy
6d0aa09969 bump to 5.8 2021-06-30 13:15:09 +03:00
Egor Kovetskiy
a4fe28ed22 Add @beeme1mr as a contributor 2021-06-30 13:14:40 +03:00
Egor Kovetskiy
76c60c33d5
Merge pull request #89 from beeme1mr/feat/sidebar
feat(sidebar): Add sidebar support
2021-06-30 13:13:59 +03:00
Michael Beemer
75717374a8 feat(sidebar): Add sidebar support 2021-06-17 14:56:27 -04:00
Egor Kovetskiy
a9bb6df01f Add @carnei-ro as a contributor 2021-04-21 09:43:10 +03:00
Egor Kovetskiy
76cc033af3 bump to 5.7 2021-04-21 09:41:20 +03:00
Egor Kovetskiy
e1e9015f51
Merge pull request #86 from carnei-ro/feat/macro/emoticon
feat(macro): Add emoticon support
2021-04-21 09:40:43 +03:00
Leandro Carneiro
99119d8aff feat(macro): Add emoticon support 2021-04-20 15:35:17 -03:00
Egor Kovetskiy
74e67141ba add contributors 2021-04-09 08:31:47 +03:00
Egor Kovetskiy
5fd862c450 use docopt struct instead of map since we already have a struct 2021-04-05 08:17:26 +03:00
Egor Kovetskiy
e72577327c
Merge pull request #85 from wbh1/add/fileglobbing
Add support for fileglobs
2021-04-05 07:50:08 +03:00
Egor Kovetskiy
1daedcd88b
Merge pull request #83 from mrdavidlaing/master
Add support for Confluence blog posts
2021-04-05 07:47:50 +03:00
wbhegedus
403efe3acc add support for fileglobs 2021-04-02 13:19:22 -04:00
David Laing
07aa3700eb Add Type metadata header to enable support for Confluence Blog Posts 2021-03-31 17:49:01 +01:00
Egor Kovetskiy
12510a1208 bump to 5.5 2021-03-29 09:28:16 +03:00
Egor Kovetskiy
7dbf14a7e3
Merge pull request #77 from rofafor/feature/box_macro
Add ac:box macro
2021-03-29 09:25:20 +03:00
Rolf Ahrenberg
82d418e17d Add ac:box macro 2021-03-26 21:20:30 +02:00
Egor Kovetskiy
7cd0ebd782 bump to 5.4 2021-03-17 09:24:58 +03:00
Egor Kovetskiy
400b85de11 option to turn off color output, close #69 2021-03-17 09:24:26 +03:00
Egor Kovetskiy
b17a955ec7 fix #74 meta now knows about Include directives 2021-03-16 07:24:51 +03:00
Egor Kovetskiy
bf4bbbe20a use my fork of blackfriday
Turns out the PR to the upstream is not merged and looks like the
maintainers didn't have a chance to merge it. The `replace` directive
causes some problems: https://github.com/kovetskiy/mark/issues/71
2021-03-11 09:00:45 +03:00
Egor Kovetskiy
398cc7375e 5.2.1 2021-02-25 08:07:15 +03:00
Egor Kovetskiy
12674bab9c
Merge pull request #70 from GezimSejdiu/master
Align ancestors printouts (actual, expected) while validating ancestry
2021-02-25 08:06:37 +03:00
Gezim Sejdiu
2fc8ba3614 Align ancestors printouts (actual, expected) while validating ancestry 2021-02-24 23:07:32 +01:00
Egor Kovetskiy
c51629a9b8 bump to 5.2, add note about password in stdin 2021-02-09 06:58:31 +03:00
Egor Kovetskiy
3d95418d9f use fork of blackfriday until the upstream is fixed 2021-02-09 06:55:49 +03:00
Egor Kovetskiy
a3064a64af bump to 5.1 2021-02-04 12:18:59 +03:00
Egor Kovetskiy
c17a3d02d0 fix regression after #60 2021-02-04 12:18:28 +03:00
Egor Kovetskiy
931d9f706a confirm #64 by a test (broken now) 2021-02-04 11:58:44 +03:00
Egor Kovetskiy
f338ed90fd bump to 5.0, new markdown renderer 2021-02-03 07:06:35 +03:00
Egor Kovetskiy
c22c6c1e43 tidy up go modules 2021-02-02 17:45:36 +03:00
Egor Kovetskiy
7f6466a882 upgrade to blackfriday v2, fixes #53 and #61, add tests 2021-02-02 17:44:56 +03:00
Egor Kovetskiy
e190cdfb34 rollback #53 because of #63, add tests 2021-02-02 17:44:56 +03:00
Egor Kovetskiy
cb1093d6b5 bump version to 4.2 2021-02-02 08:06:45 +03:00
Egor Kovetskiy
58ad4ff85b show what ancestors were expected 2021-02-02 08:06:14 +03:00
Egor Kovetskiy
4b2f1cd739
Merge pull request #60 from tyler-copilot/master
fix for https://github.com/kovetskiy/mark/issues/58
2021-02-02 07:59:44 +03:00
Egor Kovetskiy
ffd572899a
Merge pull request #62 from emead-indeed/emead-indeed-password-stdin
Add support to reading password from stdin
2021-02-02 07:53:42 +03:00
emead-indeed
7c68fec718
Add support to reading password from stdin 2021-01-27 10:58:44 +09:00
Tyler Cole
6633ff22ba fix for https://github.com/kovetskiy/mark/issues/58 2021-01-25 16:10:41 -06:00
Egor Kovetskiy
9bd12bc2e5 bump to 4.1 2021-01-15 10:01:18 +03:00
Egor Kovetskiy
c355ed66b1
Merge pull request #53 from MattyRad/master
Join lines in compiled markdown so lists render in Confluence without…
2021-01-15 10:00:18 +03:00
Matt Radford
2db0de7228 Join lines in compiled markdown so lists render in Confluence without extra whitespace 2021-01-12 21:37:07 -08:00
Egor Kovetskiy
abb1fc9c05 release 4.0 2021-01-06 12:16:00 +03:00
Egor Kovetskiy
8b28912866
Merge pull request #51 from rofafor/feature/labels
Add label support fix #34 fix #50
2021-01-06 07:53:19 +03:00
Rolf Ahrenberg
1d8e43add2 Add --minor-edit command-line flag 2021-01-04 16:18:38 +02:00
Rolf Ahrenberg
f86dd2723a Add label support 2021-01-04 15:23:12 +02:00
elgreco247
8018b54627
Fix RestrictPageUpdatesServer (#45) 2020-12-08 11:53:15 +03:00
Egor Kovetskiy
ff17a4034c 3.5, get rid of duplicated docs in help 2020-12-04 00:31:44 +03:00
Egor Kovetskiy
d4008a5b72 Fix replacing relative links, fix #43 2020-12-04 00:28:52 +03:00
Egor Kovetskiy
f6e542c6c2 bump version to 3.4 2020-11-30 10:49:04 +03:00
Juho Saarinen
63fe97beaa
Support for relative links (#33)
* Support for relative links

Fixes #25

* Error logging fixes

* Better regexp
2020-11-30 10:47:46 +03:00
Šarūnas Nejus
bcf2acb39f
Incorporate the collapsible code block (#39)
* Incorporate the collapsible code block

* Press tilda 3 times
2020-11-30 10:46:29 +03:00
Šarūnas Nejus
27e7af5b85
Parametrize the toc but have default values (#38) 2020-11-30 10:45:02 +03:00
Josip Ćavar
f8e70476bd
Remove unused variable (#37) 2020-11-26 17:53:16 +03:00
Luke Fritz
4ea476ace2
Add Option for Dropping Leading H1 Heading (#36)
* Add goland IDE prefs to .gitignore

* Add flag for removing leading H1 heading in markdown; fixes #35

* Remove manpage reference to unused -n and -c args

* Add manpage docs for --drop-h1 argument

* Update docs; add docs for --drop-h1 argument

* Rename method to be more verbose; add code comments

Renames to `DropDocumentLeadingH1` to better denote the method's
purpose and adds code docs to this effect in order to hopefully
negate potential misuse on single lines.

* Revert "Add manpage docs for --drop-h1 argument"

This reverts commit 48a641baed05540f6172fe9b780b06eacad4dc28.

The `--drop-h1` flag is not understood as an argument in this
context, but rather is folded under `[options]`. This change
caused the arguments to not be properly parsed when the --drop-h1
flag was passed.
2020-11-20 17:44:41 +03:00
Egor Kovetskiy
b82421a2a6 add FUNDING 2020-11-16 13:30:26 +03:00
Egor Kovetskiy
b17dd7d76b update version to 3.3.1 2020-11-05 13:44:33 +03:00
Egor Kovetskiy
055af49f89 update api dependencies 2020-11-05 13:44:12 +03:00
95 changed files with 7468 additions and 1291 deletions

413
.all-contributorsrc Normal file
View File

@ -0,0 +1,413 @@
{
"projectName": "mark",
"projectOwner": "kovetskiy",
"repoType": "github",
"repoHost": "https://github.com",
"files": [
"README.md"
],
"imageSize": 100,
"commit": true,
"commitConvention": "none",
"contributors": [
{
"login": "mrueg",
"name": "Manuel Rüger",
"avatar_url": "https://avatars.githubusercontent.com/u/489370?v=4",
"profile": "https://mastodon.social/@mrueg",
"contributions": [
"maintenance",
"code"
]
},
{
"login": "kovetskiy",
"name": "Egor Kovetskiy",
"avatar_url": "https://avatars.githubusercontent.com/u/8445924?v=4",
"profile": "https://github.com/kovetskiy",
"contributions": [
"maintenance",
"code"
]
},
{
"login": "klauern",
"name": "Nick Klauer",
"avatar_url": "https://avatars.githubusercontent.com/u/4735?v=4",
"profile": "https://klauer.dev/",
"contributions": [
"code"
]
},
{
"login": "rofafor",
"name": "Rolf Ahrenberg",
"avatar_url": "https://avatars.githubusercontent.com/u/9297850?v=4",
"profile": "https://github.com/rofafor",
"contributions": [
"code"
]
},
{
"login": "csoutherland",
"name": "Charles Southerland",
"avatar_url": "https://avatars.githubusercontent.com/u/840471?v=4",
"profile": "https://github.com/csoutherland",
"contributions": [
"code"
]
},
{
"login": "snejus",
"name": "Šarūnas Nejus",
"avatar_url": "https://avatars.githubusercontent.com/u/16212750?v=4",
"profile": "https://github.com/snejus",
"contributions": [
"code"
]
},
{
"login": "brnv",
"name": "Alexey Baranov",
"avatar_url": "https://avatars.githubusercontent.com/u/1925213?v=4",
"profile": "https://github.com/brnv",
"contributions": [
"code"
]
},
{
"login": "princespaghetti",
"name": "Anthony Barbieri",
"avatar_url": "https://avatars.githubusercontent.com/u/2935312?v=4",
"profile": "https://github.com/princespaghetti",
"contributions": [
"code"
]
},
{
"login": "dauc",
"name": "Devin Auclair",
"avatar_url": "https://avatars.githubusercontent.com/u/29129213?v=4",
"profile": "https://github.com/dauc",
"contributions": [
"code"
]
},
{
"login": "GezimSejdiu",
"name": "Gezim Sejdiu",
"avatar_url": "https://avatars.githubusercontent.com/u/5259296?v=4",
"profile": "https://gezimsejdiu.github.io/",
"contributions": [
"code"
]
},
{
"login": "jcavar",
"name": "Josip Ćavar",
"avatar_url": "https://avatars.githubusercontent.com/u/3751289?v=4",
"profile": "https://github.com/jcavar",
"contributions": [
"code"
]
},
{
"login": "Hi-Fi",
"name": "Juho Saarinen",
"avatar_url": "https://avatars.githubusercontent.com/u/1499780?v=4",
"profile": "https://github.com/Hi-Fi",
"contributions": [
"code"
]
},
{
"login": "lukiffer",
"name": "Luke Fritz",
"avatar_url": "https://avatars.githubusercontent.com/u/2278911?v=4",
"profile": "https://github.com/lukiffer",
"contributions": [
"code"
]
},
{
"login": "MattyRad",
"name": "Matt Radford",
"avatar_url": "https://avatars.githubusercontent.com/u/1143595?v=4",
"profile": "https://github.com/MattyRad",
"contributions": [
"code"
]
},
{
"login": "Planktonette",
"name": "Planktonette",
"avatar_url": "https://avatars.githubusercontent.com/u/5514719?v=4",
"profile": "https://github.com/Planktonette",
"contributions": [
"code"
]
},
{
"login": "teopost",
"name": "Stefano Teodorani",
"avatar_url": "https://avatars.githubusercontent.com/u/2573389?v=4",
"profile": "http://www.stefanoteodorani.it/",
"contributions": [
"code"
]
},
{
"login": "tillepille",
"name": "Tim Schrumpf",
"avatar_url": "https://avatars.githubusercontent.com/u/16536696?v=4",
"profile": "https://github.com/tillepille",
"contributions": [
"code"
]
},
{
"login": "tyler-copilot",
"name": "Tyler Cole",
"avatar_url": "https://avatars.githubusercontent.com/u/18539108?v=4",
"profile": "https://github.com/tyler-copilot",
"contributions": [
"code"
]
},
{
"login": "elgreco247",
"name": "elgreco247",
"avatar_url": "https://avatars.githubusercontent.com/u/8968417?v=4",
"profile": "https://github.com/elgreco247",
"contributions": [
"code"
]
},
{
"login": "emead-indeed",
"name": "emead-indeed",
"avatar_url": "https://avatars.githubusercontent.com/u/44018145?v=4",
"profile": "https://github.com/emead-indeed",
"contributions": [
"code"
]
},
{
"login": "wbh1",
"name": "Will Hegedus",
"avatar_url": "https://avatars.githubusercontent.com/u/11506822?v=4",
"profile": "https://wbhegedus.me/",
"contributions": [
"code"
]
},
{
"login": "carnei-ro",
"name": "Leandro Carneiro",
"avatar_url": "https://avatars.githubusercontent.com/u/42899277?v=4",
"profile": "https://github.com/carnei-ro",
"contributions": [
"code"
]
},
{
"login": "beeme1mr",
"name": "beeme1mr",
"avatar_url": "https://avatars.githubusercontent.com/u/682996?v=4",
"profile": "https://github.com/beeme1mr",
"contributions": [
"code"
]
},
{
"login": "Taldrain",
"name": "Taldrain",
"avatar_url": "https://avatars.githubusercontent.com/u/1081600?v=4",
"profile": "https://github.com/Taldrain",
"contributions": [
"code"
]
},
{
"login": "eitchugo",
"name": "Hugo Cisneiros",
"avatar_url": "https://avatars.githubusercontent.com/u/349457?v=4",
"profile": "http://www.devin.com.br/",
"contributions": [
"code"
]
},
{
"login": "jevfok",
"name": "jevfok",
"avatar_url": "https://avatars.githubusercontent.com/u/54530686?v=4",
"profile": "https://github.com/jevfok",
"contributions": [
"code"
]
},
{
"login": "mmiranda",
"name": "Mateus Miranda",
"avatar_url": "https://avatars.githubusercontent.com/u/16670310?v=4",
"profile": "https://dev.to/mmiranda",
"contributions": [
"code"
]
},
{
"login": "Skeeve",
"name": "Stephan Hradek",
"avatar_url": "https://avatars.githubusercontent.com/u/725404?v=4",
"profile": "https://github.com/Skeeve",
"contributions": [
"code"
]
},
{
"login": "dreampuf",
"name": "Dreampuf",
"avatar_url": "https://avatars.githubusercontent.com/u/353644?v=4",
"profile": "http://huangx.in/",
"contributions": [
"code"
]
},
{
"login": "JAndritsch",
"name": "Joel Andritsch",
"avatar_url": "https://avatars.githubusercontent.com/u/190611?v=4",
"profile": "https://github.com/JAndritsch",
"contributions": [
"code"
]
},
{
"login": "guoweis-outreach",
"name": "guoweis-outreach",
"avatar_url": "https://avatars.githubusercontent.com/u/639243?v=4",
"profile": "https://github.com/guoweis-outreach",
"contributions": [
"code"
]
},
{
"login": "klysunkin",
"name": "klysunkin",
"avatar_url": "https://avatars.githubusercontent.com/u/2611187?v=4",
"profile": "https://github.com/klysunkin",
"contributions": [
"code"
]
},
{
"login": "EppO",
"name": "Florent Monbillard",
"avatar_url": "https://avatars.githubusercontent.com/u/6111?v=4",
"profile": "https://github.com/EppO",
"contributions": [
"code"
]
},
{
"login": "jfreeland",
"name": "Joey Freeland",
"avatar_url": "https://avatars.githubusercontent.com/u/30938344?v=4",
"profile": "https://github.com/jfreeland",
"contributions": [
"code"
]
},
{
"login": "prokod",
"name": "Noam Asor",
"avatar_url": "https://avatars.githubusercontent.com/u/877414?v=4",
"profile": "https://github.com/prokod",
"contributions": [
"code"
]
},
{
"login": "PhilippReinke",
"name": "Philipp",
"avatar_url": "https://avatars.githubusercontent.com/u/81698819?v=4",
"profile": "https://github.com/PhilippReinke",
"contributions": [
"code"
]
},
{
"login": "vpommier",
"name": "Pommier Vincent",
"avatar_url": "https://avatars.githubusercontent.com/u/8139328?v=4",
"profile": "https://github.com/vpommier",
"contributions": [
"code"
]
},
{
"login": "ToruKawaguchi",
"name": "Toru Kawaguchi",
"avatar_url": "https://avatars.githubusercontent.com/u/17423222?v=4",
"profile": "https://github.com/ToruKawaguchi",
"contributions": [
"code"
]
},
{
"login": "willgorman",
"name": "Will Gorman",
"avatar_url": "https://avatars.githubusercontent.com/u/49793?v=4",
"profile": "https://coaxialflutter.com/",
"contributions": [
"code"
]
},
{
"login": "zgriesinger",
"name": "Zackery Griesinger",
"avatar_url": "https://avatars.githubusercontent.com/u/15172516?v=4",
"profile": "https://zackery.dev/",
"contributions": [
"code"
]
},
{
"login": "chrisjaimon2012",
"name": "cc-chris",
"avatar_url": "https://avatars.githubusercontent.com/u/57173930?v=4",
"profile": "https://github.com/chrisjaimon2012",
"contributions": [
"code"
]
},
{
"login": "datsickkunt",
"name": "datsickkunt",
"avatar_url": "https://avatars.githubusercontent.com/u/105289244?v=4",
"profile": "https://github.com/datsickkunt",
"contributions": [
"code"
]
},
{
"login": "recrtl",
"name": "recrtl",
"avatar_url": "https://avatars.githubusercontent.com/u/14078835?v=4",
"profile": "https://github.com/recrtl",
"contributions": [
"code"
]
},
{
"login": "seletskiy",
"name": "Stanislav Seletskiy",
"avatar_url": "https://avatars.githubusercontent.com/u/674812?v=4",
"profile": "https://github.com/seletskiy",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7
}

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
/docker

43
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,43 @@
---
name: Report a bug
about: Create a bug report to help us improve mark
title: ''
labels: bug
assignees: ''
---
## What happened?
A clear and concise description of what the bug is.
## What did you expect to happen?
A clear and concise description of what you expected to happen.
## How can we reproduce the behavior you experienced?
Steps to reproduce the behavior:
1. Step 1
2. Step 2
3. Step 3
4. Step 4
In case this is related to specific markdown, please provide a minimal markdown example here.
## Further Information (please complete the following information)
* Mark Version (`mark --version`): [e.g. v9.1.4]
* Mark Parameters: [e.g. `--drop-h1 --title-from-h1`]
* Confluence Hosting: [e.g. Cloud, Server or Datacenter]
* Confluence Version: [e.g. v7.13]
* Environment specific Information: [e.g. running in Github Actions, or on Mac OS X, etc.]
## Logs or other output
Please provide logs, other kind of output here.
## Additional context
Add any other context about the problem here.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: true

View File

@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for mark
title: ''
labels: feature
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is and what the feature provides.
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"

113
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,113 @@
name: continuous-integration
on:
push:
branches:
- master
tags:
- '*'
pull_request:
branches:
- master
env:
GO_VERSION: "~1.24"
jobs:
# Runs Golangci-lint on the source code
ci-go-lint:
name: ci-go-lint
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
# Runs markdown-lint on the markdown files
ci-markdown-lint:
name: ci-markdown-lint
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: markdownlint-cli2-action
uses: DavidAnson/markdownlint-cli2-action@v20
# Executes Unit Tests
ci-unit-tests:
name: ci-unit-tests
runs-on: ubuntu-22.04
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Run unit tests
run: |
make test
# Builds mark binary
ci-build:
name: ci-build
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
id: go
- name: Build mark
run: |
make build
# Build and push Dockerimage
ci-docker-build:
name: ci-docker-build
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build only (on commits)
uses: docker/build-push-action@v6
if: ${{ github.ref_type != 'tag' }}
with:
push: false
tags: kovetskiy/mark:latest
- name: Login to Docker Hub
uses: docker/login-action@v3
if: ${{ github.ref_type == 'tag' }}
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Build and push (on tag)
uses: docker/build-push-action@v6
if: ${{ github.ref_type == 'tag' }}
with:
push: true
platforms: linux/amd64,linux/arm64
tags: |
kovetskiy/mark:${{ github.ref_name }}
kovetskiy/mark:latest

View File

@ -10,18 +10,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.14
go-version: "1.24"
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --rm-dist
version: "~> 2"
args: release --clean
env:
GOPATH: /home/runner/work/
GITHUB_TOKEN: ${{ secrets.TOKEN }}

5
.gitignore vendored
View File

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

View File

@ -1,5 +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
before:
hooks:
# You may remove this if you don't use go modules.
@ -12,14 +13,18 @@ builds:
goos:
- darwin
- linux
- windows
# windows fails with an error https://github.com/kovetskiy/mark/runs/5034726426?check_suite_focus=true
# - windows
goarch:
- amd64
- arm64
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
- name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
checksum:
name_template: 'checksums.txt'
snapshot:
@ -30,3 +35,27 @@ changelog:
exclude:
- '^docs:'
- '^test:'
# Publish on Homebrew Tap
brews:
-
name: mark
repository:
owner: kovetskiy
name: homebrew-mark
branch: master
commit_author:
name: Egor Kovetskiy
email: e.kovetskiy@gmail.com
commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}"
directory: Formula
homepage: "https://github.com/kovetskiy/mark"
description: "Sync your markdown files with Confluence pages."
license: "Apache 2.0"
test: |
system "#{bin}/program", "version"

12
.markdownlint-cli2.jsonc Normal file
View File

@ -0,0 +1,12 @@
{
"globs": [
"*.md",
".github/**/*.md"
],
// ToDo: Following rules can't be fixed automatically. They should be enabled when fixed.
"config": {
"MD013": false, // https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md013---line-length
"MD033": false // https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md#md033---inline-html
}
}

View File

@ -1,12 +1,18 @@
FROM golang:latest
FROM golang:1.24.4 AS builder
ENV GOPATH="/go"
WORKDIR /go/src/github.com/kovetskiy/mark
COPY / .
RUN make get
RUN make build
RUN make get \
&& make build
FROM alpine:latest
RUN apk --no-cache add ca-certificates bash git
COPY --from=0 /go/src/github.com/kovetskiy/mark/mark /bin/
RUN mkdir -p /docs
FROM chromedp/headless-shell:latest
RUN apt-get update \
&& apt-get upgrade -qq \
&& apt-get install --no-install-recommends -qq ca-certificates bash sed git dumb-init \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY --from=builder /go/src/github.com/kovetskiy/mark/mark /bin/
WORKDIR /docs
ENTRYPOINT ["dumb-init", "--"]

211
LICENSE
View File

@ -1,22 +1,201 @@
“Commons Clause” License Condition v1.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
The Software is provided to you by the Licensor under the License, as defined
below, subject to the following condition.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Without limiting other conditions in the License, the grant of rights under the
License will not include, and the License does not grant to you, the right to
Sell the Software.
1. Definitions.
For purposes of the foregoing, “Sell” means practicing any or all of the rights
granted to you under the License to provide to third parties, for a fee or other
consideration (including without limitation fees for hosting or consulting/
support services related to the Software), a product or service whose value
derives, entirely or substantially, from the functionality of the Software. Any
license notice or attribution required by the License must also include this
Commons Clause License Condition notice.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
Software: Mark — github.com/kovetskiy/mark
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
License: Apache 2.0
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
Licensor: Egor Kovetskiy
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Egor Kovetskiy
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -4,8 +4,6 @@ VERSION = $(shell git describe --tags --abbrev=0)
GO111MODULE = on
BRANCH = $(shell git rev-parse --abbrev-ref HEAD)
REMOTE = kovetskiy
version:
@ -20,6 +18,9 @@ build:
-ldflags "-X main.version=$(VERSION)" \
-gcflags "-trimpath $(GOPATH)/src"
test:
go test -race -coverprofile=profile.cov ./... -v
image:
@echo :: building image $(NAME):$(VERSION)
@docker build -t $(NAME):$(VERSION) -f Dockerfile .

831
README.md
View File

@ -1,9 +1,13 @@
# Mark
Mark — tool for syncing your markdown documentation with Atlassian Confluence
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-44-orange.svg?style=flat-square)](#contributors-)
<!-- ALL-CONTRIBUTORS-BADGE:END -->
Mark — a tool for syncing your markdown documentation with Atlassian Confluence
pages.
Read the blog post discussing the tool — https://samizdat.dev/use-markdown-for-confluence/
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
repository and don't want to do an extra job of updating Confluence page using
@ -19,12 +23,15 @@ contains several HTML-ish metadata headers, which can be used to locate page ins
Confluence instance and update it accordingly.
File in the extended format should follow the specification:
```markdown
<!-- Space: <space key> -->
<!-- Parent: <parent 1> -->
<!-- Parent: <parent 2> -->
<!-- Title: <title> -->
<!-- Attachment: <local path> -->
<!-- Label: <label 1> -->
<!-- Label: <label 2> -->
<page contents>
```
@ -42,6 +49,32 @@ Also, optional following headers are supported:
reading;
* plain: content will fill all page;
```markdown
<!-- Type: (page|blogpost) -->
```
* (default) page: normal Confluence page - defaults to this if omitted
* blogpost: [Blog post](https://confluence.atlassian.com/doc/blog-posts-834222533.html) in `Space`. Cannot have `Parent`(s)
```markdown
<!-- Content-Appearance: (full-width|fixed) -->
```
* (default) full-width: content will fill the full page width
* fixed: content will be rendered in a fixed narrow view
```markdown
<!-- Sidebar: <h2>Test</h2> -->
```
Setting the sidebar creates a column on the right side. You're able to add any valid HTML content. Adding this property sets the layout to `article`.
```markdown
<!-- Emoji: 🚀 -->
```
You can set a page emoji icon by specifying the icon in the headers.
Mark supports Go templates, which can be included into article by using path
to the template relative to current working dir, e.g.:
@ -49,8 +82,30 @@ to the template relative to current working dir, e.g.:
<!-- Include: <path> -->
```
If the template cannot be found relative to the current directory, a fallback directory can be defined via `--include-path`. This way it is possible to have global include files while local ones will still take precedence.
Optionally the delimiters can be defined:
```markdown
<!-- Include: <path>
Delims: "<<", ">>"
-->
```
Or they can be switched off to disable processing:
```markdown
<!-- Include: <path>
Delims: none
-->
```
**Note:** Switching delimiters off really simply changes
them to ASCII characters "\x00" and "\x01" which, usually
should not occure in a template.
Templates can accept configuration data in YAML format which immediately
follows the `Include` tag:
follows the `Include` and `Delims` tag, if present:
```markdown
<!-- Include: <path>
@ -81,6 +136,9 @@ be replaced with specified template:
<yaml-data> -->
```
**NOTE**: Make sure to define your macros after your metadata (Title/Space),
mark will stop processing metadata if it hits a Macro.
Capture groups can be defined in the macro's <regexp> which can be later
referenced in the `<yaml-data>` using `${<number>}` syntax, where `<number>` is
number of a capture group in regexp (`${0}` is used for entire regexp match),
@ -92,25 +150,353 @@ for example:
Ticket: ${0} -->
```
Macros can also use inline templates.
Inline templates are templates where the template content
is described in the `<yaml-data>`.
The `Template` value starts with a `#`, followed by the key
used in the `<yaml-data>`.
The key's value must be a string which defines the template's content.
```markdown
<!-- Macro: <tblbox\s+(.*?)\s*>
Template: #inline
title: ${1}
inline: |
<table>
<thead><tr><th>{{ .title }}</th></tr></thead>
<tbody><tr><td>
-->
<!-- Macro: </tblbox>
Template: #also_inline
also_inline: |
</td></tr></tbody></table>
-->
<tblbox with a title>
and some
content
</tblbox>
```
## Customizing the page layout
If you set the Layout to plain, the page layout can be customized using HTML comments inside the markdown:
```markdown
<!-- Layout: plain -->
<!-- ac:layout -->
<!-- ac:layout-section type:three_with_sidebars -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
Even More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout-section type:single -->
<!-- ac:layout-cell -->
Still More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout end -->
```
Please be aware that mark does not validate the layout, so it's your responsibility to create a valid layout.
### Placeholders
You can use this to define placeholders:
```markdown
<!-- ac:placeholder -->
Placeholder
<!-- ac:placeholder end -->
```
### Code Blocks
If you have long code blocks, you can make them collapsible with the [Code Block Macro]:
```bash collapse
...
some long bash code block
...
```
And you can also add a title:
```bash collapse title Some long long bash function
...
some long bash code block
...
```
Or linenumbers, by giving the first number
```bash 1 collapse title Some long long bash function
...
some long bash code block
...
```
And even themes
```bash 1 collapse midnight title Some long long bash function
...
some long bash code block
...
```
Please note that, if you want to have a code block without a language
use `-` as the first character, if you want to have the other goodies
``` - 1 collapse midnight title Some long long code
...
some long code block
...
```
[Code Block Macro]: https://confluence.atlassian.com/doc/code-block-macro-139390.html
### Block Quotes
Block Quotes are converted to Confluence Info/Warn/Note box when the following conditions are met
1. The BlockQuote is on the root level of the document (not nested)
1. The first line of the BlockQuote contains one of the following patterns `Info/Warn/Note` or [Github MD Alerts style](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts) `[!NOTE]/[!TIP]/[!IMPORTANT]/[!WARNING]/[!CAUTION]`
| Github Alerts | Confluence |
|---------------|------------|
| Tip (green lightbulb) | Tip (green checkmark in circle) |
| Note (blue I in circle) | Info (blue I in circle) |
| Important (purple exclamation mark in speech bubble) | Info (blue I in circle) |
| Warning (yellow exclamation mark in triangle) | Note (yellow exclamation mark in triangle) |
| Caution (red exclamation mark in hexagon) | Warning (red exclamation mark in hexagon) |
In any other case the default behaviour will be resumed and html `<blockquote>` tag will be used
## Template & Macros
By default, mark provides several built-in templates and macros:
* template `ac:status` to include badge-like text, which accepts following
parameters:
- Title: text to display in the badge
- Color: color to use as background/border for badge
- Grey
- Red
- Yellow
- Green
- Blue
- Subtle: specify to fill badge with background or not
- true
- false
* Title: text to display in the badge
* Color: color to use as background/border for badge
* Grey
* Red
* Yellow
* Green
* Blue
* Subtle: specify to fill badge with background or not
* true
* false
* template `ac:box`to include info, tip, note, and warning text boxes. Parameters:
* Name: select box style
* info
* tip
* note
* warning
* Icon: show information/tip/exclamation mark/warning icon
* true
* false
* Title: title text of the box
* Body: text to display in the box
See: <https://confluence.atlassian.com/conf59/info-tip-note-and-warning-macros-792499127.html>
* template `ac:jira:ticket` to include JIRA ticket link. Parameters:
- Ticket: Jira ticket number like BUGS-123.
* Ticket: Jira ticket number like BUGS-123.
See: https://confluence.atlassian.com/conf59/status-macro-792499207.html
See: <https://confluence.atlassian.com/conf59/status-macro-792499207.html>
* template `ac:jira:filter` to include JIRA Filters/Searches. Parameters:
* JQL: The "JQL" query of the search
* Server (Optional): The Jira server to fetch the query from if its not the default of "System Jira"
* template `ac:jiraissues` to include a list of JIRA tickets. Parameters:
* URL (Required), The URL of the XML view of your selected issues. (link to the filter)
* Anonymous (Optional) If this parameter is set to 'true', your JIRA application will return only the issues which allow unrestricted viewing. That is, the issues which are visible to anonymous viewers. If this parameter is omitted or set to 'false', then the results depend on how your administrator has configured the communication between the JIRA application and Confluence. By default, Confluence will show only the issues which the user is authorised to view.
* BaseURL (Optional) If you specify a 'baseurl', then the link in the header, pointing to your JIRA application, will use this base URL instead of the value of the 'url' parameter. This is useful when Confluence connects to JIRA with a different URL from the one used by other users.
* Columns (Optional) A list of JIRA column names, separated by semi-colons (;). You can include many columns recognized by your JIRA application, including custom columns.
* Count (Optional) If this parameter is set to 'true', the issue list will show the number of issues in JIRA. The count will be linked to your JIRA site.
* Cache (Optional) The macro maintains a cache of the issues which result from the JIRA query. If the 'cache' parameter is set to 'off', the relevant part of the cache is cleared each time the macro is reloaded. (The value 'false' also works and has the same effect as 'off'.)
* Height (Optional) The height in pixels of the table displaying the issues.
* RenderMode (Optional) If the value is 'dynamic', the JIRA Issues macro offers an interactive display.
* Title (Optional) You can customise the title text at the top of the issues table with this parameter. For instance, setting the title to 'Bugs-to-fix' will replace the default 'JIRA Issues' text. This can help provide more context to the list of issues displayed.
* Width (Optional) The width of the table displaying the issues. Can be entered as a percentage (%) or in pixels (px).
See: <https://confluence.atlassian.com/doc/jira-issues-macro-139380.html>
* template: `ac:emoticon` to include emoticons. Parameters:
* Name: select emoticon
* smile
* sad
* cheeky
* laugh
* wink
* thumbs-up
* thumbs-down
* information
* tick
* cross
* warning
* plus
* minus
* question
* light-on
* light-off
* yellow-star
* red-star
* green-star
* blue-star
See: <https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html>
* template: `ac:youtube` to include YouTube Widget. Parameters:
* URL: YouTube video endpoint
* Width: Width in px. Defaults to "640px"
* Height: Height in px. Defaults to "360px"
See: <https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube>
* template: `ac:children` to include Children Display macro
* Reverse (Reverse Sort): Use with the `Sort Children By` parameter. When set, the sort order changes from ascending to descending.
* `true`
* `false` (Default)
* Sort (Sort Children By):
* `creation` — to sort by content creation date
* `title` — to sort alphabetically on title
* `modified` — to sort of last modification date.
* If not specified, manual sorting is used if manually ordered, otherwise alphabetical.
* Style (Heading Style): Choose the style used to display descendants.
* from `h1` to `h6`
* If not specified, default style is applied.
* Page (Parent Page):
* `/` — to list the top-level pages of the current space, i.e. those without parents.
* `pagename` — to list the children of the specified page.
* `spacekey:pagename` — to list the children of the specified page in the specified space.
* If not specified, the current page is used.
* Excerpt (Include Excerpts): Allows you to include a short excerpt under each page in the list.
* `none` - no excerpt will be displayed. (Default)
* `simple` - displays the first line of text contained in an Excerpt macro any of the returned pages. If there is not an Excerpt macro on the page, nothing will be shown.
* `rich content` - displays the contents of an Excerpt macro, or if there is not an Excerpt macro on the page, the first part of the page content, including formatted text, images and some macros.
* First (Number of Children): Restrict the number of child pages that are displayed at the top level.
* If not specified, no limit is applied.
* Depth (Depth of Descendants): Enter a number to specify the depth of descendants to display. For example, if the value is 2, the macro will display 2 levels of child pages. This setting has no effect if `Show Descendants` is enabled.
* If not specified, no limit is applied.
* All (Show Descendants): Choose whether to display all the parent page's descendants.
* `true`
* `false` (Default)
See: <https://confluence.atlassian.com/doc/children-display-macro-139501.html>
* template: `ac:iframe` to include iframe macro (cloud only)
* URL: URL to the iframe.
* Frameborder: Choose whether to draw a border around content in the iframe.
* `show` (Default)
* `hide`
* Width: Width in px. Defaults to "640px"
* Height: Height in px. Defaults to "360px"
* Scrolling: Allow or prevent scrolling in the iframe to see additional content.
* `yes`
* `no`
* `auto` (Default)
* Align: Align the iframe to the left or right of the page.
* `left` (Default)
* `right`
See: <https://support.atlassian.com/confluence-cloud/docs/insert-the-iframe-macro>
* template: `ac:blog-posts`to include blog-posts
* Content: How much content will be shown
* titles (default)
* excerpts
* entire
* Time: Specify how much back in time Confluence should look for blog posts (default: unlimited)
* Label: Restrict to blog posts with specific labels
* Author: Restrict to blog posts by specific authors
* Spaces: Restrict to blog posts in specific spaces
* Max: Maximum number of blog posts shown (default: 15)
* Sort: Sorting posts by
* title
* creation (default)
* modified
* Reverse: Reverses the Sort parameter from oldest to newest (default: false)
See: <https://confluence.atlassian.com/doc/blog-posts-macro-139470.html>
* template: `ac:include` to include a page
* Page: the page to be included
* Space: the space the page is in (optional, otherwise same space)
* template: `ac:excerpt-include` to include the excerpt from another page
* Page: the page the excerpt should be included from
* Name: The specific identifier for the excerpt, allowing multiple Excerpt macros on one page to be referenced individually. If not provided, the first excerpt from the page will be used (optional, cloud only)
* NoPanel: Determines whether Confluence will display a panel around the excerpted content (optional, default: false)
* template: `ac:excerpt` to create an excerpt and include it in the page
* Excerpt: The text you want to include
* Name: Allows you to identify this macro so that you can add multiple Excerpt macros to one page and use a specific one on another page using the Excerpt Include macro (optional, cloud only)
* OutputType: Determines whether the content of the Excerpt macro body is displayed on a new line or inline (optional, options: "BLOCK" or "INLINE", default: BLOCK)
* Hidden: Hide the excerpt content (optional, default: false)
* template: `ac:anchor` to set an anchor inside a page
* Anchor: Text for the anchor
* template: `ac:expand` to display an expandable/collapsible section of text on your page
* Title: Defines the text next to the expand/collapse icon.
* Body: The Text that it is expanded to.
* template: `ac:profile` to display a short summary of a given Confluence user's profile.
* Name: The username of the Confluence user whose profile summary you wish to show.
* template: `ac:contentbylabel` to display a list of pages, blog posts or attachments that have particular labels
* CQL: The CQL query to discover the content
* template: `ac:detailssummary` to show summary information from one page on a another page
* Headings: Column headings to show
* FirstColumn: Name of the Title Column
* CQL: The CQL query to discover the pages
* SortBy: Sort by a specific column heading
* template: `ac:details` to create page properties
* Body: Must contain a table with two rows, the table headings are used as property key. The table content is the value.
* template: `ac:panel` to display a block of text within a customisable panel
* Title: Panel title (optional)
* Body: Body text of the panel
* BGColor: Background Color
* TitleBGColor: Background color of the title bar
* TitleColor: Text color of the title
* BorderStyle: Style of the panel's border
* template `ac:recently-updated` to display a list of most recently changed content
* Spaces: List of Spaces to watch (optional, default is current Space)
* ShowProfilePic: Show profile picture of editor
* Max: Maximum number of changes
* Types: Include these content types only (comments, blogposts, pages)
* Theme: Apperance of the macro (concise, social, sidebar)
* HideHeading: Determines whether the macro hides or displays the text 'Recently Updated' as a title above the list of content
* Labels: Filter the results by label. The macro will display only the pages etc which are tagged with the label(s) you specify here.
* template: `ac:pagetreesearch` to add a search box to your Confluence page.
* Root: Name of the root page whose hierarchy of pages will be searched by this macro. If this not specified, the root page is the current page.
* template: `ac:column` To be used with the section macro to define the columns in a page.
* Width: Width of the column
* Body: The content of the column
* template: `ac:multimedia` to embedd an attached video, animation or other multimedia files in a Confluence page
* Name: Name of the file
* Width: Width of the video (optional)
* AutoPlay: Start playing the file on page load (default: false)
* macro `@{...}` to mention user by name specified in the braces.
@ -118,13 +504,14 @@ By default, mark provides several built-in templates and macros:
### Insert Disclaimer
**disclaimer.md**
This should be in **disclaimer.md**.
```markdown
**NOTE**: this document is generated, do not edit manually.
```
**article.md**
Add this to your **article.md**.
```markdown
<!-- Space: TEST -->
<!-- Title: My Article -->
@ -136,8 +523,6 @@ This is my article.
### Insert Status Badge
**article.md**
```markdown
<!-- Space: TEST -->
<!-- Title: TODO List -->
@ -156,15 +541,128 @@ This is my article.
* :todo: Publish Article
```
### Insert Colored Text Box
```markdown
<!-- Space: TEST -->
<!-- Title: Announcement -->
<!-- Macro: :box:([^:]+):([^:]*):(.+):
Template: ac:box
Icon: true
Name: ${1}
Title: ${2}
Body: ${3} -->
:box:info::Foobar:
:box:tip:Tip of day:Foobar:
:box:note::Foobar:
:box:warning:Alert!:Foobar:
```
### Insert Table of Contents
```markdown
<!-- Include: ac:toc -->
```
### Insert Jira Ticket
If default TOC looks don't find a way to your heart, try [parametrizing it][Confluence TOC Macro], for example:
**article.md**
```markdown
<!-- Macro: :toc:
Template: ac:toc
Printable: 'false'
MinLevel: 2 -->
# This is my nice title
:toc:
```
You can call the `Macro` as you like but the `Template` field must have the `ac:toc` value.
Also, note the single quotes around `'false'`.
See [Confluence TOC Macro] for the list of parameters - keep in mind that here
they start with capital letters. Every skipped field will have the default
value, so feel free to include only the ones that you require.
[Confluence TOC Macro]:https://confluence.atlassian.com/conf59/table-of-contents-macro-792499210.html
### Insert PageTree
```markdown
# My First Heading
<!-- Include: ac:pagetree -->
```
The pagetree macro works almost the same as the TOC above, but the tree behavior
is more desirable for creating placeholder pages above collections of SOPs.
The default pagetree macro behavior is to insert a tree rooted @self.
The following parameters can be used to alter your default configuration with
parameters described more in depth here:[Confluence Pagetree Macro].
Parameters:
* Title (of tree root page)
* Sort
* Excerpt
* Reverse
* SearchBox
* ExpandCollapseAll
* StartDepth
[Confluence Pagetree Macro]:https://confluence.atlassian.com/conf59/page-tree-macro-792499177.html
E.G.
```markdown
<!-- Macro: :pagetree:
Template: ac:pagetree
Reverse: 'true'
ExpandCollapseAll: 'true'
StartDepth: 2 -->
# My First Heading
:pagetree:
```
### Insert Children Display
To include Children Display (TOC displaying children pages) use following macro:
```markdown
<!-- Macro: :children:
Template: ac:children
-->
# This is my nicer title
:children:
```
You can use various [parameters](https://confluence.atlassian.com/conf59/children-display-macro-792499081.html) to modify Children Display:
```markdown
<!-- Macro: :children:
Template: ac:children
Sort: title
Style: h3
Excerpt: simple
First: 10
Page: Space:Page title
Depth: 2
Reverse: false
All: false -->
# This is my nicest title
:children:
```
### Insert Jira Ticket
```markdown
<!-- Space: TEST -->
@ -177,9 +675,88 @@ This is my article.
See task MYJIRA-123.
```
### Insert link to existing confluence page by title
```markdown
This is a [link to an existing confluence page](ac:Pagetitle)
And this is how to link when the linktext is the same as the [Pagetitle](ac:)
Link to a [page title containing spaces](<ac:With Multiple Words>)
```
### Upload and included inline images
```markdown
![Example](../images/examples.png)
```
will automatically upload the inlined image as an attachment and inline the image using the `ac:image` template.
If the file is not found, it will inline the image using the `ac:image` template and link to the image.
### Add width for an image
Use the following macro:
```markdown
<!-- Macro: \!\[.*\]\((.+)\)\<\!\-\- width=(.*) \-\-\>
Template: ac:image
Attachment: ${1}
Width: ${2} -->
```
And attach any image with the following
```markdown
![Example](../images/example.png)<!-- width=300 -->
```
The width will be the commented html after the image (in this case 300px).
Currently this is not compatible with the automated upload of inline images.
### Render Mermaid Diagram
Confluence doesn't provide [mermaid.js](https://github.com/mermaid-js/mermaid) support natively. Mark provides a convenient way to enable the feature like [Github does](https://github.blog/2022-02-14-include-diagrams-markdown-files-mermaid/).
As long as you have a code block marked as "mermaid", mark will automatically render it as a PNG image and attach it to the page as a rendered version of the code block.
```mermaid title diagrams_example
graph TD;
A-->B;
```
In order to properly render mermaid, you can choose between the following mermaid providers:
* "mermaid-go" via [mermaid.go](https://github.com/dreampuf/mermaid.go)
* "cloudscript" via [cloudscript-io-mermaid-addon](https://marketplace.atlassian.com/apps/1219878/cloudscript-io-mermaid-addon) (deprecated)
### Render D2 Diagram
Optionally you can enable [D2](https://github.com/terrastruct/d2) rendering via `--features="d2"`.
This will transform the d2 diagram into a png that will be attached to Confluence, similar to how mermaid-go support works.
All you need is a codeblock marked as "d2".
```d2
X -> Y
```
## Installation
### Go Get
### Homebrew
```bash
brew tap kovetskiy/mark
brew install mark
```
### Go Install / Go Get
```bash
go install github.com/kovetskiy/mark@latest
```
For older versions
```bash
go get -v github.com/kovetskiy/mark
@ -192,45 +769,90 @@ go get -v github.com/kovetskiy/mark
### Docker
```bash
$ docker run --rm -i kovetskiy/mark:latest mark <params>
docker run --rm -i kovetskiy/mark:latest mark <params>
```
### Compile and install using docker-compose
Mostly useful when you intend to enhance `mark`.
```bash
# Create the binary
$ docker-compose run markbuilder
# "install" the binary
$ cp mark /usr/local/bin
```
## Usage
```
mark [options] [-u <username>] [-p <password>] [-k] [-l <url>] -f <file>
mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <file>
mark -v | --version
mark -h | --help
```
```bash
NAME:
mark - A tool for updating Atlassian Confluence pages from markdown.
- `-u <username>` — Use specified username for updating Confluence page.
- `-p <password>` — Use specified password for updating Confluence page.
- `-l <url>` — Edit specified Confluence page.
If -l is not specified, file should contain metadata (see above).
- `-f <file>` — Use specified markdown file for converting to html.
- `-c <file>` — Specify configuration file which should be used for reading
Confluence page URL and markdown file path.
- `-k` — Lock page editing to current user only to prevent accidental
manual edits over Confluence Web UI.
- `--dry-run` — Show resulting HTML and don't update Confluence page content.
- `--trace` — Enable trace logs.
- `-v | --version` — Show version.
- `-h | --help` — Show help screen and call 911.
USAGE:
mark [global options]
VERSION:
13.0.0
DESCRIPTION:
Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark
GLOBAL OPTIONS:
--files string, -f string use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted). [$MARK_FILES]
--continue-on-error don't exit if an error occurs while processing a file, continue processing remaining files. (default: false) [$MARK_CONTINUE_ON_ERROR]
--compile-only show resulting HTML and don't update Confluence page content. (default: false) [$MARK_COMPILE_ONLY]
--dry-run resolve page and ancestry, show resulting HTML and exit. (default: false) [$MARK_DRY_RUN]
--edit-lock, -k lock page editing to current user only to prevent accidental manual edits over Confluence Web UI. (default: false) [$MARK_EDIT_LOCK]
--drop-h1 don't include the first H1 heading in Confluence output. (default: false) [$MARK_DROP_H1]
--strip-linebreaks, -L remove linebreaks inside of tags, to accommodate non-standard Confluence behavior (default: false) [$MARK_STRIP_LINEBREAKS]
--title-from-h1 extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata. (default: false) [$MARK_TITLE_FROM_H1]
--title-append-generated-hash appends a short hash generated from the path of the page (space, parents, and title) to the title (default: false) [$MARK_TITLE_APPEND_GENERATED_HASH]
--minor-edit don't send notifications while updating Confluence page. (default: false) [$MARK_MINOR_EDIT]
--version-message string add a message to the page version, to explain the edit (default: "") [$MARK_VERSION_MESSAGE]
--color string display logs in color. Possible values: auto, never. (default: "auto") [$MARK_COLOR]
--log-level string set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL. (default: "info") [$MARK_LOG_LEVEL]
--username string, -u string use specified username for updating Confluence page. [$MARK_USERNAME]
--password string, -p string use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication. [$MARK_PASSWORD]
--target-url string, -l string edit specified Confluence page. If -l is not specified, file should contain metadata (see above). [$MARK_TARGET_URL]
--base-url string, -b string base URL for Confluence. Alternative option for base_url config field. [$MARK_BASE_URL]
--config string, -c string use the specified configuration file. (default: $HOME/.config/mark.toml") [$MARK_CONFIG]
--ci run on CI mode. It won't fail if files are not found. (default: false) [$MARK_CI]
--space string use specified space key. If the space key is not specified, it must be set in the page metadata. [$MARK_SPACE]
--parents string A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself. [$MARK_PARENTS]
--parents-delimiter string The delimiter used for the parents list (default: "/") [$MARK_PARENTS_DELIMITER]
--mermaid-provider string defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go. (default: "cloudscript") [$MARK_MERMAID_PROVIDER]
--mermaid-scale float defines the scaling factor for mermaid renderings. (default: 1) [$MARK_MERMAID_SCALE]
--include-path string Path for shared includes, used as a fallback if the include doesn't exist in the current directory. [$MARK_INCLUDE_PATH]
--changes-only Avoids re-uploading pages that haven't changed since the last run. (default: false) [$MARK_CHANGES_ONLY]
--d2-scale float defines the scaling factor for d2 renderings. (default: 1) [$MARK_D2_SCALE]
--features string [ --features string ] Enables optional features. Current features: d2, mermaid (default: "mermaid") [$MARK_FEATURES]
--help, -h show help
--version, -v print the version
```
You can store user credentials in the configuration file, which should be
located in ~/.config/mark with the following format (TOML):
located in a system specific directory (or specified via `-c --config <path>`) with the following format (TOML):
```toml
username = "smith"
password = "matrixishere"
username = "your-email"
password = "password-or-api-key-for-confluence-cloud"
# If you are using Confluence Cloud add the /wiki suffix to base_url
base_url = "http://confluence.local"
base-url = "http://confluence.local"
title-from-h1 = true
drop-h1 = true
```
# Tricks
**NOTE**: Labels aren't supported when using `minor-edit`!
## Continuous Integration
**NOTE**: The system specific locations are described in here:
<https://pkg.go.dev/os#UserConfigDir>.
Currently, these are:
On Unix systems, it returns $XDG_CONFIG_HOME as specified by https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if non-empty, else $HOME/.config. On Darwin, it returns $HOME/Library/Application Support. On Windows, it returns %AppData%. On Plan 9, it returns $home/lib.
## Tricks
### Continuous Integration
It's quite trivial to integrate Mark into a CI/CD system, here is an example with [Snake CI](https://snake-ci.com/)
in case of self-hosted Bitbucket Server / Data Center.
@ -266,7 +888,7 @@ done
The following directive tells the CI to run this particular job only if the changes are pushed into the
`main` branch. It means you can safely push your changes into feature branches without being afraid
that they 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.
```yaml
@ -274,3 +896,114 @@ only:
branches:
- main
```
### File Globbing
Rather than running `mark` multiple times, or looping through a list of files from `find`, you can use file globbing (i.e. wildcard patterns) to match files in subdirectories. For example:
```bash
mark -f "helpful_cmds/*.md"
```
You can also use `**` to get all files recursively.
```bash
mark -f "**/docs/*.md"
```
### Linting markdown
We recommend to lint your markdown files with [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) before publishing them to confluence to catch any conversion errors early.
## Issues, Bugs & Contributions
I've started the project to solve my own problem and open sourced the solution so anyone who has a problem like me can solve it too.
I have no profits/sponsors from these projects which means I don't really prioritize working on this project in my free time.
I still check the issues and do code reviews for Pull Requests which means if you encounter a bug in
the program, you should not expect me to fix it as soon as possible, but I'll be very glad to
merge your own contributions into the project and release the new version.
I try to label all new issues, so it's easy to find a bug or a feature request to fix/implement, if
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:
* [label:feature-request](https://github.com/kovetskiy/mark/issues?q=is%3Aissue+is%3Aopen+label%3Afeature-request)
* [label:bug](https://github.com/kovetskiy/mark/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.social/@mrueg"><img src="https://avatars.githubusercontent.com/u/489370?v=4?s=100" width="100px;" alt="Manuel Rüger"/><br /><sub><b>Manuel Rüger</b></sub></a><br /><a href="#maintenance-mrueg" title="Maintenance">🚧</a> <a href="https://github.com/kovetskiy/mark/commits?author=mrueg" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kovetskiy"><img src="https://avatars.githubusercontent.com/u/8445924?v=4?s=100" width="100px;" alt="Egor Kovetskiy"/><br /><sub><b>Egor Kovetskiy</b></sub></a><br /><a href="#maintenance-kovetskiy" title="Maintenance">🚧</a> <a href="https://github.com/kovetskiy/mark/commits?author=kovetskiy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://klauer.dev/"><img src="https://avatars.githubusercontent.com/u/4735?v=4?s=100" width="100px;" alt="Nick Klauer"/><br /><sub><b>Nick Klauer</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=klauern" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rofafor"><img src="https://avatars.githubusercontent.com/u/9297850?v=4?s=100" width="100px;" alt="Rolf Ahrenberg"/><br /><sub><b>Rolf Ahrenberg</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=rofafor" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/csoutherland"><img src="https://avatars.githubusercontent.com/u/840471?v=4?s=100" width="100px;" alt="Charles Southerland"/><br /><sub><b>Charles Southerland</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=csoutherland" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/snejus"><img src="https://avatars.githubusercontent.com/u/16212750?v=4?s=100" width="100px;" alt="Šarūnas Nejus"/><br /><sub><b>Šarūnas Nejus</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=snejus" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/brnv"><img src="https://avatars.githubusercontent.com/u/1925213?v=4?s=100" width="100px;" alt="Alexey Baranov"/><br /><sub><b>Alexey Baranov</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=brnv" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/princespaghetti"><img src="https://avatars.githubusercontent.com/u/2935312?v=4?s=100" width="100px;" alt="Anthony Barbieri"/><br /><sub><b>Anthony Barbieri</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=princespaghetti" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dauc"><img src="https://avatars.githubusercontent.com/u/29129213?v=4?s=100" width="100px;" alt="Devin Auclair"/><br /><sub><b>Devin Auclair</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=dauc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://gezimsejdiu.github.io/"><img src="https://avatars.githubusercontent.com/u/5259296?v=4?s=100" width="100px;" alt="Gezim Sejdiu"/><br /><sub><b>Gezim Sejdiu</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=GezimSejdiu" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jcavar"><img src="https://avatars.githubusercontent.com/u/3751289?v=4?s=100" width="100px;" alt="Josip Ćavar"/><br /><sub><b>Josip Ćavar</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=jcavar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Hi-Fi"><img src="https://avatars.githubusercontent.com/u/1499780?v=4?s=100" width="100px;" alt="Juho Saarinen"/><br /><sub><b>Juho Saarinen</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=Hi-Fi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lukiffer"><img src="https://avatars.githubusercontent.com/u/2278911?v=4?s=100" width="100px;" alt="Luke Fritz"/><br /><sub><b>Luke Fritz</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=lukiffer" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/MattyRad"><img src="https://avatars.githubusercontent.com/u/1143595?v=4?s=100" width="100px;" alt="Matt Radford"/><br /><sub><b>Matt Radford</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=MattyRad" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Planktonette"><img src="https://avatars.githubusercontent.com/u/5514719?v=4?s=100" width="100px;" alt="Planktonette"/><br /><sub><b>Planktonette</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=Planktonette" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.stefanoteodorani.it/"><img src="https://avatars.githubusercontent.com/u/2573389?v=4?s=100" width="100px;" alt="Stefano Teodorani"/><br /><sub><b>Stefano Teodorani</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=teopost" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tillepille"><img src="https://avatars.githubusercontent.com/u/16536696?v=4?s=100" width="100px;" alt="Tim Schrumpf"/><br /><sub><b>Tim Schrumpf</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=tillepille" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tyler-copilot"><img src="https://avatars.githubusercontent.com/u/18539108?v=4?s=100" width="100px;" alt="Tyler Cole"/><br /><sub><b>Tyler Cole</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=tyler-copilot" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/elgreco247"><img src="https://avatars.githubusercontent.com/u/8968417?v=4?s=100" width="100px;" alt="elgreco247"/><br /><sub><b>elgreco247</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=elgreco247" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emead-indeed"><img src="https://avatars.githubusercontent.com/u/44018145?v=4?s=100" width="100px;" alt="emead-indeed"/><br /><sub><b>emead-indeed</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=emead-indeed" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://wbhegedus.me/"><img src="https://avatars.githubusercontent.com/u/11506822?v=4?s=100" width="100px;" alt="Will Hegedus"/><br /><sub><b>Will Hegedus</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=wbh1" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/carnei-ro"><img src="https://avatars.githubusercontent.com/u/42899277?v=4?s=100" width="100px;" alt="Leandro Carneiro"/><br /><sub><b>Leandro Carneiro</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=carnei-ro" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/beeme1mr"><img src="https://avatars.githubusercontent.com/u/682996?v=4?s=100" width="100px;" alt="beeme1mr"/><br /><sub><b>beeme1mr</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=beeme1mr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Taldrain"><img src="https://avatars.githubusercontent.com/u/1081600?v=4?s=100" width="100px;" alt="Taldrain"/><br /><sub><b>Taldrain</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=Taldrain" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.devin.com.br/"><img src="https://avatars.githubusercontent.com/u/349457?v=4?s=100" width="100px;" alt="Hugo Cisneiros"/><br /><sub><b>Hugo Cisneiros</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=eitchugo" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jevfok"><img src="https://avatars.githubusercontent.com/u/54530686?v=4?s=100" width="100px;" alt="jevfok"/><br /><sub><b>jevfok</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=jevfok" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dev.to/mmiranda"><img src="https://avatars.githubusercontent.com/u/16670310?v=4?s=100" width="100px;" alt="Mateus Miranda"/><br /><sub><b>Mateus Miranda</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=mmiranda" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Skeeve"><img src="https://avatars.githubusercontent.com/u/725404?v=4?s=100" width="100px;" alt="Stephan Hradek"/><br /><sub><b>Stephan Hradek</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=Skeeve" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://huangx.in/"><img src="https://avatars.githubusercontent.com/u/353644?v=4?s=100" width="100px;" alt="Dreampuf"/><br /><sub><b>Dreampuf</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=dreampuf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/JAndritsch"><img src="https://avatars.githubusercontent.com/u/190611?v=4?s=100" width="100px;" alt="Joel Andritsch"/><br /><sub><b>Joel Andritsch</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=JAndritsch" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/guoweis-outreach"><img src="https://avatars.githubusercontent.com/u/639243?v=4?s=100" width="100px;" alt="guoweis-outreach"/><br /><sub><b>guoweis-outreach</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=guoweis-outreach" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/klysunkin"><img src="https://avatars.githubusercontent.com/u/2611187?v=4?s=100" width="100px;" alt="klysunkin"/><br /><sub><b>klysunkin</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=klysunkin" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/EppO"><img src="https://avatars.githubusercontent.com/u/6111?v=4?s=100" width="100px;" alt="Florent Monbillard"/><br /><sub><b>Florent Monbillard</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=EppO" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jfreeland"><img src="https://avatars.githubusercontent.com/u/30938344?v=4?s=100" width="100px;" alt="Joey Freeland"/><br /><sub><b>Joey Freeland</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=jfreeland" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prokod"><img src="https://avatars.githubusercontent.com/u/877414?v=4?s=100" width="100px;" alt="Noam Asor"/><br /><sub><b>Noam Asor</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=prokod" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PhilippReinke"><img src="https://avatars.githubusercontent.com/u/81698819?v=4?s=100" width="100px;" alt="Philipp"/><br /><sub><b>Philipp</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=PhilippReinke" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vpommier"><img src="https://avatars.githubusercontent.com/u/8139328?v=4?s=100" width="100px;" alt="Pommier Vincent"/><br /><sub><b>Pommier Vincent</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=vpommier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ToruKawaguchi"><img src="https://avatars.githubusercontent.com/u/17423222?v=4?s=100" width="100px;" alt="Toru Kawaguchi"/><br /><sub><b>Toru Kawaguchi</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=ToruKawaguchi" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://coaxialflutter.com/"><img src="https://avatars.githubusercontent.com/u/49793?v=4?s=100" width="100px;" alt="Will Gorman"/><br /><sub><b>Will Gorman</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=willgorman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://zackery.dev/"><img src="https://avatars.githubusercontent.com/u/15172516?v=4?s=100" width="100px;" alt="Zackery Griesinger"/><br /><sub><b>Zackery Griesinger</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=zgriesinger" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/chrisjaimon2012"><img src="https://avatars.githubusercontent.com/u/57173930?v=4?s=100" width="100px;" alt="cc-chris"/><br /><sub><b>cc-chris</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=chrisjaimon2012" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/datsickkunt"><img src="https://avatars.githubusercontent.com/u/105289244?v=4?s=100" width="100px;" alt="datsickkunt"/><br /><sub><b>datsickkunt</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=datsickkunt" title="Code">💻</a></td>
</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/seletskiy"><img src="https://avatars.githubusercontent.com/u/674812?v=4?s=100" width="100px;" alt="Stanislav Seletskiy"/><br /><sub><b>Stanislav Seletskiy</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=seletskiy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nr18"><img src="https://avatars.githubusercontent.com/u/1660601?v=4?s=100" width="100px;" alt="Joris Conijn"/><br /><sub><b>Joris Conijn</b></sub></a><br /><a href="https://github.com/kovetskiy/mark/commits?author=nr18" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@ -1,14 +1,40 @@
version: '2'
version: '3'
vars:
version: 7.13.0
pwd:
sh: pwd
tasks:
confluence:
volume:
cmds:
- docker run -v {{ .pwd }}/docker:/var/atlassian/application-data/confluence
- mkdir -p docker/{{.version}}
network:
desc: create docker network
cmds:
- docker network create confluence || true
postgres:
desc: start postgres for confluence
deps: [network, volume]
cmds:
- docker run -it -p 5432:5432
--name confluence-postgres
--network confluence
-v {{.pwd}}/docker/{{.version}}/postgres:/var/lib/postgresql/data
-e POSTGRES_PASSWORD=confluence
-e POSTGRES_DB=confluence
-e POSTGRES_USER=confluence
postgres
confluence:
desc: start confluence server
deps: [network, volume]
cmds:
- docker run -v {{ .pwd }}/docker/{{.version}}/confluence:/var/atlassian/application-data/confluence
--name="confluence"
--network confluence
-p 8090:8090
-p 8091:8091
atlassian/confluence-server
atlassian/confluence-server:{{.version}}

296
attachment/attachment.go Normal file
View File

@ -0,0 +1,296 @@
package attachment
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io"
"net/url"
"path"
"path/filepath"
"sort"
"strings"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/vfs"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
const (
AttachmentChecksumPrefix = `mark:checksum: `
)
type Attachment struct {
ID string
Name string
Filename string
FileBytes []byte
Checksum string
Link string
Width string
Height string
Replace string
}
type Attacher interface {
Attach(Attachment)
}
func ResolveAttachments(
api *confluence.API,
page *confluence.PageInfo,
attachments []Attachment,
) ([]Attachment, error) {
for i := range attachments {
checksum, err := GetChecksum(bytes.NewReader(attachments[i].FileBytes))
if err != nil {
return nil, karma.Format(
err,
"unable to get checksum for attachment: %q", attachments[i].Name,
)
}
attachments[i].Checksum = checksum
}
remotes, err := api.GetAttachments(page.ID)
if err != nil {
panic(err)
}
existing := []Attachment{}
creating := []Attachment{}
updating := []Attachment{}
for _, attachment := range attachments {
var found bool
var same bool
for _, remote := range remotes {
if remote.Filename == attachment.Filename {
same = attachment.Checksum == strings.TrimPrefix(
remote.Metadata.Comment,
AttachmentChecksumPrefix,
)
attachment.ID = remote.ID
attachment.Link = path.Join(
remote.Links.Context,
remote.Links.Download,
)
found = true
break
}
}
if found {
if same {
existing = append(existing, attachment)
} else {
updating = append(updating, attachment)
}
} else {
creating = append(creating, attachment)
}
}
for i, attachment := range creating {
log.Infof(nil, "creating attachment: %q", attachment.Name)
info, err := api.CreateAttachment(
page.ID,
attachment.Filename,
AttachmentChecksumPrefix+attachment.Checksum,
bytes.NewReader(attachment.FileBytes),
)
if err != nil {
return nil, karma.Format(
err,
"unable to create attachment %q",
attachment.Name,
)
}
attachment.ID = info.ID
attachment.Link = path.Join(
info.Links.Context,
info.Links.Download,
)
creating[i] = attachment
}
for i, attachment := range updating {
log.Infof(nil, "updating attachment: %q", attachment.Name)
info, err := api.UpdateAttachment(
page.ID,
attachment.ID,
attachment.Filename,
AttachmentChecksumPrefix+attachment.Checksum,
bytes.NewReader(attachment.FileBytes),
)
if err != nil {
return nil, karma.Format(
err,
"unable to update attachment %q",
attachment.Name,
)
}
attachment.Link = path.Join(
info.Links.Context,
info.Links.Download,
)
updating[i] = attachment
}
for i := range existing {
log.Infof(nil, "keeping unmodified attachment: %q", attachments[i].Name)
}
attachments = []Attachment{}
attachments = append(attachments, existing...)
attachments = append(attachments, creating...)
attachments = append(attachments, updating...)
return attachments, nil
}
func ResolveLocalAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) {
attachments, err := prepareAttachments(opener, base, replacements)
if err != nil {
return nil, err
}
for _, attachment := range attachments {
checksum, err := GetChecksum(bytes.NewReader(attachment.FileBytes))
if err != nil {
return nil, karma.Format(
err,
"unable to get checksum for attachment: %q", attachment.Name,
)
}
attachment.Checksum = checksum
}
return attachments, err
}
// prepareAttachements creates an array of attachement objects based on an array of filepaths
func prepareAttachments(opener vfs.Opener, base string, replacements []string) ([]Attachment, error) {
attachments := []Attachment{}
for _, name := range replacements {
attachment, err := prepareAttachment(opener, base, name)
if err != nil {
return nil, err
}
attachments = append(attachments, attachment)
}
return attachments, nil
}
// prepareAttachement opens the file, reads its content and creates an attachement object
func prepareAttachment(opener vfs.Opener, base, name string) (Attachment, error) {
attachmentPath := filepath.Join(base, name)
file, err := opener.Open(attachmentPath)
if err != nil {
return Attachment{}, karma.Format(err, "unable to open file: %q", attachmentPath)
}
defer func() {
_ = file.Close()
}()
fileBytes, err := io.ReadAll(file)
if err != nil {
return Attachment{}, karma.Format(err, "unable to read file: %q", attachmentPath)
}
return Attachment{
Name: name,
Filename: strings.ReplaceAll(name, "/", "_"),
FileBytes: fileBytes,
Replace: name,
}, nil
}
func CompileAttachmentLinks(markdown []byte, attachments []Attachment) []byte {
links := map[string]string{}
replaces := []string{}
for _, attachment := range attachments {
links[attachment.Replace] = parseAttachmentLink(attachment.Link)
replaces = append(replaces, attachment.Replace)
}
// sort by length so first items will have bigger length
// it's helpful for replacing in case of following names
// attachments/a.jpg
// attachments/a.jpg.jpg
// so we replace longer and then shorter
sort.SliceStable(replaces, func(i, j int) bool {
return len(replaces[i]) > len(replaces[j])
})
for _, replace := range replaces {
to := links[replace]
found := false
if bytes.Contains(markdown, []byte("attachment://"+replace)) {
from := "attachment://" + replace
log.Debugf(nil, "replacing legacy link: %q -> %q", from, to)
markdown = bytes.ReplaceAll(
markdown,
[]byte(from),
[]byte(to),
)
found = true
}
if bytes.Contains(markdown, []byte(replace)) {
from := replace
log.Debugf(nil, "replacing link: %q -> %q", from, to)
markdown = bytes.ReplaceAll(
markdown,
[]byte(from),
[]byte(to),
)
found = true
}
if !found {
log.Warningf(nil, "unused attachment: %s", replace)
}
}
return markdown
}
func GetChecksum(reader io.Reader) (string, error) {
hash := sha256.New()
if _, err := io.Copy(hash, reader); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func parseAttachmentLink(attachLink string) string {
uri, err := url.ParseRequestURI(attachLink)
if err != nil {
return strings.ReplaceAll(attachLink, "&", "&amp;")
} else {
return uri.Path +
"?" + url.QueryEscape(uri.Query().Encode())
}
}

View File

@ -0,0 +1,90 @@
package attachment
import (
"bytes"
"io"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
var (
replacements = []string{
"image1.jpg",
"images/image2.jpg",
"../image3.jpg",
}
)
type bufferCloser struct {
*bytes.Buffer
}
func (bufferCloser) Close() error { return nil }
type virtualOpener struct {
PathToBuf map[string]*bufferCloser
}
func (o *virtualOpener) Open(name string) (io.ReadWriteCloser, error) {
if buf, ok := o.PathToBuf[name]; ok {
return buf, nil
}
return nil, os.ErrNotExist
}
func TestPrepareAttachmentsWithWorkDirBase(t *testing.T) {
testingOpener := &virtualOpener{
PathToBuf: map[string]*bufferCloser{
"image1.jpg": {bytes.NewBuffer(nil)},
"images/image2.jpg": {bytes.NewBuffer(nil)},
"../image3.jpg": {bytes.NewBuffer(nil)},
},
}
attaches, err := prepareAttachments(testingOpener, ".", replacements)
t.Logf("attaches: %v", err)
if err != nil {
println(err.Error())
t.Fatal(err)
}
assert.Equal(t, "image1.jpg", attaches[0].Name)
assert.Equal(t, "image1.jpg", attaches[0].Replace)
assert.Equal(t, "images/image2.jpg", attaches[1].Name)
assert.Equal(t, "images/image2.jpg", attaches[1].Replace)
assert.Equal(t, "../image3.jpg", attaches[2].Name)
assert.Equal(t, "../image3.jpg", attaches[2].Replace)
assert.Equal(t, len(attaches), 3)
}
func TestPrepareAttachmentsWithSubDirBase(t *testing.T) {
testingOpener := &virtualOpener{
PathToBuf: map[string]*bufferCloser{
"a/b/image1.jpg": {bytes.NewBuffer(nil)},
"a/b/images/image2.jpg": {bytes.NewBuffer(nil)},
"a/image3.jpg": {bytes.NewBuffer(nil)},
},
}
attaches, err := prepareAttachments(testingOpener, "a/b", replacements)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "image1.jpg", attaches[0].Name)
assert.Equal(t, "image1.jpg", attaches[0].Replace)
assert.Equal(t, "images/image2.jpg", attaches[1].Name)
assert.Equal(t, "images/image2.jpg", attaches[1].Replace)
assert.Equal(t, "../image3.jpg", attaches[2].Name)
assert.Equal(t, "../image3.jpg", attaches[2].Replace)
assert.Equal(t, len(attaches), 3)
}

View File

@ -1,27 +0,0 @@
package main
import (
"os"
"github.com/kovetskiy/ko"
)
type Config struct {
Username string `env:"MARK_USERNAME" toml:"username"`
Password string `env:"MARK_PASSWORD" toml:"password"`
BaseURL string `env:"MARK_BASE_URL" toml:"base_url"`
}
func LoadConfig(path string) (*Config, error) {
config := &Config{}
err := ko.Load(path, config)
if err != nil {
if os.IsNotExist(err) {
return config, nil
}
return nil, err
}
return config, nil
}

View File

@ -2,14 +2,14 @@ package confluence
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"strings"
"unicode/utf8"
"github.com/kovetskiy/gopencils"
"github.com/kovetskiy/lorg"
@ -18,7 +18,8 @@ import (
)
type User struct {
AccountID string `json:"accountId"`
AccountID string `json:"accountId,omitempty"`
UserKey string `json:"userKey,omitempty"`
}
type API struct {
@ -26,19 +27,34 @@ type API struct {
// it's deprecated accordingly to Atlassian documentation,
// but it's only way to set permissions
json *gopencils.Resource
json *gopencils.Resource
BaseURL string
}
type SpaceInfo struct {
ID int `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
Homepage PageInfo `json:"homepage"`
Links struct {
Full string `json:"webui"`
} `json:"_links"`
}
type PageInfo struct {
ID string `json:"id"`
Title string `json:"title"`
Type string `json:"type"`
Version struct {
Number int64 `json:"number"`
Number int64 `json:"number"`
Message string `json:"message"`
} `json:"version"`
Ancestors []struct {
Id string `json:"id"`
ID string `json:"id"`
Title string `json:"title"`
} `json:"ancestors"`
@ -59,6 +75,15 @@ type AttachmentInfo struct {
} `json:"_links"`
}
type Label struct {
ID string `json:"id"`
Prefix string `json:"prefix"`
Name string `json:"name"`
}
type LabelInfo struct {
Labels []Label `json:"results"`
Size int `json:"number"`
}
type form struct {
buffer io.Reader
writer *multipart.Writer
@ -73,13 +98,22 @@ func (tracer *tracer) Printf(format string, args ...interface{}) {
}
func NewAPI(baseURL string, username string, password string) *API {
auth := &gopencils.BasicAuth{username, password}
var auth *gopencils.BasicAuth
if username != "" {
auth = &gopencils.BasicAuth{
Username: username,
Password: password,
}
}
rest := gopencils.Api(baseURL+"/rest/api", auth, 3) // set option for 3 retries on failure
if username == "" {
if rest.Headers == nil {
rest.Headers = http.Header{}
}
rest.SetHeader("Authorization", fmt.Sprintf("Bearer %s", password))
}
rest := gopencils.Api(baseURL+"/rest/api", auth)
json := gopencils.Api(
baseURL+"/rpc/json-rpc/confluenceservice-v2",
auth,
)
json := gopencils.Api(baseURL+"/rpc/json-rpc/confluenceservice-v2", auth, 3)
if log.GetLevel() == lorg.LevelTrace {
rest.Logger = &tracer{"rest:"}
@ -87,13 +121,14 @@ func NewAPI(baseURL string, username string, password string) *API {
}
return &API{
rest: rest,
json: json,
rest: rest,
json: json,
BaseURL: strings.TrimSuffix(baseURL, "/"),
}
}
func (api *API) FindRootPage(space string) (*PageInfo, error) {
page, err := api.FindPage(space, ``)
page, err := api.FindPage(space, ``, "page")
if err != nil {
return nil, karma.Format(
err,
@ -114,12 +149,35 @@ func (api *API) FindRootPage(space string) (*PageInfo, error) {
}
return &PageInfo{
ID: page.Ancestors[0].Id,
ID: page.Ancestors[0].ID,
Title: page.Ancestors[0].Title,
}, nil
}
func (api *API) FindPage(space string, title string) (*PageInfo, error) {
func (api *API) FindHomePage(space string) (*PageInfo, error) {
payload := map[string]string{
"expand": "homepage",
}
request, err := api.rest.Res(
"space/"+space, &SpaceInfo{},
).Get(payload)
if err != nil {
return nil, err
}
if request.Raw.StatusCode == http.StatusNotFound || request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
return &request.Response.(*SpaceInfo).Homepage, nil
}
func (api *API) FindPage(
space string,
title string,
pageType string,
) (*PageInfo, error) {
result := struct {
Results []PageInfo `json:"results"`
}{}
@ -127,6 +185,7 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
payload := map[string]string{
"spaceKey": space,
"expand": "ancestors,version",
"type": pageType,
}
if title != "" {
@ -142,7 +201,7 @@ func (api *API) FindPage(space string, title string) (*PageInfo, error) {
// allow 404 because it's fine if page is not found,
// the function will return nil, nil
if request.Raw.StatusCode != 404 && request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusNotFound && request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
@ -157,11 +216,11 @@ func (api *API) CreateAttachment(
pageID string,
name string,
comment string,
path string,
reader io.Reader,
) (AttachmentInfo, error) {
var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path)
form, err := getAttachmentPayload(name, comment, reader)
if err != nil {
return AttachmentInfo{}, err
}
@ -178,7 +237,11 @@ func (api *API) CreateAttachment(
)
resource.Payload = form.buffer
oldHeaders := resource.Headers.Clone()
resource.Headers = http.Header{}
if resource.Api.BasicAuth == nil {
resource.Headers.Set("Authorization", oldHeaders.Get("Authorization"))
}
resource.SetHeader("Content-Type", form.writer.FormDataContentType())
resource.SetHeader("X-Atlassian-Token", "no-check")
@ -188,13 +251,13 @@ func (api *API) CreateAttachment(
return info, err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return info, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
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",
)
}
@ -212,33 +275,43 @@ func (api *API) CreateAttachment(
return info, nil
}
// UpdateAttachment uploads a new version of the same attachment if the
// checksums differs from the previous one.
// It also handles a case where Confluence returns sort of "short" variant of
// the response instead of an extended one.
func (api *API) UpdateAttachment(
pageID string,
attachID string,
name string,
comment string,
path string,
reader io.Reader,
) (AttachmentInfo, error) {
var info AttachmentInfo
form, err := getAttachmentPayload(name, comment, path)
form, err := getAttachmentPayload(name, comment, reader)
if err != nil {
return AttachmentInfo{}, err
}
var result struct {
var extendedResponse struct {
Links struct {
Context string `json:"context"`
} `json:"_links"`
Results []AttachmentInfo `json:"results"`
}
var result json.RawMessage
resource := api.rest.Res(
"content/"+pageID+"/child/attachment/"+attachID+"/data", &result,
)
resource.Payload = form.buffer
oldHeaders := resource.Headers.Clone()
resource.Headers = http.Header{}
if resource.Api.BasicAuth == nil {
resource.Headers.Set("Authorization", oldHeaders.Get("Authorization"))
}
resource.SetHeader("Content-Type", form.writer.FormDataContentType())
resource.SetHeader("X-Atlassian-Token", "no-check")
@ -248,47 +321,52 @@ func (api *API) UpdateAttachment(
return info, err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return info, newErrorStatusNotOK(request)
}
if len(result.Results) == 0 {
return info, errors.New(
"Confluence REST API for creating attachments returned " +
"0 json objects, expected at least 1",
err = json.Unmarshal(result, &extendedResponse)
if err != nil {
return info, karma.Format(
err,
"unable to unmarshal JSON response as full response format: %s",
string(result),
)
}
for i, info := range result.Results {
if info.Links.Context == "" {
info.Links.Context = result.Links.Context
if len(extendedResponse.Results) > 0 {
for i, info := range extendedResponse.Results {
if info.Links.Context == "" {
info.Links.Context = extendedResponse.Links.Context
}
extendedResponse.Results[i] = info
}
result.Results[i] = info
info = extendedResponse.Results[0]
return info, nil
}
info = result.Results[0]
var shortResponse AttachmentInfo
err = json.Unmarshal(result, &shortResponse)
if err != nil {
return info, karma.Format(
err,
"unable to unmarshal JSON response as short response format: %s",
string(result),
)
}
return info, nil
return shortResponse, nil
}
func getAttachmentPayload(name, comment, path string) (*form, error) {
func getAttachmentPayload(name, comment string, reader io.Reader) (*form, error) {
var (
payload = bytes.NewBuffer(nil)
writer = multipart.NewWriter(payload)
)
file, err := os.Open(path)
if err != nil {
return nil, karma.Format(
err,
"unable to open file: %q",
path,
)
}
defer file.Close()
content, err := writer.CreateFormFile("file", name)
if err != nil {
return nil, karma.Format(
@ -297,7 +375,7 @@ func getAttachmentPayload(name, comment, path string) (*form, error) {
)
}
_, err = io.Copy(content, file)
_, err = io.Copy(content, reader)
if err != nil {
return nil, karma.Format(
err,
@ -345,6 +423,7 @@ func (api *API) GetAttachments(pageID string) ([]AttachmentInfo, error) {
payload := map[string]string{
"expand": "version,container",
"limit": "1000",
}
request, err := api.rest.Res(
@ -354,7 +433,7 @@ func (api *API) GetAttachments(pageID string) ([]AttachmentInfo, error) {
return nil, err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
@ -377,7 +456,7 @@ func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
return nil, err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
@ -386,12 +465,13 @@ func (api *API) GetPageByID(pageID string) (*PageInfo, error) {
func (api *API) CreatePage(
space string,
pageType string,
parent *PageInfo,
title string,
body string,
) (*PageInfo, error) {
payload := map[string]interface{}{
"type": "page",
"type": pageType,
"title": title,
"space": map[string]interface{}{
"key": space,
@ -424,45 +504,66 @@ func (api *API) CreatePage(
return nil, err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*PageInfo), nil
}
func (api *API) UpdatePage(
page *PageInfo, newContent 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
oldAncestors := []map[string]interface{}{}
if len(page.Ancestors) == 0 {
return fmt.Errorf(
"page %q info does not contain any information about parents",
page.ID,
)
if page.Type != "blogpost" && len(page.Ancestors) > 0 {
// picking only the last one, which is required by confluence
oldAncestors = []map[string]interface{}{
{"id": page.Ancestors[len(page.Ancestors)-1].ID},
}
}
// picking only the last one, which is required by confluence
oldAncestors := []map[string]interface{}{
{"id": page.Ancestors[len(page.Ancestors)-1].Id},
properties := map[string]interface{}{
// Fix to set full-width as has changed on Confluence APIs again.
// https://jira.atlassian.com/browse/CONFCLOUD-65447
//
"content-appearance-published": map[string]interface{}{
"value": appearance,
},
// content-appearance-draft should not be set as this is impacted by
// the user editor default configurations - which caused the sporadic published widths.
}
if emojiString != "" {
r, _ := utf8.DecodeRuneInString(emojiString)
unicodeHex := fmt.Sprintf("%x", r)
properties["emoji-title-draft"] = map[string]interface{}{
"value": unicodeHex,
}
properties["emoji-title-published"] = map[string]interface{}{
"value": unicodeHex,
}
}
payload := map[string]interface{}{
"id": page.ID,
"type": "page",
"type": page.Type,
"title": page.Title,
"version": map[string]interface{}{
"number": nextPageVersion,
"minorEdit": false,
"minorEdit": minorEdit,
"message": versionMessage,
},
"ancestors": oldAncestors,
"body": map[string]interface{}{
"storage": map[string]interface{}{
"value": string(newContent),
"value": newContent,
"representation": "storage",
},
},
"metadata": map[string]interface{}{
"properties": properties,
},
}
request, err := api.rest.Res(
@ -472,13 +573,73 @@ func (api *API) UpdatePage(
return err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request)
}
return nil
}
func (api *API) AddPageLabels(page *PageInfo, newLabels []string) (*LabelInfo, error) {
labels := []map[string]interface{}{}
for _, label := range newLabels {
if label != "" {
item := map[string]interface{}{
"prefix": "global",
"name": label,
}
labels = append(labels, item)
}
}
payload := labels
request, err := api.rest.Res(
"content/"+page.ID+"/label", &LabelInfo{},
).Post(payload)
if err != nil {
return nil, err
}
if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*LabelInfo), nil
}
func (api *API) DeletePageLabel(page *PageInfo, label string) (*LabelInfo, error) {
request, err := api.rest.Res(
"content/"+page.ID+"/label", &LabelInfo{},
).SetQuery(map[string]string{"name": label}).Delete()
if err != nil {
return nil, err
}
if request.Raw.StatusCode != http.StatusOK && request.Raw.StatusCode != http.StatusNoContent {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*LabelInfo), nil
}
func (api *API) GetPageLabels(page *PageInfo, prefix string) (*LabelInfo, error) {
request, err := api.rest.Res(
"content/"+page.ID+"/label", &LabelInfo{},
).Get(map[string]string{"prefix": prefix})
if err != nil {
return nil, err
}
if request.Raw.StatusCode != http.StatusOK {
return nil, newErrorStatusNotOK(request)
}
return request.Response.(*LabelInfo), nil
}
func (api *API) GetUserByName(name string) (*User, error) {
var response struct {
Results []struct {
@ -486,6 +647,7 @@ func (api *API) GetUserByName(name string) (*User, error) {
}
}
// Try the new path first
_, err := api.rest.
Res("search").
Res("user", &response).
@ -496,7 +658,20 @@ func (api *API) GetUserByName(name string) (*User, error) {
return nil, err
}
// Try old path
if len(response.Results) == 0 {
_, err := api.rest.
Res("search", &response).
Get(map[string]string{
"cql": fmt.Sprintf("user.fullname~%q", name),
})
if err != nil {
return nil, err
}
}
if len(response.Results) == 0 {
return nil, karma.
Describe("name", name).
Reason(
@ -553,7 +728,7 @@ func (api *API) RestrictPageUpdatesCloud(
return err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request)
}
@ -574,15 +749,17 @@ func (api *API) RestrictPageUpdatesServer(
).Post([]interface{}{
page.ID,
"Edit",
map[string]interface{}{
"userName": allowedUser,
[]map[string]interface{}{
{
"userName": allowedUser,
},
},
})
if err != nil {
return err
}
if request.Raw.StatusCode != 200 {
if request.Raw.StatusCode != http.StatusOK {
return newErrorStatusNotOK(request)
}
@ -602,7 +779,7 @@ func (api *API) RestrictPageUpdates(
) error {
var err error
if strings.HasSuffix(api.rest.Api.BaseUrl.Host, "atlassian.net") {
if strings.HasSuffix(api.rest.Api.BaseUrl.Host, "jira.com") || strings.HasSuffix(api.rest.Api.BaseUrl.Host, "atlassian.net") {
err = api.RestrictPageUpdatesCloud(page, allowedUser)
} else {
err = api.RestrictPageUpdatesServer(page, allowedUser)
@ -612,24 +789,26 @@ func (api *API) RestrictPageUpdates(
}
func newErrorStatusNotOK(request *gopencils.Resource) error {
if request.Raw.StatusCode == 401 {
if request.Raw.StatusCode == http.StatusUnauthorized {
return errors.New(
"Confluence API returned unexpected status: 401 (Unauthorized)",
"the Confluence API returned unexpected status: 401 (Unauthorized)",
)
}
if request.Raw.StatusCode == 404 {
if request.Raw.StatusCode == http.StatusNotFound {
return errors.New(
"Confluence API returned unexpected status: 404 (Not Found)",
"the Confluence API returned unexpected status: 404 (Not Found)",
)
}
output, _ := ioutil.ReadAll(request.Raw.Body)
defer request.Raw.Body.Close()
output, _ := io.ReadAll(request.Raw.Body)
defer func() {
_ = request.Raw.Body.Close()
}()
return fmt.Errorf(
"Confluence API returned unexpected status: %v, "+
"output: %s",
"the Confluence API returned unexpected status: %v, "+
"output: %q",
request.Raw.Status, output,
)
}

107
d2/d2.go Normal file
View File

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

102
d2/d2_test.go Normal file
View 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: "58fa387384181445e2d8f90a8c7fda945cb75174f73e8b9853ff59b9e0103ddd",
ID: "",
Width: "198",
Height: "441",
},
assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessD2(tt.name, tt.markdown, tt.scale)
if !tt.wantErr(t, err, fmt.Sprintf("processD2(%v, %v)", tt.name, string(tt.markdown))) {
return
}
assert.Equal(t, tt.want.Filename, got.Filename, "processD2(%v, %v)", tt.name, string(tt.markdown))
// We only test for the header as png changes based on system png library
assert.Equal(t, tt.want.FileBytes, got.FileBytes[0:8], "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Name, got.Name, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Replace, got.Replace, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Checksum, got.Checksum, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.ID, got.ID, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Width, got.Width, "processD2(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Height, got.Height, "processD2(%v, %v)", tt.name, string(tt.markdown))
})
}
}

41
docker-compose.yaml Normal file
View File

@ -0,0 +1,41 @@
version: "3.5"
services:
markbuilder:
image: golang:latest
environment:
# Set them in your environment or .env
- GOOS=${GOOS?Missing GOOS.}
- GOARCH=${GOARCH?Missing GOARCH.}
# Example Values
# MacOS 64-bit
# - GOOS=darwin
# - GOARCH=amd64
# MacOS 32-bit
# - GOOS=darwin
# - GOARCH=386
# Linux 64-bit
# - GOOS=linux
# - GOARCH=amd64
# Linux 32-bit
# - GOOS=linux
# - GOARCH=386
# Windows 64-bit
# - GOOS=windows
# - GOARCH=amd64
# Windows 32-bit
# - GOOS=windows
# - GOARCH=386
volumes:
- type: bind
source: ./
target: /go/src/github.com/kovetskiy/mark
working_dir: /go/src/github.com/kovetskiy/mark/
command: make build

66
go.mod
View File

@ -1,20 +1,58 @@
module github.com/kovetskiy/mark
go 1.14
go 1.24.0
toolchain go1.24.2
require (
github.com/BurntSushi/toml v0.3.1 // indirect
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815
github.com/go-yaml/yaml v2.1.0+incompatible // indirect
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 // indirect
github.com/kovetskiy/gopencils v0.0.0-20201103141120-610929377f9b
github.com/kovetskiy/ko v0.0.0-20190324102900-26b8dd0988bf
github.com/kovetskiy/lorg v0.0.0-20200107130803-9a7136a95634
github.com/kovetskiy/toml v0.2.0 // indirect
github.com/reconquest/karma-go v0.0.0-20200326104714-79480464fdb5
github.com/reconquest/pkg v0.0.0-20201028091908-8e9a5e0226ef
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b
github.com/chromedp/chromedp v0.13.6
github.com/dreampuf/mermaid.go v0.0.27
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4
github.com/reconquest/karma-go v1.5.0
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4
github.com/russross/blackfriday v1.5.2
github.com/stretchr/testify v1.5.1 // indirect
gopkg.in/yaml.v2 v2.2.8
github.com/stretchr/testify v1.10.0
github.com/urfave/cli-altsrc/v3 v3.0.1
github.com/urfave/cli/v3 v3.3.3
github.com/yuin/goldmark v1.7.12
golang.org/x/tools v0.34.0
gopkg.in/yaml.v3 v3.0.1
oss.terrastruct.com/d2 v0.7.0
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a
)
require (
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/PuerkitoBio/goquery v1.10.0 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 // indirect
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mazznoer/csscolorparser v0.1.5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 // indirect
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
golang.org/x/image v0.20.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
gonum.org/v1/plot v0.14.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

209
go.sum
View File

@ -1,57 +1,168 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385/go.mod h1:0vRUJqYpeSZifjYj7uP3BG/gKcuzL9xWVV/Y+cK33KM=
github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334 h1:VHgatEHNcBFEB7inlalqfNqw65aNkM1lGX2yt3NmbS8=
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/kovetskiy/gopencils v0.0.0-20201103141120-610929377f9b h1:+PnJcuiUVcU3ixOvpvhyjswKPkxBVN+a2CaFCNNBfvw=
github.com/kovetskiy/gopencils v0.0.0-20201103141120-610929377f9b/go.mod h1:rn9YsgK4kxBDPZn+hOwSmg6MdtWfF2ejC3tvgDjWyBM=
github.com/kovetskiy/ko v0.0.0-20190324102900-26b8dd0988bf h1:4QsqgCcPoqDB91dcp4GffoV6TjwfVURaWpjKWFi0ae0=
github.com/kovetskiy/ko v0.0.0-20190324102900-26b8dd0988bf/go.mod h1:5RTDadc76NCMKavfnEcGrGVdoQ02h8dLHBUEN4h3xsM=
github.com/kovetskiy/lorg v0.0.0-20200107130803-9a7136a95634 h1:szpgh20EtHoQhJ38jrp7S2nlrhf56GSwa4de0hMfc2U=
github.com/kovetskiy/lorg v0.0.0-20200107130803-9a7136a95634/go.mod h1:B8HeKAukXULNzWWsW5k/SQyDkiQZPn7lTBJDB46MZ9I=
github.com/kovetskiy/toml v0.2.0 h1:tMsPGWE3ejTjXop10/17b/tDtbwQJZdBfc0e+l3WndA=
github.com/kovetskiy/toml v0.2.0/go.mod h1:+nh++V8wCesSlfPA3DSXGO1hiAHDVHDqem4ixTsWuRY=
git.sr.ht/~sbinet/gg v0.5.0 h1:6V43j30HM623V329xA9Ntq+WJrMjDxRjuAB1LFWF5m8=
git.sr.ht/~sbinet/gg v0.5.0/go.mod h1:G2C0eRESqlKhS7ErsNey6HHrqU1PwsnCQlekFi9Q2Oo=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/Shopify/toxiproxy/v2 v2.12.0 h1:d1x++lYZg/zijXPPcv7PH0MvHMzEI5aX/YuUi/Sw+yg=
github.com/Shopify/toxiproxy/v2 v2.12.0/go.mod h1:R9Z38Pw6k2cGZWXHe7tbxjGW9azmY1KbDQJ1kd+h7Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b h1:jJmiCljLNTaq/O1ju9Bzz2MPpFlmiTn0F7LwCoeDZVw=
github.com/chromedp/cdproto v0.0.0-20250403032234-65de8f5d025b/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.13.6 h1:xlNunMyzS5bu3r/QKrb3fzX6ow3WBQ6oao+J65PGZxk=
github.com/chromedp/chromedp v0.13.6/go.mod h1:h8GPP6ZtLMLsU8zFbTcb7ZDGCvCy8j/vRoFmRltQx9A=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2 h1:Ux9RXuPQmTB4C1MKagNLme0krvq8ulewfor+ORO/QL4=
github.com/dop251/goja v0.0.0-20240927123429-241b342198c2/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/dreampuf/mermaid.go v0.0.27 h1:uriWHpcc4clTaAUdJqpyDzyGvAZumeLb61n2VBxc0ZQ=
github.com/dreampuf/mermaid.go v0.0.27/go.mod h1:13PeW5y49ouLGlP3RdZm6ke+lQIcz3z7rdVoqRkt5hY=
github.com/go-fonts/liberation v0.3.1 h1:9RPT2NhUpxQ7ukUvz3jeUckmN42T9D9TpjtQcqK/ceM=
github.com/go-fonts/liberation v0.3.1/go.mod h1:jdJ+cqF+F4SUL2V+qxBth8fvBpBDS7yloUL5Fi8GTGY=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9 h1:NxXI5pTAtpEaU49bpLpQoDsu1zrteW/vxzTz8Cd2UAs=
github.com/go-latex/latex v0.0.0-20230307184459-12ec69307ad9/go.mod h1:gWuR/CrFDDeVRFQwHPvsv9soJVB/iqymhuZQuJ3a9OM=
github.com/go-pdf/fpdf v0.8.0 h1:IJKpdaagnWUeSkUFUjTcSzTppFxmv8ucGQyNPQWxYOQ=
github.com/go-pdf/fpdf v0.8.0/go.mod h1:gfqhcNwXrsd3XYKte9a7vM3smvU/jB4ZRDrmWSxpfdc=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/pprof v0.0.0-20240927180334-d43a67379298 h1:dMHbguTqGtorivvHTaOnbYp+tFzrw5M9gjkU4lCplgg=
github.com/google/pprof v0.0.0-20240927180334-d43a67379298/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a h1:OPt6gCghZXQ/WZpT6EhGkA7v+YMAYzcCb8SPQWmsb/8=
github.com/kovetskiy/gopencils v0.0.0-20250404051442-0b776066936a/go.mod h1:gRW37oDEg9LzOHApv31YzxKBICcCmPtDogaImsxZ6xc=
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4 h1:2eV8tF1u58dqRJMlFUD/Df26BxcIlGVy71rZHN+aNoI=
github.com/kovetskiy/lorg v1.2.1-0.20240830111423-ba4fe8b6f7c4/go.mod h1:p1RuSvyflTF/G4ubeATGurCRKWkULOrN/4PUAEFRq0s=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mazznoer/csscolorparser v0.1.5 h1:Wr4uNIE+pHWN3TqZn2SGpA2nLRG064gB7WdSfSS5cz4=
github.com/mazznoer/csscolorparser v0.1.5/go.mod h1:OQRVvgCyHDCAquR1YWfSwwaDcM0LhnSffGnlbOew/3I=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/reconquest/cog v0.0.0-20191208202052-266c2467b936 h1:jSaVCkKLAGc8VWBRVKk0Ffxrv/NKD1ixkOyjwPWrPd4=
github.com/reconquest/cog v0.0.0-20191208202052-266c2467b936/go.mod h1:IYiTfZ8/UKTz5svWOy+2ri5NuS+pJ3ynXMg8V0IHkXU=
github.com/reconquest/colorgful v0.0.0-20190805091748-28d18b838c4a h1:LGyNu9LpBpJ+puxKBLuB8L+YTBgW8xLmiBqbTKuniec=
github.com/reconquest/colorgful v0.0.0-20190805091748-28d18b838c4a/go.mod h1:S7SVqgAB8m04PAsywFMzl2UfDPGfBGRqpk3wWZG2y70=
github.com/reconquest/karma-go v0.0.0-20200326104714-79480464fdb5 h1:zDWjDur+l8W6pKksuc1VdKcdYrfHrTO9jRN131XoG1g=
github.com/reconquest/karma-go v0.0.0-20200326104714-79480464fdb5/go.mod h1:oTXKs9J7KQ1gCpnvSwCbH9vlvELZFfUSbEbrr2ABeo0=
github.com/reconquest/loreley v0.0.0-20200601121626-621c1cd37fd1 h1:J1vuEtEaaHo01+gxE6jIMtTwLrYzsraHmnqbNvha2Jw=
github.com/reconquest/loreley v0.0.0-20200601121626-621c1cd37fd1/go.mod h1:1NF/j951kWm+ZnRXpOkBqweImgwhlzFVwTA4A0V7TEU=
github.com/reconquest/pkg v0.0.0-20201028091908-8e9a5e0226ef h1:7Vr6ItE8C41xDgTNQqX3ir3gtbSIzub0XhKp3FW6Li8=
github.com/reconquest/pkg v0.0.0-20201028091908-8e9a5e0226ef/go.mod h1:T3ej/s+DtNaxXSOhM8rZX9bTlhnfHeETwQpK5PAPvwo=
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb h1:hJ1ExqE2lTMgTRmjmSiC2hm+sMXCCjjbyiGo3irbEW8=
github.com/reconquest/cog v0.0.0-20240830113510-c7ba12d0beeb/go.mod h1:n+lvvNLeoQmYVvYTFGCtLvoyD9Wz46RO3yCk6GKyZ/4=
github.com/reconquest/karma-go v1.5.0 h1:Chn4LtauwnvKfz13ZbmGNrRLKO1NciExHQSOBOsQqt4=
github.com/reconquest/karma-go v1.5.0/go.mod h1:52XRXXa2ec/VNrlCirwasdJfNmjI1O87q098gmqILh0=
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64 h1:OBNLiZay5PYLmGRXGIMEgWSIgbSjOj8nHZxqwLbSsF4=
github.com/reconquest/pkg v1.3.1-0.20240901105413-68c2adbf2b64/go.mod h1:r1Z1JNh3in9xLWbhv5u7cdox9vvGFjlKp89VI10Jrdo=
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4 h1:bcDXaTFC09IIg13Z8gfQHk4gSu001ET7ssW/wKRvPzg=
github.com/reconquest/regexputil-go v0.0.0-20160905154124-38573e70c1f4/go.mod h1:OI1di2iiFSwX3D70iZjzdmCPPfssjOl+HX40tI3VaXA=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
github.com/zazab/zhash v0.0.0-20170403032415-ad45b89afe7a h1:8gf6DUwu6F8Fh3rN8Ei9TM66KkWrNC04FP3HlcbxPuQ=
github.com/zazab/zhash v0.0.0-20170403032415-ad45b89afe7a/go.mod h1:P+yVThXQrjx7yGmgsdI4WQ/XDDmcyBMZzK1b39TXteA=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli-altsrc/v3 v3.0.1 h1:v+gHk59syLk8ao9rYybZs43+D5ut/gzj0omqQ1XYl8k=
github.com/urfave/cli-altsrc/v3 v3.0.1/go.mod h1:8UtsKKcxFVzvaoySFPfvQOk413T+IXJhaCWyyoPW3yM=
github.com/urfave/cli/v3 v3.3.3 h1:byCBaVdIXuLPIDm5CYZRVG6NvT7tv1ECqdU4YzlEa3I=
github.com/urfave/cli/v3 v3.3.3/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446 h1:75pcOSsb40+ub185cJI7g5uykl9Uu76rD5ONzK/4s40=
github.com/zazab/zhash v0.0.0-20221031090444-2b0d50417446/go.mod h1:NtepZ8TEXErPsmQDMUoN72f8aIy4+xNinSJ3f1giess=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/plot v0.14.0 h1:+LBDVFYwFe4LHhdP8coW6296MBEY4nQ+Y4vuUpJopcE=
gonum.org/v1/plot v0.14.0/go.mod h1:MLdR9424SJed+5VqC6MsouEpig9pZX2VZ57H9ko2bXU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
oss.terrastruct.com/d2 v0.7.0 h1:nFTap/RgAQtm1aAmUOOJxO8vgSCj3SLILcOkStnyHeI=
oss.terrastruct.com/d2 v0.7.0/go.mod h1:QseS95MrwfSRDJcFmVpBBIKuPIr8/RUoR3526QQ3rVk=
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a h1:UXF/Z9i9tOx/wqGUOn/T12wZeez1Gg0sAVKKl7YUDwM=
oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a/go.mod h1:eMWv0sOtD9T2RUl90DLWfuShZCYp4NrsqNpI8eqO6U4=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -3,25 +3,35 @@ package includes
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
// <!-- Include: <template path>
// <optional yaml data> -->
//
// (Delims: (none | "<left>","<right>"))?
// <optional yaml data> -->
var reIncludeDirective = regexp.MustCompile(
`(?s)<!--\s*Include:\s*(?P<template>\S+)\s*(\n(?P<config>.*?))?-->`)
`(?s)` +
`<!--\s*Include:\s*(?P<template>.+?)\s*` +
`(?:\n\s*Delims:\s*(?:(none|"(?P<left>.*?)"\s*,\s*"(?P<right>.*?)")))?\s*` +
`(?:\n(?P<config>.*?))?-->`,
)
func LoadTemplate(
base string,
includePath string,
path string,
left string,
right string,
templates *template.Template,
) (*template.Template, error) {
var (
@ -35,17 +45,28 @@ func LoadTemplate(
var body []byte
body, err := ioutil.ReadFile(path)
body, err := os.ReadFile(filepath.Join(base, path))
if err != nil {
err = facts.Format(
err,
"unable to read template file",
)
if includePath != "" {
body, err = os.ReadFile(filepath.Join(includePath, path))
}
if err != nil {
err = facts.Format(
err,
"unable to read template file",
)
return nil, err
}
return nil, err
}
templates, err = templates.New(name).Parse(string(body))
body = bytes.ReplaceAll(
body,
[]byte("\r\n"),
[]byte("\n"),
)
templates, err = templates.New(name).Delims(left, right).Parse(string(body))
if err != nil {
err = facts.Format(
err,
@ -59,6 +80,8 @@ func LoadTemplate(
}
func ProcessIncludes(
base string,
includePath string,
contents []byte,
templates *template.Template,
) (*template.Template, []byte, bool, error) {
@ -96,12 +119,21 @@ func ProcessIncludes(
groups := reIncludeDirective.FindSubmatch(spec)
var (
path, config = string(groups[1]), groups[2]
data = map[string]interface{}{}
path = string(groups[1])
delimsNone = string(groups[2])
left = string(groups[3])
right = string(groups[4])
config = groups[5]
data = map[string]interface{}{}
facts = karma.Describe("path", path)
)
if delimsNone == "none" {
left = "\x00"
right = "\x01"
}
err = yaml.Unmarshal(config, &data)
if err != nil {
err = facts.
@ -116,10 +148,9 @@ func ProcessIncludes(
log.Tracef(vardump(facts, data), "including template %q", path)
templates, err = LoadTemplate(path, templates)
templates, err = LoadTemplate(base, includePath, path, left, right, templates)
if err != nil {
err = facts.Format(err, "unable to load template")
return nil
}

View File

@ -7,11 +7,11 @@ import (
"strings"
"text/template"
"github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/includes"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"github.com/reconquest/regexputil-go"
"gopkg.in/yaml.v2"
"gopkg.in/yaml.v3"
)
var reMacroDirective = regexp.MustCompile(
@ -21,7 +21,7 @@ var reMacroDirective = regexp.MustCompile(
`(?s)` + // dot capture newlines
/**/ `<!--\s*Macro:\s*(?P<expr>[^\n]+)\n` +
/* */ `\s*Template:\s*(?P<template>\S+)\s*` +
/* */ `\s*Template:\s*(?P<template>.+?)\s*` +
/* */ `(?P<config>\n.*?)?-->`,
)
@ -105,6 +105,8 @@ func (macro *Macro) configure(node interface{}, groups [][]byte) interface{} {
}
func ExtractMacros(
base string,
includePath string,
contents []byte,
templates *template.Template,
) ([]Macro, []byte, error) {
@ -123,18 +125,55 @@ func ExtractMacros(
var (
expr = regexputil.Subexp(reMacroDirective, groups, "expr")
template = regexputil.Subexp(reMacroDirective, groups, "template")
config = regexputil.Subexp(reMacroDirective, groups, "config")
macro Macro
template = regexputil.Subexp(
reMacroDirective,
groups,
"template",
)
config = regexputil.Subexp(reMacroDirective, groups, "config")
)
macro.Template, err = includes.LoadTemplate(template, templates)
var macro Macro
if err != nil {
err = karma.Format(err, "unable to load template")
if strings.HasPrefix(template, "#") {
cfg := map[string]interface{}{}
return nil
err = yaml.Unmarshal([]byte(config), &cfg)
if err != nil {
err = karma.Format(
err,
"unable to unmarshal macros config template",
)
return nil
}
body, ok := cfg[template[1:]].(string)
if !ok {
err = fmt.Errorf(
"the template config doesn't have '%s' field",
template[1:],
)
return nil
}
macro.Template, err = templates.New(template).Parse(body)
if err != nil {
err = karma.Format(
err,
"unable to parse template",
)
return nil
}
} else {
macro.Template, err = includes.LoadTemplate(base, includePath, template, "{{", "}}", templates)
if err != nil {
err = karma.Format(err, "unable to load template")
return nil
}
}
facts := karma.

342
main.go
View File

@ -1,343 +1,33 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"context"
"os"
"path/filepath"
"github.com/docopt/docopt-go"
"github.com/kovetskiy/lorg"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark"
"github.com/kovetskiy/mark/pkg/mark/includes"
"github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/karma-go"
"github.com/kovetskiy/mark/util"
"github.com/reconquest/pkg/log"
"github.com/urfave/cli/v3"
)
const (
usage = `mark - tool for updating Atlassian Confluence pages from markdown.
This is very usable if you store documentation to your orthodox software in git
repository and don't want to do a handjob with updating Confluence page using
fucking tinymce wysiwyg enterprise core editor.
You can store a user credentials in the configuration file, which should be
located in ~/.config/mark with following format:
username = "smith"
password = "matrixishere"
base_url = "http://confluence.local"
where 'smith' it's your username, 'matrixishere' it's your password and
'http://confluence.local' is base URL for your Confluence instance.
Mark understands extended file format, which, still being valid markdown,
contains several metadata headers, which can be used to locate page inside
Confluence instance and update it accordingly.
File in extended format should follow specification:
<!-- Space: <space key> -->
<!-- Parent: <parent 1> -->
<!-- Parent: <parent 2> -->
<!-- Title: <title> -->
<page contents>
There can be any number of 'Parent' headers, if mark can't find specified
parent by title, it will be created.
Also, optional following headers are supported:
* <!-- Layout: (article|plain) -->
- (default) article: content will be put in narrow column for ease of
reading;
- plain: content will fill all page;
Mark supports Go templates, which can be included into article by using path
to the template relative to current working dir, e.g.:
<!-- Include: <path> -->
Templates may accept configuration data in YAML format which immediately
follows include tag:
<!-- Include: <path>
<yaml-data> -->
Mark also supports macro definitions, which are defined as regexps which will
be replaced with specified template:
<!-- Macro: <regexp>
Template: <path>
<yaml-data> -->
Capture groups can be defined in the macro's <regexp> which can be later
referenced in the <yaml-data> using ${<number>} syntax, where <number> is
number of a capture group in regexp (${0} is used for entire regexp match), for
example:
<!-- Macro: MYJIRA-\d+
Template: ac:jira:ticket
Ticket: ${0} -->
By default, mark provides several built-in templates and macros:
* template 'ac:status' to include badge-like text, which accepts following
parameters:
- Title: text to display in the badge
- Color: color to use as background/border for badge
- Grey
- Yellow
- Red
- Blue
- Subtle: specify to fill badge with background or not
- true
- false
See: https://confluence.atlassian.com/conf59/status-macro-792499207.html
* template 'ac:jira:ticket' to include JIRA ticket link. Parameters:
- Ticket: Jira ticket number like BUGS-123.
* macro '@{...}' to mention user by name specified in the braces.
Usage:
mark [options] [-u <username>] [-p <token>] [-k] [-l <url>] -f <file>
mark [options] [-u <username>] [-p <password>] [-k] [-b <url>] -f <file>
mark [options] [-u <username>] [-p <password>] [-k] [-n] -c <file>
mark -v | --version
mark -h | --help
Options:
-u <username> Use specified username for updating Confluence page.
-p <token> Use specified token for updating Confluence page.
-l <url> Edit specified Confluence page.
If -l is not specified, file should contain metadata (see
above).
-b --base-url <url> Base URL for Confluence.
Alternative option for base_url config field.
-f <file> Use specified markdown file for converting to html.
-k Lock page editing to current user only to prevent accidental
manual edits over Confluence Web UI.
--dry-run Resolve page and ancestry, show resulting HTML and exit.
--compile-only Show resulting HTML and don't update Confluence page content.
--debug Enable debug logs.
--trace Enable trace logs.
-h --help Show this screen and call 911.
-v --version Show version.
`
version = "13.0.0"
usage = "A tool for updating Atlassian Confluence pages from markdown."
description = `Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark`
)
func main() {
args, err := docopt.Parse(usage, nil, true, "3.3", false)
if err != nil {
panic(err)
cmd := &cli.Command{
Name: "mark",
Usage: usage,
Description: description,
Version: version,
Flags: util.Flags,
EnableShellCompletion: true,
HideHelpCommand: true,
Action: util.RunMark,
}
var (
targetFile, _ = args["-f"].(string)
compileOnly = args["--compile-only"].(bool)
dryRun = args["--dry-run"].(bool)
editLock = args["-k"].(bool)
)
if args["--debug"].(bool) {
log.SetLevel(lorg.LevelDebug)
}
if args["--trace"].(bool) {
log.SetLevel(lorg.LevelTrace)
}
config, err := LoadConfig(filepath.Join(os.Getenv("HOME"), ".config/mark"))
if err != nil {
if err := cmd.Run(context.TODO(), os.Args); err != nil {
log.Fatal(err)
}
creds, err := GetCredentials(args, config)
if err != nil {
log.Fatal(err)
}
api := confluence.NewAPI(creds.BaseURL, creds.Username, creds.Password)
markdown, err := ioutil.ReadFile(targetFile)
if err != nil {
log.Fatal(err)
}
meta, markdown, err := mark.ExtractMeta(markdown)
if err != nil {
log.Fatal(err)
}
stdlib, err := stdlib.New(api)
if err != nil {
log.Fatal(err)
}
templates := stdlib.Templates
var recurse bool
for {
templates, markdown, recurse, err = includes.ProcessIncludes(
markdown,
templates,
)
if err != nil {
log.Fatal(err)
}
if !recurse {
break
}
}
macros, markdown, err := macro.ExtractMacros(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)
}
}
if dryRun {
compileOnly = true
_, _, err := mark.ResolvePage(dryRun, api, meta)
if err != nil {
log.Fatalf(err, "unable to resolve page location")
}
}
if compileOnly {
fmt.Println(mark.CompileMarkdown(markdown, stdlib))
os.Exit(0)
}
if creds.PageID != "" && meta != nil {
log.Warning(
`specified file contains metadata, ` +
`but it will be ignored due specified command line URL`,
)
meta = nil
}
if creds.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`,
)
}
var target *confluence.PageInfo
if meta != nil {
parent, page, err := mark.ResolvePage(dryRun, api, meta)
if err != nil {
log.Fatalf(
karma.Describe("title", meta.Title).Reason(err),
"unable to resolve page",
)
}
if page == nil {
page, err = api.CreatePage(meta.Space, parent, meta.Title, ``)
if err != nil {
log.Fatalf(
err,
"can't create page %q",
meta.Title,
)
}
}
target = page
} else {
if creds.PageID == "" {
log.Fatalf(nil, "URL should provide 'pageId' GET-parameter")
}
page, err := api.GetPageByID(creds.PageID)
if err != nil {
log.Fatalf(err, "unable to retrieve page by id")
}
target = page
}
attaches, err := mark.ResolveAttachments(api, target, ".", meta.Attachments)
if err != nil {
log.Fatalf(err, "unable to create/update attachments")
}
markdown = mark.CompileAttachmentLinks(markdown, attaches)
html := mark.CompileMarkdown(markdown, stdlib)
{
var buffer bytes.Buffer
err := stdlib.Templates.ExecuteTemplate(
&buffer,
"ac:layout",
struct {
Layout string
Body string
}{
Layout: meta.Layout,
Body: html,
},
)
if err != nil {
log.Fatal(err)
}
html = buffer.String()
}
err = api.UpdatePage(target, html)
if err != nil {
log.Fatal(err)
}
if editLock {
log.Infof(
nil,
`edit locked on page %q by user %q to prevent manual edits`,
target.Title,
creds.Username,
)
err := api.RestrictPageUpdates(
target,
creds.Username,
)
if err != nil {
log.Fatal(err)
}
}
log.Infof(
nil,
"page successfully updated: %s",
creds.BaseURL+target.Links.Full,
)
fmt.Println(
creds.BaseURL + target.Links.Full,
)
}

51
main_test.go Normal file
View 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())
}
})
}
}

103
markdown/markdown.go Normal file
View File

@ -0,0 +1,103 @@
package mark
import (
"bytes"
"github.com/kovetskiy/mark/attachment"
cparser "github.com/kovetskiy/mark/parser"
crenderer "github.com/kovetskiy/mark/renderer"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/reconquest/pkg/log"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// Renderer renders anchor [Node]s.
type ConfluenceExtension struct {
html.Config
Stdlib *stdlib.Lib
Path string
MarkConfig types.MarkConfig
Attachments []attachment.Attachment
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceExtension(stdlib *stdlib.Lib, path string, cfg types.MarkConfig) *ConfluenceExtension {
return &ConfluenceExtension{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
MarkConfig: cfg,
Attachments: []attachment.Attachment{},
}
}
func (c *ConfluenceExtension) Attach(a attachment.Attachment) {
c.Attachments = append(c.Attachments, a)
}
func (c *ConfluenceExtension) Extend(m goldmark.Markdown) {
m.Renderer().AddOptions(renderer.WithNodeRenderers(
util.Prioritized(crenderer.NewConfluenceTextRenderer(c.MarkConfig.StripNewlines), 100),
util.Prioritized(crenderer.NewConfluenceBlockQuoteRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceCodeBlockRenderer(c.Stdlib, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceFencedCodeBlockRenderer(c.Stdlib, c, c.MarkConfig), 100),
util.Prioritized(crenderer.NewConfluenceHTMLBlockRenderer(c.Stdlib), 100),
util.Prioritized(crenderer.NewConfluenceHeadingRenderer(c.MarkConfig.DropFirstH1), 100),
util.Prioritized(crenderer.NewConfluenceImageRenderer(c.Stdlib, c, c.Path), 100),
util.Prioritized(crenderer.NewConfluenceParagraphRenderer(), 100),
util.Prioritized(crenderer.NewConfluenceLinkRenderer(), 100),
))
m.Parser().AddOptions(parser.WithInlineParsers(
// Must be registered with a higher priority than goldmark's linkParser to make sure goldmark doesn't parse
// the <ac:*/> tags.
util.Prioritized(cparser.NewConfluenceTagParser(), 199),
))
}
func CompileMarkdown(markdown []byte, stdlib *stdlib.Lib, path string, cfg types.MarkConfig) (string, []attachment.Attachment) {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
confluenceExtension := NewConfluenceExtension(stdlib, path, cfg)
converter := goldmark.New(
goldmark.WithExtensions(
extension.Footnote,
extension.DefinitionList,
extension.NewTable(
extension.WithTableCellAlignMethod(extension.TableCellAlignStyle),
),
confluenceExtension,
extension.GFM,
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithUnsafe(),
html.WithXHTML(),
))
ctx := parser.NewContext(parser.WithIDs(&cparser.ConfluenceIDs{Values: map[string]bool{}}))
var buf bytes.Buffer
err := converter.Convert(markdown, &buf, parser.WithContext(ctx))
if err != nil {
panic(err)
}
html := buf.Bytes()
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
return string(html), confluenceExtension.Attachments
}

183
markdown/markdown_test.go Normal file
View File

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

55
mermaid/mermaid.go Normal file
View File

@ -0,0 +1,55 @@
package mermaid
import (
"bytes"
"context"
"strconv"
"time"
mermaid "github.com/dreampuf/mermaid.go"
"github.com/kovetskiy/mark/attachment"
"github.com/reconquest/pkg/log"
)
var renderTimeout = 120 * time.Second
func ProcessMermaidLocally(title string, mermaidDiagram []byte, scale float64) (attachment.Attachment, error) {
ctx, cancel := context.WithTimeout(context.TODO(), renderTimeout)
defer cancel()
log.Debugf(nil, "Setting up Mermaid renderer: %q", title)
renderer, err := mermaid.NewRenderEngine(ctx)
if err != nil {
return attachment.Attachment{}, err
}
log.Debugf(nil, "Rendering: %q", title)
pngBytes, boxModel, err := renderer.RenderAsScaledPng(string(mermaidDiagram), scale)
if err != nil {
return attachment.Attachment{}, err
}
checkSum, err := attachment.GetChecksum(bytes.NewReader(mermaidDiagram))
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
}

49
mermaid/mermaid_test.go Normal file
View File

@ -0,0 +1,49 @@
package mermaid
import (
"fmt"
"testing"
"github.com/kovetskiy/mark/attachment"
"github.com/stretchr/testify/assert"
)
func TestExtractMermaidImage(t *testing.T) {
tests := []struct {
name string
markdown []byte
scale float64
want attachment.Attachment
wantErr assert.ErrorAssertionFunc
}{
{"example", []byte("graph TD;\n A-->B;"), 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: "1743a4f31ab66244591f06c8056e08053b8e0a554eb9a38709af6e9d145ac84f",
ID: "",
Width: "87",
Height: "174",
},
assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ProcessMermaidLocally(tt.name, tt.markdown, tt.scale)
if !tt.wantErr(t, err, fmt.Sprintf("processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))) {
return
}
assert.Equal(t, tt.want.Filename, got.Filename, "processMermaidLocally(%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], "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Name, got.Name, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Replace, got.Replace, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Checksum, got.Checksum, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.ID, got.ID, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Width, got.Width, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
assert.Equal(t, tt.want.Height, got.Height, "processMermaidLocally(%v, %v)", tt.name, string(tt.markdown))
})
}
}

199
metadata/metadata.go Normal file
View File

@ -0,0 +1,199 @@
package metadata
import (
"bufio"
"bytes"
"crypto/sha256"
"fmt"
"regexp"
"strings"
"github.com/reconquest/pkg/log"
)
const (
HeaderParent = `Parent`
HeaderSpace = `Space`
HeaderType = `Type`
HeaderTitle = `Title`
HeaderLayout = `Layout`
HeaderEmoji = `Emoji`
HeaderAttachment = `Attachment`
HeaderLabel = `Label`
HeaderInclude = `Include`
HeaderSidebar = `Sidebar`
ContentAppearance = `Content-Appearance`
)
type Meta struct {
Parents []string
Space string
Type string
Title string
Layout string
Sidebar string
Emoji string
Attachments []string
Labels []string
ContentAppearance string
}
const (
FullWidthContentAppearance = "full-width"
FixedContentAppearance = "fixed"
)
var (
reHeaderPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`)
reHeaderPatternMacro = regexp.MustCompile(`<!-- Macro: .*`)
)
func ExtractMeta(data []byte, spaceFromCli string, titleFromH1 bool, parents []string, titleAppendGeneratedHash bool) (*Meta, []byte, error) {
var (
meta *Meta
offset int
)
scanner := bufio.NewScanner(bytes.NewBuffer(data))
for scanner.Scan() {
line := scanner.Text()
if err := scanner.Err(); err != nil {
return nil, nil, err
}
offset += len(line) + 1
matches := reHeaderPatternV2.FindStringSubmatch(line)
if matches == nil {
matches = reHeaderPatternMacro.FindStringSubmatch(line)
// If we have a match, then we started reading a macro.
// We want to keep it in the document for it to be read by ExtractMacros
if matches != nil {
offset -= len(line) + 1
}
break
}
if meta == nil {
meta = &Meta{}
meta.Type = "page" // Default if not specified
meta.ContentAppearance = FullWidthContentAppearance // Default to full-width for backwards compatibility
}
//nolint:staticcheck
header := strings.Title(matches[1])
var value string
if len(matches) > 1 {
value = strings.TrimSpace(matches[2])
}
switch header {
case HeaderParent:
meta.Parents = append(meta.Parents, value)
case HeaderSpace:
meta.Space = strings.TrimSpace(value)
case HeaderType:
meta.Type = strings.TrimSpace(value)
case HeaderTitle:
meta.Title = strings.TrimSpace(value)
case HeaderLayout:
meta.Layout = strings.TrimSpace(value)
case HeaderSidebar:
meta.Layout = "article"
meta.Sidebar = strings.TrimSpace(value)
case HeaderEmoji:
meta.Emoji = strings.TrimSpace(value)
case HeaderAttachment:
meta.Attachments = append(meta.Attachments, value)
case HeaderLabel:
meta.Labels = append(meta.Labels, value)
case HeaderInclude:
// Includes are parsed by a different func
continue
case ContentAppearance:
if strings.TrimSpace(value) == FixedContentAppearance {
meta.ContentAppearance = FixedContentAppearance
} else {
meta.ContentAppearance = FullWidthContentAppearance
}
default:
log.Errorf(
nil,
`encountered unknown header %q line: %#v`,
header,
line,
)
continue
}
}
if titleFromH1 || spaceFromCli != "" {
if meta == nil {
meta = &Meta{}
}
if meta.Type == "" {
meta.Type = "page"
}
if meta.ContentAppearance == "" {
meta.ContentAppearance = FullWidthContentAppearance // Default to full-width for backwards compatibility
}
if titleFromH1 && meta.Title == "" {
meta.Title = ExtractDocumentLeadingH1(data)
}
if spaceFromCli != "" && meta.Space == "" {
meta.Space = spaceFromCli
}
}
if meta == nil {
return nil, data, nil
}
// Prepend parent pages that are defined via the cli flag
if len(parents) > 0 && parents[0] != "" {
meta.Parents = append(parents, meta.Parents...)
}
// deterministically generate a hash from the page's parents, space, and title
if titleAppendGeneratedHash {
path := strings.Join(append(meta.Parents, meta.Space, meta.Title), "/")
pathHash := sha256.Sum256([]byte(path))
// postfix is an 8-character hexadecimal string representation of the first 4 out of 32 bytes of the hash
meta.Title = fmt.Sprintf("%s - %x", meta.Title, pathHash[0:4])
log.Debugf(
nil,
"appended hash to page title: %s",
meta.Title,
)
}
return meta, data[offset:], nil
}
// ExtractDocumentLeadingH1 will extract leading H1 heading
func ExtractDocumentLeadingH1(markdown []byte) string {
h1 := regexp.MustCompile(`#[^#]\s*(.*)\s*\n`)
groups := h1.FindSubmatch(markdown)
if groups == nil {
return ""
} else {
return string(groups[1])
}
}

30
metadata/metadata_test.go Normal file
View File

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

View File

@ -1,10 +1,10 @@
package mark
package page
import (
"fmt"
"strings"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
@ -20,7 +20,7 @@ func EnsureAncestry(
rest := ancestry
for i, title := range ancestry {
page, err := api.FindPage(space, title)
page, err := api.FindPage(space, title, "page")
if err != nil {
return nil, karma.Format(
err,
@ -66,7 +66,7 @@ func EnsureAncestry(
if !dryRun {
for _, title := range rest {
page, err := api.CreatePage(space, parent, title, ``)
page, err := api.CreatePage(space, "page", parent, title, ``)
if err != nil {
return nil, karma.Format(
err,
@ -95,7 +95,7 @@ func ValidateAncestry(
space string,
ancestry []string,
) (*confluence.PageInfo, error) {
page, err := api.FindPage(space, ancestry[len(ancestry)-1])
page, err := api.FindPage(space, ancestry[len(ancestry)-1], "page")
if err != nil {
return nil, err
}
@ -104,23 +104,62 @@ func ValidateAncestry(
return nil, nil
}
isHomepage := false
if len(page.Ancestors) < 1 {
return nil, fmt.Errorf(`page %q has no parents`, page.Title)
homepage, err := api.FindHomePage(space)
if err != nil {
return nil, karma.Format(
err,
"can't obtain home page from space %q",
space,
)
}
if page.ID == homepage.ID {
log.Debugf(nil, "page is homepage for space %q", space)
isHomepage = true
} else {
return nil, fmt.Errorf(`page %q has no parents`, page.Title)
}
}
if len(page.Ancestors) < len(ancestry) {
return nil, fmt.Errorf(
"page %q has fewer parents than specified: %s",
page.Title,
strings.Join(ancestry, ` > `),
)
if !isHomepage && len(page.Ancestors) < len(ancestry) {
actual := []string{}
for _, ancestor := range page.Ancestors {
actual = append(actual, ancestor.Title)
}
valid := false
if len(actual) == len(ancestry)-1 {
broken := false
for i := 0; i < len(actual); i++ {
if actual[i] != ancestry[i] {
broken = true
break
}
}
if !broken {
if ancestry[len(ancestry)-1] == page.Title {
valid = true
}
}
}
if !valid {
return nil, karma.Describe("title", page.Title).
Describe("actual", strings.Join(actual, " > ")).
Describe("expected", strings.Join(ancestry, " > ")).
Format(nil, "the page has fewer parents than expected")
}
}
for _, parent := range ancestry[:len(ancestry)-1] {
found := false
// skipping root article title
for _, ancestor := range page.Ancestors[1:] {
for _, ancestor := range page.Ancestors {
if ancestor.Title == parent {
found = true
break
@ -130,7 +169,7 @@ func ValidateAncestry(
if !found {
list := []string{}
for _, ancestor := range page.Ancestors[1:] {
for _, ancestor := range page.Ancestors {
list = append(list, ancestor.Title)
}

220
page/link.go Normal file
View File

@ -0,0 +1,220 @@
package page
import (
"bytes"
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/metadata"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
"golang.org/x/tools/godoc/util"
)
type LinkSubstitution struct {
From string
To string
}
type markdownLink struct {
full string
filename string
hash string
}
func ResolveRelativeLinks(
api *confluence.API,
meta *metadata.Meta,
markdown []byte,
base string,
spaceFromCli string,
titleFromH1 bool,
parents []string,
titleAppendGeneratedHash bool,
) ([]LinkSubstitution, error) {
matches := parseLinks(string(markdown))
links := []LinkSubstitution{}
for _, match := range matches {
log.Tracef(
nil,
"found a relative link: full=%s filename=%s hash=%s",
match.full,
match.filename,
match.hash,
)
resolved, err := resolveLink(api, base, match, spaceFromCli, titleFromH1, parents, titleAppendGeneratedHash)
if err != nil {
return nil, karma.Format(err, "resolve link: %q", match.full)
}
if resolved == "" {
continue
}
links = append(links, LinkSubstitution{
From: match.full,
To: resolved,
})
}
return links, nil
}
func resolveLink(
api *confluence.API,
base string,
link markdownLink,
spaceFromCli string,
titleFromH1 bool,
parents []string,
titleAppendGeneratedHash bool,
) (string, error) {
var result string
if len(link.filename) > 0 {
filepath := filepath.Join(base, link.filename)
log.Tracef(nil, "filepath: %s", filepath)
stat, err := os.Stat(filepath)
if err != nil {
return "", nil
}
if stat.IsDir() {
return "", nil
}
linkContents, err := os.ReadFile(filepath)
if !util.IsText(linkContents) {
return "", nil
}
if err != nil {
return "", karma.Format(err, "read file: %s", filepath)
}
linkContents = bytes.ReplaceAll(
linkContents,
[]byte("\r\n"),
[]byte("\n"),
)
// This helps to determine if found link points to file that's
// not markdown or have mark required metadata
linkMeta, _, err := metadata.ExtractMeta(linkContents, spaceFromCli, titleFromH1, parents, titleAppendGeneratedHash)
if err != nil {
log.Errorf(
err,
"unable to extract metadata from %q; ignoring the relative link",
filepath,
)
return "", nil
}
if linkMeta == nil {
return "", nil
}
log.Tracef(
nil,
"extracted metadata: space=%s title=%s",
linkMeta.Space,
linkMeta.Title,
)
result, err = getConfluenceLink(api, linkMeta.Space, linkMeta.Title)
if err != nil {
return "", karma.Format(
err,
"find confluence page: %s / %s / %s",
filepath,
linkMeta.Space,
linkMeta.Title,
)
}
if result == "" {
return "", nil
}
}
if len(link.hash) > 0 {
result = result + "#" + link.hash
}
return result, nil
}
func SubstituteLinks(markdown []byte, links []LinkSubstitution) []byte {
for _, link := range links {
if link.From == link.To {
continue
}
log.Tracef(nil, "substitute link: %q -> %q", link.From, link.To)
markdown = bytes.ReplaceAll(
markdown,
[]byte(fmt.Sprintf("](%s)", link.From)),
[]byte(fmt.Sprintf("](%s)", link.To)),
)
}
return markdown
}
func parseLinks(markdown string) []markdownLink {
// Matches links but not inline images
re := regexp.MustCompile(`[^\!]\[.+\]\((([^\)#]+)?#?([^\)]+)?)\)`)
matches := re.FindAllStringSubmatch(markdown, -1)
links := make([]markdownLink, len(matches))
for i, match := range matches {
links[i] = markdownLink{
full: match[1],
filename: match[2],
hash: match[3],
}
}
return links
}
// getConfluenceLink build (to be) link for Confluence, and tries to verify from
// API if there's real link available
func getConfluenceLink(
api *confluence.API,
space, title string,
) (string, error) {
link := fmt.Sprintf(
"%s/display/%s/%s",
api.BaseURL,
space,
url.QueryEscape(title),
)
page, err := api.FindPage(space, title, "page")
if err != nil {
return "", karma.Format(err, "api: find page")
}
if page != nil {
link = api.BaseURL + page.Links.Full
}
linkUrl, err := url.Parse(link)
if err != nil {
return "", karma.Format(err, "parse URL: %s", link)
}
// Confluence supports relative links to reference other pages:
// https://confluence.atlassian.com/doc/links-776656293.html
linkPath := linkUrl.Path
return linkPath, nil
}

53
page/link_test.go Normal file
View File

@ -0,0 +1,53 @@
package page
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseLinks(t *testing.T) {
markdown := `
[example1](../path/to/example.md#second-heading)
[example2](../path/to/example.md)
[example3](#heading-in-document)
[Text link that should be put as attachment](../path/to/example.txt)
[Image link that should be put as attachment](../path/to/example.png)
[relative link without dots](relative-link-without-dots.md)
[relative link without dots but with hash](relative-link-without-dots-but-with-hash.md#hash)
[example [example]](example.md)
`
links := parseLinks(markdown)
assert.Equal(t, "../path/to/example.md#second-heading", links[0].full)
assert.Equal(t, "../path/to/example.md", links[0].filename)
assert.Equal(t, "second-heading", links[0].hash)
assert.Equal(t, "../path/to/example.md", links[1].full)
assert.Equal(t, "../path/to/example.md", links[1].filename)
assert.Equal(t, "", links[1].hash)
assert.Equal(t, "#heading-in-document", links[2].full)
assert.Equal(t, "", links[2].filename)
assert.Equal(t, "heading-in-document", links[2].hash)
assert.Equal(t, "../path/to/example.txt", links[3].full)
assert.Equal(t, "../path/to/example.txt", links[3].filename)
assert.Equal(t, "", links[3].hash)
assert.Equal(t, "../path/to/example.png", links[4].full)
assert.Equal(t, "../path/to/example.png", links[4].filename)
assert.Equal(t, "", links[4].hash)
assert.Equal(t, "relative-link-without-dots.md", links[5].full)
assert.Equal(t, "relative-link-without-dots.md", links[5].filename)
assert.Equal(t, "", links[5].hash)
assert.Equal(t, "relative-link-without-dots-but-with-hash.md#hash", links[6].full)
assert.Equal(t, "relative-link-without-dots-but-with-hash.md", links[6].filename)
assert.Equal(t, "hash", links[6].hash)
assert.Equal(t, "example.md", links[7].full)
assert.Equal(t, len(links), 8)
}

View File

@ -1,9 +1,10 @@
package mark
package page
import (
"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/pkg/log"
)
@ -11,9 +12,9 @@ import (
func ResolvePage(
dryRun bool,
api *confluence.API,
meta *Meta,
meta *metadata.Meta,
) (*confluence.PageInfo, *confluence.PageInfo, error) {
page, err := api.FindPage(meta.Space, meta.Title)
page, err := api.FindPage(meta.Space, meta.Title, meta.Type)
if err != nil {
return nil, nil, karma.Format(
err,
@ -22,8 +23,35 @@ func ResolvePage(
)
}
if meta.Type == "blogpost" {
log.Infof(
nil,
"blog post will be stored as: %s",
meta.Title,
)
return nil, page, nil
}
// check to see if home page is in Parents
homepage, err := api.FindHomePage(meta.Space)
if err != nil {
return nil, nil, karma.Format(
err,
"can't obtain home page from space %q",
meta.Space,
)
}
skipHomeAncestry := false
if len(meta.Parents) > 0 {
if homepage.Title == meta.Parents[0] {
skipHomeAncestry = true
}
}
ancestry := meta.Parents
if page != nil {
if page != nil && !skipHomeAncestry {
ancestry = append(ancestry, page.Title)
}

55
parser/confluenceids.go Normal file
View 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
}

114
parser/confluencetags.go Normal file
View File

@ -0,0 +1,114 @@
package parser
import (
"bytes"
"regexp"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
// NewConfluenceTagParser returns an inline parser that parses <ac:* /> and <ri:* /> tags to ensure that Confluence specific tags are parsed
// as ast.KindRawHtml so they are not escaped at render time. The parser must be registered with a higher priority
// than goldmark's linkParser. Otherwise, the linkParser would parse the <ac:* /> tags.
func NewConfluenceTagParser() parser.InlineParser {
return &confluenceTagParser{}
}
var _ parser.InlineParser = (*confluenceTagParser)(nil)
// confluenceTagParser is a stripped down version of goldmark's rawHTMLParser.
// See: https://github.com/yuin/goldmark/blob/master/parser/raw_html.go
type confluenceTagParser struct {
}
func (s *confluenceTagParser) Trigger() []byte {
return []byte{'<'}
}
func (s *confluenceTagParser) Parse(_ ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
if len(line) > 1 && util.IsAlphaNumeric(line[1]) {
return s.parseMultiLineRegexp(openTagRegexp, block, pc)
}
if len(line) > 2 && line[1] == '/' && util.IsAlphaNumeric(line[2]) {
return s.parseMultiLineRegexp(closeTagRegexp, block, pc)
}
if len(line) > 2 && line[1] == '!' && line[2] >= 'A' && line[2] <= 'Z' {
return s.parseUntil(block, closeDecl, pc)
}
if bytes.HasPrefix(line, openCDATA) {
return s.parseUntil(block, closeCDATA, pc)
}
return nil
}
var tagnamePattern = `([A-Za-z][A-Za-z0-9-]*)`
var spaceOrOneNewline = `(?:[ \t]|(?:\r\n|\n){0,1})`
var attributePattern = `(?:[\r\n \t]+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:[\r\n \t]*=[\r\n \t]*(?:[^\"'=<>` + "`" + `\x00-\x20]+|'[^']*'|"[^"]*"))?)`
// Only match <ac:*/> and <ri:*/> tags
var openTagRegexp = regexp.MustCompile("^<(ac|ri):" + tagnamePattern + attributePattern + `*` + spaceOrOneNewline + `*/?>`)
var closeTagRegexp = regexp.MustCompile("^</ac:" + tagnamePattern + spaceOrOneNewline + `*>`)
var openCDATA = []byte("<![CDATA[")
var closeCDATA = []byte("]]>")
var closeDecl = []byte(">")
func (s *confluenceTagParser) parseUntil(block text.Reader, closer []byte, _ parser.Context) ast.Node {
savedLine, savedSegment := block.Position()
node := ast.NewRawHTML()
for {
line, segment := block.PeekLine()
if line == nil {
break
}
index := bytes.Index(line, closer)
if index > -1 {
node.Segments.Append(segment.WithStop(segment.Start + index + len(closer)))
block.Advance(index + len(closer))
return node
}
node.Segments.Append(segment)
block.AdvanceLine()
}
block.SetPosition(savedLine, savedSegment)
return nil
}
func (s *confluenceTagParser) parseMultiLineRegexp(reg *regexp.Regexp, block text.Reader, _ parser.Context) ast.Node {
sline, ssegment := block.Position()
if block.Match(reg) {
node := ast.NewRawHTML()
eline, esegment := block.Position()
block.SetPosition(sline, ssegment)
for {
line, segment := block.PeekLine()
if line == nil {
break
}
l, _ := block.Position()
start := segment.Start
if l == sline {
start = ssegment.Start
}
end := segment.Stop
if l == eline {
end = esegment.Start
}
node.Segments.Append(text.NewSegment(start, end))
if l == eline {
block.Advance(end - start)
break
} else {
block.AdvanceLine()
}
}
return node
}
return nil
}

View File

@ -1,244 +0,0 @@
package mark
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strings"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/reconquest/karma-go"
"github.com/reconquest/pkg/log"
)
const (
AttachmentChecksumPrefix = `mark:checksum: `
)
type Attachment struct {
ID string
Name string
Filename string
Path string
Checksum string
Link string
Replace string
}
func ResolveAttachments(
api *confluence.API,
page *confluence.PageInfo,
base string,
replacements map[string]string,
) ([]Attachment, error) {
attaches := []Attachment{}
for replace, name := range replacements {
attach := Attachment{
Name: name,
Filename: strings.ReplaceAll(name, "/", "_"),
Path: filepath.Join(base, name),
Replace: replace,
}
checksum, err := getChecksum(attach.Path)
if err != nil {
return nil, karma.Format(
err,
"unable to get checksum for attachment: %q", attach.Name,
)
}
attach.Checksum = checksum
attaches = append(attaches, attach)
}
remotes, err := api.GetAttachments(page.ID)
if err != nil {
panic(err)
}
existing := []Attachment{}
creating := []Attachment{}
updating := []Attachment{}
for _, attach := range attaches {
var found bool
var same bool
for _, remote := range remotes {
if remote.Filename == attach.Filename {
same = attach.Checksum == strings.TrimPrefix(
remote.Metadata.Comment,
AttachmentChecksumPrefix,
)
attach.ID = remote.ID
attach.Link = path.Join(
remote.Links.Context,
remote.Links.Download,
)
found = true
break
}
}
if found {
if same {
existing = append(existing, attach)
} else {
updating = append(updating, attach)
}
} else {
creating = append(creating, attach)
}
}
for i, attach := range creating {
log.Infof(nil, "creating attachment: %q", attach.Name)
info, err := api.CreateAttachment(
page.ID,
attach.Filename,
AttachmentChecksumPrefix+attach.Checksum,
attach.Path,
)
if err != nil {
return nil, karma.Format(
err,
"unable to create attachment %q",
attach.Name,
)
}
attach.ID = info.ID
attach.Link = path.Join(
info.Links.Context,
info.Links.Download,
)
creating[i] = attach
}
for i, attach := range updating {
log.Infof(nil, "updating attachment: %q", attach.Name)
info, err := api.UpdateAttachment(
page.ID,
attach.ID,
attach.Name,
AttachmentChecksumPrefix+attach.Checksum,
attach.Path,
)
if err != nil {
return nil, karma.Format(
err,
"unable to update attachment %q",
attach.Name,
)
}
attach.Link = path.Join(
info.Links.Context,
info.Links.Download,
)
updating[i] = attach
}
attaches = []Attachment{}
attaches = append(attaches, existing...)
attaches = append(attaches, creating...)
attaches = append(attaches, updating...)
return attaches, nil
}
func CompileAttachmentLinks(markdown []byte, attaches []Attachment) []byte {
links := map[string]string{}
replaces := []string{}
for _, attach := range attaches {
uri, err := url.ParseRequestURI(attach.Link)
if err != nil {
links[attach.Replace] = strings.ReplaceAll("&", "&amp;", attach.Link)
} else {
links[attach.Replace] = uri.Path +
"?" + url.QueryEscape(uri.Query().Encode())
}
replaces = append(replaces, attach.Replace)
}
// sort by length so first items will have bigger length
// it's helpful for replacing in case of following names
// attachments/a.jpg
// attachments/a.jpg.jpg
// so we replace longer and then shorter
sort.SliceStable(replaces, func(i, j int) bool {
return len(replaces[i]) > len(replaces[j])
})
for _, replace := range replaces {
to := links[replace]
found := false
if bytes.Contains(markdown, []byte("attachment://"+replace)) {
from := "attachment://" + replace
log.Debugf(nil, "replacing legacy link: %q -> %q", from, to)
markdown = bytes.ReplaceAll(
markdown,
[]byte(from),
[]byte(to),
)
found = true
}
if bytes.Contains(markdown, []byte(replace)) {
from := replace
log.Debugf(nil, "replacing link: %q -> %q", from, to)
markdown = bytes.ReplaceAll(
markdown,
[]byte(from),
[]byte(to),
)
found = true
}
if !found {
log.Warningf(nil, "unused attachment: %s", replace)
}
}
return markdown
}
func getChecksum(filename string) (string, error) {
file, err := os.Open(filename)
if err != nil {
return "", karma.Format(
err,
"unable to open file",
)
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

View File

@ -1,92 +0,0 @@
package mark
import (
"bytes"
"regexp"
"github.com/kovetskiy/mark/pkg/mark/stdlib"
"github.com/reconquest/pkg/log"
"github.com/russross/blackfriday"
)
type ConfluenceRenderer struct {
blackfriday.Renderer
Stdlib *stdlib.Lib
}
func (renderer ConfluenceRenderer) BlockCode(
out *bytes.Buffer,
text []byte,
lang string,
) {
renderer.Stdlib.Templates.ExecuteTemplate(
out,
"ac:code",
struct {
Language string
Text string
}{
lang,
string(text),
},
)
}
// compileMarkdown will replace tags like <ac:rich-tech-body> with escaped
// equivalent, because blackfriday markdown parser replaces that tags with
// <a href="ac:rich-text-body">ac:rich-text-body</a> for whatever reason.
func CompileMarkdown(
markdown []byte,
stdlib *stdlib.Lib,
) string {
log.Tracef(nil, "rendering markdown:\n%s", string(markdown))
colon := regexp.MustCompile(`---BLACKFRIDAY-COLON---`)
tags := regexp.MustCompile(`<(/?\S+?):(\S+?)>`)
markdown = tags.ReplaceAll(
markdown,
[]byte(`<$1`+colon.String()+`$2>`),
)
renderer := ConfluenceRenderer{
Renderer: blackfriday.HtmlRenderer(
blackfriday.HTML_USE_XHTML|
blackfriday.HTML_USE_SMARTYPANTS|
blackfriday.HTML_SMARTYPANTS_FRACTIONS|
blackfriday.HTML_SMARTYPANTS_DASHES|
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES,
"", "",
),
Stdlib: stdlib,
}
html := blackfriday.MarkdownOptions(
markdown,
renderer,
blackfriday.Options{
Extensions: blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
blackfriday.EXTENSION_TABLES |
blackfriday.EXTENSION_FENCED_CODE |
blackfriday.EXTENSION_AUTOLINK |
blackfriday.EXTENSION_LAX_HTML_BLOCKS |
blackfriday.EXTENSION_STRIKETHROUGH |
blackfriday.EXTENSION_SPACE_HEADERS |
blackfriday.EXTENSION_HEADER_IDS |
blackfriday.EXTENSION_AUTO_HEADER_IDS |
blackfriday.EXTENSION_TITLEBLOCK |
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
blackfriday.EXTENSION_DEFINITION_LISTS |
blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK,
},
)
html = colon.ReplaceAll(html, []byte(`:`))
log.Tracef(nil, "rendered markdown to html:\n%s", string(html))
return string(html)
}

View File

@ -1,124 +0,0 @@
package mark
import (
"bufio"
"bytes"
"fmt"
"regexp"
"strings"
"github.com/reconquest/pkg/log"
)
const (
HeaderParent = `Parent`
HeaderSpace = `Space`
HeaderTitle = `Title`
HeaderLayout = `Layout`
HeaderAttachment = `Attachment`
)
type Meta struct {
Parents []string
Space string
Title string
Layout string
Attachments map[string]string
}
var (
reHeaderPatternV1 = regexp.MustCompile(`\[\]:\s*#\s*\(([^:]+):\s*(.*)\)`)
reHeaderPatternV2 = regexp.MustCompile(`<!--\s*([^:]+):\s*(.*)\s*-->`)
)
func ExtractMeta(data []byte) (*Meta, []byte, error) {
var (
meta *Meta
offset int
)
scanner := bufio.NewScanner(bytes.NewBuffer(data))
for scanner.Scan() {
line := scanner.Text()
if err := scanner.Err(); err != nil {
return nil, nil, err
}
offset += len(line) + 1
matches := reHeaderPatternV2.FindStringSubmatch(line)
if matches == nil {
matches = reHeaderPatternV1.FindStringSubmatch(line)
if matches == nil {
break
}
log.Warningf(
fmt.Errorf(`legacy header usage found: %s`, line),
"please use new header format: <!-- %s: %s -->",
matches[1],
matches[2],
)
}
if meta == nil {
meta = &Meta{}
meta.Attachments = make(map[string]string)
}
header := strings.Title(matches[1])
var value string
if len(matches) > 1 {
value = strings.TrimSpace(matches[2])
}
switch header {
case HeaderParent:
meta.Parents = append(meta.Parents, value)
case HeaderSpace:
meta.Space = strings.TrimSpace(value)
case HeaderTitle:
meta.Title = strings.TrimSpace(value)
case HeaderLayout:
meta.Layout = strings.TrimSpace(value)
case HeaderAttachment:
meta.Attachments[value] = value
default:
log.Errorf(
nil,
`encountered unknown header %q line: %#v`,
header,
line,
)
continue
}
}
if meta == nil {
return nil, data, nil
}
if meta.Space == "" {
return nil, nil, fmt.Errorf(
"space key is not set (%s header is not set)",
HeaderSpace,
)
}
if meta.Title == "" {
return nil, nil, fmt.Errorf(
"page title is not set (%s header is not set)",
HeaderTitle,
)
}
return meta, data[offset:], nil
}

View File

@ -1,166 +0,0 @@
package stdlib
import (
"strings"
"text/template"
"github.com/kovetskiy/mark/pkg/confluence"
"github.com/kovetskiy/mark/pkg/mark/macro"
"github.com/reconquest/pkg/log"
"github.com/reconquest/karma-go"
)
type Lib struct {
Macros []macro.Macro
Templates *template.Template
}
func New(api *confluence.API) (*Lib, error) {
var (
lib Lib
err error
)
lib.Templates, err = templates(api)
if err != nil {
return nil, err
}
lib.Macros, err = macros(lib.Templates)
if err != nil {
return nil, err
}
return &lib, nil
}
func macros(templates *template.Template) ([]macro.Macro, error) {
text := func(line ...string) []byte {
return []byte(strings.Join(line, "\n"))
}
macros, _, err := macro.ExtractMacros(
[]byte(text(
`<!-- Macro: @\{([^}]+)\}`,
` Template: ac:link:user`,
` Name: ${1} -->`,
// TODO(seletskiy): more macros here
)),
templates,
)
if err != nil {
return nil, err
}
return macros, nil
}
func templates(api *confluence.API) (*template.Template, error) {
text := func(line ...string) string {
return strings.Join(line, ``)
}
templates := template.New(`stdlib`).Funcs(
template.FuncMap{
"user": func(name string) *confluence.User {
user, err := api.GetUserByName(name)
if err != nil {
log.Error(err)
}
return user
},
// The only way to escape CDATA end marker ']]>' is to split it
// into two CDATA sections.
"cdata": func(data string) string {
return strings.ReplaceAll(
data,
"]]>",
"]]><![CDATA[]]]]><![CDATA[>",
)
},
},
)
var err error
for name, body := range map[string]string{
// This template is used to select whole article layout
`ac:layout`: text(
`{{ if eq .Layout "article" }}`,
/**/ `<ac:layout>`,
/**/ `<ac:layout-section ac:type="two_right_sidebar">`,
/**/ `<ac:layout-cell>{{ .Body }}</ac:layout-cell>`,
/**/ `<ac:layout-cell></ac:layout-cell>`,
/**/ `</ac:layout-section>`,
/**/ `</ac:layout>`,
`{{ else }}`,
/**/ `{{ .Body }}`,
`{{ end }}`,
),
// This template is used for rendering code in ```
`ac:code`: text(
`<ac:structured-macro ac:name="code">`,
`<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>`,
`<ac:parameter ac:name="collapse">false</ac:parameter>`,
`<ac:plain-text-body><![CDATA[{{ .Text | cdata }}]]></ac:plain-text-body>`,
`</ac:structured-macro>`,
),
`ac:status`: text(
`<ac:structured-macro ac:name="status">`,
`<ac:parameter ac:name="colour">{{ or .Color "Grey" }}</ac:parameter>`,
`<ac:parameter ac:name="title">{{ or .Title .Color }}</ac:parameter>`,
`<ac:parameter ac:name="subtle">{{ or .Subtle false }}</ac:parameter>`,
`</ac:structured-macro>`,
),
`ac:link:user`: text(
`{{ with .Name | user }}`,
/**/ `<ac:link>`,
/**/ `<ri:user ri:account-id="{{ .AccountID }}"/>`,
/**/ `</ac:link>`,
`{{ else }}`,
/**/ `{{ .Name }}`,
`{{ end }}`,
),
`ac:jira:ticket`: text(
`<ac:structured-macro ac:name="jira">`,
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/table-of-contents-macro-792499210.html */
`ac:toc`: text(
`<ac:structured-macro ac:name="toc">`,
`<ac:parameter ac:name="printable">true</ac:parameter>`,
`<ac:parameter ac:name="style">disc</ac:parameter>`,
`<ac:parameter ac:name="maxLevel">7</ac:parameter>`,
`<ac:parameter ac:name="minLevel">1</ac:parameter>`,
`<ac:parameter ac:name="exclude">{{ .Exclude }}</ac:parameter>`,
`<ac:parameter ac:name="outline">false</ac:parameter>`,
`</ac:structured-macro>`,
),
// TODO(seletskiy): more templates here
} {
templates, err = templates.New(name).Parse(body)
if err != nil {
return nil, karma.
Describe("template", body).
Format(
err,
"unable to parse template",
)
}
}
return templates, nil
}

221
renderer/blockquote.go Normal file
View File

@ -0,0 +1,221 @@
package renderer
import (
"fmt"
"regexp"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceBlockQuoteRenderer struct {
html.Config
LevelMap BlockQuoteLevelMap
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceBlockQuoteRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceBlockQuoteRenderer{
Config: html.NewConfig(),
LevelMap: nil,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceBlockQuoteRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindBlockquote, r.renderBlockQuote)
}
// Define BlockQuoteType enum
type BlockQuoteType int
const (
Info BlockQuoteType = iota
Note
Warn
Tip
None
)
func (t BlockQuoteType) String() string {
return []string{"info", "note", "warning", "tip", "none"}[t]
}
type BlockQuoteLevelMap map[ast.Node]int
func (m BlockQuoteLevelMap) Level(node ast.Node) int {
return m[node]
}
type BlockQuoteClassifier struct {
patternMap map[string]*regexp.Regexp
}
func LegacyBlockQuoteClassifier() BlockQuoteClassifier {
return BlockQuoteClassifier{
patternMap: map[string]*regexp.Regexp{
"info": regexp.MustCompile(`(?i)info`),
"note": regexp.MustCompile(`(?i)note`),
"warn": regexp.MustCompile(`(?i)warn`),
"tip": regexp.MustCompile(`(?i)tip`),
},
}
}
func GHAlertsBlockQuoteClassifier() BlockQuoteClassifier {
return BlockQuoteClassifier{
patternMap: map[string]*regexp.Regexp{
"info": regexp.MustCompile(`(?i)^\!(note|important)`),
"note": regexp.MustCompile(`(?i)^\!warning`),
"warn": regexp.MustCompile(`(?i)^\!caution`),
"tip": regexp.MustCompile(`(?i)^\!tip`),
},
}
}
// ClassifyingBlockQuote compares a string against a set of patterns and returns a BlockQuoteType
func (classifier BlockQuoteClassifier) ClassifyingBlockQuote(literal string) BlockQuoteType {
var t = None
switch {
case classifier.patternMap["info"].MatchString(literal):
t = Info
case classifier.patternMap["note"].MatchString(literal):
t = Note
case classifier.patternMap["warn"].MatchString(literal):
t = Warn
case classifier.patternMap["tip"].MatchString(literal):
t = Tip
}
return t
}
// ParseBlockQuoteType parses the first line of a blockquote and returns its type
func ParseBlockQuoteType(node ast.Node, source []byte) BlockQuoteType {
var t = None
var legacyClassifier = LegacyBlockQuoteClassifier()
var ghAlertsClassifier = GHAlertsBlockQuoteClassifier()
countParagraphs := 0
_ = ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
if node.Kind() == ast.KindParagraph && entering {
countParagraphs += 1
}
// Type of block quote should be defined on the first blockquote line
if countParagraphs < 2 && entering {
if node.Kind() == ast.KindText {
n := node.(*ast.Text)
t = legacyClassifier.ClassifyingBlockQuote(string(n.Value(source)))
// If the node is a text node but classification returned none do not give up!
// Find the next two sibling nodes midNode and rightNode,
// 1. If both are also a text node
// 2. and the original node (node) text value is '['
// 3. and the rightNode text value is ']'
// It means with high degree of confidence that the original md doc contains a Github alert type of blockquote
// Classifying the next text type node (midNode) will confirm that.
if t == None {
midNode := node.NextSibling()
if midNode != nil && midNode.Kind() == ast.KindText {
rightNode := midNode.NextSibling()
midTextNode := midNode.(*ast.Text)
if rightNode != nil && rightNode.Kind() == ast.KindText {
rightTextNode := rightNode.(*ast.Text)
if string(n.Value(source)) == "[" && string(rightTextNode.Value(source)) == "]" {
t = ghAlertsClassifier.ClassifyingBlockQuote(string(midTextNode.Value(source)))
}
}
}
}
countParagraphs += 1
}
if node.Kind() == ast.KindHTMLBlock {
n := node.(*ast.HTMLBlock)
for i := 0; i < n.BaseBlock.Lines().Len(); i++ {
line := n.BaseBlock.Lines().At(i)
t = legacyClassifier.ClassifyingBlockQuote(string(line.Value(source)))
if t != None {
break
}
}
countParagraphs += 1
}
} else if countParagraphs > 1 && entering {
return ast.WalkStop, nil
}
return ast.WalkContinue, nil
})
return t
}
// GenerateBlockQuoteLevel walks a given node and returns a map of blockquote levels
func GenerateBlockQuoteLevel(someNode ast.Node) BlockQuoteLevelMap {
// We define state variable that track BlockQuote level while we walk the tree
blockQuoteLevel := 0
blockQuoteLevelMap := 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 {
blockQuoteLevelMap[node] = blockQuoteLevel
blockQuoteLevel += 1
}
if node.Kind() == ast.KindBlockquote && !entering {
blockQuoteLevel -= 1
}
return ast.WalkContinue, nil
})
return blockQuoteLevelMap
}
// renderBlockQuote will render a BlockQuote
func (r *ConfluenceBlockQuoteRenderer) renderBlockQuote(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// Initialize BlockQuote level map
if r.LevelMap == nil {
r.LevelMap = GenerateBlockQuoteLevel(node)
}
quoteType := ParseBlockQuoteType(node, source)
quoteLevel := r.LevelMap.Level(node)
if quoteLevel == 0 && entering && quoteType != None {
prefix := fmt.Sprintf("<ac:structured-macro ac:name=\"%s\"><ac:parameter ac:name=\"icon\">true</ac:parameter><ac:rich-text-body>\n", quoteType)
if _, err := writer.Write([]byte(prefix)); err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}
if quoteLevel == 0 && !entering && quoteType != None {
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.goldmarkRenderBlockquote(writer, source, node, entering)
}
// goldmarkRenderBlockquote is the default renderBlockquote implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L286
func (r *ConfluenceBlockQuoteRenderer) goldmarkRenderBlockquote(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if n.Attributes() != nil {
_, _ = w.WriteString("<blockquote")
html.RenderAttributes(w, n, html.BlockquoteAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<blockquote>\n")
}
} else {
_, _ = w.WriteString("</blockquote>\n")
}
return ast.WalkContinue, nil
}

77
renderer/codeblock.go Normal file
View File

@ -0,0 +1,77 @@
package renderer
import (
"strings"
"github.com/kovetskiy/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceCodeBlockRenderer struct {
html.Config
Stdlib *stdlib.Lib
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceCodeBlockRenderer(stdlib *stdlib.Lib, path string, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceCodeBlockRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceCodeBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindCodeBlock, r.renderCodeBlock)
}
// renderCodeBlock renders a CodeBlock
func (r *ConfluenceCodeBlockRenderer) renderCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
title := ""
var lval []byte
lines := node.Lines().Len()
for i := 0; i < lines; i++ {
line := node.Lines().At(i)
lval = append(lval, line.Value(source)...)
}
err := r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:code",
struct {
Language string
Collapse bool
Title string
Theme string
Linenumbers bool
Firstline int
Text string
}{
lang,
collapse,
title,
theme,
linenumbers,
firstline,
strings.TrimSuffix(string(lval), "\n"),
},
)
if err != nil {
return ast.WalkStop, err
}
return ast.WalkContinue, nil
}

221
renderer/fencedcodeblock.go Normal file
View File

@ -0,0 +1,221 @@
package renderer
import (
"fmt"
"regexp"
"slices"
"strings"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/d2"
"github.com/kovetskiy/mark/mermaid"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/types"
"github.com/reconquest/pkg/log"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceFencedCodeBlockRenderer struct {
html.Config
Stdlib *stdlib.Lib
MarkConfig types.MarkConfig
Attachments attachment.Attacher
}
var reBlockDetails = regexp.MustCompile(
// (<Lang>|-) (collapse|<theme>|\d)* (title <title>)?
`^(?:(\w*)|-)\s*\b(\S.*?\S?)??\s*(?:\btitle\s+(\S.*\S?))?$`,
)
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceFencedCodeBlockRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, cfg types.MarkConfig, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceFencedCodeBlockRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
MarkConfig: cfg,
Attachments: attachments,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceFencedCodeBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindFencedCodeBlock, r.renderFencedCodeBlock)
}
func ParseLanguage(lang string) string {
// lang takes the following form: language? "collapse"? ("title"? <any string>*)?
// let's split it by spaces
paramlist := strings.Fields(lang)
// get the word in question, aka the first one
first := lang
if len(paramlist) > 0 {
first = paramlist[0]
}
if first == "collapse" || first == "title" {
// collapsing or including a title without a language
return ""
}
// the default case with language being the first one
return first
}
func ParseTitle(lang string) string {
index := strings.Index(lang, "title")
if index >= 0 {
// it's found, check if title is given and return it
start := index + 6
if len(lang) > start {
return lang[start:]
}
}
return ""
}
// renderFencedCodeBlock renders a FencedCodeBlock
func (r *ConfluenceFencedCodeBlockRenderer) renderFencedCodeBlock(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
var info []byte
nodeFencedCodeBlock := node.(*ast.FencedCodeBlock)
if nodeFencedCodeBlock.Info != nil {
segment := nodeFencedCodeBlock.Info.Segment
info = segment.Value(source)
}
groups := reBlockDetails.FindStringSubmatch(string(info))
linenumbers := false
firstline := 0
theme := ""
collapse := false
lang := ""
var options []string
title := ""
if len(groups) > 0 {
lang, options, title = groups[1], strings.Fields(groups[2]), groups[3]
for _, option := range options {
if option == "collapse" {
collapse = true
continue
}
if option == "nocollapse" {
collapse = false
continue
}
var i int
if _, err := fmt.Sscanf(option, "%d", &i); err == nil {
linenumbers = i > 0
firstline = i
continue
}
theme = option
}
}
var lval []byte
lines := node.Lines().Len()
for i := 0; i < lines; i++ {
line := node.Lines().At(i)
lval = append(lval, line.Value(source)...)
}
if lang == "d2" && slices.Contains(r.MarkConfig.Features, "d2") {
attachment, err := d2.ProcessD2(title, lval, r.MarkConfig.D2Scale)
if err != nil {
log.Debugf(nil, "error: %v", err)
return ast.WalkStop, err
}
r.Attachments.Attach(attachment)
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
attachment.Width,
attachment.Height,
attachment.Name,
"",
attachment.Filename,
"",
},
)
if err != nil {
return ast.WalkStop, err
}
} else if lang == "mermaid" && slices.Contains(r.MarkConfig.Features, "mermaid") && r.MarkConfig.MermaidProvider == "mermaid-go" {
attachment, err := mermaid.ProcessMermaidLocally(title, lval, r.MarkConfig.MermaidScale)
if 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 {
err := r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:code",
struct {
Language string
Collapse bool
Title string
Theme string
Linenumbers bool
Firstline int
Text string
}{
lang,
collapse,
title,
theme,
linenumbers,
firstline,
strings.TrimSuffix(string(lval), "\n"),
},
)
if err != nil {
return ast.WalkStop, err
}
}
return ast.WalkContinue, nil
}

57
renderer/heading.go Normal file
View File

@ -0,0 +1,57 @@
package renderer
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceHeadingRenderer struct {
html.Config
DropFirstH1 bool
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceHeadingRenderer(dropFirstH1 bool, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceHeadingRenderer{
Config: html.NewConfig(),
DropFirstH1: dropFirstH1,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceHeadingRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHeading, r.renderHeading)
}
func (r *ConfluenceHeadingRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
// If this is the first h1 heading of the document and we want to drop it, let's not render it at all.
if n.Level == 1 && r.DropFirstH1 {
if !entering {
r.DropFirstH1 = false
}
return ast.WalkSkipChildren, nil
}
return r.goldmarkRenderHeading(w, source, node, entering)
}
func (r *ConfluenceHeadingRenderer) goldmarkRenderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Heading)
if entering {
_, _ = w.WriteString("<h")
_ = w.WriteByte("0123456"[n.Level])
if n.Attributes() != nil {
html.RenderAttributes(w, node, html.HeadingAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</h")
_ = w.WriteByte("0123456"[n.Level])
_, _ = w.WriteString(">\n")
}
return ast.WalkContinue, nil
}

110
renderer/htmlblock.go Normal file
View File

@ -0,0 +1,110 @@
package renderer
import (
"strings"
"github.com/kovetskiy/mark/stdlib"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceHTMLBlockRenderer struct {
html.Config
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceHTMLBlockRenderer(stdlib *stdlib.Lib, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceHTMLBlockRenderer{
Config: html.NewConfig(),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceHTMLBlockRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
}
func (r *ConfluenceHTMLBlockRenderer) renderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
n := node.(*ast.HTMLBlock)
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
switch strings.Trim(string(line.Value(source)), "\n") {
case "<!-- ac:layout -->":
_, _ = w.WriteString("<ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout end -->":
_, _ = w.WriteString("</ac:layout>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:single -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"single\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_equal -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"two_equal\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_left_sidebar -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"two_left_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:two_right_sidebar -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"two_right_sidebar\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"three\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section type:three_with_sidebars -->":
_, _ = w.WriteString("<ac:layout-section ac:type=\"three_with_sidebars\">\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-section end -->":
_, _ = w.WriteString("</ac:layout-section>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell -->":
_, _ = w.WriteString("<ac:layout-cell>\n")
return ast.WalkContinue, nil
case "<!-- ac:layout-cell end -->":
_, _ = w.WriteString("</ac:layout-cell>\n")
return ast.WalkContinue, nil
case "<!-- ac:placeholder -->":
_, _ = w.WriteString("<ac:placeholder>\n")
return ast.WalkContinue, nil
case "<!-- ac:placeholder end -->":
_, _ = w.WriteString("</ac:placeholder>\n")
return ast.WalkContinue, nil
}
}
return r.goldmarkRenderHTMLBlock(w, source, node, entering)
}
func (r *ConfluenceHTMLBlockRenderer) goldmarkRenderHTMLBlock(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.HTMLBlock)
if entering {
if r.Unsafe {
l := n.Lines().Len()
for i := 0; i < l; i++ {
line := n.Lines().At(i)
r.Writer.SecureWrite(w, line.Value(source))
}
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
} else {
if n.HasClosure() {
if r.Unsafe {
closure := n.ClosureLine
r.Writer.SecureWrite(w, closure.Value(source))
} else {
_, _ = w.WriteString("<!-- raw HTML omitted -->\n")
}
}
}
return ast.WalkContinue, nil
}

118
renderer/image.go Normal file
View File

@ -0,0 +1,118 @@
package renderer
import (
"bytes"
"path/filepath"
"strings"
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/vfs"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceImageRenderer struct {
html.Config
Stdlib *stdlib.Lib
Path string
Attachments attachment.Attacher
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceImageRenderer(stdlib *stdlib.Lib, attachments attachment.Attacher, path string, opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceImageRenderer{
Config: html.NewConfig(),
Stdlib: stdlib,
Path: path,
Attachments: attachments,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceImageRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindImage, r.renderImage)
}
// renderImage renders an inline image
func (r *ConfluenceImageRenderer) renderImage(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Image)
attachments, err := attachment.ResolveLocalAttachments(vfs.LocalOS, filepath.Dir(r.Path), []string{string(n.Destination)})
// We were unable to resolve it locally, treat as URL
if err != nil {
escapedURL := string(n.Destination)
escapedURL = strings.ReplaceAll(escapedURL, "&", "&amp;")
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
"",
escapedURL,
},
)
} else {
r.Attachments.Attach(attachments[0])
err = r.Stdlib.Templates.ExecuteTemplate(
writer,
"ac:image",
struct {
Width string
Height string
Title string
Alt string
Attachment string
Url string
}{
"",
"",
string(n.Title),
string(nodeToHTMLText(n, source)),
attachments[0].Filename,
"",
},
)
}
if err != nil {
return ast.WalkStop, err
}
return ast.WalkSkipChildren, nil
}
// https://github.com/yuin/goldmark/blob/c446c414ef3a41fb562da0ae5badd18f1502c42f/renderer/html/html.go
func nodeToHTMLText(n ast.Node, source []byte) []byte {
var buf bytes.Buffer
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
if s, ok := c.(*ast.String); ok && s.IsCode() {
buf.Write(s.Value)
} else if t, ok := c.(*ast.Text); ok {
buf.Write(util.EscapeHTML(t.Value(source)))
} else {
buf.Write(nodeToHTMLText(c, source))
}
}
return buf.Bytes()
}

92
renderer/link.go Normal file
View File

@ -0,0 +1,92 @@
package renderer
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceLinkRenderer struct {
html.Config
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceLinkRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceLinkRenderer{
Config: html.NewConfig(),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceLinkRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindLink, r.renderLink)
}
// renderLink renders links specifically for confluence
func (r *ConfluenceLinkRenderer) renderLink(writer util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if string(n.Destination[0:3]) == "ac:" {
if entering {
_, err := writer.Write([]byte("<ac:link><ri:page ri:content-title=\""))
if err != nil {
return ast.WalkStop, err
}
if len(string(n.Destination)) < 4 {
//nolint:staticcheck
_, err := writer.Write(node.Text(source))
if err != nil {
return ast.WalkStop, err
}
} else {
_, err := writer.Write(n.Destination[3:])
if err != nil {
return ast.WalkStop, err
}
}
_, err = writer.Write([]byte("\"/><ac:plain-text-link-body><![CDATA["))
if err != nil {
return ast.WalkStop, err
}
//nolint:staticcheck
_, err = writer.Write(node.Text(source))
if err != nil {
return ast.WalkStop, err
}
_, err = writer.Write([]byte("]]></ac:plain-text-link-body></ac:link>"))
if err != nil {
return ast.WalkStop, err
}
}
return ast.WalkSkipChildren, nil
}
return r.goldmarkRenderLink(writer, source, node, entering)
}
// goldmarkRenderLink is the default renderLink implementation from https://github.com/yuin/goldmark/blob/9d6f314b99ca23037c93d76f248be7b37de6220a/renderer/html/html.go#L552
func (r *ConfluenceLinkRenderer) goldmarkRenderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
n := node.(*ast.Link)
if entering {
_, _ = w.WriteString("<a href=\"")
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
}
_ = w.WriteByte('"')
if n.Title != nil {
_, _ = w.WriteString(` title="`)
r.Writer.Write(w, n.Title)
_ = w.WriteByte('"')
}
if n.Attributes() != nil {
html.RenderAttributes(w, n, html.LinkAttributeFilter)
}
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("</a>")
}
return ast.WalkContinue, nil
}

44
renderer/paragraph.go Normal file
View File

@ -0,0 +1,44 @@
package renderer
import (
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
type ConfluenceParagraphRenderer struct {
html.Config
}
// NewConfluenceRenderer creates a new instance of the ConfluenceRenderer
func NewConfluenceParagraphRenderer(opts ...html.Option) renderer.NodeRenderer {
return &ConfluenceParagraphRenderer{
Config: html.NewConfig(),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceParagraphRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindParagraph, r.renderParagraph)
}
func (r *ConfluenceParagraphRenderer) renderParagraph(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
if entering {
if n.FirstChild().Kind() != ast.KindRawHTML {
if n.Attributes() != nil {
_, _ = w.WriteString("<p")
html.RenderAttributes(w, n, html.ParagraphAttributeFilter)
_ = w.WriteByte('>')
} else {
_, _ = w.WriteString("<p>")
}
}
} else {
if n.FirstChild().Kind() != ast.KindRawHTML {
_, _ = w.WriteString("</p>")
}
_, _ = w.WriteString("\n")
}
return ast.WalkContinue, nil
}

136
renderer/text.go Normal file
View File

@ -0,0 +1,136 @@
package renderer
import (
"unicode"
"unicode/utf8"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
)
// ConfluenceTextRenderer slightly alters the default goldmark behavior for
// inline text block. It allows for soft breaks
// (c.f. https://spec.commonmark.org/0.30/#softbreak)
// to be rendered into HTML as either '\n' (the goldmark default)
// or as ' '.
// This latter option is useful for Confluence,
// which inserts <br> tags into uploaded HTML where it sees '\n'.
// See also https://sembr.org/ for partial motivation.
type ConfluenceTextRenderer struct {
html.Config
softBreak rune
}
// NewConfluenceTextRenderer creates a new instance of the ConfluenceTextRenderer
func NewConfluenceTextRenderer(stripNL bool, opts ...html.Option) renderer.NodeRenderer {
sb := '\n'
if stripNL {
sb = ' '
}
return &ConfluenceTextRenderer{
Config: html.NewConfig(),
softBreak: sb,
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs .
func (r *ConfluenceTextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(ast.KindText, r.renderText)
}
// This is taken from https://github.com/yuin/goldmark/blob/v1.6.0/renderer/html/html.go#L719
// with the hardcoded '\n' for soft breaks swapped for the configurable r.softBreak
func (r *ConfluenceTextRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
n := node.(*ast.Text)
segment := n.Segment
if n.IsRaw() {
r.Writer.RawWrite(w, segment.Value(source))
} else {
value := segment.Value(source)
r.Writer.Write(w, value)
if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
if r.XHTML {
_, _ = w.WriteString("<br />\n")
} else {
_, _ = w.WriteString("<br>\n")
}
} else if n.SoftLineBreak() {
if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
sibling := node.NextSibling()
if sibling != nil && sibling.Kind() == ast.KindText {
if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
thisLastRune := util.ToRune(value, len(value)-1)
siblingFirstRune, _ := utf8.DecodeRune(siblingText)
// Inline the softLineBreak function as it's not public
writeLineBreak := false
switch r.EastAsianLineBreaks {
case html.EastAsianLineBreaksNone:
writeLineBreak = false
case html.EastAsianLineBreaksSimple:
writeLineBreak = !util.IsEastAsianWideRune(thisLastRune) || !util.IsEastAsianWideRune(siblingFirstRune)
case html.EastAsianLineBreaksCSS3Draft:
writeLineBreak = eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune, siblingFirstRune)
}
if writeLineBreak {
_ = w.WriteByte(byte(r.softBreak))
}
}
}
} else {
_ = w.WriteByte(byte(r.softBreak))
}
}
}
return ast.WalkContinue, nil
}
func eastAsianLineBreaksCSS3DraftSoftLineBreak(thisLastRune rune, siblingFirstRune rune) bool {
// Implements CSS text level3 Segment Break Transformation Rules with some enhancements.
// References:
// - https://www.w3.org/TR/2020/WD-css-text-3-20200429/#line-break-transform
// - https://github.com/w3c/csswg-drafts/issues/5086
// Rule1:
// If the character immediately before or immediately after the segment break is
// the zero-width space character (U+200B), then the break is removed, leaving behind the zero-width space.
if thisLastRune == '\u200B' || siblingFirstRune == '\u200B' {
return false
}
// Rule2:
// Otherwise, if the East Asian Width property of both the character before and after the segment break is
// F, W, or H (not A), and neither side is Hangul, then the segment break is removed.
thisLastRuneEastAsianWidth := util.EastAsianWidth(thisLastRune)
siblingFirstRuneEastAsianWidth := util.EastAsianWidth(siblingFirstRune)
if (thisLastRuneEastAsianWidth == "F" ||
thisLastRuneEastAsianWidth == "W" ||
thisLastRuneEastAsianWidth == "H") &&
(siblingFirstRuneEastAsianWidth == "F" ||
siblingFirstRuneEastAsianWidth == "W" ||
siblingFirstRuneEastAsianWidth == "H") {
return unicode.Is(unicode.Hangul, thisLastRune) || unicode.Is(unicode.Hangul, siblingFirstRune)
}
// Rule3:
// Otherwise, if either the character before or after the segment break belongs to
// the space-discarding character set and it is a Unicode Punctuation (P*) or U+3000,
// then the segment break is removed.
if util.IsSpaceDiscardingUnicodeRune(thisLastRune) ||
unicode.IsPunct(thisLastRune) ||
thisLastRune == '\u3000' ||
util.IsSpaceDiscardingUnicodeRune(siblingFirstRune) ||
unicode.IsPunct(siblingFirstRune) ||
siblingFirstRune == '\u3000' {
return false
}
// Rule4:
// Otherwise, the segment break is converted to a space (U+0020).
return true
}

467
stdlib/stdlib.go Normal file
View File

@ -0,0 +1,467 @@
package stdlib
import (
"strings"
"text/template"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/macro"
"github.com/reconquest/pkg/log"
"github.com/reconquest/karma-go"
)
type Lib struct {
Macros []macro.Macro
Templates *template.Template
}
func New(api *confluence.API) (*Lib, error) {
var (
lib Lib
err error
)
lib.Templates, err = templates(api)
if err != nil {
return nil, err
}
lib.Macros, err = macros(lib.Templates)
if err != nil {
return nil, err
}
return &lib, nil
}
func macros(templates *template.Template) ([]macro.Macro, error) {
text := func(line ...string) []byte {
return []byte(strings.Join(line, "\n"))
}
macros, _, err := macro.ExtractMacros(
"",
"",
text(
`<!-- Macro: @\{([^}]+)\}`,
` Template: ac:link:user`,
` Name: ${1} -->`,
// TODO(seletskiy): more macros here
),
templates,
)
if err != nil {
return nil, err
}
return macros, nil
}
func templates(api *confluence.API) (*template.Template, error) {
text := func(line ...string) string {
return strings.Join(line, ``)
}
templates := template.New(`stdlib`).Funcs(
template.FuncMap{
"user": func(name string) *confluence.User {
user, err := api.GetUserByName(name)
if err != nil {
log.Error(err)
}
return user
},
// The only way to escape CDATA end marker ']]>' is to split it
// into two CDATA sections.
"cdata": func(data string) string {
return strings.ReplaceAll(
data,
"]]>",
"]]><![CDATA[]]]]><![CDATA[>",
)
},
"convertAttachment": func(data string) string {
return strings.ReplaceAll(
data,
"/",
"_",
)
},
},
)
var err error
for name, body := range map[string]string{
// This template is used to select whole article layout
`ac:layout`: text(
`{{ if eq .Layout "article" }}`,
/**/ `<ac:layout>`,
/**/ `<ac:layout-section ac:type="two_right_sidebar">`,
/**/ `<ac:layout-cell>{{ .Body }}</ac:layout-cell>`,
/**/ `<ac:layout-cell>{{ .Sidebar }}</ac:layout-cell>`,
/**/ `</ac:layout-section>`,
/**/ `</ac:layout>`,
`{{ else }}`,
/**/ `{{ .Body }}`,
`{{ end }}`,
),
// This template is used for rendering code in ```
`ac:code`: text(
`<ac:structured-macro ac:name="{{ if eq .Language "mermaid" }}cloudscript-confluence-mermaid{{ else }}code{{ end }}">`,
/**/ `{{ if eq .Language "mermaid" }}<ac:parameter ac:name="showSource">true</ac:parameter>{{ else }}`,
/**/ `<ac:parameter ac:name="language">{{ .Language }}</ac:parameter>{{ end }}`,
/**/ `<ac:parameter ac:name="collapse">{{ .Collapse }}</ac:parameter>`,
/**/ `{{ if .Theme }}<ac:parameter ac:name="theme">{{ .Theme }}</ac:parameter>{{ end }}`,
/**/ `{{ if .Linenumbers }}<ac:parameter ac:name="linenumbers">{{ .Linenumbers }}</ac:parameter>{{ end }}`,
/**/ `{{ if .Firstline }}<ac:parameter ac:name="firstline">{{ .Firstline }}</ac:parameter>{{ end }}`,
/**/ `{{ if .Title }}<ac:parameter ac:name="title">{{ .Title }}</ac:parameter>{{ end }}`,
/**/ `<ac:plain-text-body><![CDATA[{{ .Text | cdata }}]]></ac:plain-text-body>`,
`</ac:structured-macro>`,
),
`ac:status`: text(
`<ac:structured-macro ac:name="status">`,
`<ac:parameter ac:name="colour">{{ or .Color "Grey" }}</ac:parameter>`,
`<ac:parameter ac:name="title">{{ or .Title .Color }}</ac:parameter>`,
`<ac:parameter ac:name="subtle">{{ or .Subtle false }}</ac:parameter>`,
`</ac:structured-macro>`,
),
`ac:link:user`: text(
`{{ with .Name | user }}`,
/**/ `<ac:link>`,
/**/ `{{ if .AccountID }}`,
/****/ `<ri:user ri:account-id="{{ .AccountID }}" />`,
/**/ `{{ else }}`,
/****/ `<ri:user ri:userkey="{{ .UserKey }}" />`,
/**/ `{{ end }}`,
/**/ `</ac:link>`,
`{{ else }}`,
/**/ `{{ .Name }}`,
`{{ end }}`,
),
`ac:jira:ticket`: text(
`<ac:structured-macro ac:name="jira">`,
`<ac:parameter ac:name="key">{{ .Ticket }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* Used for rendering Jira Filters */
`ac:jira:filter`: text(
`<ac:structured-macro ac:name="jira">`,
`<ac:parameter ac:name="server">{{ or .Server "System JIRA" }}</ac:parameter>`,
`<ac:parameter ac:name="jqlQuery">{{ .JQL }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/doc/jira-issues-macro-139380.html */
`ac:jiraissues`: text(
`<ac:structured-macro ac:name="jiraissues">`,
`<ac:parameter ac:name="anonymous">{{ or .Anonymous false }}</ac:parameter>`,
`<ac:parameter ac:name="baseurl"><ri:url ri:value="{{ or .BaseURL .URL }}" /></ac:parameter>`,
`<ac:parameter ac:name="columns">{{ or .Columns "type;key;summary;assignee;reporter;priority;status;resolution;created;updated;due" }}</ac:parameter>`,
`<ac:parameter ac:name="count">{{ or .Count false }}</ac:parameter>`,
`<ac:parameter ac:name="cache">{{ or .Cache "on" }}</ac:parameter>`,
`<ac:parameter ac:name="height">{{ or .Height 480 }}</ac:parameter>`,
`<ac:parameter ac:name="renderMode">{{ or .RenderMode "static" }}</ac:parameter>`,
`<ac:parameter ac:name="title">{{ or .Title "Jira Issues" }}</ac:parameter>`,
`<ac:parameter ac:name="url"><ri:url ri:value="{{ .URL }}" /></ac:parameter>`,
`<ac:parameter ac:name="width">{{ or .Width "100%" }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/info-tip-note-and-warning-macros-792499127.html */
`ac:box`: text(
`<ac:structured-macro ac:name="{{ .Name }}">`,
`<ac:parameter ac:name="icon">{{ or .Icon "false" }}</ac:parameter>`,
`{{ if .Title }}<ac:parameter ac:name="title">{{ .Title }}</ac:parameter>{{ end }}`,
`<ac:rich-text-body>{{ .Body }}</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/table-of-contents-macro-792499210.html */
`ac:toc`: text(
`<ac:structured-macro ac:name="toc">`,
`<ac:parameter ac:name="printable">{{ or .Printable "true" }}</ac:parameter>`,
`<ac:parameter ac:name="style">{{ or .Style "disc" }}</ac:parameter>`,
`<ac:parameter ac:name="maxLevel">{{ or .MaxLevel "7" }}</ac:parameter>`,
`<ac:parameter ac:name="indent">{{ or .Indent "" }}</ac:parameter>`,
`<ac:parameter ac:name="minLevel">{{ or .MinLevel "1" }}</ac:parameter>`,
`<ac:parameter ac:name="exclude">{{ or .Exclude "" }}</ac:parameter>`,
`<ac:parameter ac:name="type">{{ or .Type "list" }}</ac:parameter>`,
`<ac:parameter ac:name="outline">{{ or .Outline "clear" }}</ac:parameter>`,
`<ac:parameter ac:name="include">{{ or .Include "" }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/doc/children-display-macro-139501.html */
`ac:children`: text(
`<ac:structured-macro ac:name="children">`,
`{{ if .Reverse }}<ac:parameter ac:name="reverse">{{ or .Reverse }}</ac:parameter>{{ end }}`,
`{{ if .Sort }}<ac:parameter ac:name="sort">{{ .Sort }}</ac:parameter>{{ end }}`,
`{{ if .Style }}<ac:parameter ac:name="style">{{ .Style }}</ac:parameter>{{ end }}`,
`{{ if .Page }}`,
/**/ `<ac:parameter ac:name="page">`,
/**/ `<ac:link>`,
/**/ `<ri:page ri:content-title="{{ .Page }}"/>`,
/**/ `</ac:link>`,
/**/ `</ac:parameter>`,
`{{ end }}`,
`{{ if .Excerpt }}<ac:parameter ac:name="excerptType">{{ .Excerpt }}</ac:parameter>{{ end }}`,
`{{ if .First }}<ac:parameter ac:name="first">{{ .First }}</ac:parameter>{{ end }}`,
`{{ if .Depth }}<ac:parameter ac:name="depth">{{ .Depth }}</ac:parameter>{{ end }}`,
`{{ if .All }}<ac:parameter ac:name="all">{{ .All }}</ac:parameter>{{ end }}`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/doc/confluence-storage-format-790796544.html */
`ac:emoticon`: text(
`<ac:emoticon ac:name="{{ .Name }}"/>`,
),
`ac:image`: text(
`<ac:image`,
`{{ if .Width }} ac:width="{{ .Width }}"{{ end }}`,
`{{ if .Height }} ac:height="{{ .Height }}"{{ end }}`,
`{{ if .Title }} ac:title="{{ .Title }}"{{ end }}`,
`{{ if .Alt }} ac:alt="{{ .Alt }}"{{ end }}>`,
`{{ if .Attachment }}<ri:attachment ri:filename="{{ .Attachment | convertAttachment }}"/>{{ end }}`,
`{{ if .Url }}<ri:url ri:value="{{ .Url }}"/>{{ end }}`,
`</ac:image>`,
),
/* https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html#WidgetConnectorMacro-YouTube */
`ac:youtube`: text(
`<ac:structured-macro ac:name="widget">`,
`<ac:parameter ac:name="overlay">youtube</ac:parameter>`,
`<ac:parameter ac:name="_template">com/atlassian/confluence/extra/widgetconnector/templates/youtube.vm</ac:parameter>`,
`<ac:parameter ac:name="width">{{ or .Width "640px" }}</ac:parameter>`,
`<ac:parameter ac:name="height">{{ or .Height "360px" }}</ac:parameter>`,
`<ac:parameter ac:name="url"><ri:url ri:value="{{ .URL }}" /></ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-iframe-macro/ */
`ac:iframe`: text(
`<ac:structured-macro ac:name="iframe">`,
`<ac:parameter ac:name="src"><ri:url ri:value="{{ .URL }}" /></ac:parameter>`,
`{{ if .Frameborder }}<ac:parameter ac:name="frameborder">{{ .Frameborder }}</ac:parameter>{{ end }}`,
`{{ if .Scrolling }}<ac:parameter ac:name="id">{{ .Scrolling }}</ac:parameter>{{ end }}`,
`{{ if .Align }}<ac:parameter ac:name="align">{{ .Align }}</ac:parameter>{{ end }}`,
`<ac:parameter ac:name="width">{{ or .Width "640px" }}</ac:parameter>`,
`<ac:parameter ac:name="height">{{ or .Height "360px" }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/doc/blog-posts-macro-139470.html */
`ac:blog-posts`: text(
`<ac:structured-macro ac:name="blog-posts">`,
`{{ if .Content }}<ac:parameter ac:name="content">{{ .Content }}</ac:parameter>{{ end }}`,
`{{ if .Spaces }}<ac:parameter ac:name="spaces">{{ .Spaces }}</ac:parameter>{{ end }}`,
`{{ if .Author }}<ac:parameter ac:name="author">{{ .Author }}</ac:parameter>{{ end }}`,
`{{ if .Time }}<ac:parameter ac:name="time">{{ .Time }}</ac:parameter>{{ end }}`,
`{{ if .Reverse }}<ac:parameter ac:name="reverse">{{ .Reverse }}</ac:parameter>{{ end }}`,
`{{ if .Sort }}<ac:parameter ac:name="sort">{{ .Sort }}</ac:parameter>{{ end }}`,
`{{ if .Max }}<ac:parameter ac:name="max">{{ .Max }}</ac:parameter>{{ end }}`,
`{{ if .Label }}<ac:parameter ac:name="label">{{ .Label }}</ac:parameter>{{ end }}`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/include-page-macro-792499125.html */
`ac:include`: text(
`<ac:structured-macro ac:name="include">`,
`<ac:parameter ac:name="">`,
`<ac:link>`,
`<ri:page ri:content-title="{{ .Page }}" {{if .Space }}ri:space-key="{{ .Space }}"{{ end }}/>`,
`</ac:link>`,
`</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/excerpt-include-macro-792499101.html */
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-include-macro/ */
`ac:excerpt-include`: text(
`<ac:macro ac:name="excerpt-include">`,
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
`<ac:parameter ac:name="nopanel">{{ if .NoPanel }}{{ .NoPanel }}{{ else }}false{{ end }}</ac:parameter>`,
`<ac:default-parameter>{{ .Page }}</ac:default-parameter>`,
`</ac:macro>`,
),
/* https://confluence.atlassian.com/conf59/excerpt-macro-792499102.html */
/* https://support.atlassian.com/confluence-cloud/docs/insert-the-excerpt-macro/ */
`ac:excerpt`: text(
`<ac:structured-macro ac:name="excerpt">`,
`{{ if .Name }}<ac:parameter ac:name="name">{{ .Name }}</ac:parameter>{{ end }}`,
`<ac:parameter ac:name="hidden">{{ if .Hidden }}{{ .Hidden }}{{ else }}false{{ end }}</ac:parameter>`,
`<ac:parameter ac:name="atlassian-macro-output-type">{{ if .OutputType }}{{ .OutputType }}{{ else }}BLOCK{{ end }}</ac:parameter>`,
`<ac:rich-text-body>`,
`{{ .Excerpt }}`,
`</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/anchor-macro-792499068.html */
`ac:anchor`: text(
`<ac:structured-macro ac:name="anchor">`,
`<ac:parameter ac:name="">{{ .Anchor }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/expand-macro-792499106.html */
`ac:expand`: text(
`<ac:structured-macro ac:name="expand">`,
`<ac:parameter ac:name="title">{{ .Title }}</ac:parameter>`,
`<ac:rich-text-body>{{ .Body }}</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/user-profile-macro-792499223.html */
`ac:profile`: text(
`{{ with .Name | user }}`,
`<ac:structured-macro ac:name="profile">`,
`<ac:parameter ac:name="user">`,
`{{ if .AccountID }}`,
/**/ `<ri:user ri:account-id="{{ .AccountID }}" />`,
`{{ else }}`,
/**/ `<ri:user ri:userkey="{{ .UserKey }}" />`,
`{{ end }}`,
`</ac:parameter>`,
`</ac:structured-macro>`,
`{{ end }}`,
),
/* https://confluence.atlassian.com/conf59/content-by-label-macro-792499087.html */
`ac:contentbylabel`: text(
`<ac:structured-macro ac:name="contentbylabel" ac:schema-version="3">`,
`<ac:parameter ac:name="cql">{{ .CQL }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/page-properties-report-macro-792499165.html */
`ac:detailssummary`: text(
`<ac:structured-macro ac:name="detailssummary" ac:schema-version="2">`,
`<ac:parameter ac:name="headings">{{ .Headings }}</ac:parameter>`,
`<ac:parameter ac:name="firstcolumn">{{ .FirstColumn }}</ac:parameter>`,
`<ac:parameter ac:name="sortBy">{{ .SortBy }}</ac:parameter>`,
`<ac:parameter ac:name="cql">{{ .CQL }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/page-properties-macro-792499154.html */
`ac:details`: text(
`<ac:structured-macro ac:name="details" ac:schema-version="1">`,
`<ac:rich-text-body>{{ .Body }}</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/page-tree-macro-792499177.html */
`ac:pagetree`: text(
`<ac:structured-macro ac:name="pagetree" ac:schema-version="1">`,
`<ac:parameter ac:name="root">`,
`<ac:link>`,
`<ri:page ri:content-title="{{ or .Title "@self" }}"/>`,
`</ac:link>`,
`</ac:parameter>`,
`<ac:parameter ac:name="sort">{{ or .Sort "" }}</ac:parameter>`,
`<ac:parameter ac:name="excerpt">{{ or .Excerpt "" }}</ac:parameter>`,
`<ac:parameter ac:name="reverse">{{ or .Reverse "" }}</ac:parameter>`,
`<ac:parameter ac:name="searchBox">{{ or .SearchBox "" }}</ac:parameter>`,
`<ac:parameter ac:name="expandCollapseAll">{{ or .ExpandCollapseAll "" }}</ac:parameter>`,
`<ac:parameter ac:name="startDepth">{{ or .StartDepth "" }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/page-tree-search-macro-792499178.html */
`ac:pagetreesearch`: text(
`<ac:structured-macro ac:name="pagetreesearch">`,
`{{ if .Root }}<ac:parameter ac:name="root">{{ .Root }}</ac:parameter>{{ end }}`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/panel-macro-792499179.html */
`ac:panel`: text(
`<ac:structured-macro ac:name="panel">`,
`<ac:parameter ac:name="bgColor">{{ or .BGColor "" }}</ac:parameter>`,
`<ac:parameter ac:name="titleBGColor">{{ or .TitleBGColor "" }}</ac:parameter>`,
`<ac:parameter ac:name="title">{{ or .Title "" }}</ac:parameter>`,
`<ac:parameter ac:name="borderStyle">{{ or .BorderStyle "" }}</ac:parameter>`,
`<ac:parameter ac:name="borderColor">{{ or .BorderColor "" }}</ac:parameter>`,
`<ac:parameter ac:name="titleColor">{{ or .TitleColor "" }}</ac:parameter>`,
`<ac:rich-text-body>{{ .Body }}</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/recently-updated-macro-792499187.html */
`ac:recently-updated`: text(
`<ac:structured-macro ac:name="recently-updated">`,
`{{ if .Spaces }}<ac:parameter ac:name="spaces"><ri:space ri:space-key={{ .Spaces }}/></ac:parameter>{{ end }}`,
`<ac:parameter ac:name="showProfilePic">{{ or .ShowProfilePic "" }}</ac:parameter>`,
`<ac:parameter ac:name="types">{{ or .Types "page, comment, blogpost" }}</ac:parameter>`,
`<ac:parameter ac:name="max">{{ or .Max "" }}</ac:parameter>`,
`<ac:parameter ac:name="labels">{{ or .Labels "" }}</ac:parameter>`,
`<ac:parameter ac:name="hideHeading">{{ or .HideHeading "" }}</ac:parameter>`,
`<ac:parameter ac:name="theme">{{ or .Theme "" }}</ac:parameter>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/column-macro-792499085.html */
`ac:column`: text(
`<ac:structured-macro ac:name="column">`,
`<ac:parameter ac:name="width">{{ or .Width "" }}</ac:parameter>`,
`<ac:rich-text-body>{{ or .Body "" }}</ac:rich-text-body>`,
`</ac:structured-macro>`,
),
/* https://confluence.atlassian.com/conf59/multimedia-macro-792499140.html */
`ac:multimedia`: text(
`<ac:structured-macro ac:name="multimedia">`,
`<ac:parameter ac:name="width">{{ or .Width 500 }}</ac:parameter>`,
`<ac:parameter ac:name="name">`,
`<ri:attachment ri:filename="{{ .Name | convertAttachment }}"/>`,
`</ac:parameter>`,
`<ac:parameter ac:name="autoplay">{{ or .AutoPlay "false"}}</ac:parameter>`,
`</ac:structured-macro>`,
),
// TODO(seletskiy): more templates here
} {
templates, err = templates.New(name).Parse(body)
if err != nil {
return nil, karma.
Describe("template", body).
Format(
err,
"unable to parse template",
)
}
}
return templates, nil
}

6
testdata/batch-tests/bad-test.md vendored Normal file
View File

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

6
testdata/batch-tests/errord-test.md vendored Normal file
View File

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

10
testdata/batch-tests/good-test.md vendored Normal file
View 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
View 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
View 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

68
testdata/codes-stripnewlines.html vendored Normal file
View File

@ -0,0 +1,68 @@
<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><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>

69
testdata/codes.html vendored Normal file
View File

@ -0,0 +1,69 @@
<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><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>

120
testdata/codes.md vendored Normal file
View File

@ -0,0 +1,120 @@
`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
```

7
testdata/header-droph1.html vendored Normal file
View File

@ -0,0 +1,7 @@
<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>
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>

8
testdata/header.html vendored Normal file
View File

@ -0,0 +1,8 @@
<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>
<h1 id="This/is-some_Heading.yml">This/is some_Heading.yml</h1>

12
testdata/header.md vendored Normal file
View File

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

1
testdata/issue-64-broken-link.html vendored Normal file
View File

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

1
testdata/issue-64-broken-link.md vendored Normal file
View File

@ -0,0 +1 @@
[v71](Page2#Page2-Releasev71-22-Feb-2018(intern))

21
testdata/links.html vendored Normal file
View File

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

26
testdata/links.md vendored Normal file
View File

@ -0,0 +1,26 @@
Use <https://example.com>
Use <ac:rich-text-body>aaa</ac:rich-text-body>
Use [page link](ac:Page)
Use [AnotherPage](ac:)
Use [Another Page](ac:)
Use [Another Page](ac:test_link)
Use [page link with spaces](<ac:Page With Space>)
![My Image](test.png)
![My External Image](http://confluence.atlassian.com/images/logo/confluence_48_trans.png?key1=value1&key2=value2)
[My test_link](ac:test_link)
[Another [Link]](ac:test_link_link)
Use footnotes link [^1]
[^1]: a footnote link
Use [Link [Text]](foo)

21
testdata/lists.html vendored Normal file
View File

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

13
testdata/lists.md vendored Normal file
View File

@ -0,0 +1,13 @@
- dash 1-1
- dash 1-2
- dash 1-3
- dash 1-3-1
- dash 1-3-2
- dash 1-3-3
- dash 1-3-3-1
text
* a
* b
* c

View File

@ -0,0 +1,18 @@
<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>

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

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

18
testdata/macro-include.md vendored Normal file
View File

@ -0,0 +1,18 @@
<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>
| Header 1 | Header 2 |
|---|---|
| Cell A | Cell B |
</ac:rich-text-body>
</ac:structured-macro>

8
testdata/newlines-stripnewlines.html vendored Normal file
View File

@ -0,0 +1,8 @@
<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>

10
testdata/newlines.html vendored Normal file
View File

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

17
testdata/newlines.md vendored Normal file
View File

@ -0,0 +1,17 @@
one-1
one-2
two-1
two-2
three-1
three-2
space-1
space-2
2space-1
2space-2

18
testdata/pagelayout.html vendored Normal file
View File

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

21
testdata/pagelayout.md vendored Normal file
View File

@ -0,0 +1,21 @@
<!-- ac:layout -->
<!-- ac:layout-section type:three_with_sidebars -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-cell -->
Even More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout-section type:single -->
<!-- ac:layout-cell -->
Still More Content
<!-- ac:layout-cell end -->
<!-- ac:layout-section end -->
<!-- ac:layout end -->

104
testdata/quotes-droph1.html vendored Normal file
View File

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

101
testdata/quotes-stripnewlines.html vendored Normal file
View File

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

105
testdata/quotes.html vendored Normal file
View File

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

95
testdata/quotes.md vendored Normal file
View File

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

28
testdata/table.html vendored Normal file
View File

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

7
testdata/table.md vendored Normal file
View File

@ -0,0 +1,7 @@
|HEADER1|HEADER2|
|---|---|
|row1|row2|
|HEADER1|HEADER2|
|:---:|---:|
|row1|row2|

6
testdata/tags.html vendored Normal file
View File

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

11
testdata/tags.md vendored Normal file
View File

@ -0,0 +1,11 @@
<b>bold</b>
**bold**
<i>vitalik</i>
*vitalik*
<s>strikethrough</s>
~~strikethrough~~

BIN
testdata/test.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

10
types/types.go Normal file
View File

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

View File

@ -1,8 +1,10 @@
package main
package util
import (
"errors"
"io"
"net/url"
"os"
"strings"
"github.com/reconquest/karma-go"
@ -16,35 +18,39 @@ type Credentials struct {
}
func GetCredentials(
args map[string]interface{},
config *Config,
) (*Credentials, error) {
var (
username, _ = args["-u"].(string)
password, _ = args["-p"].(string)
targetURL, _ = args["-l"].(string)
)
username string,
password string,
targetURL string,
baseURL string,
compileOnly bool,
) (*Credentials, error) {
var err error
if username == "" {
username = config.Username
if username == "" {
if password == "" {
if !compileOnly {
return nil, errors.New(
"Confluence username should be specified using -u " +
"confluence password should be specified using -p " +
"flag or be stored in configuration file",
)
}
password = "none"
}
if password == "" {
password = config.Password
if password == "" {
return nil, errors.New(
"Confluence password should be specified using -p " +
"flag or be stored in configuration file",
if password == "-" {
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, karma.Format(
err,
"unable to read password from stdin",
)
}
password = string(stdin)
}
if compileOnly && targetURL == "" {
targetURL = "http://localhost"
}
url, err := url.Parse(targetURL)
@ -55,20 +61,15 @@ func GetCredentials(
)
}
baseURL := url.Scheme + "://" + url.Host
if url.Host == "" {
var ok bool
baseURL, ok = args["--base-url"].(string)
if !ok {
baseURL = config.BaseURL
if baseURL == "" {
return nil, errors.New(
"Confluence base URL should be specified using -l " +
"flag or be stored in configuration file",
)
}
if baseURL == "" {
return nil, errors.New(
"confluence base URL should be specified using -l " +
"flag or be stored in configuration file",
)
}
} else {
baseURL = url.Scheme + "://" + url.Host
}
baseURL = strings.TrimRight(baseURL, `/`)

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

34
util/error_handler.go Normal file
View File

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

196
util/flags.go Normal file
View File

@ -0,0 +1,196 @@
package util
import (
altsrc "github.com/urfave/cli-altsrc/v3"
altsrctoml "github.com/urfave/cli-altsrc/v3/toml"
"github.com/urfave/cli/v3"
)
var filename string
var Flags = []cli.Flag{
&cli.StringFlag{
Name: "files",
Aliases: []string{"f"},
Value: "",
Usage: "use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted).",
TakesFile: true,
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_FILES"), altsrctoml.TOML("files", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "continue-on-error",
Value: false,
Usage: "don't exit if an error occurs while processing a file, continue processing remaining files.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CONTINUE_ON_ERROR"), altsrctoml.TOML("continue-on-error", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "compile-only",
Value: false,
Usage: "show resulting HTML and don't update Confluence page content.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_COMPILE_ONLY"), altsrctoml.TOML("compile-only", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "dry-run",
Value: false,
Usage: "resolve page and ancestry, show resulting HTML and exit.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_DRY_RUN"), altsrctoml.TOML("dry-run", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "edit-lock",
Value: false,
Aliases: []string{"k"},
Usage: "lock page editing to current user only to prevent accidental manual edits over Confluence Web UI.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_EDIT_LOCK"), altsrctoml.TOML("edit-lock", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "drop-h1",
Value: false,
Usage: "don't include the first H1 heading in Confluence output.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_DROP_H1"), altsrctoml.TOML("drop-h1", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "strip-linebreaks",
Value: false,
Aliases: []string{"L"},
Usage: "remove linebreaks inside of tags, to accommodate non-standard Confluence behavior",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_STRIP_LINEBREAKS"), altsrctoml.TOML("strip-linebreaks", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "title-from-h1",
Value: false,
Usage: "extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TITLE_FROM_H1"), altsrctoml.TOML("title-from-h1", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "title-append-generated-hash",
Value: false,
Usage: "appends a short hash generated from the path of the page (space, parents, and title) to the title",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TITLE_APPEND_GENERATED_HASH"), altsrctoml.TOML("title-append-generated-hash", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "minor-edit",
Value: false,
Usage: "don't send notifications while updating Confluence page.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_MINOR_EDIT"), altsrctoml.TOML("minor-edit", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "version-message",
Value: "",
Usage: "add a message to the page version, to explain the edit (default: \"\")",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_VERSION_MESSAGE"), altsrctoml.TOML("version-message", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "color",
Value: "auto",
Usage: "display logs in color. Possible values: auto, never.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_COLOR"),
altsrctoml.TOML("color", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "log-level",
Value: "info",
Usage: "set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_LOG_LEVEL"), altsrctoml.TOML("log-level", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "username",
Aliases: []string{"u"},
Value: "",
Usage: "use specified username for updating Confluence page.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_USERNAME"),
altsrctoml.TOML("username", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "password",
Aliases: []string{"p"},
Value: "",
Usage: "use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PASSWORD"), altsrctoml.TOML("password", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "target-url",
Aliases: []string{"l"},
Value: "",
Usage: "edit specified Confluence page. If -l is not specified, file should contain metadata (see above).",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_TARGET_URL"), altsrctoml.TOML("target-url", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "base-url",
Aliases: []string{"b"},
Value: "",
Usage: "base URL for Confluence. Alternative option for base_url config field.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_BASE_URL"),
altsrctoml.TOML("base-url", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Value: ConfigFilePath(),
Usage: "use the specified configuration file.",
TakesFile: true,
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CONFIG")),
Destination: &filename,
},
&cli.BoolFlag{
Name: "ci",
Value: false,
Usage: "run on CI mode. It won't fail if files are not found.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CI"), altsrctoml.TOML("ci", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "space",
Value: "",
Usage: "use specified space key. If the space key is not specified, it must be set in the page metadata.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_SPACE"), altsrctoml.TOML("space", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "parents",
Value: "",
Usage: "A list containing the parents of the document separated by parents-delimiter (default: '/'). These will be prepended to the ones defined in the document itself.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PARENTS"), altsrctoml.TOML("parents", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "parents-delimiter",
Value: "/",
Usage: "The delimiter used for the parents list",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_PARENTS_DELIMITER"), altsrctoml.TOML("parents-delimiter", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "mermaid-provider",
Value: "cloudscript",
Usage: "defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_MERMAID_PROVIDER"), altsrctoml.TOML("mermaid-provider", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.FloatFlag{
Name: "mermaid-scale",
Value: 1.0,
Usage: "defines the scaling factor for mermaid renderings.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_MERMAID_SCALE"), altsrctoml.TOML("mermaid-scale", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringFlag{
Name: "include-path",
Value: "",
Usage: "Path for shared includes, used as a fallback if the include doesn't exist in the current directory.",
TakesFile: true,
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_INCLUDE_PATH"), altsrctoml.TOML("include-path", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.BoolFlag{
Name: "changes-only",
Value: false,
Usage: "Avoids re-uploading pages that haven't changed since the last run.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_CHANGES_ONLY"), altsrctoml.TOML("changes-only", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.FloatFlag{
Name: "d2-scale",
Value: 1.0,
Usage: "defines the scaling factor for d2 renderings.",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_D2_SCALE"), altsrctoml.TOML("d2-scale", altsrc.NewStringPtrSourcer(&filename))),
},
&cli.StringSliceFlag{
Name: "features",
Value: []string{"mermaid"},
Usage: "Enables optional features. Current features: d2, mermaid",
Sources: cli.NewValueSourceChain(cli.EnvVar("MARK_FEATURES"), altsrctoml.TOML("features", altsrc.NewStringPtrSourcer(&filename))),
},
}

19
vfs/vfs.go Normal file
View File

@ -0,0 +1,19 @@
package vfs
import (
"io"
"os"
)
type Opener interface {
Open(name string) (io.ReadWriteCloser, error)
}
type LocalOSOpener struct {
}
func (o LocalOSOpener) Open(name string) (io.ReadWriteCloser, error) {
return os.Open(name)
}
var LocalOS = LocalOSOpener{}