diff --git a/README.md b/README.md index 18750cf..3bd259e 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,69 @@ folderA/ ``` +## Replacements + +By using either command line replacement pairs or specifying a file with json defintions of replacements an arbitrary regexp ("pattern") can be detected and replaced with another string, including expanding captured groups in the pattern. + +The replacement phase is taking place just before upsert, so all other textual manipulations are done by that time. + +Replacements happen in a deterministic sequence. There are ample opportunities to get unexpected (but logically consistent) results by inadvertently result of a previous replacement. +
+Format of json file + +```json +{ + "environment": [ + { + "import": "", + "path": "" + } + ], + "replacements":[ + { + "name": "", + "pattern": "", + "new_value": "" + "evaluate": + }, + ] +} +``` +
+ +### Advanced replacements + +The `environment` block is optional and used for very dynamic replacements. By specifying a python source file, it will be dynamically imported at run time. The `new_value` field can then specify a `.` that returns a string value. As an example, the following adds a replacement of "TODAY" to an iso-formatted datetime. + +```json +{ + "environment": [ + { + "import": "funcs", + "path": "funcs.py" + } + ], + "replacements":[ + { + "name": "Todays date", + "pattern": "TODAY", + "new_value": "funcs.today" + "evaluate": true + }, + ] +} +``` + +Funcs.py +```python +import datetime + +def today(term): + return datetime.datetime.now().isoformat() +``` + +The parameter `term` is a Match object as per using [re.subn](https://docs.python.org/3/library/re.html#re.subn). + ## Terminal output format By default, `md2cf` produces rich output with animated progress bars that are meant for human consumption. If the output is redirected to a file, the progress bars will not be displayed and only the final result will be written to the file. Error messages are always printed to standard error. diff --git a/md2cf/__main__.py b/md2cf/__main__.py index 7db41fb..7eb6808 100644 --- a/md2cf/__main__.py +++ b/md2cf/__main__.py @@ -23,6 +23,7 @@ minimal_output_console, ) from md2cf.document import Page +from md2cf.replacements import create_replacements from md2cf.tui import Md2cfTUI from md2cf.upsert import upsert_attachment, upsert_page @@ -258,6 +259,20 @@ def get_parser(): help="number of retry attempts if any API call fails", ) + parser.add_argument( + "--replace", + nargs="+", + action="append", + dest="replacements", + help="Specify replacements on the form =. Can be repeated many times", + ) + + parser.add_argument( + "--replacements", + dest="replacementfile", + help="Filename with replacement definition in json format", + ) + return parser @@ -299,6 +314,8 @@ def main(): console.quiet = True json_output_console.quiet = False + replacements = create_replacements(args.replacements, args.replacementfile) + confluence = api.MinimalConfluence( host=args.host, username=args.username, @@ -398,6 +415,8 @@ def main(): for page in pages_to_upload: pre_process_page(page, args, postface_markup, preface_markup, space_info) tui.start_item_task(page.original_title) + for replacement in replacements: + page = replacement.replace(page) upsert_page_result = None try: tui.set_item_progress_label(page.original_title, "Upserting") diff --git a/md2cf/replacements.py b/md2cf/replacements.py new file mode 100644 index 0000000..a5caa48 --- /dev/null +++ b/md2cf/replacements.py @@ -0,0 +1,70 @@ +import importlib.util +import json +import re +from typing import List + +from md2cf.console_output import console + + +class Replacement: + def __init__( + self, name: str, pattern: str, new_value: str, evaluate: bool = False + ) -> None: + self.name = name + self.pattern = pattern + self.new_value = new_value + self.evaluate = evaluate + + def replace(self, page): + console.print(f"Performing replacement '{self.name}'") + if self.evaluate: + new_value = eval(self.new_value) + else: + new_value = self.new_value + page.body, count = re.subn(f"({self.pattern})", new_value, page.body) + console.print(f">> {count} replacements made") + return page + + def __repr__(self) -> str: + return self.name + + +def create_replacements(replacements, replacementfile: str) -> List[Replacement]: + result = [] + commandline_replacements = ( + [item for sublist in replacements for item in sublist] if replacements else [] + ) + + # Create Replacement objects for the commandline replacements + for i, r in enumerate(commandline_replacements): + result.append(Replacement(f"CLI replacement {i}", *r.split("=", 1))) + + # Opt out if no file specified + if not replacementfile: + return result + + file_replacements = json.load(open(replacementfile)) + # Do we need to load any modules? + for env in file_replacements.get("environment", []): + if env.get("import"): + spec = importlib.util.spec_from_file_location( + env["import"], env.get("path") + ) + globals()[env["import"]] = importlib.util.module_from_spec(spec) + spec.loader.exec_module(globals()[env["import"]]) + + # Get the replacement definitions + for i, r in enumerate(file_replacements["replacements"]): + new_value = r["new_value"] + if isinstance(new_value, list): + new_value = "\n".join(new_value) + result.append( + Replacement( + r.get("name", f"File replacement {i}"), + r["pattern"], + new_value, + r.get("evaluate", False), + ) + ) + + return result