Publish vNote to Hugo blog post
$ python publish_vnote_to_hugo.py <
import fileinput
import os
import re
import shutil
import subprocess
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from datetime import datetime
from pathlib import Path
from tempfile import NamedTemporaryFile
from py_executable_checklist.workflow import WorkflowBase, run_command, run_workflow
from slug import slug
Common functions
def relative_image_directory():
now = datetime.now()
year = now.strftime("%Y")
month = now.strftime("%m")
day = now.strftime("%d")
return f"images/{year}/{month}/{day}"
def replace_string_in_file(f, from_string, to_string):
print(f"Replacing {from_string} in {f} to {to_string}")
with fileinput.FileInput(f, inplace=True) as file:
for line in file:
print(line.replace(from_string, to_string), end="")
Workflow steps
Load vNote post
class LoadVNotePost(WorkflowBase):
vnote: str
def run(self, context):
vnote_post_path = Path(self.vnote)
vnote_post = vnote_post_path.read_text()
vnote_post_type = vnote_post_path.parent.name
target_file_name = vnote_post_path.name
context["vnote_post_type"] = vnote_post_type
context["file_name"] = target_file_name
context["vnote_post"] = vnote_post
Swap PlantUML with image tag
class SwapPlantUmlWithImageTag(WorkflowBase):
vnote_post: str
def run(self, context):
puml_files = []
modified_post = self.vnote_post
for m in re.finditer("```puml(.*?)```", self.vnote_post, re.DOTALL):
diagram = m.group(0)
temp_diagram = Path(NamedTemporaryFile(suffix=".puml").name)
temp_diagram.write_text(diagram)
image_directory = relative_image_directory()
image_path = f"{image_directory}/{temp_diagram.stem}.png"
image_tag = f"![](/{image_path})"
modified_post = modified_post.replace(
diagram,
f"<!---{os.linesep}{diagram}{os.linesep}--->{os.linesep}{image_tag}",
)
puml_files.append(temp_diagram)
context["puml_files"] = puml_files
context["vnote_post"] = modified_post
def find_title_in_puml(self, diagram):
find_title_in_diagram = re.search("title:?(.*)", diagram)
if not find_title_in_diagram:
raise KeyError("Please provide a title for PlantUML diagram(s)")
return slug(find_title_in_diagram.group(1))
Write Hugo post in blog directory
class WriteHugoPost(WorkflowBase):
vnote_post: str
blog: str
file_name: str
vnote_post_type: str
def run(self, context):
blog_page = f"{self.blog}/content/{self.vnote_post_type}/{self.file_name}"
Path(blog_page).write_text(self.vnote_post)
print(f"Created note at {blog_page}")
context["blog_page"] = blog_page
Convert PlantUML diagram(s) to PNG
class ConvertPlantUmlToPng(WorkflowBase):
puml_files: dict
blog: str
def run(self, _):
plantuml_path = os.getenv("PLANTUML_PATH")
for puml_file in self.puml_files:
target_image_dir = Path(self.blog) / "static" / relative_image_directory()
target_image_dir.mkdir(parents=True, exist_ok=True)
print(f"Converting PlantUML diagram {puml_file} to PNG")
cmd = f"java -DPLANTUML_LIMIT_SIZE=8192 -jar {plantuml_path} {puml_file} -o '{target_image_dir}' -tpng"
run_command(cmd)
Copy image files to blog directory
class CopyImageFiles(WorkflowBase):
vnote: str
blog: str
def rgx_find_all(self, document, search_query):
compiled_rgx = re.compile(search_query, re.IGNORECASE)
return compiled_rgx.findall(document)
def image_tags_from_note(self, note_path):
return self.rgx_find_all(Path(note_path).read_text(), r"!\[.*\]\(vx_images\/(.*)\)") # noqa: W605
def image_path_in_vnote(self, note_path, image):
note = Path(note_path)
return (note / ".." / "vx_images" / image).resolve()
def image_path_in_blog(self, blog_root, image):
blog = Path(blog_root)
target_image_dir = blog / "static" / relative_image_directory()
target_image_dir.mkdir(parents=True, exist_ok=True)
return (target_image_dir / image).resolve()
def run(self, _):
images = self.image_tags_from_note(self.vnote)
print(f"Found {len(images)} images in {self.vnote}")
for image in images:
source_full_path = self.image_path_in_vnote(self.vnote, image)
target_full_path = self.image_path_in_blog(self.blog, image)
shutil.copyfile(source_full_path, target_full_path)
print(f"Copied {image}")
Replace image links from vNote format to Hugo format
class ReplaceImageLinks(WorkflowBase):
blog_page: str
def run(self, _):
image_directory = relative_image_directory()
replace_string_in_file(self.blog_page, "vx_images/", f"/{image_directory}/")
print(f"Replace all images in Blog Post {self.blog_page}")
Open blog in editor
class OpenInEditor(WorkflowBase):
open_in_editor: bool
def run(self, context):
if not self.open_in_editor:
return
blog_root = context["blog"]
editor = os.environ.get("EDITOR")
print(f"Opening {blog_root} in {editor}")
subprocess.check_call(f"{editor} {blog_root}", shell=True) # nosemgrep
def workflow():
return [
LoadVNotePost,
SwapPlantUmlWithImageTag,
WriteHugoPost,
ConvertPlantUmlToPng,
CopyImageFiles,
ReplaceImageLinks,
OpenInEditor,
]
def main(args):
context = {
"blog": args.blog_directory,
"vnote": args.vnote_file_path,
"open_in_editor": args.open_in_editor,
}
run_workflow(context, workflow())
def parse_args():
parser = ArgumentParser(description=__doc__, formatter_class=RawDescriptionHelpFormatter)
parser.add_argument("-b", "--blog-directory", type=str, help="Blog directory")
parser.add_argument("-n", "--vnote-file-path", type=str, required=True, help="vNote file path")
parser.add_argument(
"-e",
"--open-in-editor",
action="store_true",
default=False,
help="Open blog site in editor",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
main(args)