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
Unsortedcatch-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
autofocusso 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
#!/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