diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4fc0fb --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# mkdocs-with-confluence + +MkDocs plugin that converts markdown pages into confluence markup +and export it to the Confluence page + +## Setup +Install the plugin using pip: + +`pip install mkdocs-with-confluence` + +Activate the plugin in `mkdocs.yml`: + +```yaml +plugins: + - search + - mkdocs-with-confluence +``` + +More information about plugins in the [MkDocs documentation][mkdocs-plugins]. + +## Usage + +Use following config and adjust it according to your needs: + +```yaml + - mkdocs-with-confluence: + host_url: https:///rest/api/content + space: + parent_page_name: + username: + password: + enabled_if_env: MKDOCS_TO_CONFLUENCE + #verbose: true + #debug: true + dryrun: true +``` + +## Parameters: + +### Requirements +- md2cf +- mimetypes +- mistune diff --git a/mkdocs_with_confluence/__init__.py b/mkdocs_with_confluence/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mkdocs_with_confluence/plugin.py b/mkdocs_with_confluence/plugin.py new file mode 100644 index 0000000..b4f1365 --- /dev/null +++ b/mkdocs_with_confluence/plugin.py @@ -0,0 +1,367 @@ +import time +import os +import re +import tempfile +import shutil +import requests +import mimetypes +import mistune +from mkdocs.config import config_options +from mkdocs.plugins import BasePlugin +from md2cf.confluence_renderer import ConfluenceRenderer + +TEMPLATE_BODY = "

TEMPLATE

