2015-09-19 23:51:49 +06:00
package main
import (
2019-08-02 22:58:08 +03:00
"bytes"
2025-01-09 18:39:18 -06:00
"crypto/sha1"
"encoding/hex"
2015-09-19 23:51:49 +06:00
"fmt"
"os"
"path/filepath"
2025-01-09 18:39:18 -06:00
"regexp"
2024-06-11 22:57:31 +12:00
"slices"
2023-08-09 13:06:31 +02:00
"strings"
2022-04-22 04:17:48 -05:00
"time"
2015-09-19 23:51:49 +06:00
2023-12-01 12:31:09 +00:00
"github.com/bmatcuk/doublestar/v4"
2020-11-03 17:12:51 +03:00
"github.com/kovetskiy/lorg"
2024-09-26 15:24:39 +02:00
"github.com/kovetskiy/mark/attachment"
"github.com/kovetskiy/mark/confluence"
"github.com/kovetskiy/mark/includes"
"github.com/kovetskiy/mark/macro"
mark "github.com/kovetskiy/mark/markdown"
"github.com/kovetskiy/mark/metadata"
"github.com/kovetskiy/mark/page"
"github.com/kovetskiy/mark/stdlib"
"github.com/kovetskiy/mark/vfs"
2019-04-08 22:44:27 +03:00
"github.com/reconquest/karma-go"
2020-11-03 17:12:51 +03:00
"github.com/reconquest/pkg/log"
2023-04-18 15:06:16 +02:00
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
2015-09-19 23:51:49 +06:00
)
const (
2025-01-13 19:09:29 +01:00
version = "12.0.0"
2023-04-18 15:06:16 +02:00
usage = "A tool for updating Atlassian Confluence pages from markdown."
2023-04-12 13:43:34 +02:00
description = ` Mark is a tool to update Atlassian Confluence pages from markdown. Documentation is available here: https://github.com/kovetskiy/mark `
2015-09-19 23:51:49 +06:00
)
2023-04-18 15:06:16 +02:00
var flags = [ ] cli . Flag {
altsrc . NewStringFlag ( & cli . StringFlag {
2023-05-03 15:59:18 +02:00
Name : "files" ,
Aliases : [ ] string { "f" } ,
Value : "" ,
Usage : "use specified markdown file(s) for converting to html. Supports file globbing patterns (needs to be quoted)." ,
TakesFile : true ,
EnvVars : [ ] string { "MARK_FILES" } ,
2023-04-18 15:06:16 +02:00
} ) ,
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "compile-only" ,
Value : false ,
Usage : "show resulting HTML and don't update Confluence page content." ,
EnvVars : [ ] string { "MARK_COMPILE_ONLY" } ,
} ) ,
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "dry-run" ,
Value : false ,
Usage : "resolve page and ancestry, show resulting HTML and exit." ,
EnvVars : [ ] string { "MARK_DRY_RUN" } ,
} ) ,
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "edit-lock" ,
Value : false ,
Aliases : [ ] string { "k" } ,
Usage : "lock page editing to current user only to prevent accidental manual edits over Confluence Web UI." ,
EnvVars : [ ] string { "MARK_EDIT_LOCK" } ,
} ) ,
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "drop-h1" ,
Value : false ,
2023-04-20 12:43:08 +02:00
Aliases : [ ] string { "h1_drop" } ,
2023-05-19 18:47:55 +02:00
Usage : "don't include the first H1 heading in Confluence output." ,
2023-04-18 15:06:16 +02:00
EnvVars : [ ] string { "MARK_H1_DROP" } ,
} ) ,
2023-09-06 16:19:09 -07:00
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "strip-linebreaks" ,
Value : false ,
Aliases : [ ] string { "L" } ,
Usage : "remove linebreaks inside of tags, to accomodate non-standard Confluence behavior" ,
2024-12-04 17:25:00 +01:00
EnvVars : [ ] string { "MARK_STRIP_LINEBREAKS" } ,
2023-09-06 16:19:09 -07:00
} ) ,
2023-04-18 15:06:16 +02:00
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "title-from-h1" ,
Value : false ,
2023-04-20 12:43:08 +02:00
Aliases : [ ] string { "h1_title" } ,
2023-04-18 15:06:16 +02:00
Usage : "extract page title from a leading H1 heading. If no H1 heading on a page exists, then title must be set in the page metadata." ,
EnvVars : [ ] string { "MARK_H1_TITLE" } ,
} ) ,
2024-09-30 21:00:49 -04:00
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "title-append-generated-hash" ,
Value : false ,
Usage : "appends a short hash generated from the path of the page (space, parents, and title) to the title" ,
EnvVars : [ ] string { "MARK_TITLE_APPEND_GENERATED_HASH" } ,
} ) ,
2023-04-18 15:06:16 +02:00
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "minor-edit" ,
Value : false ,
Usage : "don't send notifications while updating Confluence page." ,
EnvVars : [ ] string { "MARK_MINOR_EDIT" } ,
} ) ,
2024-04-10 10:45:16 +01:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "version-message" ,
Value : "" ,
2024-04-11 19:39:11 +02:00
Usage : "add a message to the page version, to explain the edit (default: \"\")" ,
2024-04-10 10:45:16 +01:00
EnvVars : [ ] string { "MARK_VERSION_MESSAGE" } ,
} ) ,
2023-04-18 15:06:16 +02:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "color" ,
Value : "auto" ,
Usage : "display logs in color. Possible values: auto, never." ,
EnvVars : [ ] string { "MARK_COLOR" } ,
} ) ,
2024-12-30 21:40:33 +02:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "log-level" ,
Value : "info" ,
Usage : "set the log level. Possible values: TRACE, DEBUG, INFO, WARNING, ERROR, FATAL." ,
EnvVars : [ ] string { "MARK_LOG_LEVEL" } ,
2023-04-18 15:06:16 +02:00
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "username" ,
Aliases : [ ] string { "u" } ,
Value : "" ,
Usage : "use specified username for updating Confluence page." ,
EnvVars : [ ] string { "MARK_USERNAME" } ,
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "password" ,
Aliases : [ ] string { "p" } ,
Value : "" ,
Usage : "use specified token for updating Confluence page. Specify - as password to read password from stdin, or your Personal access token. Username is not mandatory if personal access token is provided. For more info please see: https://developer.atlassian.com/server/confluence/confluence-server-rest-api/#authentication." ,
EnvVars : [ ] string { "MARK_PASSWORD" } ,
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "target-url" ,
Aliases : [ ] string { "l" } ,
Value : "" ,
Usage : "edit specified Confluence page. If -l is not specified, file should contain metadata (see above)." ,
EnvVars : [ ] string { "MARK_TARGET_URL" } ,
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "base-url" ,
2023-04-20 12:43:08 +02:00
Aliases : [ ] string { "b" , "base_url" } ,
2023-04-18 15:06:16 +02:00
Value : "" ,
Usage : "base URL for Confluence. Alternative option for base_url config field." ,
EnvVars : [ ] string { "MARK_BASE_URL" } ,
} ) ,
& cli . StringFlag {
Name : "config" ,
Aliases : [ ] string { "c" } ,
2023-09-07 17:35:06 +02:00
Value : configFilePath ( ) ,
2023-04-18 15:06:16 +02:00
Usage : "use the specified configuration file." ,
TakesFile : true ,
EnvVars : [ ] string { "MARK_CONFIG" } ,
} ,
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "ci" ,
Value : false ,
Usage : "run on CI mode. It won't fail if files are not found." ,
EnvVars : [ ] string { "MARK_CI" } ,
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "space" ,
Value : "" ,
Usage : "use specified space key. If the space key is not specified, it must be set in the page metadata." ,
EnvVars : [ ] string { "MARK_SPACE" } ,
} ) ,
2023-08-09 13:06:31 +02:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "parents" ,
Value : "" ,
2023-11-22 15:11:55 +00:00
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." ,
2023-08-09 13:06:31 +02:00
EnvVars : [ ] string { "MARK_PARENTS" } ,
} ) ,
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "parents-delimiter" ,
Value : "/" ,
Usage : "The delimiter used for the parents list" ,
EnvVars : [ ] string { "MARK_PARENTS_DELIMITER" } ,
} ) ,
2023-04-12 13:43:34 +02:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "mermaid-provider" ,
Value : "cloudscript" ,
Usage : "defines the mermaid provider to use. Supported options are: cloudscript, mermaid-go." ,
EnvVars : [ ] string { "MARK_MERMAID_PROVIDER" } ,
} ) ,
2023-08-30 14:50:53 +02:00
altsrc . NewFloat64Flag ( & cli . Float64Flag {
Name : "mermaid-scale" ,
Value : 1.0 ,
Usage : "defines the scaling factor for mermaid renderings." ,
EnvVars : [ ] string { "MARK_MERMAID_SCALE" } ,
} ) ,
2023-10-17 12:53:10 +02:00
altsrc . NewStringFlag ( & cli . StringFlag {
Name : "include-path" ,
Value : "" ,
Usage : "Path for shared includes, used as a fallback if the include doesn't exist in the current directory." ,
TakesFile : true ,
EnvVars : [ ] string { "MARK_INCLUDE_PATH" } ,
} ) ,
2025-01-09 18:39:18 -06:00
altsrc . NewBoolFlag ( & cli . BoolFlag {
Name : "changes-only" ,
Value : false ,
Usage : "Avoids re-uploading pages that haven't changed since the last run." ,
2025-01-13 10:27:42 -06:00
EnvVars : [ ] string { "MARK_CHANGES_ONLY" } ,
2025-01-09 18:39:18 -06:00
} ) ,
2023-04-18 15:06:16 +02:00
}
2015-09-19 23:51:49 +06:00
func main ( ) {
2023-04-18 15:06:16 +02:00
app := & cli . App {
Name : "mark" ,
Usage : usage ,
Description : description ,
Version : version ,
Flags : flags ,
Before : altsrc . InitInputSourceWithContext ( flags ,
func ( context * cli . Context ) ( altsrc . InputSourceContext , error ) {
if context . IsSet ( "config" ) {
filePath := context . String ( "config" )
return altsrc . NewTomlSourceFromFile ( filePath )
} else {
2023-04-25 22:45:15 +02:00
// Fall back to default if config is unset and path exists
2023-09-07 17:35:06 +02:00
_ , err := os . Stat ( configFilePath ( ) )
2023-04-25 22:45:15 +02:00
if os . IsNotExist ( err ) {
return & altsrc . MapInputSource { } , nil
}
2023-09-07 17:35:06 +02:00
return altsrc . NewTomlSourceFromFile ( configFilePath ( ) )
2023-04-18 15:06:16 +02:00
}
} ) ,
EnableBashCompletion : true ,
HideHelpCommand : true ,
2023-08-09 13:06:31 +02:00
Action : RunMark ,
2015-09-19 23:51:49 +06:00
}
2023-04-18 15:06:16 +02:00
if err := app . Run ( os . Args ) ; err != nil {
2021-04-05 07:56:25 +03:00
log . Fatal ( err )
2021-04-02 13:15:54 -04:00
}
2023-04-18 15:06:16 +02:00
}
func RunMark ( cCtx * cli . Context ) error {
2024-12-30 21:40:33 +02:00
if err := setLogLevel ( cCtx ) ; err != nil {
return err
2020-11-03 17:12:51 +03:00
}
2016-10-25 17:23:28 +07:00
2023-04-18 15:06:16 +02:00
if cCtx . String ( "color" ) == "never" {
2021-03-17 09:24:26 +03:00
log . GetLogger ( ) . SetFormat (
lorg . NewFormat (
` $ { time:2006-01-02 15:04:05.000} $ { level:%s:left:true} $ { prefix}%s ` ,
) ,
)
log . GetLogger ( ) . SetOutput ( os . Stderr )
}
2023-04-18 15:06:16 +02:00
creds , err := GetCredentials ( cCtx . String ( "username" ) , cCtx . String ( "password" ) , cCtx . String ( "target-url" ) , cCtx . String ( "base-url" ) , cCtx . Bool ( "compile-only" ) )
2019-07-31 01:35:17 +03:00
if err != nil {
2023-04-18 15:06:16 +02:00
return err
2016-10-25 17:23:28 +07:00
}
2019-08-02 22:58:08 +03:00
api := confluence . NewAPI ( creds . BaseURL , creds . Username , creds . Password )
2023-12-01 12:31:09 +00:00
files , err := doublestar . FilepathGlob ( cCtx . String ( "files" ) )
2021-04-02 13:15:54 -04:00
if err != nil {
2023-04-18 15:06:16 +02:00
return err
2021-04-02 13:15:54 -04:00
}
if len ( files ) == 0 {
2021-12-02 09:38:47 +01:00
msg := "No files matched"
2023-04-18 15:06:16 +02:00
if cCtx . Bool ( "ci" ) {
2021-12-02 09:38:47 +01:00
log . Warning ( msg )
} else {
log . Fatal ( msg )
}
2021-04-02 13:15:54 -04:00
}
2023-05-03 15:59:18 +02:00
log . Debug ( "config:" )
for _ , f := range cCtx . Command . Flags {
flag := f . Names ( )
if flag [ 0 ] == "password" {
log . Debugf ( nil , "%20s: %v" , flag [ 0 ] , "******" )
} else {
log . Debugf ( nil , "%20s: %v" , flag [ 0 ] , cCtx . Value ( flag [ 0 ] ) )
}
}
2021-04-02 13:15:54 -04:00
// Loop through files matched by glob pattern
for _ , file := range files {
log . Infof (
nil ,
2021-04-05 07:56:25 +03:00
"processing %s" ,
2021-04-02 13:15:54 -04:00
file ,
)
2023-04-18 15:06:16 +02:00
target := processFile ( file , api , cCtx , creds . PageID , creds . Username )
2021-04-02 13:15:54 -04:00
2025-02-17 15:31:53 -05:00
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 )
}
2021-04-02 13:15:54 -04:00
}
2023-04-18 15:06:16 +02:00
return nil
2021-04-02 13:15:54 -04:00
}
2021-04-05 07:56:25 +03:00
func processFile (
file string ,
api * confluence . API ,
2023-04-18 15:06:16 +02:00
cCtx * cli . Context ,
2021-04-05 07:56:25 +03:00
pageID string ,
username string ,
) * confluence . PageInfo {
2023-01-03 18:54:04 +01:00
markdown , err := os . ReadFile ( file )
2016-10-25 17:23:28 +07:00
if err != nil {
2019-04-19 10:31:41 +03:00
log . Fatal ( err )
2015-09-20 00:58:39 +06:00
}
2021-11-08 20:15:59 +06:00
markdown = bytes . ReplaceAll ( markdown , [ ] byte ( "\r\n" ) , [ ] byte ( "\n" ) )
2023-08-09 13:06:31 +02:00
parents := strings . Split ( cCtx . String ( "parents" ) , cCtx . String ( "parents-delimiter" ) )
2024-09-30 21:00:49 -04:00
meta , markdown , err := metadata . ExtractMeta ( markdown , cCtx . String ( "space" ) , cCtx . Bool ( "title-from-h1" ) , parents , cCtx . Bool ( "title-append-generated-hash" ) )
2019-08-02 22:58:08 +03:00
if err != nil {
log . Fatal ( err )
2015-09-20 00:58:39 +06:00
}
2022-04-22 15:26:25 +06:00
2022-04-22 14:48:59 +05:30
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 {
2022-01-18 12:10:46 +06:00
log . Fatal (
2023-03-20 19:56:02 +01:00
` specified file doesn't contain metadata ` +
` and URL is not specified via command line ` +
` or doesn't contain pageId GET-parameter ` ,
2022-01-18 12:10:46 +06:00
)
2022-01-18 09:05:26 +03:00
}
2023-03-20 19:56:02 +01:00
if meta . Space == "" {
2023-03-20 22:54:11 +01:00
log . Fatal (
"space is not set ('Space' header is not set and '--space' option is not set)" ,
)
2022-01-18 09:05:26 +03:00
}
if meta . Title == "" {
log . Fatal (
` page title is not set ('Title' header is not set ` +
2022-06-06 08:38:23 +02:00
` and '--title-from-h1' option and 'h1_title' config is not set or there is no H1 in the file) ` ,
2022-01-18 09:05:26 +03:00
)
}
2019-08-02 22:58:08 +03:00
stdlib , err := stdlib . New ( api )
2015-09-19 23:51:49 +06:00
if err != nil {
2019-04-19 10:31:41 +03:00
log . Fatal ( err )
2015-09-19 23:51:49 +06:00
}
2019-08-02 22:58:08 +03:00
templates := stdlib . Templates
var recurse bool
for {
templates , markdown , recurse , err = includes . ProcessIncludes (
2022-02-02 18:02:01 +08:00
filepath . Dir ( file ) ,
2023-10-17 12:53:10 +02:00
cCtx . String ( "include-path" ) ,
2019-08-02 22:58:08 +03:00
markdown ,
templates ,
)
if err != nil {
log . Fatal ( err )
}
if ! recurse {
break
}
}
2022-02-02 16:03:06 +06:00
macros , markdown , err := macro . ExtractMacros (
filepath . Dir ( file ) ,
2023-10-17 12:53:10 +02:00
cCtx . String ( "include-path" ) ,
2022-02-02 16:03:06 +06:00
markdown ,
templates ,
)
2019-08-02 22:58:08 +03:00
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 )
}
}
2024-09-30 21:00:49 -04:00
links , err := page . ResolveRelativeLinks ( api , meta , markdown , filepath . Dir ( file ) , cCtx . String ( "space" ) , cCtx . Bool ( "title-from-h1" ) , parents , cCtx . Bool ( "title-append-generated-hash" ) )
2020-11-30 09:47:46 +02:00
if err != nil {
log . Fatalf ( err , "unable to resolve relative links" )
}
2020-12-04 00:28:52 +03:00
2024-09-26 15:24:39 +02:00
markdown = page . SubstituteLinks ( markdown , links )
2020-11-30 09:47:46 +02:00
2023-04-18 15:06:16 +02:00
if cCtx . Bool ( "dry-run" ) {
2024-09-26 15:24:39 +02:00
_ , _ , err := page . ResolvePage ( cCtx . Bool ( "dry-run" ) , api , meta )
2020-01-03 23:05:14 +03:00
if err != nil {
log . Fatalf ( err , "unable to resolve page location" )
}
}
2023-04-18 15:06:16 +02:00
if cCtx . Bool ( "compile-only" ) || cCtx . Bool ( "dry-run" ) {
if cCtx . Bool ( "drop-h1" ) {
2022-06-06 08:38:23 +02:00
log . Info (
"the leading H1 heading will be excluded from the Confluence output" ,
)
}
2022-10-19 20:44:04 +06:00
2023-09-06 16:19:09 -07:00
html , _ := mark . CompileMarkdown ( markdown , stdlib , file , cCtx . String ( "mermaid-provider" ) , cCtx . Float64 ( "mermaid-scale" ) , cCtx . Bool ( "drop-h1" ) , cCtx . Bool ( "strip-linebreaks" ) )
2023-04-12 13:43:34 +02:00
fmt . Println ( html )
2025-02-17 15:31:53 -05:00
return nil
2019-08-02 22:58:08 +03:00
}
2016-10-25 17:23:28 +07:00
2019-04-08 22:44:27 +03:00
var target * confluence . PageInfo
2016-10-25 17:23:28 +07:00
2019-04-08 22:04:30 +03:00
if meta != nil {
2024-09-26 15:24:39 +02:00
parent , page , err := page . ResolvePage ( cCtx . Bool ( "dry-run" ) , api , meta )
2016-10-25 17:23:28 +07:00
if err != nil {
2019-04-19 10:31:41 +03:00
log . Fatalf (
karma . Describe ( "title" , meta . Title ) . Reason ( err ) ,
2021-03-31 17:49:01 +01:00
"unable to resolve %s" ,
meta . Type ,
2019-04-19 10:31:41 +03:00
)
2016-10-25 17:23:28 +07:00
}
2020-01-03 23:05:14 +03:00
if page == nil {
2021-04-05 07:56:25 +03:00
page , err = api . CreatePage (
meta . Space ,
meta . Type ,
parent ,
meta . Title ,
` ` ,
)
2020-01-03 23:05:14 +03:00
if err != nil {
log . Fatalf (
err ,
2021-03-31 17:49:01 +01:00
"can't create %s %q" ,
meta . Type ,
2020-01-03 23:05:14 +03:00
meta . Title ,
)
}
2022-04-22 04:17:48 -05:00
// (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 )
2020-01-03 23:05:14 +03:00
}
2016-10-25 17:23:28 +07:00
target = page
} else {
2021-04-02 13:15:54 -04:00
if pageID == "" {
2019-04-19 10:31:41 +03:00
log . Fatalf ( nil , "URL should provide 'pageId' GET-parameter" )
2016-10-25 17:23:28 +07:00
}
2021-04-02 13:15:54 -04:00
page , err := api . GetPageByID ( pageID )
2016-10-25 17:23:28 +07:00
if err != nil {
2019-04-19 10:31:41 +03:00
log . Fatalf ( err , "unable to retrieve page by id" )
2016-10-25 17:23:28 +07:00
}
target = page
}
2023-04-12 13:43:34 +02:00
// Resolve attachments created from <!-- Attachment: --> directive
2023-09-01 22:59:04 +02:00
localAttachments , err := attachment . ResolveLocalAttachments ( vfs . LocalOS , filepath . Dir ( file ) , meta . Attachments )
2023-04-12 13:43:34 +02:00
if err != nil {
log . Fatalf ( err , "unable to locate attachments" )
}
2023-09-01 22:59:04 +02:00
attaches , err := attachment . ResolveAttachments (
2022-01-18 12:10:46 +06:00
api ,
target ,
2023-04-12 13:43:34 +02:00
localAttachments ,
2022-01-18 12:10:46 +06:00
)
2019-04-19 17:35:05 +03:00
if err != nil {
log . Fatalf ( err , "unable to create/update attachments" )
}
2019-04-19 10:31:41 +03:00
2023-09-01 22:59:04 +02:00
markdown = attachment . CompileAttachmentLinks ( markdown , attaches )
2019-04-20 10:24:30 +03:00
2023-04-18 15:06:16 +02:00
if cCtx . Bool ( "drop-h1" ) {
2021-03-17 09:24:26 +03:00
log . Info (
2021-04-05 07:56:25 +03:00
"the leading H1 heading will be excluded from the Confluence output" ,
2021-03-17 09:24:26 +03:00
)
2020-11-20 08:44:41 -06:00
}
2023-09-06 16:19:09 -07:00
html , inlineAttachments := mark . CompileMarkdown ( markdown , stdlib , file , cCtx . String ( "mermaid-provider" ) , cCtx . Float64 ( "mermaid-scale" ) , cCtx . Bool ( "drop-h1" ) , cCtx . Bool ( "strip-linebreaks" ) )
2023-04-12 13:43:34 +02:00
// Resolve attachements detected from markdown
2023-09-01 22:59:04 +02:00
_ , err = attachment . ResolveAttachments (
2023-04-12 13:43:34 +02:00
api ,
target ,
inlineAttachments ,
)
if err != nil {
log . Fatalf ( err , "unable to create/update attachments" )
}
2019-04-20 10:24:30 +03:00
2019-08-02 22:58:08 +03:00
{
var buffer bytes . Buffer
err := stdlib . Templates . ExecuteTemplate (
& buffer ,
"ac:layout" ,
struct {
2021-06-17 14:56:27 -04:00
Layout string
Sidebar string
Body string
2019-08-02 22:58:08 +03:00
} {
2021-06-17 14:56:27 -04:00
Layout : meta . Layout ,
Sidebar : meta . Sidebar ,
Body : html ,
2019-08-02 22:58:08 +03:00
} ,
)
if err != nil {
log . Fatal ( err )
}
html = buffer . String ( )
}
2025-01-09 18:39:18 -06:00
var finalVersionMessage string
var shouldUpdatePage bool = true
if cCtx . Bool ( "changes-only" ) {
contentHash := getSHA1Hash ( html )
log . Debugf (
2025-01-09 18:44:25 -06:00
nil ,
"content hash: %s" ,
contentHash ,
2025-01-09 18:39:18 -06:00
)
versionPattern := ` \[v([a-f0-9] { 40})]$ `
2025-02-14 17:10:45 +01:00
re := regexp . MustCompile ( versionPattern )
2025-01-09 18:39:18 -06:00
matches := re . FindStringSubmatch ( target . Version . Message )
if len ( matches ) > 1 {
log . Debugf (
2025-01-09 18:44:25 -06:00
nil ,
"previous content hash: %s" ,
matches [ 1 ] ,
2025-01-09 18:39:18 -06:00
)
if matches [ 1 ] == contentHash {
log . Infof (
2025-01-09 18:44:25 -06:00
nil ,
"page %q is already up to date" ,
target . Title ,
2025-01-09 18:39:18 -06:00
)
shouldUpdatePage = false
}
}
finalVersionMessage = fmt . Sprintf ( "%s [v%s]" , cCtx . String ( "version-message" ) , contentHash )
} else {
finalVersionMessage = cCtx . String ( "version-message" )
}
if shouldUpdatePage {
2025-02-14 17:10:45 +01:00
err = api . UpdatePage ( target , html , cCtx . Bool ( "minor-edit" ) , finalVersionMessage , meta . Labels , meta . ContentAppearance , meta . Emoji )
2025-01-09 18:39:18 -06:00
if err != nil {
log . Fatal ( err )
}
2015-09-19 23:51:49 +06:00
}
2024-06-12 08:51:13 +12:00
updateLabels ( api , target , meta )
2024-06-11 22:57:31 +12:00
2023-04-18 15:06:16 +02:00
if cCtx . Bool ( "edit-lock" ) {
2019-04-19 10:31:41 +03:00
log . Infof (
nil ,
` edit locked on page %q by user %q to prevent manual edits ` ,
2016-10-25 17:23:28 +07:00
target . Title ,
2021-04-02 13:15:54 -04:00
username ,
2016-10-25 17:23:28 +07:00
)
2021-04-05 07:56:25 +03:00
err := api . RestrictPageUpdates ( target , username )
2016-10-25 17:23:28 +07:00
if err != nil {
2019-04-19 10:31:41 +03:00
log . Fatal ( err )
2016-10-25 17:23:28 +07:00
}
}
2021-04-02 13:15:54 -04:00
return target
2015-09-19 23:51:49 +06:00
}
2023-09-07 17:35:06 +02:00
2024-09-26 15:24:39 +02:00
func updateLabels ( api * confluence . API , target * confluence . PageInfo , meta * metadata . Meta ) {
2024-06-11 22:57:31 +12:00
labelInfo , err := api . GetPageLabels ( target , "global" )
if err != nil {
log . Fatal ( err )
}
log . Debug ( "Page Labels:" )
log . Debug ( labelInfo . Labels )
log . Debug ( "Meta Labels:" )
log . Debug ( meta . Labels )
delLabels := determineLabelsToRemove ( labelInfo , meta )
log . Debug ( "Del Labels:" )
log . Debug ( delLabels )
addLabels := determineLabelsToAdd ( meta , labelInfo )
log . Debug ( "Add Labels:" )
log . Debug ( addLabels )
if len ( addLabels ) > 0 {
_ , err = api . AddPageLabels ( target , addLabels )
if err != nil {
log . Fatal ( err )
}
}
for _ , label := range delLabels {
_ , err = api . DeletePageLabel ( target , label )
if err != nil {
log . Fatal ( err )
}
}
}
// Page has label but label not in Metadata
2024-09-26 15:24:39 +02:00
func determineLabelsToRemove ( labelInfo * confluence . LabelInfo , meta * metadata . Meta ) [ ] string {
2024-06-11 22:57:31 +12:00
var labels [ ] string
for _ , label := range labelInfo . Labels {
if ! slices . ContainsFunc ( meta . Labels , func ( metaLabel string ) bool {
2024-06-13 08:55:48 +12:00
return strings . EqualFold ( metaLabel , label . Name )
2024-06-11 22:57:31 +12:00
} ) {
labels = append ( labels , label . Name )
}
}
return labels
}
// Metadata has label but Page does not have it
2024-09-26 15:24:39 +02:00
func determineLabelsToAdd ( meta * metadata . Meta , labelInfo * confluence . LabelInfo ) [ ] string {
2024-06-11 22:57:31 +12:00
var labels [ ] string
for _ , metaLabel := range meta . Labels {
if ! slices . ContainsFunc ( labelInfo . Labels , func ( label confluence . Label ) bool {
2024-06-13 08:55:48 +12:00
return strings . EqualFold ( label . Name , metaLabel )
2024-06-11 22:57:31 +12:00
} ) {
labels = append ( labels , metaLabel )
}
}
return labels
}
2023-09-07 17:35:06 +02:00
func configFilePath ( ) string {
fp , err := os . UserConfigDir ( )
if err != nil {
log . Fatal ( err )
}
return filepath . Join ( fp , "mark" )
}
2024-12-30 21:40:33 +02:00
func setLogLevel ( cCtx * cli . Context ) error {
logLevel := cCtx . String ( "log-level" )
2025-01-09 21:03:15 +02:00
switch strings . ToUpper ( logLevel ) {
case lorg . LevelTrace . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelTrace )
2025-01-09 21:03:15 +02:00
case lorg . LevelDebug . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelDebug )
2025-01-09 21:03:15 +02:00
case lorg . LevelInfo . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelInfo )
2025-01-09 21:03:15 +02:00
case lorg . LevelWarning . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelWarning )
2025-01-09 21:03:15 +02:00
case lorg . LevelError . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelError )
2025-01-09 21:03:15 +02:00
case lorg . LevelFatal . String ( ) :
2024-12-30 21:40:33 +02:00
log . SetLevel ( lorg . LevelFatal )
default :
return fmt . Errorf ( "unknown log level: %s" , logLevel )
}
log . GetLevel ( )
return nil
}
2025-01-09 18:39:18 -06:00
func getSHA1Hash ( input string ) string {
hash := sha1 . New ( )
hash . Write ( [ ] byte ( input ) )
return hex . EncodeToString ( hash . Sum ( nil ) )
2025-01-13 19:09:29 +01:00
}