mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 08:22:09 +01:00
Introduces a modular exporter pattern supporting DOCX and Markdown formats by implementing Exporter interfaces and restructuring application logic. Enhances CI to install UPX for binary compression, excluding recent macOS binaries due to compatibility issues. Enables CGO when building binaries for all platforms, addressing potential cross-platform compatibility concerns. Bumps version to 0.1.1.
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
// Package exporters provides implementations of the Exporter interface
|
|
// for converting Articulate Rise courses into various file formats.
|
|
package exporters
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
|
"github.com/kjanat/articulate-parser/internal/models"
|
|
"github.com/kjanat/articulate-parser/internal/services"
|
|
)
|
|
|
|
// MarkdownExporter implements the Exporter interface for Markdown format.
|
|
// It converts Articulate Rise course data into a structured Markdown document.
|
|
type MarkdownExporter struct {
|
|
// htmlCleaner is used to convert HTML content to plain text
|
|
htmlCleaner *services.HTMLCleaner
|
|
}
|
|
|
|
// NewMarkdownExporter creates a new MarkdownExporter instance.
|
|
// It takes an HTMLCleaner to handle HTML content conversion.
|
|
//
|
|
// Parameters:
|
|
// - htmlCleaner: Service for cleaning HTML content in course data
|
|
//
|
|
// Returns:
|
|
// - An implementation of the Exporter interface for Markdown format
|
|
func NewMarkdownExporter(htmlCleaner *services.HTMLCleaner) interfaces.Exporter {
|
|
return &MarkdownExporter{
|
|
htmlCleaner: htmlCleaner,
|
|
}
|
|
}
|
|
|
|
// Export exports a course to Markdown format.
|
|
// It generates a structured Markdown document from the course data
|
|
// and writes it to the specified output path.
|
|
//
|
|
// Parameters:
|
|
// - course: The course data model to export
|
|
// - outputPath: The file path where the Markdown content will be written
|
|
//
|
|
// Returns:
|
|
// - An error if writing to the output file fails
|
|
func (e *MarkdownExporter) Export(course *models.Course, outputPath string) error {
|
|
var buf bytes.Buffer
|
|
|
|
// Write course header
|
|
buf.WriteString(fmt.Sprintf("# %s\n\n", course.Course.Title))
|
|
|
|
if course.Course.Description != "" {
|
|
buf.WriteString(fmt.Sprintf("%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description)))
|
|
}
|
|
|
|
// Add metadata
|
|
buf.WriteString("## Course Information\n\n")
|
|
buf.WriteString(fmt.Sprintf("- **Course ID**: %s\n", course.Course.ID))
|
|
buf.WriteString(fmt.Sprintf("- **Share ID**: %s\n", course.ShareID))
|
|
buf.WriteString(fmt.Sprintf("- **Navigation Mode**: %s\n", course.Course.NavigationMode))
|
|
if course.Course.ExportSettings != nil {
|
|
buf.WriteString(fmt.Sprintf("- **Export Format**: %s\n", course.Course.ExportSettings.Format))
|
|
}
|
|
buf.WriteString("\n---\n\n")
|
|
|
|
// Process lessons
|
|
for i, lesson := range course.Course.Lessons {
|
|
if lesson.Type == "section" {
|
|
buf.WriteString(fmt.Sprintf("# %s\n\n", lesson.Title))
|
|
continue
|
|
}
|
|
|
|
buf.WriteString(fmt.Sprintf("## Lesson %d: %s\n\n", i+1, lesson.Title))
|
|
|
|
if lesson.Description != "" {
|
|
buf.WriteString(fmt.Sprintf("%s\n\n", e.htmlCleaner.CleanHTML(lesson.Description)))
|
|
}
|
|
|
|
// Process lesson items
|
|
for _, item := range lesson.Items {
|
|
e.processItemToMarkdown(&buf, item, 3)
|
|
}
|
|
|
|
buf.WriteString("\n---\n\n")
|
|
}
|
|
|
|
return os.WriteFile(outputPath, buf.Bytes(), 0644)
|
|
}
|
|
|
|
// GetSupportedFormat returns the format name this exporter supports
|
|
// It indicates the file format that the MarkdownExporter can generate.
|
|
//
|
|
// Returns:
|
|
// - A string representing the supported format ("markdown")
|
|
func (e *MarkdownExporter) GetSupportedFormat() string {
|
|
return "markdown"
|
|
}
|
|
|
|
// processItemToMarkdown converts a course item into Markdown format
|
|
// and appends it to the provided buffer. It handles different item types
|
|
// with appropriate Markdown formatting.
|
|
//
|
|
// Parameters:
|
|
// - buf: The buffer to write the Markdown content to
|
|
// - item: The course item to process
|
|
// - level: The heading level for the item (determines the number of # characters)
|
|
func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.Item, level int) {
|
|
headingPrefix := strings.Repeat("#", level)
|
|
|
|
switch item.Type {
|
|
case "text":
|
|
for _, subItem := range item.Items {
|
|
if subItem.Heading != "" {
|
|
heading := e.htmlCleaner.CleanHTML(subItem.Heading)
|
|
if heading != "" {
|
|
buf.WriteString(fmt.Sprintf("%s %s\n\n", headingPrefix, heading))
|
|
}
|
|
}
|
|
if subItem.Paragraph != "" {
|
|
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
|
if paragraph != "" {
|
|
buf.WriteString(fmt.Sprintf("%s\n\n", paragraph))
|
|
}
|
|
}
|
|
}
|
|
|
|
case "list":
|
|
for _, subItem := range item.Items {
|
|
if subItem.Paragraph != "" {
|
|
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
|
if paragraph != "" {
|
|
buf.WriteString(fmt.Sprintf("- %s\n", paragraph))
|
|
}
|
|
}
|
|
}
|
|
buf.WriteString("\n")
|
|
|
|
case "multimedia":
|
|
buf.WriteString(fmt.Sprintf("%s Media Content\n\n", headingPrefix))
|
|
for _, subItem := range item.Items {
|
|
if subItem.Media != nil {
|
|
if subItem.Media.Video != nil {
|
|
buf.WriteString(fmt.Sprintf("**Video**: %s\n", subItem.Media.Video.OriginalUrl))
|
|
if subItem.Media.Video.Duration > 0 {
|
|
buf.WriteString(fmt.Sprintf("**Duration**: %d seconds\n", subItem.Media.Video.Duration))
|
|
}
|
|
}
|
|
if subItem.Media.Image != nil {
|
|
buf.WriteString(fmt.Sprintf("**Image**: %s\n", subItem.Media.Image.OriginalUrl))
|
|
}
|
|
}
|
|
if subItem.Caption != "" {
|
|
caption := e.htmlCleaner.CleanHTML(subItem.Caption)
|
|
buf.WriteString(fmt.Sprintf("*%s*\n", caption))
|
|
}
|
|
}
|
|
buf.WriteString("\n")
|
|
|
|
case "image":
|
|
buf.WriteString(fmt.Sprintf("%s Image\n\n", headingPrefix))
|
|
for _, subItem := range item.Items {
|
|
if subItem.Media != nil && subItem.Media.Image != nil {
|
|
buf.WriteString(fmt.Sprintf("**Image**: %s\n", subItem.Media.Image.OriginalUrl))
|
|
}
|
|
if subItem.Caption != "" {
|
|
caption := e.htmlCleaner.CleanHTML(subItem.Caption)
|
|
buf.WriteString(fmt.Sprintf("*%s*\n", caption))
|
|
}
|
|
}
|
|
buf.WriteString("\n")
|
|
|
|
case "knowledgeCheck":
|
|
buf.WriteString(fmt.Sprintf("%s Knowledge Check\n\n", headingPrefix))
|
|
for _, subItem := range item.Items {
|
|
if subItem.Title != "" {
|
|
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
|
buf.WriteString(fmt.Sprintf("**Question**: %s\n\n", title))
|
|
}
|
|
|
|
buf.WriteString("**Answers**:\n")
|
|
for i, answer := range subItem.Answers {
|
|
correctMark := ""
|
|
if answer.Correct {
|
|
correctMark = " ✓"
|
|
}
|
|
buf.WriteString(fmt.Sprintf("%d. %s%s\n", i+1, answer.Title, correctMark))
|
|
}
|
|
|
|
if subItem.Feedback != "" {
|
|
feedback := e.htmlCleaner.CleanHTML(subItem.Feedback)
|
|
buf.WriteString(fmt.Sprintf("\n**Feedback**: %s\n", feedback))
|
|
}
|
|
}
|
|
buf.WriteString("\n")
|
|
|
|
case "interactive":
|
|
buf.WriteString(fmt.Sprintf("%s Interactive Content\n\n", headingPrefix))
|
|
for _, subItem := range item.Items {
|
|
if subItem.Title != "" {
|
|
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
|
buf.WriteString(fmt.Sprintf("**%s**\n\n", title))
|
|
}
|
|
}
|
|
|
|
case "divider":
|
|
buf.WriteString("---\n\n")
|
|
|
|
default:
|
|
// Handle unknown types
|
|
if len(item.Items) > 0 {
|
|
buf.WriteString(fmt.Sprintf("%s %s Content\n\n", headingPrefix, strings.Title(item.Type)))
|
|
for _, subItem := range item.Items {
|
|
if subItem.Title != "" {
|
|
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
|
buf.WriteString(fmt.Sprintf("**%s**\n\n", title))
|
|
}
|
|
if subItem.Paragraph != "" {
|
|
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
|
buf.WriteString(fmt.Sprintf("%s\n\n", paragraph))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|