" + +class MkdocsWithConfluence(BasePlugin): + _id = 0 + config_scheme = ( + ('host_url', config_options.Type(str, default=None)), + ('space', config_options.Type(str, default=None)), + ('parent_page_name', config_options.Type(str, default=None)), + ('username', config_options.Type(str, default=None)), + ('password', config_options.Type(str, default=None)), + ('enabled_if_env', config_options.Type(str, default=None)), + ('verbose', config_options.Type(bool, default=False)), + ('debug', config_options.Type(bool, default=False)), + ('dryrun', config_options.Type(bool, default=False)), + ) + + def __init__(self): + self.enabled = True + self.confluence_renderer = ConfluenceRenderer(use_xhtml=True) + self.confluence_mistune = mistune.Markdown(renderer=self.confluence_renderer) + self.simple_log = True + self.flen = 1 + + def on_files(self, files, config): + self.flen = len(files.documentation_pages()) + print(f"Number of Files: {self.flen}") + + #def on_pre_template(self, template, template_name, config): + def on_post_template(self, output_content, template_name, config): + if self.config['verbose'] is False and self.config['debug'] is False: + self.simple_log = True + print("INFO - Mkdocs With Confluence: Start exporting markdown pages... (simple logging)") + else: + self.simple_log = False + def on_config(self, config): + if 'enabled_if_env' in self.config: + env_name = self.config['enabled_if_env'] + if env_name: + self.enabled = os.environ.get(env_name) == '1' + if not self.enabled: + print( + 'WARNING - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned OFF: ' + f'(set environment variable {env_name} to 1 to enable)' + ) + return + else: + print("INFO - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned ON by var {env_name}==1!") + self.enabled = True + else: + print( + 'WARNING - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned OFF: ' + f'(set environment variable {env_name} to 1 to enable)' + ) + return + else: + print("INFO - Mkdocs With Confluence: Exporting MKDOCS pages to Confluence turned ON by default!") + self.enabled = True + + if self.config['dryrun']: + print("WARNING - Mkdocs With Confluence - DRYRUN MODE turned ON") + self.dryrun = True + else: + self.dryrun = False + + def on_page_markdown(self, markdown, page, config, files): + MkdocsWithConfluence._id += 1 + self.pw = self.config['password'] + self.user = self.config['username'] + + if self.enabled: + if self.simple_log is True: + print("INFO - Mkdocs With Confluence: Page export progress: [", end='', flush=True) + for i in range(MkdocsWithConfluence._id): + print("#", end='', flush=True) + for j in range(self.flen - MkdocsWithConfluence._id): + print("-", end='', flush=True) + print(f"] ({MkdocsWithConfluence._id} / {self.flen})", end='\r', flush=True) + + if self.config['debug']: + print(f"\nHandling Page '{page.title}' (And Parent Nav Pages if necessary):\n") + if not all(self.config_scheme): + print("ERR: YOU HAVE EMPTY VALUES IN YOUR CONFIG. ABORTING") + return markdown + try: + parent = self.__get_section_title(page.ancestors[0].__repr__()) + + if self.config['parent_page_name'] is not None: + main_parent = self.config['parent_page_name'] + else: + main_parent = self.config['space'] + + try: + parent1 = self.__get_section_title(page.ancestors[1].__repr__()) + except: + parent1 = main_parent + if self.config['debug']: + print(f"ONLY ONE PARENT FOUND. ASSUMING AS A FIRST NODE after main parent config {main_parent}") + + if self.config['debug']: + print(f"PARENT0: {parent}, PARENT1: {parent1}, MAIN PARENT: {main_parent}") + + tf = tempfile.NamedTemporaryFile(delete=False) + f = open(tf.name, "w") + + files = [] + try: + for match in re.finditer(r'img src="file://(.*)" s', markdown): + if self.config['debug']: + print(f"FOUND IMAGE: {match.group(1)}") + files.append(match.group(1)) + except AttributeError: + if self.config['debug']: + print("WARN: No images found in markdown. Proceed..") + + new_markdown = re.sub(r'', '!', new_markdown) + confluence_body = self.confluence_mistune(new_markdown) + f.write(confluence_body) + page_name = page.title + new_name = "confluence_page_" + page_name.replace(" ", "_") + ".html" + shutil.copy(f.name, new_name) + f.close() + + if self.config['debug']: + print(f"\nUPDATING PAGE TO CONFLUENCE, DETAILS:\n" + f"HOST: {self.config['host_url']}\n" + f"SPACE: {self.config['space']}\n" + f"TITLE: {page.title}\n" + f"PARENT: {parent}\n" + f"BODY: {confluence_body}\n" + ) + + page_id = self.find_page_id(page.title) + if page_id is not None: + if self.config['debug']: + print(f"JUST ONE STEP FROM UPDATE OF PAGE '{page.title}' \n" \ + f"CHECKING IF PARENT PAGE ON CONFLUENCE IS THE SAME AS HERE") + + parent_name = self.find_parent_name_of_page(page.title) + + if parent_name == parent: + if self.config['debug']: + print(f" - OK, Parents match. Continue...") + else: + if self.config['debug']: + print(f" - ERR, Parents does not match: '{parent}' =/= '{parent_name}' Abort before messing up...") + return markdown + self.update_page(page.title, confluence_body) + else: + parent_id = self.find_page_id(parent) + if not parent_id: + if not parent_id: + second_parent_id = self.find_page_id(parent1) + if not second_parent_id: + main_parent_id = self.find_page_id(main_parent) + if not main_parent_id: + print("ERR: MAIN PARENT UNKNOWN. ABORTING!") + return markdown + body = TEMPLATE_BODY.replace("TEMPLATE", parent1) + self.add_page(parent1, main_parent_id, body) + #time.sleep(1) + body = TEMPLATE_BODY.replace("TEMPLATE", parent) + self.add_page(parent, parent_id, body) + #time.sleep(1) + self.add_page(page.title, parent_id, confluence_body) + + if files: + if self.config['debug']: + print(f"\nUPLOADING ATTACHMENTS TO CONFLUENCE, DETAILS:\n" + f"FILES: {files}\n" + ) + for f in files: + self.add_attachment(page.title, f) + + except IndexError: + return markdown + + return markdown + + def on_page_content(self, html, page, config, files): + return html + + def __get_section_title(self, section): + return re.search("Section\(title=\'(.*)\'\)", section).group(1) + + def add_attachment(self, page_name, filepath): + if self.config['verbose']: + print(f"INFO - Mkdocs With Confluence * {page_name} *NEW ATTACHMENT* {filepath}") + if self.config['debug']: + print(f" * Mkdocs With Confluence: Add Attachment: PAGE NAME: {page_name}, FILE: {filepath}") + page_id = self.find_page_id(page_name) + if page_id: + url = self.config['host_url'] + "/" + \ + page_id + '/child/attachment/' + headers = {'X-Atlassian-Token': 'no-check'} #no content-type here! + if self.config['debug']: + print(f"URL: {url}") + filename = filepath + auth = (self.user, self.pw) + + # determine content-type + content_type, encoding = mimetypes.guess_type(filename) + if content_type is None: + content_type = 'multipart/form-data' + files = {'file': (filename, open(filename, 'rb'), content_type)} + + if not self.dryrun: + r = requests.post(url, headers=headers, files=files, auth=auth) + r.raise_for_status() + if r.status_code == 200: + print ('OK!') + else: + print ('ERR!') + else: + if self.config['debug']: + print("PAGE DOES NOT EXISTS") + + def find_page_id(self, page_name): + if self.config['debug']: + print(f"INFO - * Mkdocs With Confluence: Find Page ID: PAGE NAME: {page_name}") + name_confl = page_name.replace(" ", "+") + url = self.config['host_url'] + "?title=" + \ + name_confl + '&spaceKey=' + self.config['space'] + \ + '&expand=history' + if self.config['debug']: + print(f"URL: {url}") + auth = (self.user, self.pw) + r = requests.get(url, auth=auth) + r.raise_for_status() + response_json = r.json() + if response_json["results"]: + if self.config['debug']: + print(f"ID: {response_json['results'][0]['id']}") + return response_json['results'][0]['id'] + else: + if self.config['debug']: + print("PAGE DOES NOT EXIST") + return None + + def add_page(self, page_name, parent_page_id, page_content_in_storage_format): + if self.config['verbose']: + print(f"INFO - * Mkdocs With Confluence: {page_name} - *NEW PAGE*") + if self.config['debug']: + print(f" * Mkdocs With Confluence: Adding Page: PAGE NAME: {page_name}, parent ID: {parent_page_id}") + url = self.config['host_url'] + "/" + if self.config['debug']: + print(f"URL: {url}") + headers = {'Content-Type': 'application/json'} + auth = (self.user, self.pw) + space = self.config['space'] + data = { + "type": "page", + "title": page_name, + "space":{"key": space}, + "ancestors": [{"id": parent_page_id}], + "body":{ + "storage": { + "value": page_content_in_storage_format, + "representation": "storage" + } + } + } + + if not self.dryrun: + r = requests.post(url, json=data, headers=headers, auth=auth) + r.raise_for_status() + if r.status_code == 200: + if self.config['debug']: + print ('OK!') + else: + if self.config['debug']: + print ('ERR!') + + def update_page(self, page_name, page_content_in_storage_format): + page_id = self.find_page_id(page_name) + if self.config['verbose']: + print(f"INFO - * Mkdocs With Confluence: {page_name} - *UPDATE*") + if self.config['debug']: + print(f" * Mkdocs With Confluence: Update PAGE ID: {page_id}, PAGE NAME: {page_name}") + if page_id: + page_version = self.find_page_version(page_name) + page_version = page_version + 1 + url = self.config['host_url'] + "/" + page_id + if self.config['debug']: + print(f"URL: {url}") + pid = f"{page_id}" + headers = {'Content-Type': 'application/json'} + auth = (self.user, self.pw) + data = { + "id": page_id, + "title": page_name, + "type": "page", + "space":{"key":"RFSW"}, + "body":{ + "storage": { + "value": page_content_in_storage_format, + "representation": "storage" + } + }, + "version":{"number": page_version} + } + + if not self.dryrun: + r = requests.put(url, json=data, headers=headers, auth=auth) + r.raise_for_status() + if r.status_code == 200: + if self.config['debug']: + print ('OK!') + else: + if self.config['debug']: + print ('ERR!') + else: + if self.config['debug']: + print("PAGE DOES NOT EXIST YET!") + + def find_page_version(self, page_name): + if self.config['debug']: + print(f"INFO - * Mkdocs With Confluence: Find PAGE VERSION, PAGE NAME: {page_name}") + name_confl = page_name.replace(" ", "+") + url = self.config['host_url'] + "?title=" + \ + name_confl + '&spaceKey=' + \ + self.config['space'] + '&expand=version' + + auth = (self.user, self.pw) + r = requests.get(url, auth=auth) + r.raise_for_status() + response_json = r.json() + if response_json["results"]: + if self.config['debug']: + print(f"VERSION: {response_json['results'][0]['version']['number']}") + return response_json['results'][0]['version']['number'] + else: + if self.config['debug']: + print("PAGE DOES NOT EXISTS") + return None + + def find_parent_name_of_page(self, name): + if self.config['debug']: + print(f"INFO - * Mkdocs With Confluence: Find PARENT OF PAGE, PAGE NAME: {name}") + idp = self.find_page_id(name) + name_confl = name.replace(" ", "+") + url = self.config['host_url'] + "/" + idp + "?expand=ancestors" + + auth = (self.user, self.pw) + r = requests.get(url, auth=auth) + r.raise_for_status() + response_json = r.json() + if response_json: + if self.config['debug']: + print(f"PARENT NAME: {response_json['ancestors'][0]['title']}") + return response_json['ancestors'][0]['title'] + else: + if self.config['debug']: + print("PAGE DOES NOT HAVE PARENT") + return None diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..71d5f14 --- /dev/null +++ b/setup.py @@ -0,0 +1,26 @@ +from setuptools import setup, find_packages + +setup( + name='mkdocs-with-confluence', + version='0.1.0', + description='MkDocs plugin for uploading markdown documentation to Confluence via Confluence REST API', + keywords='mkdocs markdown confluence documentation rest python', + url='https://github.com/pawelsikora/mkdocs-with-confluence/', + author='Pawel Sikora', + author_email='sikor6@gmail.com', + license='MIT', + python_requires='>=3.6', + install_requires=[ + 'mkdocs>=1.1', + 'md2cf', + 'mimetypes', + 'mistune', + 'jinja2' + ], + packages=find_packages(), + entry_points={ + 'mkdocs.plugins': [ + 'mkdocs-with-confluence = mkdocs_with_confluence.plugin:MkdocsWithConfluence' + ] + } +)