Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement placeholder replacement #46

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ usage: md2cf [-h] [-o HOST] [-u USERNAME] [-p PASSWORD] [--token TOKEN]
[--insecure] [-s SPACE] [-a PARENT_TITLE | -A PARENT_ID]
[-t TITLE] [-m MESSAGE] [-i PAGE_ID] [--prefix PREFIX]
[--strip-top-header] [--remove-text-newlines]
[--replace-all-labels] [--preface-markdown [PREFACE_MARKDOWN] |
--preface-file PREFACE_FILE]
[--replace-all-labels]
[--replace-placeholders-file REPLACE_PLACEHOLDERS_FILE]
[--preface-markdown [PREFACE_MARKDOWN] | --preface-file PREFACE_FILE]
[--postface-markdown [POSTFACE_MARKDOWN] | --postface-file
POSTFACE_FILE] [--collapse-single-pages] [--no-gitignore]
[--beautify-folders | --use-pages-file]
Expand Down Expand Up @@ -119,6 +120,51 @@ This is a document with hardcoded newlines in its paragraphs.
It's not that nice to read.
```

### Repalcing text before upload

The `--replace-placeholders-file` allows to specify a YAML formated configuration file that defines strings or regular expressions to be replaces by Confluence macros or other strings.
Example definitions can be found in [sample_placeholders.yaml](https://github.com/iamjackg/md2cf/blob/master/sample_placeholders.yaml).

This configuration file supports the following replacements:
* replace string with Confluence macro:

```yaml
'string-to-be-replaced':
type: 'macro'
name: 'name-of-confluence-macro'
parameters:
'macro-parameter-name1': 'macro-parameter-value1'
'macro-parameter-name2': 'macro-parameter-value2'
```

* repalce string with an other string:

```yaml
'long string to be replaced':
type: 'static'
text: 'new string'
```

* replace regular expression with Confluence macro:

```yaml
# replace hugo-hint-macro with Confluence info-macro
'(?ms){{% hint [^%=]*(?!{{% /hint %}})info.*? %}}(.*?){{% /hint %}}':
type: 'macro'
name: 'info'
parameters:
icon: 'true'
# additions extend the macro definition as required by some Confluence macros
additions:
# \1 is replaced with group matched in regex
- '<ac:rich-text-body>\1</ac:rich-text-body>'
```

The search string is always processed using [python-re](https://docs.python.org/3/library/re.html).
To test your regular expression you can use [Pythex](https://pythex.org/).
For syntax of Confluence macros you can check XHTML layout in [Confluence 5.7](https://confluence.atlassian.com/display/CONF57/Macros) documentation.
For a list of available Confluence macros see [latest Confluence macro list](https://confluence.atlassian.com/doc/macros-139387.html).

### Adding a preface and/or postface

The `--preface-markdown`, `--preface-file`, `--postface-markdown`, and `--postface-file` commands allow you to add some text at the top or bottom of each page. This is useful if you're mirroring documentation to Confluence and want people to know that it's going to be overwritten in an automated fashion.
Expand Down
27 changes: 27 additions & 0 deletions md2cf/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pprint
import re
import sys
import yaml
from collections import Counter
from pathlib import Path
from typing import List
Expand Down Expand Up @@ -105,6 +106,12 @@ def get_parser():
action="store_true",
help="replace all labels instead of only adding new ones",
)
page_group.add_argument(
"--replace-placeholders-file",
type=Path,
help="provide a YAML or JSON file with mapping of placeholders "
"in your markdown files and their replacements, e.g. Confluence Macro",
)

preface_group = page_group.add_mutually_exclusive_group()
preface_group.add_argument(
Expand Down Expand Up @@ -383,12 +390,32 @@ def main():
if args.password is None and args.token is None:
args.password = getpass.getpass()

if args.replace_placeholders_file:
try:
placeholders = yaml.load(
args.replace_placeholders_file.open(), Loader=yaml.SafeLoader
)
except yaml.YAMLError as error:
sys.stderr.write(
"Failed to load placeholders from file "
f"'{args.replace_placeholders_file}' due to\n{error}"
)
exit(1)
if not isinstance(placeholders, dict):
sys.stderr.write(
"The content of given placeholders file "
f"({args.replace_placeholders_file.open}) is not a valid.\n"
"Please check github-link for the syntax."
)
exit(1)

confluence = api.MinimalConfluence(
host=args.host,
username=args.username,
password=args.password,
token=args.token,
verify=not args.insecure,
placeholders=placeholders,
)

if (args.title or args.page_id) and (
Expand Down
77 changes: 74 additions & 3 deletions md2cf/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import tortilla
from requests.auth import HTTPBasicAuth
import requests.packages
import html
import re
from md2cf.confluence_renderer import ConfluenceRenderer, ConfluenceTag


class MinimalConfluence:
def __init__(self, host, username=None, password=None, token=None, verify=True):
def __init__(
self,
host,
username=None,
password=None,
token=None,
verify=True,
placeholders=None,
):
if token is not None:
self.api = tortilla.wrap(
host,
Expand All @@ -27,6 +38,8 @@ def __init__(self, host, username=None, password=None, token=None, verify=True):
requests.packages.urllib3.exceptions.InsecureRequestWarning
)

self.placeholders = placeholders or {}

def get_page(
self, title=None, space_key=None, page_id=None, additional_expansions=None
):
Expand Down Expand Up @@ -66,6 +79,54 @@ def get_page(
else:
raise ValueError("At least one of title or page_id must not be None")

def generate_macro(self, name, parameters=None, additions=None):
"""
generate and return Confluence macro syntax

based on https://community.atlassian.com/t5/Confluence-questions/Can-I-insert-the-History-macro-through-REST-API-call/qaq-p/1038703#M173031

Args:
name (str): short name of the Confluence macro
params (dict): optional. Parameters to configure the macro
additions (list of str): optional. Text to be added to the macro

Returns:
String with Confluence XHTML syntax of macro
"""
parameters = parameters or {}
additions = additions or []
cf_render = ConfluenceRenderer()
macro = cf_render.structured_macro(name)
for key, value in parameters.items():
macro.append(cf_render.parameter(name=key, value=value))
macro.append("".join(additions))
macro_text = macro.render()
return macro_text

def process_body(self, body):
"""
apply placeholder replacements to body

Args:
body (str): generated body

Returns:
String body with replacements
"""
for match, config in self.placeholders.items():
replace = ""
match = html.escape(match)
if config.get("type") == "macro":
replace = self.generate_macro(
config.get("name"),
config.get("parameters"),
config.get("additions"),
)
elif config.get("type") == "static":
replace = config.get("text")
body = re.sub(match, replace, body)
return body

def create_page(
self, space, title, body, parent_id=None, update_message=None, labels=None
):
Expand All @@ -88,7 +149,12 @@ def create_page(
"title": title,
"type": "page",
"space": {"key": space},
"body": {"storage": {"value": body, "representation": "storage"}},
"body": {
"storage": {
"value": self.process_body(body),
"representation": "storage",
}
},
}

if parent_id is not None:
Expand All @@ -113,7 +179,12 @@ def update_page(self, page, body, parent_id=None, update_message=None, labels=No
},
"title": page.title,
"type": "page",
"body": {"storage": {"value": body, "representation": "storage"}},
"body": {
"storage": {
"value": self.process_body(body),
"representation": "storage",
}
},
}

if parent_id is not None:
Expand Down
5 changes: 4 additions & 1 deletion md2cf/confluence_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ def render(self):
)
if namespaced_attribs
else "",
"".join([child.render() for child in self.children]),
"".join([child.render()
if isinstance(child, ConfluenceTag)
else child
for child in self.children]),
"<![CDATA[{}]]>".format(self.text) if self.cdata else self.text,
namespaced_name,
)
Expand Down
35 changes: 35 additions & 0 deletions sample_placeholders.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
# yamllint disable rule:line-length
# for regex you can test using:
# https://pythex.org/
# for macro syntax see:
# https://confluence.atlassian.com/doc/macros-139387.html
# https://confluence.atlassian.com/display/CONF57/Macros
# replace table-of-content macro of hugo theme geekdocs with Confluence macro.
'{{< toc >}}':
type: 'macro'
name: 'toc'
parameters:
printable: 'true'
style: 'disc'
maxLevel: '4'
minLevel: '1'
class: 'bigpink'
type: 'list'
outline: 'true'
include: '.*'
# replace placeholder with empty text
'long string to be replaced as example':
type: 'static'
text: ''
# replace hint-macro info with info-macro
'(?ms){{% hint [^%=]*(?!{{% /hint %}})info.*? %}}(.*?){{% /hint %}}':
type: 'macro'
name: 'info'
parameters:
icon: 'true'
title: ''
# additions extend the macro definition as required by some Confluence macros
additions:
# \1 is replaced with group matched in regex
- '<ac:rich-text-body>\1</ac:rich-text-body>'