Skip to content

Bookmark Parser & Viewer Script

This page documents the local, privacy-first Python utility designed to parse native browser HTML bookmark backups into a structured, responsive, and completely self-contained local web panel with a real-time JS filter engine.


[] Setup & Implementation

The solution uses the built-in html.parser library in Python to crawl the specific Netscape format used by browser exports. It handles nested tags dynamically, allowing for multi-level directories to match perfectly.

The code avoids external system binary or parser dependencies (like Pandoc or BeautifulSoup), keeping executions incredibly lightweight and fast.

⚙ Configuration Details

The engine applies specific filter rules right at the parsing stage to eliminate unwanted UI structures.

  • Toolbar Strip: Automatically detects and drops the repetitive Bookmarks Toolbar / prefix text.
  • Fallback Routing: Safely maps unclassified root directories or individual loose bookmarks into an Unsorted catch-all parent folder.
  • Alphabetical Ordering: Forces deep-nested keys into a strict sorted array to ensure accurate parent-child structural alignment during list compilation.

🖥 Browser Interface Integration

A minimal vanilla JavaScript listener is injected natively into the generated file header. This maps an input element to immediate page object states.

  • Focus States: Uses autofocus so searching begins immediately upon loading the page.
  • TOC Toggling: The main static Table of Contents is automatically hidden from view the moment input text is parsed, clearing immediate page layout clutter.
  • Cascade Hiding: If a link matches, the parent directory container stays visible. If a directory ends up containing zero matches, the entire block is removed dynamically.

🎨 Layout & Design Framework

Instead of using inline attribute injection, the script relies strictly on localized cascading rules. This ensures a uniform look across custom tools and PHP landing projects:

  • css/bookmarks.css — Manages form inputs, layout limits, and lists.
  • css/php.css — Holds global system fonts, structural properties, and shared theme styling.

A single command converts a bookmarks.html into the interactive, styled page.

python3 bin/parse_bookmarks.py Documents/Computer/bookmarks.html > ~/public_html/bookmarks.html
The python script is

#!/usr/bin/env python3
import sys
import os
import re
from html.parser import HTMLParser

class BookmarkParser(HTMLParser):
    def __init__(self):
        super().__init__()
        self.current_path = []
        self.categories = {}
        self.last_tag = None
        self.current_title = ""
        self.current_url = None
        self.capture_text = False

    def handle_starttag(self, tag, attrs):
        self.last_tag = tag
        attrs_dict = dict(attrs)

        if tag == 'h3':
            self.capture_text = True
            self.current_title = ""
        elif tag == 'a':
            self.capture_text = True
            self.current_title = ""
            self.current_url = attrs_dict.get('href')
        elif tag == 'dl':
            pass

    def handle_endtag(self, tag):
        if tag == 'h3':
            self.capture_text = False
            folder_name = self.current_title.strip()
            if folder_name:
                self.current_path.append(folder_name)
        elif tag == 'a':
            self.capture_text = False
            title = self.current_title.strip() or self.current_url

            path_str = " / ".join(self.current_path) if self.current_path else "Unsorted"

            if path_str.startswith("Bookmarks Toolbar / "):
                path_str = path_str.replace("Bookmarks Toolbar / ", "", 1)
            elif path_str == "Bookmarks Toolbar":
                path_str = "Unsorted"

            if path_str not in self.categories:
                self.categories[path_str] = []
            self.categories[path_str].append((title, self.current_url))
        elif tag == 'dl':
            if self.current_path:
                self.current_path.pop()

    def handle_data(self, data):
        if self.capture_text:
            self.current_title += data

def slugify(text):
    text = text.lower()
    text = re.sub(r'[^a-z0-9\s-]', '', text)
    return re.sub(r'[\s]+', '-', text)

