mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-10-13 03:35: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/interfaces"
|
||||||
"github.com/kjanat/articulate-parser/internal/models"
|
"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).
|
// Font sizes for DOCX document headings (in half-points, so "32" = 16pt).
|
||||||
@@ -28,7 +27,7 @@ const (
|
|||||||
// using the go-docx package.
|
// using the go-docx package.
|
||||||
type DocxExporter struct {
|
type DocxExporter struct {
|
||||||
// htmlCleaner is used to convert HTML content to plain text
|
// 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 is used for logging warnings and errors
|
||||||
logger interfaces.Logger
|
logger interfaces.Logger
|
||||||
}
|
}
|
||||||
@@ -42,7 +41,7 @@ type DocxExporter struct {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An implementation of the Exporter interface for DOCX format
|
// - 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{
|
return &DocxExporter{
|
||||||
htmlCleaner: htmlCleaner,
|
htmlCleaner: htmlCleaner,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||||
"github.com/kjanat/articulate-parser/internal/services"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Format constants for supported export formats.
|
// Format constants for supported export formats.
|
||||||
@@ -24,7 +23,7 @@ const (
|
|||||||
// It creates appropriate exporter instances based on the requested format.
|
// It creates appropriate exporter instances based on the requested format.
|
||||||
type Factory struct {
|
type Factory struct {
|
||||||
// htmlCleaner is used by exporters to convert HTML content to plain text
|
// 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 is used by exporters for logging
|
||||||
logger interfaces.Logger
|
logger interfaces.Logger
|
||||||
}
|
}
|
||||||
@@ -39,7 +38,7 @@ type Factory struct {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An implementation of the ExporterFactory interface
|
// - 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{
|
return &Factory{
|
||||||
htmlCleaner: htmlCleaner,
|
htmlCleaner: htmlCleaner,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||||
"github.com/kjanat/articulate-parser/internal/models"
|
"github.com/kjanat/articulate-parser/internal/models"
|
||||||
"github.com/kjanat/articulate-parser/internal/services"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed html_styles.css
|
//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.
|
// It converts Articulate Rise course data into a structured HTML document using templates.
|
||||||
type HTMLExporter struct {
|
type HTMLExporter struct {
|
||||||
// htmlCleaner is used to convert HTML content to plain text when needed
|
// 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 holds the parsed HTML template
|
||||||
tmpl *template.Template
|
tmpl *template.Template
|
||||||
// logger is used for logging warnings and errors
|
// logger is used for logging warnings and errors
|
||||||
@@ -39,7 +38,7 @@ type HTMLExporter struct {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An implementation of the Exporter interface for HTML format
|
// - 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
|
// Parse the template with custom functions
|
||||||
funcMap := template.FuncMap{
|
funcMap := template.FuncMap{
|
||||||
"safeHTML": func(s string) template.HTML {
|
"safeHTML": func(s string) template.HTML {
|
||||||
|
|||||||
@@ -6,24 +6,10 @@ import (
|
|||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"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/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.
|
// templateData represents the data structure passed to the HTML template.
|
||||||
type templateData struct {
|
type templateData struct {
|
||||||
Course models.CourseInfo
|
Course models.CourseInfo
|
||||||
@@ -61,7 +47,7 @@ type templateSubItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prepareTemplateData converts a Course model into template-friendly data.
|
// 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{
|
data := &templateData{
|
||||||
Course: course.Course,
|
Course: course.Course,
|
||||||
ShareID: course.ShareID,
|
ShareID: course.ShareID,
|
||||||
@@ -77,7 +63,7 @@ func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleane
|
|||||||
Description: lesson.Description,
|
Description: lesson.Description,
|
||||||
}
|
}
|
||||||
|
|
||||||
if lesson.Type != lessonTypeSection {
|
if lesson.Type != models.LessonTypeSection {
|
||||||
lessonCounter++
|
lessonCounter++
|
||||||
section.Number = lessonCounter
|
section.Number = lessonCounter
|
||||||
section.Items = prepareItems(lesson.Items, htmlCleaner)
|
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.
|
// 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))
|
result := make([]templateItem, 0, len(items))
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
@@ -100,9 +86,9 @@ func prepareItems(items []models.Item, htmlCleaner *services.HTMLCleaner) []temp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set type title for unknown items
|
// Set type title for unknown items
|
||||||
if tItem.Type != itemTypeText && tItem.Type != itemTypeList && tItem.Type != itemTypeKnowledgeCheck &&
|
if tItem.Type != models.ItemTypeText && tItem.Type != models.ItemTypeList && tItem.Type != models.ItemTypeKnowledgeCheck &&
|
||||||
tItem.Type != itemTypeMultimedia && tItem.Type != itemTypeImage && tItem.Type != itemTypeInteractive &&
|
tItem.Type != models.ItemTypeMultimedia && tItem.Type != models.ItemTypeImage && tItem.Type != models.ItemTypeInteractive &&
|
||||||
tItem.Type != itemTypeDivider {
|
tItem.Type != models.ItemTypeDivider {
|
||||||
caser := cases.Title(language.English)
|
caser := cases.Title(language.English)
|
||||||
tItem.TypeTitle = caser.String(item.Type)
|
tItem.TypeTitle = caser.String(item.Type)
|
||||||
}
|
}
|
||||||
@@ -120,7 +106,7 @@ func prepareItems(items []models.Item, htmlCleaner *services.HTMLCleaner) []temp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clean HTML for list items
|
// Clean HTML for list items
|
||||||
if tItem.Type == itemTypeList && subItem.Paragraph != "" {
|
if tItem.Type == models.ItemTypeList && subItem.Paragraph != "" {
|
||||||
tSubItem.CleanText = htmlCleaner.CleanHTML(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/interfaces"
|
||||||
"github.com/kjanat/articulate-parser/internal/models"
|
"github.com/kjanat/articulate-parser/internal/models"
|
||||||
"github.com/kjanat/articulate-parser/internal/services"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarkdownExporter implements the Exporter interface for Markdown format.
|
// MarkdownExporter implements the Exporter interface for Markdown format.
|
||||||
// It converts Articulate Rise course data into a structured Markdown document.
|
// It converts Articulate Rise course data into a structured Markdown document.
|
||||||
type MarkdownExporter struct {
|
type MarkdownExporter struct {
|
||||||
// htmlCleaner is used to convert HTML content to plain text
|
// 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 is used for logging warnings and errors
|
||||||
logger interfaces.Logger
|
logger interfaces.Logger
|
||||||
}
|
}
|
||||||
@@ -32,7 +31,7 @@ type MarkdownExporter struct {
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - An implementation of the Exporter interface for Markdown format
|
// - 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{
|
return &MarkdownExporter{
|
||||||
htmlCleaner: htmlCleaner,
|
htmlCleaner: htmlCleaner,
|
||||||
logger: logger,
|
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))
|
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(course.Course.Title))
|
||||||
|
|
||||||
if course.Course.Description != "" {
|
if course.Course.Description != "" {
|
||||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(course.Course.Description))
|
if desc := e.htmlCleaner.CleanAndTrim(course.Course.Description); desc != "" {
|
||||||
if desc != "" {
|
|
||||||
fmt.Fprintf(&buf, "%s\n\n", desc)
|
fmt.Fprintf(&buf, "%s\n\n", desc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,7 +64,7 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
|||||||
// Process lessons
|
// Process lessons
|
||||||
lessonCounter := 0
|
lessonCounter := 0
|
||||||
for _, lesson := range course.Course.Lessons {
|
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))
|
fmt.Fprintf(&buf, "# %s\n\n", strings.TrimSpace(lesson.Title))
|
||||||
continue
|
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))
|
fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, strings.TrimSpace(lesson.Title))
|
||||||
|
|
||||||
if lesson.Description != "" {
|
if lesson.Description != "" {
|
||||||
desc := strings.TrimSpace(e.htmlCleaner.CleanHTML(lesson.Description))
|
if desc := e.htmlCleaner.CleanAndTrim(lesson.Description); desc != "" {
|
||||||
if desc != "" {
|
|
||||||
fmt.Fprintf(&buf, "%s\n\n", 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)
|
itemType := strings.ToLower(item.Type)
|
||||||
|
|
||||||
switch itemType {
|
switch itemType {
|
||||||
case itemTypeText:
|
case models.ItemTypeText:
|
||||||
e.processTextItem(buf, item, headingPrefix)
|
e.processTextItem(buf, item, headingPrefix)
|
||||||
case itemTypeList:
|
case models.ItemTypeList:
|
||||||
e.processListItem(buf, item)
|
e.processListItem(buf, item)
|
||||||
case itemTypeMultimedia:
|
case models.ItemTypeMultimedia:
|
||||||
e.processMultimediaItem(buf, item, headingPrefix)
|
e.processMultimediaItem(buf, item, headingPrefix)
|
||||||
case itemTypeImage:
|
case models.ItemTypeImage:
|
||||||
e.processImageItem(buf, item, headingPrefix)
|
e.processImageItem(buf, item, headingPrefix)
|
||||||
case itemTypeKnowledgeCheck:
|
case models.ItemTypeKnowledgeCheck:
|
||||||
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
||||||
case itemTypeInteractive:
|
case models.ItemTypeInteractive:
|
||||||
e.processInteractiveItem(buf, item, headingPrefix)
|
e.processInteractiveItem(buf, item, headingPrefix)
|
||||||
case itemTypeDivider:
|
case models.ItemTypeDivider:
|
||||||
e.processDividerItem(buf)
|
e.processDividerItem(buf)
|
||||||
default:
|
default:
|
||||||
e.processUnknownItem(buf, item, headingPrefix)
|
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) {
|
func (e *MarkdownExporter) processTextItem(buf *bytes.Buffer, item models.Item, headingPrefix string) {
|
||||||
for _, subItem := range item.Items {
|
for _, subItem := range item.Items {
|
||||||
if subItem.Heading != "" {
|
if subItem.Heading != "" {
|
||||||
heading := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Heading))
|
if heading := e.htmlCleaner.CleanAndTrim(subItem.Heading); heading != "" {
|
||||||
if heading != "" {
|
|
||||||
fmt.Fprintf(buf, "%s %s\n\n", headingPrefix, heading)
|
fmt.Fprintf(buf, "%s %s\n\n", headingPrefix, heading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if subItem.Paragraph != "" {
|
if subItem.Paragraph != "" {
|
||||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||||
if paragraph != "" {
|
|
||||||
fmt.Fprintf(buf, "%s\n\n", 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) {
|
func (e *MarkdownExporter) processListItem(buf *bytes.Buffer, item models.Item) {
|
||||||
for _, subItem := range item.Items {
|
for _, subItem := range item.Items {
|
||||||
if subItem.Paragraph != "" {
|
if subItem.Paragraph != "" {
|
||||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||||
if paragraph != "" {
|
|
||||||
fmt.Fprintf(buf, "- %s\n", 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)
|
e.processImageMedia(buf, subItem.Media)
|
||||||
}
|
}
|
||||||
if subItem.Caption != "" {
|
if subItem.Caption != "" {
|
||||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
if caption := e.htmlCleaner.CleanAndTrim(subItem.Caption); caption != "" {
|
||||||
if caption != "" {
|
|
||||||
fmt.Fprintf(buf, "*%s*\n", 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)
|
fmt.Fprintf(buf, "**Image**: %s\n", subItem.Media.Image.OriginalURL)
|
||||||
}
|
}
|
||||||
if subItem.Caption != "" {
|
if subItem.Caption != "" {
|
||||||
caption := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Caption))
|
if caption := e.htmlCleaner.CleanAndTrim(subItem.Caption); caption != "" {
|
||||||
if caption != "" {
|
|
||||||
fmt.Fprintf(buf, "*%s*\n", 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.
|
// processQuestionSubItem processes individual question items.
|
||||||
func (e *MarkdownExporter) processQuestionSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
func (e *MarkdownExporter) processQuestionSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
||||||
if subItem.Title != "" {
|
if subItem.Title != "" {
|
||||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||||
if title != "" {
|
|
||||||
fmt.Fprintf(buf, "**Question**: %s\n\n", 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)
|
e.processAnswers(buf, subItem.Answers)
|
||||||
|
|
||||||
if subItem.Feedback != "" {
|
if subItem.Feedback != "" {
|
||||||
feedback := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Feedback))
|
if feedback := e.htmlCleaner.CleanAndTrim(subItem.Feedback); feedback != "" {
|
||||||
if feedback != "" {
|
|
||||||
fmt.Fprintf(buf, "\n**Feedback**: %s\n", 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)
|
fmt.Fprintf(buf, "%s Interactive Content\n\n", headingPrefix)
|
||||||
for _, subItem := range item.Items {
|
for _, subItem := range item.Items {
|
||||||
if subItem.Title != "" {
|
if subItem.Title != "" {
|
||||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||||
if title != "" {
|
|
||||||
fmt.Fprintf(buf, "**%s**\n\n", 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.
|
// processGenericSubItem processes sub-items for unknown types.
|
||||||
func (e *MarkdownExporter) processGenericSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
func (e *MarkdownExporter) processGenericSubItem(buf *bytes.Buffer, subItem models.SubItem) {
|
||||||
if subItem.Title != "" {
|
if subItem.Title != "" {
|
||||||
title := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Title))
|
if title := e.htmlCleaner.CleanAndTrim(subItem.Title); title != "" {
|
||||||
if title != "" {
|
|
||||||
fmt.Fprintf(buf, "**%s**\n\n", title)
|
fmt.Fprintf(buf, "**%s**\n\n", title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if subItem.Paragraph != "" {
|
if subItem.Paragraph != "" {
|
||||||
paragraph := strings.TrimSpace(e.htmlCleaner.CleanHTML(subItem.Paragraph))
|
if paragraph := e.htmlCleaner.CleanAndTrim(subItem.Paragraph); paragraph != "" {
|
||||||
if paragraph != "" {
|
|
||||||
fmt.Fprintf(buf, "%s\n\n", 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"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/net/html"
|
"golang.org/x/net/html"
|
||||||
|
|
||||||
|
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTMLCleaner provides utilities for converting HTML content to plain text.
|
// HTMLCleaner provides utilities for converting HTML content to plain text.
|
||||||
// It removes HTML tags while preserving their content and converts HTML entities
|
// It removes HTML tags while preserving their content and converts HTML entities
|
||||||
// to their plain text equivalents using proper HTML parsing instead of regex.
|
// to their plain text equivalents using proper HTML parsing instead of regex.
|
||||||
|
// Implements interfaces.HTMLCleaner.
|
||||||
type HTMLCleaner struct{}
|
type HTMLCleaner struct{}
|
||||||
|
|
||||||
|
// Compile-time check that HTMLCleaner implements interfaces.HTMLCleaner.
|
||||||
|
var _ interfaces.HTMLCleaner = (*HTMLCleaner)(nil)
|
||||||
|
|
||||||
// NewHTMLCleaner creates a new HTML cleaner instance.
|
// NewHTMLCleaner creates a new HTML cleaner instance.
|
||||||
// This service is typically injected into exporters that need to handle
|
// This service is typically injected into exporters that need to handle
|
||||||
// HTML content from Articulate Rise courses.
|
// HTML content from Articulate Rise courses.
|
||||||
@@ -46,6 +52,13 @@ func (h *HTMLCleaner) CleanHTML(htmlStr string) string {
|
|||||||
return strings.TrimSpace(cleaned)
|
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.
|
// 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.
|
// It skips script and style tags to avoid including their content in the output.
|
||||||
func extractText(w io.Writer, n *html.Node) {
|
func extractText(w io.Writer, n *html.Node) {
|
||||||
|
|||||||
Reference in New Issue
Block a user