mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-10-13 00:55:27 +02:00
refactor: extract HTMLCleaner interface and consolidate constants
- Add interfaces.HTMLCleaner interface for better testability - Add CleanAndTrim() method to simplify repeated pattern - Move item/lesson type constants to models/item_types.go - Update all exporters to use interface instead of concrete type - Replace strings.TrimSpace(CleanHTML(...)) with CleanAndTrim()
This commit is contained in:
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
"github.com/kjanat/articulate-parser/internal/services"
|
||||
)
|
||||
|
||||
// Font sizes for DOCX document headings (in half-points, so "32" = 16pt).
|
||||
@@ -28,7 +27,7 @@ const (
|
||||
// using the go-docx package.
|
||||
type DocxExporter struct {
|
||||
// htmlCleaner is used to convert HTML content to plain text
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
htmlCleaner interfaces.HTMLCleaner
|
||||
// logger is used for logging warnings and errors
|
||||
logger interfaces.Logger
|
||||
}
|
||||
@@ -42,7 +41,7 @@ type DocxExporter struct {
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for DOCX format
|
||||
func NewDocxExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
func NewDocxExporter(htmlCleaner interfaces.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
return &DocxExporter{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
"github.com/kjanat/articulate-parser/internal/services"
|
||||
)
|
||||
|
||||
// Format constants for supported export formats.
|
||||
@@ -24,7 +23,7 @@ const (
|
||||
// It creates appropriate exporter instances based on the requested format.
|
||||
type Factory struct {
|
||||
// htmlCleaner is used by exporters to convert HTML content to plain text
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
htmlCleaner interfaces.HTMLCleaner
|
||||
// logger is used by exporters for logging
|
||||
logger interfaces.Logger
|
||||
}
|
||||
@@ -39,7 +38,7 @@ type Factory struct {
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the ExporterFactory interface
|
||||
func NewFactory(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.ExporterFactory {
|
||||
func NewFactory(htmlCleaner interfaces.HTMLCleaner, logger interfaces.Logger) interfaces.ExporterFactory {
|
||||
return &Factory{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
"github.com/kjanat/articulate-parser/internal/services"
|
||||
)
|
||||
|
||||
//go:embed html_styles.css
|
||||
@@ -22,7 +21,7 @@ var htmlTemplate string
|
||||
// It converts Articulate Rise course data into a structured HTML document using templates.
|
||||
type HTMLExporter struct {
|
||||
// htmlCleaner is used to convert HTML content to plain text when needed
|
||||
htmlCleaner *services.HTMLCleaner
|
||||
htmlCleaner interfaces.HTMLCleaner
|
||||
// tmpl holds the parsed HTML template
|
||||
tmpl *template.Template
|
||||
// logger is used for logging warnings and errors
|
||||
@@ -39,7 +38,7 @@ type HTMLExporter struct {
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for HTML format
|
||||
func NewHTMLExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
func NewHTMLExporter(htmlCleaner interfaces.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
// Parse the template with custom functions
|
||||
funcMap := template.FuncMap{
|
||||
"safeHTML": func(s string) template.HTML {
|
||||
|
||||
@@ -6,24 +6,10 @@ import (
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
"github.com/kjanat/articulate-parser/internal/services"
|
||||
)
|
||||
|
||||
// Item type constants.
|
||||
const (
|
||||
itemTypeText = "text"
|
||||
itemTypeList = "list"
|
||||
itemTypeKnowledgeCheck = "knowledgecheck"
|
||||
itemTypeMultimedia = "multimedia"
|
||||
itemTypeImage = "image"
|
||||
itemTypeInteractive = "interactive"
|
||||
itemTypeDivider = "divider"
|
||||
)
|
||||
|
||||
// lessonTypeSection identifies a lesson that acts as a section header.
|
||||
const lessonTypeSection = "section"
|
||||
|
||||
// templateData represents the data structure passed to the HTML template.
|
||||
type templateData struct {
|
||||
Course models.CourseInfo
|
||||
@@ -61,7 +47,7 @@ type templateSubItem struct {
|
||||
}
|
||||
|
||||
// prepareTemplateData converts a Course model into template-friendly data.
|
||||
func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleaner) *templateData {
|
||||
func prepareTemplateData(course *models.Course, htmlCleaner interfaces.HTMLCleaner) *templateData {
|
||||
data := &templateData{
|
||||
Course: course.Course,
|
||||
ShareID: course.ShareID,
|
||||
@@ -77,7 +63,7 @@ func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleane
|
||||
Description: lesson.Description,
|
||||
}
|
||||
|
||||
if lesson.Type != lessonTypeSection {
|
||||
if lesson.Type != models.LessonTypeSection {
|
||||
lessonCounter++
|
||||
section.Number = lessonCounter
|
||||
section.Items = prepareItems(lesson.Items, htmlCleaner)
|
||||
@@ -90,7 +76,7 @@ func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleane
|
||||
}
|
||||
|
||||
// prepareItems converts model Items to template Items.
|
||||
func prepareItems(items []models.Item, htmlCleaner *services.HTMLCleaner) []templateItem {
|
||||
func prepareItems(items []models.Item, htmlCleaner interfaces.HTMLCleaner) []templateItem {
|
||||
result := make([]templateItem, 0, len(items))
|
||||
|
||||
for _, item := range items {
|
||||
@@ -100,9 +86,9 @@ func prepareItems(items []models.Item, htmlCleaner *services.HTMLCleaner) []temp
|
||||
}
|
||||
|
||||
// Set type title for unknown items
|
||||
if tItem.Type != itemTypeText && tItem.Type != itemTypeList && tItem.Type != itemTypeKnowledgeCheck &&
|
||||
tItem.Type != itemTypeMultimedia && tItem.Type != itemTypeImage && tItem.Type != itemTypeInteractive &&
|
||||
tItem.Type != itemTypeDivider {
|
||||
if tItem.Type != models.ItemTypeText && tItem.Type != models.ItemTypeList && tItem.Type != models.ItemTypeKnowledgeCheck &&
|
||||
tItem.Type != models.ItemTypeMultimedia && tItem.Type != models.ItemTypeImage && tItem.Type != models.ItemTypeInteractive &&
|
||||
tItem.Type != models.ItemTypeDivider {
|
||||
caser := cases.Title(language.English)
|
||||
tItem.TypeTitle = caser.String(item.Type)
|
||||
}
|
||||
@@ -120,7 +106,7 @@ func prepareItems(items []models.Item, htmlCleaner *services.HTMLCleaner) []temp
|
||||
}
|
||||
|
||||
// Clean HTML for list items
|
||||
if tItem.Type == itemTypeList && subItem.Paragraph != "" {
|
||||
if tItem.Type == models.ItemTypeList && subItem.Paragraph != "" {
|
||||
tSubItem.CleanText = htmlCleaner.CleanHTML(subItem.Paragraph)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,14 +11,13 @@ import (
|
||||
|
||||
"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
|
||||
htmlCleaner interfaces.HTMLCleaner
|
||||
// logger is used for logging warnings and errors
|
||||
logger interfaces.Logger
|
||||
}
|
||||
@@ -32,7 +31,7 @@ type MarkdownExporter struct {
|
||||
//
|
||||
// Returns:
|
||||
// - An implementation of the Exporter interface for Markdown format
|
||||
func NewMarkdownExporter(htmlCleaner *services.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
func NewMarkdownExporter(htmlCleaner interfaces.HTMLCleaner, logger interfaces.Logger) interfaces.Exporter {
|
||||
return &MarkdownExporter{
|
||||
htmlCleaner: htmlCleaner,
|
||||
logger: logger,
|
||||
@@ -47,8 +46,7 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(course.Course.Title))
|
||||
|
||||
if course.Course.Description != "" {
|
||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(course.Course.Description))
|
||||
if desc != "" {
|
||||
if desc := e.htmlCleaner.CleanAndTrim(course.Course.Description); desc != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", desc)
|
||||
}
|
||||
}
|
||||
@@ -66,7 +64,7 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
// Process lessons
|
||||
lessonCounter := 0
|
||||
for _, lesson := range course.Course.Lessons {
|
||||
if lesson.Type == lessonTypeSection {
|
||||
if lesson.Type == models.LessonTypeSection {
|
||||
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(lesson.Title))
|
||||
continue
|
||||
}
|
||||
@@ -75,8 +73,7 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, strings.TrimSpace(lesson.Title))
|
||||
|
||||
if lesson.Description != "" {
|
||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(lesson.Description))
|
||||
if desc != "" {
|
||||
if desc := e.htmlCleaner.CleanAndTrim(lesson.Description); desc != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", desc)
|
||||
}
|
||||
}
|
||||
@@ -143,19 +140,19 @@ func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.
|
||||
itemType := strings.ToLower(item.Type)
|
||||
|
||||
switch itemType {
|
||||
case itemTypeText:
|
||||
case models.ItemTypeText:
|
||||
e.processTextItem(buf, item, headingPrefix)
|
||||
case itemTypeList:
|
||||
case models.ItemTypeList:
|
||||
e.processListItem(buf, item)
|
||||
case itemTypeMultimedia:
|
||||
case models.ItemTypeMultimedia:
|
||||
e.processMultimediaItem(buf, item, headingPrefix)
|
||||
case itemTypeImage:
|
||||
case models.ItemTypeImage:
|
||||
e.processImageItem(buf, item, headingPrefix)
|
||||
case itemTypeKnowledgeCheck:
|
||||
case models.ItemTypeKnowledgeCheck:
|
||||
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
||||
case itemTypeInteractive:
|
||||
case models.ItemTypeInteractive:
|
||||
e.processInteractiveItem(buf, item, headingPrefix)
|
||||
case itemTypeDivider:
|
||||
case models.ItemTypeDivider:
|
||||
e.processDividerItem(buf)
|
||||
default:
|
||||
e.processUnknownItem(buf, item, headingPrefix)
|
||||
@@ -166,14 +163,12 @@ 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 := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Heading))
|
||||
if heading != "" {
|
||||
if heading := e.htmlCleaner.CleanAndTrim(subItem.Heading); heading != "" {
|
||||
fmt.Fprintf(buf, "%s %s\n\n", headingPrefix, heading)
|
||||
}
|
||||
}
|
||||
if subItem.Paragraph != "" {
|
||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||
fmt.Fprintf(buf, "%s\n\n", paragraph)
|
||||
}
|
||||
}
|
||||
@@ -184,8 +179,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 := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||
fmt.Fprintf(buf, "- %s\n", paragraph)
|
||||
}
|
||||
}
|
||||
@@ -209,8 +203,7 @@ func (e *MarkdownExporter) processMediaSubItem(buf *bytes.Buffer, subItem models
|
||||
e.processImageMedia(buf, subItem.Media)
|
||||
}
|
||||
if subItem.Caption != "" {
|
||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
||||
if caption != "" {
|
||||
if caption := e.htmlCleaner.CleanAndTrim(subItem.Caption); caption != "" {
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
}
|
||||
}
|
||||
@@ -241,8 +234,7 @@ func (e *MarkdownExporter) processImageItem(buf *bytes.Buffer, item models.Item,
|
||||
fmt.Fprintf(buf, "**Image**: %s\n", subItem.Media.Image.OriginalURL)
|
||||
}
|
||||
if subItem.Caption != "" {
|
||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
||||
if caption != "" {
|
||||
if caption := e.htmlCleaner.CleanAndTrim(subItem.Caption); caption != "" {
|
||||
fmt.Fprintf(buf, "*%s*\n", caption)
|
||||
}
|
||||
}
|
||||
@@ -262,8 +254,7 @@ 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 := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||
fmt.Fprintf(buf, "**Question**: %s\n\n", title)
|
||||
}
|
||||
}
|
||||
@@ -271,8 +262,7 @@ func (e *MarkdownExporter) processQuestionSubItem(buf *bytes.Buffer, subItem mod
|
||||
e.processAnswers(buf, subItem.Answers)
|
||||
|
||||
if subItem.Feedback != "" {
|
||||
feedback := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Feedback))
|
||||
if feedback != "" {
|
||||
if feedback := e.htmlCleaner.CleanAndTrim(subItem.Feedback); feedback != "" {
|
||||
fmt.Fprintf(buf, "\n**Feedback**: %s\n", feedback)
|
||||
}
|
||||
}
|
||||
@@ -299,8 +289,7 @@ 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 := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
}
|
||||
}
|
||||
@@ -326,14 +315,12 @@ 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 := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
||||
if title != "" {
|
||||
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||
}
|
||||
}
|
||||
if subItem.Paragraph != "" {
|
||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
||||
if paragraph != "" {
|
||||
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||
fmt.Fprintf(buf, "%s\n\n", paragraph)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package interfaces
|
||||
|
||||
// HTMLCleaner defines the interface for cleaning HTML content.
|
||||
// Implementations convert HTML to plain text, stripping tags and decoding entities.
|
||||
type HTMLCleaner interface {
|
||||
// CleanHTML removes HTML tags and decodes HTML entities from a string.
|
||||
// Returns plain text suitable for non-HTML output formats.
|
||||
CleanHTML(htmlStr string) string
|
||||
|
||||
// CleanAndTrim removes HTML tags, decodes entities, and trims whitespace.
|
||||
// Convenience method combining CleanHTML with strings.TrimSpace.
|
||||
CleanAndTrim(htmlStr string) string
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package models
|
||||
|
||||
// Item type constants used throughout the application.
|
||||
// These match the type field values in Articulate Rise course data.
|
||||
const (
|
||||
// ItemTypeText represents text content blocks with headings and paragraphs.
|
||||
ItemTypeText = "text"
|
||||
|
||||
// ItemTypeList represents bullet or numbered list content.
|
||||
ItemTypeList = "list"
|
||||
|
||||
// ItemTypeKnowledgeCheck represents quiz or assessment items.
|
||||
ItemTypeKnowledgeCheck = "knowledgecheck"
|
||||
|
||||
// ItemTypeMultimedia represents video or audio content.
|
||||
ItemTypeMultimedia = "multimedia"
|
||||
|
||||
// ItemTypeImage represents standalone image content.
|
||||
ItemTypeImage = "image"
|
||||
|
||||
// ItemTypeInteractive represents interactive elements like accordions or tabs.
|
||||
ItemTypeInteractive = "interactive"
|
||||
|
||||
// ItemTypeDivider represents visual separator elements.
|
||||
ItemTypeDivider = "divider"
|
||||
|
||||
// ItemTypeQuote represents quotation blocks.
|
||||
ItemTypeQuote = "quote"
|
||||
|
||||
// ItemTypeFlipcard represents flashcard-style content.
|
||||
ItemTypeFlipcard = "flipcard"
|
||||
)
|
||||
|
||||
// Lesson type constants.
|
||||
const (
|
||||
// LessonTypeSection represents a section header (not a real lesson).
|
||||
LessonTypeSection = "section"
|
||||
|
||||
// LessonTypeLesson represents a regular lesson.
|
||||
LessonTypeLesson = "lesson"
|
||||
)
|
||||
@@ -7,13 +7,19 @@ import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
)
|
||||
|
||||
// HTMLCleaner provides utilities for converting HTML content to plain text.
|
||||
// It removes HTML tags while preserving their content and converts HTML entities
|
||||
// to their plain text equivalents using proper HTML parsing instead of regex.
|
||||
// Implements interfaces.HTMLCleaner.
|
||||
type HTMLCleaner struct{}
|
||||
|
||||
// Compile-time check that HTMLCleaner implements interfaces.HTMLCleaner.
|
||||
var _ interfaces.HTMLCleaner = (*HTMLCleaner)(nil)
|
||||
|
||||
// NewHTMLCleaner creates a new HTML cleaner instance.
|
||||
// This service is typically injected into exporters that need to handle
|
||||
// HTML content from Articulate Rise courses.
|
||||
@@ -46,6 +52,13 @@ func (h *HTMLCleaner) CleanHTML(htmlStr string) string {
|
||||
return strings.TrimSpace(cleaned)
|
||||
}
|
||||
|
||||
// CleanAndTrim removes HTML tags, decodes entities, and trims whitespace.
|
||||
// Convenience method that combines CleanHTML with strings.TrimSpace.
|
||||
// Returns empty string if input is empty or contains only whitespace/tags.
|
||||
func (h *HTMLCleaner) CleanAndTrim(htmlStr string) string {
|
||||
return strings.TrimSpace(h.CleanHTML(htmlStr))
|
||||
}
|
||||
|
||||
// extractText recursively traverses the HTML node tree and extracts text content.
|
||||
// It skips script and style tags to avoid including their content in the output.
|
||||
func extractText(w io.Writer, n *html.Node) {
|
||||
|
||||
Reference in New Issue
Block a user