""" Markdown Editor Application using PyQt6. Key Features: - Create, edit, and preview Markdown files. - Live HTML preview of Markdown content. - Configurable settings stored in YAML. Dependencies: PyQt6, yaml, markdown2. Author: Isaak Buslovich Date: 2024-01-13 Version: 0.1 """ import logging import pathlib import platform import sys import threading import webbrowser import yaml from PyQt6.QtCore import QFileSystemWatcher, Qt, QTimer, QSize, QRegularExpression from PyQt6.QtGui import QAction, QStandardItem, QStandardItemModel, QIcon, QPalette, QSyntaxHighlighter, \ QTextCharFormat, QColor, QFont, QPainter, QPixmap, QFontDatabase from PyQt6.QtWebEngineCore import QWebEngineSettings from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWidgets import QApplication, QMainWindow, QTextEdit, QWidget, QToolBar, QStatusBar, \ QMessageBox, QTreeView, QFileDialog, QAbstractItemView, QLineEdit, QVBoxLayout, QStyle, QTabWidget, QLabel, \ QSplitter from fuzzywuzzy import fuzz from markdown2 import markdown logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') MATERIAL_ICONS_PATH = "data/material-icons.ttf" MATERIAL_ICONS = { "New": "\ue862", "Open": "\ue2c7", "Save": "\ue161", "Undo": "\ue166", "Redo": "\ue15a", "Settings": "\ue8b8", "Exit": "\ue879", "Search": "\ue8b6", "Help": "\ue8fd", "About": "\ue88e", "Folder": "\ue2c7", "File": "\ue24d", "Close": "\ue5cd", "View": "\ue8f4", "GitHub": "\ue86f", "Minimize": "\ue921", } ICON_SIZE = 42 ICON_COLOR = "#ccbbca" DEFAULT_WINDOW_SIZE = (900, 700) def load_material_icons(): font_id = QFontDatabase.addApplicationFont(MATERIAL_ICONS_PATH) if font_id == -1: raise RuntimeError("Failed to load Material Icons font.") return QFontDatabase.applicationFontFamilies(font_id)[0] class MarkdownEditor(QMainWindow): """ A PyQt6-based Markdown editor with file navigation, text editing, and live HTML preview. Attributes: config_path (pathlib.Path): Configuration file path. config (dict): Application configuration. fileSystemWatcher (QFileSystemWatcher): Monitors file system changes. dataDirectory (pathlib.Path): Data directory path. directoryTree (QTreeView): File navigation widget. editor (QTextEdit): Markdown text editor. preview (QWebEngineView): Live HTML preview widget. """ def __init__(self): super().__init__() self.config_path = pathlib.Path(__file__).parent / 'config.yaml' self.config = self.load_config() self.setup_ui() self.initialize_editor_preview() QTimer.singleShot(0, self.post_init) def initialize_editor_preview(self): """ Initialize the editor and preview widgets """ self.editor = QTextEdit() self.editor.textChanged.connect(self.update_word_count) self.editor.textChanged.connect(self.update_preview) self.highlighter = MarkdownHighlighter(self.editor.document()) self.preview = QWebEngineView() web_engine_settings = self.preview.page().settings() web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessFileUrls, True) web_engine_settings.setAttribute(QWebEngineSettings.WebAttribute.LocalContentCanAccessRemoteUrls, True) def post_init(self): """Initializes the Markdown editor application with UI setup and configuration management.""" try: logging.info("Initializing the Markdown Editor...") self.fileSystemWatcher = QFileSystemWatcher() self.initialize_data_directory() self.initialize_ui() self.update_directory_tree_view() self.fileSystemWatcher.directoryChanged.connect(self.update_directory_tree_view) self.load_initial_file() self.searchTimer = QTimer() self.searchTimer.setSingleShot(True) self.searchTimer.timeout.connect(self.perform_search) except Exception as e: logging.error(f"Error during post initialization: {e}", exc_info=True) QMessageBox.critical(self, "Initialization Error", f"An error occurred during initialization: {e}") sys.exit(1) def setup_ui(self): """ Sets up the minimal UI components necessary for initial display. """ self.setWindowTitle("Markdown Editor") width = self.config.get('window', {}).get('width', 800) height = self.config.get('window', {}).get('height', 600) self.setGeometry(100, 100, width, height) central_widget = QWidget() layout = QVBoxLayout(central_widget) layout.setContentsMargins(5, 5, 5, 5) placeholder_label = QLabel("Loading...", alignment=Qt.AlignmentFlag.AlignCenter) layout.addWidget(placeholder_label) central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.setStatusBar(QStatusBar(self)) def initialize_ui(self): """ Sets up the application's user interface components, now with a ribbon interface. """ logging.info("Setting up the user interface with a ribbon...") self.setWindowTitle("Markdown Editor") width = self.config['window']['width'] height = self.config['window']['height'] self.setGeometry(100, 100, width, height) icon_path = self.dataDirectory / 'data' / 'smile.png' self.setWindowIcon(QIcon(str(icon_path))) central_widget = QWidget() layout = QVBoxLayout(central_widget) layout.setContentsMargins(5, 5, 5, 5) layout.addWidget(self.create_ribbon_interface()) self.directoryTree = QTreeView() self.directoryTree.setHeaderHidden(True) self.directoryTree.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) self.directoryTree.doubleClicked.connect(self.on_file_double_clicked) self.searchBox = QLineEdit() self.searchBox.setPlaceholderText("Search...") self.searchBox.textChanged.connect(self.on_search_text_changed) directory_layout = QVBoxLayout() directory_layout.addWidget(self.searchBox) directory_layout.addWidget(self.directoryTree) directory_widget = QWidget() directory_widget.setLayout(directory_layout) splitter_horizontal = QSplitter(Qt.Orientation.Horizontal) splitter_vertical = QSplitter(Qt.Orientation.Vertical) splitter_horizontal.addWidget(directory_widget) splitter_horizontal.addWidget(self.editor) splitter_vertical.addWidget(splitter_horizontal) splitter_vertical.addWidget(self.preview) layout.addWidget(splitter_vertical) central_widget.setLayout(layout) self.setCentralWidget(central_widget) self.setStatusBar(QStatusBar(self)) def get_material_icon(self, unicode, size=ICON_SIZE, color=ICON_COLOR): """Create a QIcon object from a Material Icon. Args: unicode (str): The unicode character for the icon. size (int, optional): Pixel size of the icon. Defaults to ICON_SIZE. color (str, optional): Color of the icon. Defaults to ICON_COLOR. Returns: QIcon: The generated icon object. """ font_family = load_material_icons() font = QFont(font_family) font.setPixelSize(int(size)) pixmap = QPixmap(size, size) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setFont(font) painter.setPen(QColor(color)) painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, unicode) painter.end() return QIcon(pixmap) def initialize_data_directory(self): """ Initializes the data directory and sets up the file system watcher. """ logging.info("Initializing data directory.") try: data_directory = self.config.get('dataDirectory', str(pathlib.Path.home())) self.dataDirectory = pathlib.Path(data_directory).resolve() if not self.dataDirectory.is_dir(): raise FileNotFoundError(f"Data directory not found: {self.dataDirectory}") self.fileSystemWatcher.addPath(str(self.dataDirectory)) logging.info(f"Data directory set to: {self.dataDirectory}") except Exception as e: logging.error(f"Failed to initialize data directory: {e}") QMessageBox.critical(self, "Directory Error", str(e)) sys.exit(1) def create_ribbon_interface(self): """ Creates the ribbon interface with tabs for File, Edit, View, Window, and Help. """ ribbon = QTabWidget() ribbon.setTabPosition(QTabWidget.TabPosition.North) ribbon.addTab(self.create_file_tab(), "File") ribbon.addTab(self.create_edit_tab(), "Edit") ribbon.addTab(self.create_view_tab(), "View") ribbon.addTab(self.create_window_tab(), "Window") ribbon.addTab(self.create_help_tab(), "Help") ribbon.setMaximumHeight(ribbon.sizeHint().height()) return ribbon def create_file_tab(self): """ Creates the File tab for the ribbon. """ toolbar = QToolBar() toolbar.setIconSize(QSize(16, 16)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.create_action("New", MATERIAL_ICONS["New"], self.new_file)) toolbar.addAction(self.create_action("Open", MATERIAL_ICONS["Open"], self.open_file)) toolbar.addAction(self.create_action("Save", MATERIAL_ICONS["Save"], self.save_entry)) toolbar.addAction(self.create_action("Open Data Directory", MATERIAL_ICONS["Folder"], self.open_data_directory)) toolbar.addAction( self.create_action("Change Data Directory", MATERIAL_ICONS["Folder"], self.change_data_directory)) toolbar.addAction(self.create_action("Settings", MATERIAL_ICONS["Settings"], self.open_settings)) toolbar.addAction(self.create_action("Exit", MATERIAL_ICONS["Exit"], self.close)) return toolbar def create_edit_tab(self): """ Creates the Edit tab for the ribbon. """ toolbar = QToolBar() toolbar.setIconSize(QSize(16, 16)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.create_action("Undo", MATERIAL_ICONS["Undo"], self.undo_action)) toolbar.addAction(self.create_action("Redo", MATERIAL_ICONS["Redo"], self.redo_action)) return toolbar def create_view_tab(self): """ Creates the View tab for the ribbon. """ toolbar = QToolBar() toolbar.setIconSize(QSize(16, 16)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.create_action("Toggle Split View", MATERIAL_ICONS["View"], self.toggle_split_view)) return toolbar def create_window_tab(self): """ Creates the Window tab for the ribbon. """ toolbar = QToolBar() toolbar.setIconSize(QSize(16, 16)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.create_action("Close", MATERIAL_ICONS["Close"], self.close)) toolbar.addAction(self.create_action("Minimize", MATERIAL_ICONS["Minimize"], self.minimize)) toolbar.addAction(self.create_action("Search", MATERIAL_ICONS["Search"], self.search)) return toolbar def create_help_tab(self): """ Creates the Help tab for the ribbon. """ toolbar = QToolBar() toolbar.setIconSize(QSize(16, 16)) toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon) toolbar.addAction(self.create_action("About", MATERIAL_ICONS["About"], self.about)) toolbar.addAction(self.create_action("View Cheatsheet", MATERIAL_ICONS["Help"], self.view_cheatsheet)) toolbar.addAction( self.create_action("GitHub", MATERIAL_ICONS["GitHub"], self.open_git_hub)) return toolbar def create_action(self, text, icon_code, callback): """ Creates an action for the toolbar in the ribbon interface. """ action = QAction(self.get_material_icon(icon_code), text, self) action.triggered.connect(callback) return action def on_search_text_changed(self, text): self.searchText = text if not text: self.update_directory_tree_view() else: self.searchTimer.start(60) def update_search_results(self, text): model = QStandardItemModel() self.populate_filtered_model(model, self.dataDirectory, text) self.directoryTree.setModel(model) def perform_search(self): threading.Thread(target=self.update_search_results, args=(self.searchText,)).start() def populate_filtered_model(self, parent_item, directory, text, fuzziness=100, depth=0): """ Recursively populates the directory tree model with filtered contents based on the search text. Args: parent_item (QStandardItem): The parent item in the tree view model. directory (pathlib.Path): The directory to scan. text (str): The search text. fuzziness (int): The fuzziness level for matching file names. depth (int): Current depth of recursion. """ if depth > self.depth: return False dir_added = False for entry in sorted(directory.iterdir(), key=lambda e: (e.is_file(), e.name.lower())): if entry.is_dir(): dir_item = QStandardItem(entry.name) if self.populate_filtered_model(dir_item, entry, text, fuzziness, depth + 1): parent_item.appendRow(dir_item) dir_added = True elif entry.suffix == '.md' and self.is_match(entry.name, text, fuzziness): file_item = QStandardItem(entry.name) file_item.setData(str(entry)) parent_item.appendRow(file_item) dir_added = True return dir_added def update_word_count(self): text = self.editor.toPlainText() word_count = len(text.split()) self.statusBar().showMessage(f"Word Count: {word_count}") @staticmethod def is_match(filename, text, fuzziness): if not text: return False if fuzziness == 100: return text.lower() in filename.lower() return fuzz.partial_ratio(text.lower(), filename.lower()) >= fuzziness @staticmethod def add_item_to_model(model, entry, is_dir=False): """ Adds an item to the directory tree model. Args: model (QStandardItemModel): The model of the directory tree. entry (Path): The file or directory path. is_dir (bool): Indicates if the entry is a directory. """ item = QStandardItem(entry.name) item.setData(str(entry)) item.setFlags(item.flags() | Qt.ItemFlag.ItemIsEnabled) if is_dir: item.setIcon(QIcon('icons/folder_icon.png')) model.appendRow(item) @staticmethod def get_config_path(): """ Get the path to the configuration file based on the operating system. Returns: Path: The path to the configuration file. """ app_name = "YourApp" home = pathlib.Path.home() if platform.system() == "Windows": config_path = pathlib.Path(home, "AppData", "Roaming", app_name, "config.yaml") elif platform.system() == "Darwin": config_path = pathlib.Path(home, "Library", "Application Support", app_name, "config.yaml") else: config_path = pathlib.Path(home, ".config", app_name, "config.yaml") config_path.parent.mkdir(parents=True, exist_ok=True) return config_path config_file = get_config_path() def closeEvent(self, event): """ Handle the close event of the application. Save the configuration before closing. Args: event (QCloseEvent): The close event. """ self.config['window']['width'] = self.size().width() self.config['window']['height'] = self.size().height() self.config['dataDirectory'] = str(self.dataDirectory) self.save_config() super().closeEvent(event) def load_config(self): """ Load the configuration from the configuration file. Returns: dict: The loaded configuration with default values if keys are missing. """ default_config = { 'window': {'width': 800, 'height': 600}, 'dataDirectory': str(pathlib.Path(__file__).parent.resolve()), 'depth': 5 } try: with open(self.config_path) as file: config = yaml.safe_load(file) or {} except FileNotFoundError: config = {} for key, value in default_config.items(): config.setdefault(key, value) self.depth = config['depth'] return config def save_config(self): """ Save the configuration to the configuration file. """ with open(self.config_path, 'w') as file: yaml.dump(self.config, file) def load_initial_file(self): """ Load an initial Markdown file for editing. This method looks for a 'cheatsheet.md' file in the application's data directory and loads it into the editor if it exists. """ cheatsheet_path = self.dataDirectory / 'cheatsheet.md' if cheatsheet_path.is_file(): with open(cheatsheet_path, encoding='utf-8') as file: markdown_content = file.read() self.editor.setText(markdown_content) self.update_preview() else: QMessageBox.warning(self, "File Not Found", f"Cheatsheet file not found: {cheatsheet_path}") def on_file_double_clicked(self, index): """ Loads a Markdown file into the editor on double-click in the directory tree. Args: index (QModelIndex): Index of the clicked item in the directory tree. """ try: file_path = pathlib.Path(index.model().itemFromIndex(index).data()) if not file_path.exists(): QMessageBox.critical(self, "Error", f"The item does not exist: {file_path}") return if not file_path.is_file(): QMessageBox.critical(self, "Error", "The selected item is not a file.") return with open(file_path, encoding='utf-8') as file: markdown_content = file.read() self.editor.setText(markdown_content) self.update_preview() except Exception as e: QMessageBox.critical(self, "File Open Error", f"An error occurred: {e}") def add_actions_to_menu(self, menu, actions): for action_text, action_func in actions: if isinstance(action_func, list): submenu = menu.addMenu(action_text) for sub_action_text, sub_action_func in action_func: sub_action = QAction(sub_action_text, self) sub_action.triggered.connect(sub_action_func) submenu.addAction(sub_action) else: action = QAction(action_text, self) action.triggered.connect(action_func) menu.addAction(action) @staticmethod def markdown_table_to_html(markdown_content): """ Convert Markdown tables in the given content to HTML table format. Args: markdown_content (str): The Markdown content containing tables. Returns: str: The HTML content with Markdown tables converted to HTML tables. """ lines = markdown_content.split('\n') html_lines = [] in_table = False for line in lines: if '|' in line: if not in_table: html_lines.append('') in_table = True columns = line.split('|')[1:-1] if '---' in columns[0]: html_lines.append('' + ''.join(''.format(col.strip()) for col in columns) + '') else: html_lines.append('' + ''.join(''.format(col.strip()) for col in columns) + '') else: if in_table: html_lines.append('
{}
{}
') in_table = False html_lines.append(line) if in_table: html_lines.append('') return '\n'.join(html_lines) @staticmethod def convert_image_paths(html_content, base_directory): """ Converts relative image paths in the HTML content to absolute file URLs. Args: html_content (str): The HTML content with Markdown images. base_directory (Path): The base directory where the 'data' folder is located. Returns: str: Modified HTML content with absolute image paths. """ image_folder = base_directory / 'data' def replace_path(match): """ Replaces a matched image path with an absolute URL. Args: match (MatchObject): Regex match object containing the image path. Returns: str: The image URL with the absolute path. """ image_relative_path = match.group(1) image_absolute_path = image_folder / image_relative_path return f'src="{image_absolute_path.as_uri()}"' import re html_content = re.sub(r'src="([^"]+)"', replace_path, html_content) return html_content def update_preview(self): """ Updates the HTML preview based on the current Markdown content. """ markdown_content = self.editor.toPlainText() html_content = markdown(markdown_content) html_content = self.markdown_table_to_html(html_content) data_directory = pathlib.Path(__file__).parent.resolve() html_content = self.convert_image_paths(html_content, data_directory) html_content = html_content.replace('[x]', '✅') html_content = html_content.replace('[ ]', '❌') palette = QApplication.instance().palette() background_color = palette.color(QPalette.ColorRole.Window).name() foreground_color = palette.color(QPalette.ColorRole.WindowText).name() accent_color = "#9f74b3" css = f""" """ html_with_css = css + '
' + html_content + '
' self.preview.setHtml(html_with_css) def update_directory_tree_view(self): """ Refreshes the directory tree view with files and directories. """ if not self.dataDirectory or not self.dataDirectory.is_dir(): return model = QStandardItemModel() self.directoryTree.setModel(model) self.populate_model(model, self.dataDirectory) def populate_model(self, parent_item, directory, depth=0): """ Populates the directory tree model with directory contents. Shows all directories containing Markdown files and Markdown files themselves. Args: parent_item (QStandardItem): Item to populate. directory (pathlib.Path): Directory to scan. depth (int): Depth level for recursive scanning. """ if depth > self.depth: return try: directory_entries = sorted(directory.iterdir(), key=lambda dir_entry: (dir_entry.is_file(), dir_entry.name.lower())) for entry in directory_entries: if entry.is_dir(): self.process_directory(entry, parent_item, depth) elif entry.suffix == '.md': self.add_file_item(entry, parent_item) except PermissionError as permission_error: logging.error(f"Permission error accessing {directory}: {permission_error}") @staticmethod def add_file_item(file_path, parent_item): """ Adds a file item to the model. Args: file_path (pathlib.Path): The file path to add. parent_item (QStandardItem): The parent item in the model. """ file_item = QStandardItem(file_path.name) file_item.setData(str(file_path)) parent_item.appendRow(file_item) def process_directory(self, directory, parent_item, depth): """ Processes a single directory, adding it to the model if it contains Markdown files. Args: directory (pathlib.Path): The directory to process. parent_item (QStandardItem): The parent item in the model. depth (int): Current depth in the directory tree. """ if any(file.suffix == '.md' for file in directory.iterdir()): dir_item = QStandardItem(directory.name) dir_item.setData(str(directory)) dir_item.setIcon(QIcon('icons/folder_icon.png')) parent_item.appendRow(dir_item) self.populate_model(dir_item, directory, depth + 1) def on_directory_tree_clicked(self, index): """ Handle clicks on the directory tree. It loads the selected file into the editor if it is a Markdown file. Args: index (QModelIndex): The model index of the clicked item in the directory tree. """ file_path = pathlib.Path(index.data()) if file_path.is_file(): self.load_file(file_path) def load_file(self, file_path): """ Loads the content of a file into the text editor. Args: file_path (pathlib.Path): The path of the file to load. """ if not file_path.is_file(): QMessageBox.critical(self, "File Open Error", "The selected file does not exist.") return try: with open(file_path, encoding='utf-8') as file: markdown_content = file.read() except OSError as e: QMessageBox.critical(self, "File Open Error", f"Error opening file: {e}") return self.editor.setText(markdown_content) self.update_preview() def new_file(self): """ Clear the editor to create a new file. This action empties the text editor for writing new content. """ self.editor.clear() def open_file(self): """ Open a file dialog to select a Markdown file. This method allows the user to select a Markdown file from the file system and loads its content into the editor. """ filename, _ = QFileDialog.getOpenFileName( self, "Open Markdown File", str(self.dataDirectory), 'Markdown Files (*.md)') if not filename: return try: with open(filename, encoding='utf-8') as file: markdown_content = file.read() except OSError as e: QMessageBox.critical(self, "File Open Error", f"Error opening file: {e}") return self.editor.setText(markdown_content) self.update_preview() def save_entry(self): """ Save the current entry. This method is intended to save the content of the text editor to a file. """ pass def about(self): """ Displays an 'About' dialog with information about the Markdown Editor. """ QMessageBox.about(self, "About Markdown Editor", "Markdown Editor\nVersion 1.0") def open_data_directory(self): """ Opens the application's data directory in the system's default file explorer. """ try: directory_url = pathlib.Path(self.dataDirectory).resolve().as_uri() webbrowser.open(directory_url) except Exception as e: QMessageBox.critical(self, "Error", f"Failed to open directory: {e}") def change_data_directory(self): """ Opens a dialog to select a new data directory for the application. """ new_data_directory = QFileDialog.getExistingDirectory(self, "Select Data Directory", str(self.dataDirectory)) if new_data_directory: new_path = pathlib.Path(new_data_directory) if new_path.is_dir(): self.dataDirectory = new_path self.update_directory_tree_view() else: QMessageBox.critical(self, "Directory Selection Error", "Invalid directory path") def open_settings(self): pass def undo_action(self): pass def redo_action(self): pass def toggle_split_view(self): """ Toggles the visibility of the preview pane. """ self.preview.setVisible(not self.preview.isVisible()) self.centralWidget().layout().activate() def minimize(self): """ Minimize the main window. """ self.showMinimized() def search(self): pass def view_cheatsheet(self): """ Load and display the cheatsheet markdown file. """ cheatsheet_path = self.dataDirectory / 'cheatsheet.md' if cheatsheet_path.is_file(): self.load_file(cheatsheet_path) else: QMessageBox.warning(self, "Cheatsheet Not Found", f"Cheatsheet file not found: {cheatsheet_path}") def open_git_hub(self): pass class SyntaxColors: COMMENT = "#6272a4" CYAN = "#8be9fd" GREEN = "#50fa7b" ORANGE = "#ffb86c" PINK = "#ff79c6" PURPLE = "#9f74b3" RED = "#ff5555" YELLOW = "#f1fa8c" class MarkdownHighlighter(QSyntaxHighlighter): def __init__(self, parent=None): super().__init__(parent) self.setupRules() def setupRules(self): heading_format = QTextCharFormat() heading_format.setForeground(QColor(SyntaxColors.PURPLE)) italic_format = QTextCharFormat() italic_format.setFontItalic(True) italic_format.setForeground(QColor(SyntaxColors.GREEN)) bold_format = QTextCharFormat() bold_format.setFontWeight(QFont.Weight.Bold) bold_format.setForeground(QColor(SyntaxColors.ORANGE)) self.highlightingRules = [ (QRegularExpression("^#.*"), heading_format), (QRegularExpression("\*{1}[^*]+\*{1}"), italic_format), (QRegularExpression("\*{2}[^*]+\*{2}"), bold_format), ] def highlightBlock(self, text): for pattern, format in self.highlightingRules: match_iterator = pattern.globalMatch(text) while match_iterator.hasNext(): match = match_iterator.next() self.setFormat(match.capturedStart(), match.capturedLength(), format) self.setCurrentBlockState(0) def main(): """ Main function to run the Markdown Editor application. """ logging.info("Launching the application...") app = QApplication(sys.argv) editor = MarkdownEditor() editor.show() sys.exit(app.exec()) if __name__ == '__main__': main()