Information Technology Grimoire

Version .0.0.1

IT Notes from various projects because I forget, and hopefully they help you too.

Hugo YAML 2 TOML

This is a specific use case script but I’m archiving it for my notes. As I was learning about Hugo yaml, toml, and front matter - I made mistakes. I’ve decided to use toml for everything, which means I needed an easier way to convert yaml to toml. This script allows conversion both ways, but it’s not a true yaml to toml converter. Instead it converts specific front matter formatting which is yaml to toml (and vice versa).

Environments

Quick referenc,e to install environments on windows:

cd scriptswhatever
python -m venv yournewenv
cd yournewenv
scripts\activate
python -m pip install -r requirements.txt

Linux/mac is very similar:

cd scriptswhatever
python3 -m venv yournewenv
cd yournewenv
source bin\activate
python3 -m pip install -r requirements.txt

Requirements

requirements.txt

pillow==10.3.0
PyYAML==6.0.1
toml==0.10.2
ttkbootstrap==1.10.1

The Script

import tkinter as tk
from tkinter import ttk, messagebox
import toml
import yaml
from io import StringIO
import ttkbootstrap as tb

VERSION = 1

class ConverterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("TOML/YAML Converter")

        # Set the ttkbootstrap theme
        self.style = tb.Style("cosmo")

        # Configure the main window to expand
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        self.root.rowconfigure(1, weight=0)
        self.root.rowconfigure(2, weight=1)

        # Input textarea
        self.input_text = tk.Text(root, height=15)
        self.input_text.grid(row=0, column=0, padx=10, pady=5, sticky="nsew")

        # Conversion options
        self.conversion_option = tk.StringVar(value="yaml_to_toml")
        self.radio_frame = ttk.Frame(root)
        self.radio_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")

        self.radio_frame.columnconfigure(0, weight=1)
        self.radio_frame.columnconfigure(1, weight=1)
        self.radio_frame.columnconfigure(2, weight=1)

        self.yaml_to_toml_radio = ttk.Radiobutton(self.radio_frame, text="YAML to TOML", variable=self.conversion_option, value="yaml_to_toml")
        self.yaml_to_toml_radio.grid(row=0, column=0, padx=5, sticky="e")

        self.toml_to_yaml_radio = ttk.Radiobutton(self.radio_frame, text="TOML to YAML", variable=self.conversion_option, value="toml_to_yaml")
        self.toml_to_yaml_radio.grid(row=1, column=0, padx=5, sticky="e")

        self.convert_button = ttk.Button(self.radio_frame, text="Convert", command=self.convert)
        self.convert_button.grid(row=2, column=0, padx=5, pady=5, sticky="e")

        # Output textarea
        self.output_text = tk.Text(root, height=15)
        self.output_text.grid(row=2, column=0, padx=10, pady=5, sticky="nsew")

    def extract_comments_and_data(self, text):
        """
        Extract comments and data separately
        """
        lines = text.split("\n")
        extracted_lines = []
        for line in lines:
            if "#" in line:
                data_part, comment_part = line.split("#", 1)
                extracted_lines.append((data_part.strip(), comment_part.strip()))
            else:
                extracted_lines.append((line.strip(), None))
        return extracted_lines

    def reinsert_comments(self, converted_lines, comments):
        """
        Reinsert comments into the converted lines
        """
        result_lines = []
        converted_lines_split = converted_lines.split("\n")
        comment_index = 0

        for line in converted_lines_split:
            if "=" in line:
                key = line.split("=")[0].strip()
            elif ":" in line:
                key = line.split(":")[0].strip()
            else:
                result_lines.append(line)
                continue

            while comment_index < len(comments):
                data_part, comment_part = comments[comment_index]
                if data_part and data_part.startswith(key):
                    if comment_part:
                        result_lines.append(f"{line}  # {comment_part}")
                    else:
                        result_lines.append(line)
                    comment_index += 1
                    break
                elif not data_part:
                    result_lines.append(f"# {comment_part}")
                    comment_index += 1
                else:
                    comment_index += 1

        return "\n".join(result_lines)

    def convert(self):
        input_data = self.input_text.get("1.0", tk.END).strip()
        if not input_data:
            messagebox.showwarning("Warning", "Input field is empty!")
            return

        # Determine the format and strip the delimiters
        input_format = None
        if input_data.startswith("---"):
            input_format = "yaml"
            input_data = input_data.replace("---", "").strip()
        elif input_data.startswith("+++"):
            input_format = "toml"
            input_data = input_data.replace("+++", "").strip()

        # Extract comments and data
        comments = self.extract_comments_and_data(input_data)

        try:
            if self.conversion_option.get() == "yaml_to_toml":
                if input_format == "yaml":
                    data = yaml.safe_load("\n".join([line[0] for line in comments if line[0]]))
                    output_data = toml.dumps(data)
                    output_data = f"+++\n{output_data}+++"
                else:
                    raise ValueError("Input does not appear to be valid YAML")
            else:
                if input_format == "toml":
                    data = toml.loads("\n".join([line[0] for line in comments if line[0]]))
                    output_data = yaml.dump(data, sort_keys=False, default_flow_style=False)
                    output_data = f"---\n{output_data}---"
                else:
                    raise ValueError("Input does not appear to be valid TOML")

            # Add comments back to the converted data
            output_data = self.reinsert_comments(output_data, comments)

            self.output_text.delete("1.0", tk.END)
            self.output_text.insert(tk.END, output_data)
        except (toml.TomlDecodeError, yaml.YAMLError, ValueError) as e:
            messagebox.showerror("Error", f"Invalid input format:\n{e}")
        except Exception as e:
            messagebox.showerror("Error", f"An error occurred:\n{e}")

def main():
    root = tb.Window(themename="cosmo")
    app = ConverterApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()