def main():
    if len(sys.argv) < 2:
        print("Usage: python3 parse_bookmarks.py <path_to_bookmarks.html>")
        sys.exit(1)

    html_file = sys.argv[1]
    if not os.path.exists(html_file):
        print(f"Error: File '{html_file}' not found.")
        sys.exit(1)

    with open(html_file, 'r', encoding='utf-8', errors='ignore') as f:
        html_content = f.read()

    parser = BookmarkParser()
    parser.feed(html_content)

    # --- Start HTML Output ---
    print("<!DOCTYPE html>")
    print("<html lang='en'>\n<head>")
    print("    <meta charset='UTF-8'>")
    print("    <title>Bookmarks Index</title>")
    print("    <link rel='stylesheet' href='css/php.css'>")
    print("    <link rel='stylesheet' href='css/bookmarks.css'>")
    print("    <link rel='icon' type='image/png' href='img/favicon.ico'>")
    print("</head>\n<body>")

    print("<h1 id='top'>Bookmarks Index</h1>")

    # Inject Search Box Container
    print("<div class='search-container'>")
    print("    <input type='text' id='bookmark-search' placeholder='Type to filter bookmarks...' autofocus autocomplete='off'>")
    print("</div>")

    print("<h2>Categories</h2>")

    # 1. Output Table of Contents
    current_depth = 0
    print("<ul class='toc-list'>")
    for category in sorted(parser.categories.keys()):
        depth = category.count(" / ")
        display_name = category.split(" / ")[-1]
        anchor = slugify(category)

        while current_depth < depth:
            print("  " * current_depth + "<ul>")
            current_depth += 1
        while current_depth > depth:
            print("  " * (current_depth - 1) + "</ul>")
            current_depth -= 1

        print(f"{'  ' * current_depth}<li><a href='#{anchor}'>{display_name}</a></li>")

    while current_depth > 0:
        print("  " * (current_depth - 1) + "</ul>")
        current_depth -= 1
    print("</ul>")

    print("<hr>")

    # 2. Output Bookmarks wrapped in a filterable section block
    print("<div id='bookmarks-wrapper'>")
    for category in sorted(parser.categories.keys()):
        bookmarks = parser.categories[category]
        anchor_id = slugify(category)

        print(f"  <div class='bookmark-section'>")
        print(f"    <p class='back-to-top'><a href='#top'>↑ Back to top</a></p>")
        print(f"    <h2 id='{anchor_id}' class='category-heading'>{category}</h2>")
        print("    <ul>")
        for title, url in bookmarks:
            print(f"        <li><a href='{url}'>{title}</a></li>")
        print("    </ul>")
        print(f"  </div>")
    print("</div>")

    # 3. Inject Client-Side Real-Time Filter Logic
    print("""
    <script>
    document.getElementById('bookmark-search').addEventListener('input', function(e) {
        const query = e.target.value.toLowerCase().strip ? e.target.value.toLowerCase().trim() : e.target.value.toLowerCase();
        const sections = document.querySelectorAll('.bookmark-section');
        const tocList = document.querySelector('.toc-list');

        // Hide the entire category index if the user starts typing to clear up screen space
        if (query.length > 0) {
            tocList.style.display = 'none';
        } else {
            tocList.style.display = 'block';
        }

        sections.forEach(section => {
            const heading = section.querySelector('.category-heading').textContent.toLowerCase();
            const links = section.querySelectorAll('ul li');
            let sectionHasMatch = false;

            // Check if the overall category name matches the search query
            const categoryMatches = heading.includes(query);

            links.forEach(li => {
                const linkText = li.textContent.toLowerCase();

                // Show link if it matches, OR if the parent folder matches the query
                if (linkText.includes(query) || categoryMatches) {
                    li.style.display = '';
                    sectionHasMatch = true;
                } else {
                    li.style.display = 'none';
                }
            });

            // Show or hide the whole category chunk based on matching links
            if (sectionHasMatch) {
                section.style.display = '';
            } else {
                section.style.display = 'none';
            }
        });
    });
    </script>
    """)

    print("</body>\n</html>")

if __name__ == "__main__":
    main()

Posted

18:47 06-06-2026