Created
December 5, 2024 22:51
Converts a LearnDash course backup (lessons only) into a single ordered HTML file with sections for easy reference.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
# This script converts a learndash course backup with lessons (but not | |
# themes/topics!) to a single ordered HTML file with sections for easy reference | |
import argparse | |
import json | |
def analyze_course_structure(course_file, lessons_file, course_id): | |
with open(course_file, 'r', encoding='utf-8') as file: | |
lines = file.readlines() | |
course_title = "Untitled Course" | |
course_sections = [] | |
lessons = {} | |
lesson_ids_in_order = [] | |
# Parse course file for the given course_id | |
for line in lines: | |
try: | |
obj = json.loads(line) | |
if obj.get("wp_post", {}).get("ID") == course_id: | |
wp_post_meta = obj.get("wp_post_meta", {}) | |
course_title = obj.get("wp_post", {}).get("post_title", "Untitled Course") | |
course_sections_raw = wp_post_meta.get("course_sections", []) | |
# Decode course_sections | |
decoded_sections = [] | |
if (isinstance(course_sections_raw, list) and len(course_sections_raw) == 1 and isinstance(course_sections_raw[0], str)): | |
try: | |
decoded_sections = json.loads(course_sections_raw[0]) | |
except json.JSONDecodeError: | |
decoded_sections = [] | |
elif isinstance(course_sections_raw, str): | |
try: | |
decoded_sections = json.loads(course_sections_raw) | |
except json.JSONDecodeError: | |
decoded_sections = [] | |
elif isinstance(course_sections_raw, list): | |
decoded_sections = course_sections_raw | |
# Extract section headings | |
for section in decoded_sections: | |
if isinstance(section, dict) and section.get("type") == "section-heading": | |
s_order = section.get("order", 0) | |
s_title = section.get("post_title", "Untitled Section") | |
course_sections.append({ | |
"order": int(s_order), | |
"title": s_title | |
}) | |
# Extract lesson order from ld_course_steps | |
steps = wp_post_meta.get("ld_course_steps", []) | |
if steps and len(steps) > 0: | |
sfwd_lessons = steps[0].get("steps", {}).get("h", {}).get("sfwd-lessons", {}) | |
lesson_ids_in_order = list(sfwd_lessons.keys()) | |
break | |
except json.JSONDecodeError: | |
pass | |
# Parse lessons | |
with open(lessons_file, 'r', encoding='utf-8') as f: | |
for line in f: | |
try: | |
obj = json.loads(line) | |
wp_post = obj.get("wp_post", {}) | |
lesson_id = str(wp_post.get("ID")) | |
if lesson_id in lesson_ids_in_order: | |
lessons[lesson_id] = { | |
"title": wp_post.get("post_title", "Untitled Lesson"), | |
"content": wp_post.get("post_content", "") | |
} | |
except json.JSONDecodeError: | |
pass | |
# Start with all lessons in order | |
combined_structure = [lessons[lid]["title"] for lid in lesson_ids_in_order] | |
# Sort sections by ascending order | |
course_sections.sort(key=lambda x: x["order"]) | |
# Insert each section at its final desired position | |
for sec in course_sections: | |
insert_index = sec["order"] | |
if insert_index > len(combined_structure): | |
insert_index = len(combined_structure) | |
combined_structure.insert(insert_index, "!" + sec["title"]) | |
return course_title, combined_structure, lessons | |
def generate_html(course_title, structure, lessons): | |
html_output = f""" | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>{course_title}</title> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"> | |
<style> | |
.wp-block-embed {{ display: none; }} | |
</style> | |
</head> | |
<body class="bg-light" id="top"> | |
<div class="container py-5"> | |
<h1 class="text-center mb-4">{course_title}</h1> | |
<nav> | |
<h2>Table of Contents</h2> | |
<ul class="list-group mb-4"> | |
""" | |
for line in structure: | |
if line.startswith("!"): | |
html_output += f'<li class="list-group-item bg-secondary text-white"><strong>{line[1:]}</strong></li>' | |
else: | |
# Find the corresponding lesson ID | |
lesson_id = next((lid for lid, l in lessons.items() if l["title"] == line), None) | |
if lesson_id: | |
html_output += f'<li class="list-group-item ms-3"><a href="#lesson-{lesson_id}">{line}</a></li>' | |
html_output += """ | |
</ul> | |
</nav> | |
""" | |
for line in structure: | |
if line.startswith("!"): | |
# Section heading | |
html_output += f'<h1 class="my-4 text-primary">{line[1:]}</h1>' | |
else: | |
# Lesson | |
lesson_id = next((lid for lid, l in lessons.items() if l["title"] == line), None) | |
lesson = lessons.get(lesson_id, {"title": "Unknown Lesson", "content": ""}) | |
html_output += f'<h2 id="lesson-{lesson_id}" class="mt-3">{lesson["title"]}</h2>\n' | |
html_output += f'<p><a href="#top">Back to Table of Contents</a></p>\n' | |
html_output += f'<div>{lesson["content"]}</div>\n' | |
html_output += """ | |
</div> | |
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> | |
</body> | |
</html> | |
""" | |
return html_output | |
def main(): | |
parser = argparse.ArgumentParser(description="Generate course HTML from LearnDash export files.") | |
parser.add_argument("course_id", type=int, help="ID of the course to process") | |
parser.add_argument("course_file", type=str, help="Path to the course file") | |
parser.add_argument("lessons_file", type=str, help="Path to the lessons file") | |
parser.add_argument("output_file", type=str, help="Path to the output HTML file") | |
args = parser.parse_args() | |
course_title, structure, lessons = analyze_course_structure(args.course_file, args.lessons_file, args.course_id) | |
html_output = generate_html(course_title, structure, lessons) | |
with open(args.output_file, "w", encoding="utf-8") as f: | |
f.write(html_output) | |
print(f"HTML output saved to {args.output_file}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment