mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-10-13 07:35:27 +02:00
refactor: comprehensive code quality improvements
- Add logger to all exporters for consistent error handling - Add Course.Validate() method with ValidationError type - Create shared test fixtures in internal/testdata - Refactor config passing: NewArticulateParser now takes *config.Config - Add NewLoggerFromConfig() factory for logger creation - Add context cancellation with signal handling (SIGINT/SIGTERM) - Add godoc examples for loggers and exporters - Add HTML exporter error path tests - Add fuzz tests for parser and HTMLCleaner - Fix markdown output: trim whitespace, collapse blank lines - Remove test output files from repo
This commit is contained in:
@@ -6,12 +6,13 @@ import (
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
"github.com/kjanat/articulate-parser/internal/services"
|
||||
"github.com/kjanat/articulate-parser/internal/testdata"
|
||||
)
|
||||
|
||||
// BenchmarkFactory_CreateExporter_Markdown benchmarks markdown exporter creation.
|
||||
func BenchmarkFactory_CreateExporter_Markdown(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
@@ -22,7 +23,7 @@ func BenchmarkFactory_CreateExporter_Markdown(b *testing.B) {
|
||||
// BenchmarkFactory_CreateExporter_All benchmarks creating all exporter types.
|
||||
func BenchmarkFactory_CreateExporter_All(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
formats := []string{"markdown", "docx", "html"}
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -36,15 +37,15 @@ func BenchmarkFactory_CreateExporter_All(b *testing.B) {
|
||||
// BenchmarkAllExporters_Export benchmarks all exporters with the same course.
|
||||
func BenchmarkAllExporters_Export(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
course := createBenchmarkCourse()
|
||||
course := testdata.BasicCourse()
|
||||
|
||||
exporters := map[string]struct {
|
||||
exporter any
|
||||
ext string
|
||||
}{
|
||||
"Markdown": {NewMarkdownExporter(htmlCleaner), ".md"},
|
||||
"Docx": {NewDocxExporter(htmlCleaner), ".docx"},
|
||||
"HTML": {NewHTMLExporter(htmlCleaner), ".html"},
|
||||
"Markdown": {NewMarkdownExporter(htmlCleaner, nil), ".md"},
|
||||
"Docx": {NewDocxExporter(htmlCleaner, nil), ".docx"},
|
||||
"HTML": {NewHTMLExporter(htmlCleaner, nil), ".html"},
|
||||
}
|
||||
|
||||
for name, exp := range exporters {
|
||||
@@ -66,10 +67,10 @@ func BenchmarkAllExporters_Export(b *testing.B) {
|
||||
// BenchmarkExporters_LargeCourse benchmarks exporters with large course data.
|
||||
func BenchmarkExporters_LargeCourse(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
course := createLargeBenchmarkCourse()
|
||||
course := testdata.LargeCourse(50)
|
||||
|
||||
b.Run("Markdown_Large", func(b *testing.B) {
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
tempDir := b.TempDir()
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -80,7 +81,7 @@ func BenchmarkExporters_LargeCourse(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("Docx_Large", func(b *testing.B) {
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
tempDir := b.TempDir()
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -91,7 +92,7 @@ func BenchmarkExporters_LargeCourse(b *testing.B) {
|
||||
})
|
||||
|
||||
b.Run("HTML_Large", func(b *testing.B) {
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
tempDir := b.TempDir()
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -101,100 +102,3 @@ func BenchmarkExporters_LargeCourse(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// createBenchmarkCourse creates a standard-sized course for benchmarking.
|
||||
func createBenchmarkCourse() *models.Course {
|
||||
return &models.Course{
|
||||
ShareID: "benchmark-id",
|
||||
Author: "Benchmark Author",
|
||||
Course: models.CourseInfo{
|
||||
ID: "bench-course",
|
||||
Title: "Benchmark Course",
|
||||
Description: "Performance testing course",
|
||||
NavigationMode: "menu",
|
||||
Lessons: []models.Lesson{
|
||||
{
|
||||
ID: "lesson1",
|
||||
Title: "Introduction",
|
||||
Type: "lesson",
|
||||
Items: []models.Item{
|
||||
{
|
||||
Type: "text",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
Heading: "Welcome",
|
||||
Paragraph: "<p>This is a test paragraph with <strong>HTML</strong> content.</p>",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "list",
|
||||
Items: []models.SubItem{
|
||||
{Paragraph: "Item 1"},
|
||||
{Paragraph: "Item 2"},
|
||||
{Paragraph: "Item 3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// createLargeBenchmarkCourse creates a large course for stress testing.
|
||||
func createLargeBenchmarkCourse() *models.Course {
|
||||
lessons := make([]models.Lesson, 50)
|
||||
for i := range 50 {
|
||||
lessons[i] = models.Lesson{
|
||||
ID: string(rune(i)),
|
||||
Title: "Lesson " + string(rune(i)),
|
||||
Type: "lesson",
|
||||
Description: "<p>This is lesson description with <em>formatting</em>.</p>",
|
||||
Items: []models.Item{
|
||||
{
|
||||
Type: "text",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
Heading: "Section Heading",
|
||||
Paragraph: "<p>Content with <strong>bold</strong> and <em>italic</em> text.</p>",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "list",
|
||||
Items: []models.SubItem{
|
||||
{Paragraph: "Point 1"},
|
||||
{Paragraph: "Point 2"},
|
||||
{Paragraph: "Point 3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "knowledgeCheck",
|
||||
Items: []models.SubItem{
|
||||
{
|
||||
Title: "Quiz Question",
|
||||
Answers: []models.Answer{
|
||||
{Title: "Answer A", Correct: false},
|
||||
{Title: "Answer B", Correct: true},
|
||||
{Title: "Answer C", Correct: false},
|
||||
},
|
||||
Feedback: "Good job!",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &models.Course{
|
||||
ShareID: "large-benchmark-id",
|
||||
Author: "Benchmark Author",
|
||||
Course: models.CourseInfo{
|
||||
ID: "large-bench-course",
|
||||
Title: "Large Benchmark Course",
|
||||
Description: "Large performance testing course",
|
||||
Lessons: lessons,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,19 +29,23 @@ const (
|
||||
type DocxExporter struct {
|
||||
// htmlCleaner is used to convert HTML content to plain text
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
// logger is used for logging warnings and errors
|
||||
logger interfaces.Logger
|
||||
}
|
||||
|
||||
// NewDocxExporter creates a new DocxExporter instance.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion and a logger for logging.
|
||||
//
|
||||
// Parameters:
|
||||
// - htmlCleaner: Service for cleaning HTML content in course data
|
||||
// - logger: Logger instance for warnings and errors
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for DOCX format
|
||||
func NewDocxExporter(htmlCleaner *services.HTMLCleaner) interfaces.Exporter {
|
||||
func NewDocxExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
return &DocxExporter{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +95,7 @@ func (e *DocxExporter) Export(course *models.Course, outputPath string) error {
|
||||
// affect the validity of the exported file.
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "warning: failed to close output file: %v\n", err)
|
||||
e.logger.Warn("failed to close output file", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// TestNewDocxExporter tests the NewDocxExporter constructor.
|
||||
func TestNewDocxExporter(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
if exporter == nil {
|
||||
t.Fatal("NewDocxExporter() returned nil")
|
||||
@@ -32,7 +32,7 @@ func TestNewDocxExporter(t *testing.T) {
|
||||
// TestDocxExporter_SupportedFormat tests the SupportedFormat method.
|
||||
func TestDocxExporter_SupportedFormat(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
expected := "docx"
|
||||
result := exporter.SupportedFormat()
|
||||
@@ -45,7 +45,7 @@ func TestDocxExporter_SupportedFormat(t *testing.T) {
|
||||
// TestDocxExporter_Export tests the Export method.
|
||||
func TestDocxExporter_Export(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create test course
|
||||
testCourse := createTestCourseForDocx()
|
||||
@@ -79,7 +79,7 @@ func TestDocxExporter_Export(t *testing.T) {
|
||||
// TestDocxExporter_Export_AddDocxExtension tests that the .docx extension is added automatically.
|
||||
func TestDocxExporter_Export_AddDocxExtension(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForDocx()
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestDocxExporter_Export_AddDocxExtension(t *testing.T) {
|
||||
// TestDocxExporter_Export_InvalidPath tests export with invalid output path.
|
||||
func TestDocxExporter_Export_InvalidPath(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForDocx()
|
||||
|
||||
@@ -118,7 +118,7 @@ func TestDocxExporter_Export_InvalidPath(t *testing.T) {
|
||||
// TestDocxExporter_ExportLesson tests the exportLesson method indirectly through Export.
|
||||
func TestDocxExporter_ExportLesson(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create course with specific lesson content
|
||||
course := &models.Course{
|
||||
@@ -165,7 +165,7 @@ func TestDocxExporter_ExportLesson(t *testing.T) {
|
||||
// TestDocxExporter_ExportItem tests the exportItem method indirectly through Export.
|
||||
func TestDocxExporter_ExportItem(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create course with different item types
|
||||
course := &models.Course{
|
||||
@@ -231,7 +231,7 @@ func TestDocxExporter_ExportItem(t *testing.T) {
|
||||
// TestDocxExporter_ExportSubItem tests the exportSubItem method indirectly through Export.
|
||||
func TestDocxExporter_ExportSubItem(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create course with sub-item containing all possible fields
|
||||
course := &models.Course{
|
||||
@@ -284,7 +284,7 @@ func TestDocxExporter_ExportSubItem(t *testing.T) {
|
||||
// TestDocxExporter_ComplexCourse tests export of a complex course structure.
|
||||
func TestDocxExporter_ComplexCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create complex test course
|
||||
course := &models.Course{
|
||||
@@ -421,7 +421,7 @@ func TestDocxExporter_ComplexCourse(t *testing.T) {
|
||||
// TestDocxExporter_EmptyCourse tests export of an empty course.
|
||||
func TestDocxExporter_EmptyCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create minimal course
|
||||
course := &models.Course{
|
||||
@@ -450,7 +450,7 @@ func TestDocxExporter_EmptyCourse(t *testing.T) {
|
||||
// TestDocxExporter_HTMLCleaning tests that HTML content is properly cleaned.
|
||||
func TestDocxExporter_HTMLCleaning(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create course with HTML content that needs cleaning
|
||||
course := &models.Course{
|
||||
@@ -498,7 +498,7 @@ func TestDocxExporter_HTMLCleaning(t *testing.T) {
|
||||
// TestDocxExporter_ExistingDocxExtension tests that existing .docx extension is preserved.
|
||||
func TestDocxExporter_ExistingDocxExtension(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForDocx()
|
||||
|
||||
@@ -526,7 +526,7 @@ func TestDocxExporter_ExistingDocxExtension(t *testing.T) {
|
||||
// TestDocxExporter_CaseInsensitiveExtension tests that extension checking is case-insensitive.
|
||||
func TestDocxExporter_CaseInsensitiveExtension(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForDocx()
|
||||
|
||||
@@ -599,7 +599,7 @@ func createTestCourseForDocx() *models.Course {
|
||||
// BenchmarkDocxExporter_Export benchmarks the Export method.
|
||||
func BenchmarkDocxExporter_Export(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
course := createTestCourseForDocx()
|
||||
|
||||
// Create temporary directory
|
||||
@@ -618,7 +618,7 @@ func BenchmarkDocxExporter_Export(b *testing.B) {
|
||||
// BenchmarkDocxExporter_ComplexCourse benchmarks export of a complex course.
|
||||
func BenchmarkDocxExporter_ComplexCourse(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewDocxExporter(htmlCleaner)
|
||||
exporter := NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
// Create complex course for benchmarking
|
||||
course := &models.Course{
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// ExampleNewFactory demonstrates creating an exporter factory.
|
||||
func ExampleNewFactory() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := exporters.NewFactory(htmlCleaner)
|
||||
factory := exporters.NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Get supported formats
|
||||
formats := factory.SupportedFormats()
|
||||
@@ -24,7 +24,7 @@ func ExampleNewFactory() {
|
||||
// ExampleFactory_CreateExporter demonstrates creating exporters.
|
||||
func ExampleFactory_CreateExporter() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := exporters.NewFactory(htmlCleaner)
|
||||
factory := exporters.NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Create a markdown exporter
|
||||
exporter, err := factory.CreateExporter("markdown")
|
||||
@@ -39,7 +39,7 @@ func ExampleFactory_CreateExporter() {
|
||||
// ExampleFactory_CreateExporter_caseInsensitive demonstrates case-insensitive format names.
|
||||
func ExampleFactory_CreateExporter_caseInsensitive() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := exporters.NewFactory(htmlCleaner)
|
||||
factory := exporters.NewFactory(htmlCleaner, nil)
|
||||
|
||||
// All these work (case-insensitive)
|
||||
formats := []string{"MARKDOWN", "Markdown", "markdown", "MD"}
|
||||
@@ -58,7 +58,7 @@ func ExampleFactory_CreateExporter_caseInsensitive() {
|
||||
// ExampleMarkdownExporter_Export demonstrates exporting to Markdown.
|
||||
func ExampleMarkdownExporter_Export() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := exporters.NewMarkdownExporter(htmlCleaner)
|
||||
exporter := exporters.NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
course := &models.Course{
|
||||
ShareID: "example-id",
|
||||
@@ -81,7 +81,7 @@ func ExampleMarkdownExporter_Export() {
|
||||
// ExampleDocxExporter_Export demonstrates exporting to DOCX.
|
||||
func ExampleDocxExporter_Export() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := exporters.NewDocxExporter(htmlCleaner)
|
||||
exporter := exporters.NewDocxExporter(htmlCleaner, nil)
|
||||
|
||||
course := &models.Course{
|
||||
ShareID: "example-id",
|
||||
@@ -99,3 +99,33 @@ func ExampleDocxExporter_Export() {
|
||||
fmt.Println("DOCX export complete")
|
||||
// Output: DOCX export complete
|
||||
}
|
||||
|
||||
// ExampleHTMLExporter_Export demonstrates exporting to HTML.
|
||||
func ExampleHTMLExporter_Export() {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := exporters.NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
course := &models.Course{
|
||||
ShareID: "example-id",
|
||||
Course: models.CourseInfo{
|
||||
Title: "Example Course",
|
||||
Description: "<p>A sample course</p>",
|
||||
Lessons: []models.Lesson{
|
||||
{
|
||||
ID: "lesson-1",
|
||||
Title: "Introduction",
|
||||
Type: "lesson",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Export to HTML file
|
||||
err := exporter.Export(course, "output.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Println("HTML export complete")
|
||||
// Output: HTML export complete
|
||||
}
|
||||
|
||||
@@ -25,20 +25,24 @@ const (
|
||||
type Factory struct {
|
||||
// htmlCleaner is used by exporters to convert HTML content to plain text
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
// logger is used by exporters for logging
|
||||
logger interfaces.Logger
|
||||
}
|
||||
|
||||
// NewFactory creates a new exporter factory.
|
||||
// It takes an HTMLCleaner instance that will be passed to the exporters
|
||||
// It takes an HTMLCleaner instance and a logger that will be passed to the exporters
|
||||
// created by this factory.
|
||||
//
|
||||
// Parameters:
|
||||
// - htmlCleaner: Service for cleaning HTML content in course data
|
||||
// - logger: Logger instance for exporter logging
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the ExporterFactory interface
|
||||
func NewFactory(htmlCleaner *services.HTMLCleaner) interfaces.ExporterFactory {
|
||||
func NewFactory(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.ExporterFactory {
|
||||
return &Factory{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,11 +51,11 @@ func NewFactory(htmlCleaner *services.HTMLCleaner) interfaces.ExporterFactory {
|
||||
func (f *Factory) CreateExporter(format string) (interfaces.Exporter, error) {
|
||||
switch strings.ToLower(format) {
|
||||
case FormatMarkdown, formatAliasMarkdown:
|
||||
return NewMarkdownExporter(f.htmlCleaner), nil
|
||||
return NewMarkdownExporter(f.htmlCleaner, f.logger), nil
|
||||
case FormatDocx, formatAliasDocx:
|
||||
return NewDocxExporter(f.htmlCleaner), nil
|
||||
return NewDocxExporter(f.htmlCleaner, f.logger), nil
|
||||
case FormatHTML, formatAliasHTML:
|
||||
return NewHTMLExporter(f.htmlCleaner), nil
|
||||
return NewHTMLExporter(f.htmlCleaner, f.logger), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported export format: %s", format)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// TestNewFactory tests the NewFactory constructor.
|
||||
func TestNewFactory(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
if factory == nil {
|
||||
t.Fatal("NewFactory() returned nil")
|
||||
@@ -32,7 +32,7 @@ func TestNewFactory(t *testing.T) {
|
||||
// TestFactory_CreateExporter tests the CreateExporter method for all supported formats.
|
||||
func TestFactory_CreateExporter(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -135,7 +135,7 @@ func TestFactory_CreateExporter(t *testing.T) {
|
||||
// TestFactory_CreateExporter_CaseInsensitive tests that format strings are case-insensitive.
|
||||
func TestFactory_CreateExporter_CaseInsensitive(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
testCases := []struct {
|
||||
format string
|
||||
@@ -182,7 +182,7 @@ func TestFactory_CreateExporter_CaseInsensitive(t *testing.T) {
|
||||
// TestFactory_CreateExporter_ErrorMessages tests error messages for unsupported formats.
|
||||
func TestFactory_CreateExporter_ErrorMessages(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
testCases := []string{
|
||||
"pdf",
|
||||
@@ -222,7 +222,7 @@ func TestFactory_CreateExporter_ErrorMessages(t *testing.T) {
|
||||
// TestFactory_SupportedFormats tests the SupportedFormats method.
|
||||
func TestFactory_SupportedFormats(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
formats := factory.SupportedFormats()
|
||||
|
||||
@@ -255,7 +255,7 @@ func TestFactory_SupportedFormats(t *testing.T) {
|
||||
// TestFactory_SupportedFormats_Immutable tests that the returned slice is safe to modify.
|
||||
func TestFactory_SupportedFormats_Immutable(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Get formats twice
|
||||
formats1 := factory.SupportedFormats()
|
||||
@@ -281,7 +281,7 @@ func TestFactory_SupportedFormats_Immutable(t *testing.T) {
|
||||
// TestFactory_ExporterTypes tests that created exporters are of correct types.
|
||||
func TestFactory_ExporterTypes(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Test markdown exporter
|
||||
markdownExporter, err := factory.CreateExporter("markdown")
|
||||
@@ -307,7 +307,7 @@ func TestFactory_ExporterTypes(t *testing.T) {
|
||||
// TestFactory_HTMLCleanerPropagation tests that HTMLCleaner is properly passed to exporters.
|
||||
func TestFactory_HTMLCleanerPropagation(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Test with markdown exporter
|
||||
markdownExporter, err := factory.CreateExporter("markdown")
|
||||
@@ -358,7 +358,7 @@ func TestFactory_HTMLCleanerPropagation(t *testing.T) {
|
||||
// TestFactory_MultipleExporterCreation tests creating multiple exporters of same type.
|
||||
func TestFactory_MultipleExporterCreation(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Create multiple markdown exporters
|
||||
exporter1, err := factory.CreateExporter("markdown")
|
||||
@@ -395,7 +395,7 @@ func TestFactory_WithNilHTMLCleaner(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
factory := NewFactory(nil)
|
||||
factory := NewFactory(nil, nil)
|
||||
|
||||
if factory == nil {
|
||||
t.Fatal("NewFactory(nil) returned nil")
|
||||
@@ -412,7 +412,7 @@ func TestFactory_WithNilHTMLCleaner(t *testing.T) {
|
||||
// TestFactory_FormatNormalization tests that format strings are properly normalized.
|
||||
func TestFactory_FormatNormalization(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
// Test formats with extra whitespace
|
||||
testCases := []struct {
|
||||
@@ -445,7 +445,7 @@ func TestFactory_FormatNormalization(t *testing.T) {
|
||||
// BenchmarkFactory_CreateExporter benchmarks the CreateExporter method.
|
||||
func BenchmarkFactory_CreateExporter(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
for b.Loop() {
|
||||
_, _ = factory.CreateExporter("markdown")
|
||||
@@ -455,7 +455,7 @@ func BenchmarkFactory_CreateExporter(b *testing.B) {
|
||||
// BenchmarkFactory_CreateExporter_Docx benchmarks creating DOCX exporters.
|
||||
func BenchmarkFactory_CreateExporter_Docx(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
for b.Loop() {
|
||||
_, _ = factory.CreateExporter("docx")
|
||||
@@ -465,7 +465,7 @@ func BenchmarkFactory_CreateExporter_Docx(b *testing.B) {
|
||||
// BenchmarkFactory_SupportedFormats benchmarks the SupportedFormats method.
|
||||
func BenchmarkFactory_SupportedFormats(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
factory := NewFactory(htmlCleaner)
|
||||
factory := NewFactory(htmlCleaner, nil)
|
||||
|
||||
for b.Loop() {
|
||||
_ = factory.SupportedFormats()
|
||||
|
||||
@@ -25,17 +25,21 @@ type HTMLExporter struct {
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
// tmpl holds the parsed HTML template
|
||||
tmpl *template.Template
|
||||
// logger is used for logging warnings and errors
|
||||
logger interfaces.Logger
|
||||
}
|
||||
|
||||
// NewHTMLExporter creates a new HTMLExporter instance.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion when plain text is needed.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion when plain text is needed
|
||||
// and a logger for logging.
|
||||
//
|
||||
// Parameters:
|
||||
// - htmlCleaner: Service for cleaning HTML content in course data
|
||||
// - logger: Logger instance for warnings and errors
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for HTML format
|
||||
func NewHTMLExporter(htmlCleaner *services.HTMLCleaner) interfaces.Exporter {
|
||||
func NewHTMLExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
// Parse the template with custom functions
|
||||
funcMap := template.FuncMap{
|
||||
"safeHTML": func(s string) template.HTML {
|
||||
@@ -51,6 +55,7 @@ func NewHTMLExporter(htmlCleaner *services.HTMLCleaner) interfaces.Exporter {
|
||||
return &HTMLExporter{
|
||||
htmlCleaner: htmlCleaner,
|
||||
tmpl: tmpl,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+109
-109
@@ -1,175 +1,175 @@
|
||||
body {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
.course-description {
|
||||
margin-top: 1rem;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
margin-top: 1rem;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.course-info {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.course-info h2 {
|
||||
margin-top: 0;
|
||||
color: #4a5568;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-top: 0;
|
||||
color: #4a5568;
|
||||
border-bottom: 2px solid #e2e8f0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
.course-info ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
.course-info li {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 4px;
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.course-section {
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
background: #4299e1;
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.course-section h2 {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
.lesson {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #4299e1;
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
border-left: 4px solid #4299e1;
|
||||
}
|
||||
.lesson h3 {
|
||||
margin-top: 0;
|
||||
color: #2d3748;
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0;
|
||||
color: #2d3748;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.lesson-description {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4299e1;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #f7fafc;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4299e1;
|
||||
}
|
||||
.item {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.item h4 {
|
||||
margin-top: 0;
|
||||
color: #4a5568;
|
||||
font-size: 1.2rem;
|
||||
text-transform: capitalize;
|
||||
margin-top: 0;
|
||||
color: #4a5568;
|
||||
font-size: 1.2rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.text-item {
|
||||
background: #f0fff4;
|
||||
border-left: 3px solid #48bb78;
|
||||
background: #f0fff4;
|
||||
border-left: 3px solid #48bb78;
|
||||
}
|
||||
.list-item {
|
||||
background: #fffaf0;
|
||||
border-left: 3px solid #ed8936;
|
||||
background: #fffaf0;
|
||||
border-left: 3px solid #ed8936;
|
||||
}
|
||||
.knowledge-check {
|
||||
background: #e6fffa;
|
||||
border-left: 3px solid #38b2ac;
|
||||
background: #e6fffa;
|
||||
border-left: 3px solid #38b2ac;
|
||||
}
|
||||
.multimedia-item {
|
||||
background: #faf5ff;
|
||||
border-left: 3px solid #9f7aea;
|
||||
background: #faf5ff;
|
||||
border-left: 3px solid #9f7aea;
|
||||
}
|
||||
.interactive-item {
|
||||
background: #fff5f5;
|
||||
border-left: 3px solid #f56565;
|
||||
background: #fff5f5;
|
||||
border-left: 3px solid #f56565;
|
||||
}
|
||||
.unknown-item {
|
||||
background: #f7fafc;
|
||||
border-left: 3px solid #a0aec0;
|
||||
background: #f7fafc;
|
||||
border-left: 3px solid #a0aec0;
|
||||
}
|
||||
.answers {
|
||||
margin: 1rem 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.answers h5 {
|
||||
margin: 0.5rem 0;
|
||||
color: #4a5568;
|
||||
margin: 0.5rem 0;
|
||||
color: #4a5568;
|
||||
}
|
||||
.answers ol {
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
margin: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.answers li {
|
||||
margin: 0.3rem 0;
|
||||
padding: 0.3rem;
|
||||
margin: 0.3rem 0;
|
||||
padding: 0.3rem;
|
||||
}
|
||||
.correct-answer {
|
||||
background: #c6f6d5;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
background: #c6f6d5;
|
||||
border-radius: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.correct-answer::after {
|
||||
content: " ✓";
|
||||
color: #38a169;
|
||||
content: " ✓";
|
||||
color: #38a169;
|
||||
}
|
||||
.feedback {
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #edf2f7;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4299e1;
|
||||
font-style: italic;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem;
|
||||
background: #edf2f7;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid #4299e1;
|
||||
font-style: italic;
|
||||
}
|
||||
.media-info {
|
||||
background: #edf2f7;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.5rem 0;
|
||||
background: #edf2f7;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
.media-info strong {
|
||||
color: #4a5568;
|
||||
color: #4a5568;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, #667eea, #764ba2);
|
||||
margin: 2rem 0;
|
||||
border-radius: 1px;
|
||||
border: none;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right, #667eea, #764ba2);
|
||||
margin: 2rem 0;
|
||||
border-radius: 1px;
|
||||
}
|
||||
ul {
|
||||
padding-left: 1.5rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
li {
|
||||
margin: 0.5rem 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// TestNewHTMLExporter tests the NewHTMLExporter constructor.
|
||||
func TestNewHTMLExporter(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
if exporter == nil {
|
||||
t.Fatal("NewHTMLExporter() returned nil")
|
||||
@@ -37,7 +37,7 @@ func TestNewHTMLExporter(t *testing.T) {
|
||||
// TestHTMLExporter_SupportedFormat tests the SupportedFormat method.
|
||||
func TestHTMLExporter_SupportedFormat(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
expected := "html"
|
||||
result := exporter.SupportedFormat()
|
||||
@@ -50,7 +50,7 @@ func TestHTMLExporter_SupportedFormat(t *testing.T) {
|
||||
// TestHTMLExporter_Export tests the Export method.
|
||||
func TestHTMLExporter_Export(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
// Create test course
|
||||
testCourse := createTestCourseForHTML()
|
||||
@@ -129,7 +129,7 @@ func TestHTMLExporter_Export(t *testing.T) {
|
||||
// TestHTMLExporter_Export_InvalidPath tests export with invalid output path.
|
||||
func TestHTMLExporter_Export_InvalidPath(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForHTML()
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestHTMLExporter_Export_InvalidPath(t *testing.T) {
|
||||
// TestHTMLExporter_ComplexCourse tests export of a course with complex content.
|
||||
func TestHTMLExporter_ComplexCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
// Create complex test course
|
||||
course := &models.Course{
|
||||
@@ -276,7 +276,7 @@ func TestHTMLExporter_ComplexCourse(t *testing.T) {
|
||||
// TestHTMLExporter_EmptyCourse tests export of an empty course.
|
||||
func TestHTMLExporter_EmptyCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
// Create minimal course
|
||||
course := &models.Course{
|
||||
@@ -324,7 +324,7 @@ func TestHTMLExporter_EmptyCourse(t *testing.T) {
|
||||
// TestHTMLExporter_HTMLCleaning tests that HTML content is properly handled.
|
||||
func TestHTMLExporter_HTMLCleaning(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
// Create course with HTML content that needs cleaning in some places
|
||||
course := &models.Course{
|
||||
@@ -392,6 +392,227 @@ func TestHTMLExporter_HTMLCleaning(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLExporter_Export_ErrorPaths tests various error conditions for the HTML exporter.
|
||||
func TestHTMLExporter_Export_ErrorPaths(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
course *models.Course
|
||||
outputPath string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "non-existent directory",
|
||||
course: createTestCourseForHTML(),
|
||||
outputPath: "/nonexistent/deeply/nested/path/output.html",
|
||||
wantErr: true,
|
||||
errContains: "failed to create file",
|
||||
},
|
||||
{
|
||||
name: "empty output path",
|
||||
course: createTestCourseForHTML(),
|
||||
outputPath: "",
|
||||
wantErr: true,
|
||||
errContains: "failed to create file",
|
||||
},
|
||||
{
|
||||
name: "output path is directory",
|
||||
course: createTestCourseForHTML(),
|
||||
outputPath: t.TempDir(), // directory, not a file
|
||||
wantErr: true,
|
||||
errContains: "failed to create file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := exporter.Export(tt.course, tt.outputPath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("Export() expected error containing %q, got nil", tt.errContains)
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("Export() error = %v, want error containing %q", err, tt.errContains)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("Export() unexpected error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLExporter_WriteHTML_ErrorPaths tests WriteHTML error conditions.
|
||||
func TestHTMLExporter_WriteHTML_ErrorPaths(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
htmlExporter := exporter.(*HTMLExporter)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
course *models.Course
|
||||
writer *failingWriter
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "writer fails immediately",
|
||||
course: createTestCourseForHTML(),
|
||||
writer: &failingWriter{failAfter: 0},
|
||||
wantErr: true,
|
||||
errContains: "failed to execute template",
|
||||
},
|
||||
{
|
||||
name: "writer fails mid-write",
|
||||
course: createTestCourseForHTML(),
|
||||
writer: &failingWriter{failAfter: 100},
|
||||
wantErr: true,
|
||||
errContains: "failed to execute template",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := htmlExporter.WriteHTML(tt.writer, tt.course)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("WriteHTML() expected error containing %q, got nil", tt.errContains)
|
||||
return
|
||||
}
|
||||
if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("WriteHTML() error = %v, want error containing %q", err, tt.errContains)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Errorf("WriteHTML() unexpected error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestHTMLExporter_Export_NilCourse verifies that exporting a nil course panics.
|
||||
// This documents the current behavior - nil course is not a supported input.
|
||||
func TestHTMLExporter_Export_NilCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
tempDir := t.TempDir()
|
||||
outputPath := filepath.Join(tempDir, "nil-course.html")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("Export(nil) expected panic, got none")
|
||||
}
|
||||
}()
|
||||
|
||||
// Export nil course - should panic
|
||||
_ = exporter.Export(nil, outputPath)
|
||||
}
|
||||
|
||||
// TestHTMLExporter_WriteHTML_NilCourse verifies that WriteHTML with nil course panics.
|
||||
func TestHTMLExporter_WriteHTML_NilCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
htmlExporter := exporter.(*HTMLExporter)
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("WriteHTML(nil) expected panic, got none")
|
||||
}
|
||||
}()
|
||||
|
||||
var buf strings.Builder
|
||||
_ = htmlExporter.WriteHTML(&buf, nil)
|
||||
}
|
||||
|
||||
// TestHTMLExporter_WriteHTML_SuccessWithBuffer tests WriteHTML with a buffer.
|
||||
func TestHTMLExporter_WriteHTML_SuccessWithBuffer(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
htmlExporter := exporter.(*HTMLExporter)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
course *models.Course
|
||||
contains []string
|
||||
}{
|
||||
{
|
||||
name: "basic course",
|
||||
course: createTestCourseForHTML(),
|
||||
contains: []string{
|
||||
"<!DOCTYPE html>",
|
||||
"<html lang=\"en\">",
|
||||
"Test Course",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "course with special characters",
|
||||
course: &models.Course{
|
||||
ShareID: "special-chars-id",
|
||||
Course: models.CourseInfo{
|
||||
ID: "special-course",
|
||||
Title: "Course with <special> & \"characters\"",
|
||||
Description: "<p>Description with & entities</p>",
|
||||
Lessons: []models.Lesson{},
|
||||
},
|
||||
},
|
||||
contains: []string{
|
||||
"<!DOCTYPE html>",
|
||||
"Course with",
|
||||
"special",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var buf strings.Builder
|
||||
err := htmlExporter.WriteHTML(&buf, tt.course)
|
||||
if err != nil {
|
||||
t.Fatalf("WriteHTML() unexpected error: %v", err)
|
||||
}
|
||||
|
||||
output := buf.String()
|
||||
for _, want := range tt.contains {
|
||||
if !strings.Contains(output, want) {
|
||||
t.Errorf("WriteHTML() output missing %q", want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// failingWriter is a test helper that fails after writing a certain number of bytes.
|
||||
type failingWriter struct {
|
||||
written int
|
||||
failAfter int
|
||||
}
|
||||
|
||||
func (w *failingWriter) Write(p []byte) (n int, err error) {
|
||||
if w.written >= w.failAfter {
|
||||
return 0, errWriteFailed
|
||||
}
|
||||
w.written += len(p)
|
||||
if w.written > w.failAfter {
|
||||
return 0, errWriteFailed
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var errWriteFailed = &writeError{msg: "simulated write failure"}
|
||||
|
||||
type writeError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e *writeError) Error() string {
|
||||
return e.msg
|
||||
}
|
||||
|
||||
// createTestCourseForHTML creates a test course for HTML export tests.
|
||||
func createTestCourseForHTML() *models.Course {
|
||||
return &models.Course{
|
||||
@@ -439,7 +660,7 @@ func createTestCourseForHTML() *models.Course {
|
||||
// BenchmarkHTMLExporter_Export benchmarks the Export method.
|
||||
func BenchmarkHTMLExporter_Export(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
course := createTestCourseForHTML()
|
||||
|
||||
tempDir := b.TempDir()
|
||||
@@ -455,7 +676,7 @@ func BenchmarkHTMLExporter_Export(b *testing.B) {
|
||||
// BenchmarkHTMLExporter_ComplexCourse benchmarks export of a complex course.
|
||||
func BenchmarkHTMLExporter_ComplexCourse(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewHTMLExporter(htmlCleaner)
|
||||
exporter := NewHTMLExporter(htmlCleaner, nil)
|
||||
|
||||
// Create complex course for benchmarking
|
||||
course := &models.Course{
|
||||
|
||||
@@ -19,19 +19,23 @@ import (
|
||||
type MarkdownExporter struct {
|
||||
// htmlCleaner is used to convert HTML content to plain text
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
// logger is used for logging warnings and errors
|
||||
logger interfaces.Logger
|
||||
}
|
||||
|
||||
// NewMarkdownExporter creates a new MarkdownExporter instance.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion.
|
||||
// It takes an HTMLCleaner to handle HTML content conversion and a logger for logging.
|
||||
//
|
||||
// Parameters:
|
||||
// - htmlCleaner: Service for cleaning HTML content in course data
|
||||
// - logger: Logger instance for warnings and errors
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for Markdown format
|
||||
func NewMarkdownExporter(htmlCleaner *services.HTMLCleaner) interfaces.Exporter {
|
||||
func NewMarkdownExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
return &MarkdownExporter{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,19 +44,22 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Write course header
|
||||
fmt.Fprintf(&buf, "# %s\n\n", course.Course.Title)
|
||||
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(course.Course.Title))
|
||||
|
||||
if course.Course.Description != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description))
|
||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(course.Course.Description))
|
||||
if desc != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", desc)
|
||||
}
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
buf.WriteString("## Course Information\n\n")
|
||||
fmt.Fprintf(&buf, "- **Course ID**: %s\n", course.Course.ID)
|
||||
fmt.Fprintf(&buf, "- **Share ID**: %s\n", course.ShareID)
|
||||
fmt.Fprintf(&buf, "- **Navigation Mode**: %s\n", course.Course.NavigationMode)
|
||||
fmt.Fprintf(&buf, "- **Course ID**: %s\n", strings.TrimSpace(course.Course.ID))
|
||||
fmt.Fprintf(&buf, "- **Share ID**: %s\n", strings.TrimSpace(course.ShareID))
|
||||
fmt.Fprintf(&buf, "- **Navigation Mode**: %s\n", strings.TrimSpace(course.Course.NavigationMode))
|
||||
if course.Course.ExportSettings != nil {
|
||||
fmt.Fprintf(&buf, "- **Export Format**: %s\n", course.Course.ExportSettings.Format)
|
||||
fmt.Fprintf(&buf, "- **Export Format**: %s\n", strings.TrimSpace(course.Course.ExportSettings.Format))
|
||||
}
|
||||
buf.WriteString("\n---\n\n")
|
||||
|
||||
@@ -60,15 +67,18 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
lessonCounter := 0
|
||||
for _, lesson := range course.Course.Lessons {
|
||||
if lesson.Type == lessonTypeSection {
|
||||
fmt.Fprintf(&buf, "# %s\n\n", lesson.Title)
|
||||
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(lesson.Title))
|
||||
continue
|
||||
}
|
||||
|
||||
lessonCounter++
|
||||
fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, lesson.Title)
|
||||
fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, strings.TrimSpace(lesson.Title))
|
||||
|
||||
if lesson.Description != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(lesson.Description))
|
||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(lesson.Description))
|
||||
if desc != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", desc)
|
||||
}
|
||||
}
|
||||
|
||||
// Process lesson items
|
||||
@@ -76,16 +86,49 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
e.processItemToMarkdown(&buf, item, 3)
|
||||
}
|
||||
|
||||
buf.WriteString("\n---\n\n")
|
||||
buf.WriteString("---\n\n")
|
||||
}
|
||||
|
||||
// Clean up multiple consecutive blank lines before writing
|
||||
output := cleanMarkdownWhitespace(buf.Bytes())
|
||||
|
||||
// #nosec G306 - 0644 is appropriate for export files that should be readable by others
|
||||
if err := os.WriteFile(outputPath, buf.Bytes(), 0o644); err != nil {
|
||||
if err := os.WriteFile(outputPath, output, 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write markdown file: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanMarkdownWhitespace normalizes whitespace in markdown output.
|
||||
// It removes trailing whitespace from lines and collapses multiple blank lines into one.
|
||||
func cleanMarkdownWhitespace(data []byte) []byte {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
prevBlank := false
|
||||
|
||||
for _, line := range lines {
|
||||
// Trim trailing whitespace from each line
|
||||
trimmed := strings.TrimRight(line, " \t")
|
||||
|
||||
// Collapse multiple consecutive blank lines
|
||||
isBlank := trimmed == ""
|
||||
if isBlank && prevBlank {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, trimmed)
|
||||
prevBlank = isBlank
|
||||
}
|
||||
|
||||
// Remove trailing blank lines
|
||||
for len(result) > 0 && result[len(result)-1] == "" {
|
||||
result = result[:len(result)-1]
|
||||
}
|
||||
|
||||
// Ensure file ends with newline
|
||||
return []byte(strings.Join(result, "\n") + "\n")
|
||||
}
|
||||
|
||||
// SupportedFormat returns "markdown".
|
||||
func (e *MarkdownExporter) SupportedFormat() string {
|
||||
return FormatMarkdown
|
||||
@@ -123,13 +166,13 @@ func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.
|
||||
func (e *MarkdownExporter) processTextItem(buf *bytes.Buffer, item models.Item, headingPrefix string) {
|
||||
for _, subItem := range item.Items {
|
||||
if subItem.Heading != "" {
|
||||
heading := e.htmlCleaner.CleanHTML(subItem.Heading)
|
||||
heading := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Heading))
|
||||
if heading != "" {
|
||||
fmt.Fprintf(buf, "%s %s\n\n", headingPrefix, heading)
|
||||
}
|
||||
}
|
||||
if subItem.Paragraph != "" {
|
||||
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
fmt.Fprintf(buf, "%s\n\n", paragraph)
|
||||
}
|
||||
@@ -141,7 +184,7 @@ func (e *MarkdownExporter) processTextItem(buf *bytes.Buffer, item models.Item,
|
||||
func (e *MarkdownExporter) processListItem(buf *bytes.Buffer, item models.Item) {
|
||||
for _, subItem := range item.Items {
|
||||
if subItem.Paragraph != "" {
|
||||
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
fmt.Fprintf(buf, "- %s\n", paragraph)
|
||||
}
|
||||
@@ -166,8 +209,10 @@ func (e *MarkdownExporter) processMediaSubItem(buf *bytes.Buffer, subItem models
|
||||
e.processImageMedia(buf, subItem.Media)
|
||||
}
|
||||
if subItem.Caption != "" {
|
||||
caption := e.htmlCleaner.CleanHTML(subItem.Caption)
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
||||
if caption != "" {
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,8 +241,10 @@ func (e *MarkdownExporter) processImageItem(buf *bytes.Buffer, item models.Item,
|
||||
fmt.Fprintf(buf, "**Image**: %s\n", subItem.Media.Image.OriginalURL)
|
||||
}
|
||||
if subItem.Caption != "" {
|
||||
caption := e.htmlCleaner.CleanHTML(subItem.Caption)
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
||||
if caption != "" {
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
@@ -215,27 +262,35 @@ func (e *MarkdownExporter) processKnowledgeCheckItem(buf *bytes.Buffer, item mod
|
||||
// processQuestionSubItem processes individual question items.
|
||||
func (e *MarkdownExporter) processQuestionSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
||||
if subItem.Title != "" {
|
||||
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
||||
fmt.Fprintf(buf, "**Question**: %s\n\n", title)
|
||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
fmt.Fprintf(buf, "**Question**: %s\n\n", title)
|
||||
}
|
||||
}
|
||||
|
||||
e.processAnswers(buf, subItem.Answers)
|
||||
|
||||
if subItem.Feedback != "" {
|
||||
feedback := e.htmlCleaner.CleanHTML(subItem.Feedback)
|
||||
fmt.Fprintf(buf, "\n**Feedback**: %s\n", feedback)
|
||||
feedback := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Feedback))
|
||||
if feedback != "" {
|
||||
fmt.Fprintf(buf, "\n**Feedback**: %s\n", feedback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processAnswers processes answer choices for quiz questions.
|
||||
func (e *MarkdownExporter) processAnswers(buf *bytes.Buffer, answers []models.Answer) {
|
||||
if len(answers) == 0 {
|
||||
return
|
||||
}
|
||||
buf.WriteString("**Answers**:\n")
|
||||
for i, answer := range answers {
|
||||
correctMark := ""
|
||||
if answer.Correct {
|
||||
correctMark = " ✓"
|
||||
correctMark = " [correct]"
|
||||
}
|
||||
fmt.Fprintf(buf, "%d. %s%s\n", i+1, answer.Title, correctMark)
|
||||
title := strings.TrimSpace(answer.Title)
|
||||
fmt.Fprintf(buf, "%d. %s%s\n", i+1, title, correctMark)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,8 +299,10 @@ func (e *MarkdownExporter) processInteractiveItem(buf *bytes.Buffer, item models
|
||||
fmt.Fprintf(buf, "%s Interactive Content\n\n", headingPrefix)
|
||||
for _, subItem := range item.Items {
|
||||
if subItem.Title != "" {
|
||||
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,11 +326,15 @@ func (e *MarkdownExporter) processUnknownItem(buf *bytes.Buffer, item models.Ite
|
||||
// processGenericSubItem processes sub-items for unknown types.
|
||||
func (e *MarkdownExporter) processGenericSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
||||
if subItem.Title != "" {
|
||||
title := e.htmlCleaner.CleanHTML(subItem.Title)
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
}
|
||||
}
|
||||
if subItem.Paragraph != "" {
|
||||
paragraph := e.htmlCleaner.CleanHTML(subItem.Paragraph)
|
||||
fmt.Fprintf(buf, "%s\n\n", paragraph)
|
||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
fmt.Fprintf(buf, "%s\n\n", paragraph)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// TestNewMarkdownExporter tests the NewMarkdownExporter constructor.
|
||||
func TestNewMarkdownExporter(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
if exporter == nil {
|
||||
t.Fatal("NewMarkdownExporter() returned nil")
|
||||
@@ -34,7 +34,7 @@ func TestNewMarkdownExporter(t *testing.T) {
|
||||
// TestMarkdownExporter_SupportedFormat tests the SupportedFormat method.
|
||||
func TestMarkdownExporter_SupportedFormat(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
expected := "markdown"
|
||||
result := exporter.SupportedFormat()
|
||||
@@ -47,7 +47,7 @@ func TestMarkdownExporter_SupportedFormat(t *testing.T) {
|
||||
// TestMarkdownExporter_Export tests the Export method.
|
||||
func TestMarkdownExporter_Export(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
// Create test course
|
||||
testCourse := createTestCourseForMarkdown()
|
||||
@@ -108,7 +108,7 @@ func TestMarkdownExporter_Export(t *testing.T) {
|
||||
// TestMarkdownExporter_Export_InvalidPath tests export with invalid output path.
|
||||
func TestMarkdownExporter_Export_InvalidPath(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
testCourse := createTestCourseForMarkdown()
|
||||
|
||||
@@ -283,7 +283,7 @@ func TestMarkdownExporter_ProcessKnowledgeCheckItem(t *testing.T) {
|
||||
if !strings.Contains(result, "**Answers**:") {
|
||||
t.Error("Should contain answers heading")
|
||||
}
|
||||
if !strings.Contains(result, "2. Paris ✓") {
|
||||
if !strings.Contains(result, "2. Paris [correct]") {
|
||||
t.Error("Should mark correct answer")
|
||||
}
|
||||
if !strings.Contains(result, "**Feedback**: Paris is the capital of France.") {
|
||||
@@ -432,7 +432,7 @@ func TestMarkdownExporter_ProcessAnswers(t *testing.T) {
|
||||
if !strings.Contains(result, "1. Answer 1") {
|
||||
t.Error("Should contain first answer")
|
||||
}
|
||||
if !strings.Contains(result, "2. Answer 2 ✓") {
|
||||
if !strings.Contains(result, "2. Answer 2 [correct]") {
|
||||
t.Error("Should mark correct answer")
|
||||
}
|
||||
if !strings.Contains(result, "3. Answer 3") {
|
||||
@@ -510,7 +510,7 @@ func TestMarkdownExporter_ProcessItemToMarkdown_AllTypes(t *testing.T) {
|
||||
// TestMarkdownExporter_ComplexCourse tests export of a complex course structure.
|
||||
func TestMarkdownExporter_ComplexCourse(t *testing.T) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
|
||||
// Create complex test course
|
||||
course := &models.Course{
|
||||
@@ -603,7 +603,7 @@ func TestMarkdownExporter_ComplexCourse(t *testing.T) {
|
||||
"- Second objective",
|
||||
"### Knowledge Check",
|
||||
"**Question**: What will you learn?",
|
||||
"2. Everything ✓",
|
||||
"2. Everything [correct]",
|
||||
"**Feedback**: Great choice!",
|
||||
}
|
||||
|
||||
@@ -654,7 +654,7 @@ func createTestCourseForMarkdown() *models.Course {
|
||||
// BenchmarkMarkdownExporter_Export benchmarks the Export method.
|
||||
func BenchmarkMarkdownExporter_Export(b *testing.B) {
|
||||
htmlCleaner := services.NewHTMLCleaner()
|
||||
exporter := NewMarkdownExporter(htmlCleaner)
|
||||
exporter := NewMarkdownExporter(htmlCleaner, nil)
|
||||
course := createTestCourseForMarkdown()
|
||||
|
||||
// Create temporary directory
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# Example Course
|
||||
|
||||
Course description
|
||||
|
||||
## Course Information
|
||||
|
||||
- **Course ID**:
|
||||
- **Share ID**: example-id
|
||||
- **Navigation Mode**:
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user