diff --git a/.golangci.yml b/.golangci.yml index a81c519..f433de9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -10,8 +10,7 @@ run: # Skip directories (not allowed in config v2, will use issues exclude instead) - # Go version - go: "1.24" + # Go version is autodetected from go.mod # Include test files tests: true diff --git a/internal/exporters/factory.go b/internal/exporters/factory.go index bda72b0..9291449 100644 --- a/internal/exporters/factory.go +++ b/internal/exporters/factory.go @@ -13,6 +13,11 @@ const ( FormatMarkdown = "markdown" FormatDocx = "docx" FormatHTML = "html" + + // Format aliases accepted by CreateExporter. + formatAliasMarkdown = "md" + formatAliasDocx = "word" + formatAliasHTML = "htm" ) // Factory implements the ExporterFactory interface. @@ -41,11 +46,11 @@ func NewFactory(htmlCleaner *services.HTMLCleaner) interfaces.ExporterFactory { // Format strings are case-insensitive (e.g., "markdown", "DOCX"). func (f *Factory) CreateExporter(format string) (interfaces.Exporter, error) { switch strings.ToLower(format) { - case FormatMarkdown, "md": + case FormatMarkdown, formatAliasMarkdown: return NewMarkdownExporter(f.htmlCleaner), nil - case FormatDocx, "word": + case FormatDocx, formatAliasDocx: return NewDocxExporter(f.htmlCleaner), nil - case FormatHTML, "htm": + case FormatHTML, formatAliasHTML: return NewHTMLExporter(f.htmlCleaner), nil default: return nil, fmt.Errorf("unsupported export format: %s", format) @@ -55,5 +60,9 @@ func (f *Factory) CreateExporter(format string) (interfaces.Exporter, error) { // SupportedFormats returns a list of all supported export formats, // including both primary format names and their aliases. func (f *Factory) SupportedFormats() []string { - return []string{FormatMarkdown, "md", FormatDocx, "word", FormatHTML, "htm"} + return []string{ + FormatMarkdown, formatAliasMarkdown, + FormatDocx, formatAliasDocx, + FormatHTML, formatAliasHTML, + } } diff --git a/internal/exporters/html_template_data.go b/internal/exporters/html_template_data.go index 2ed5383..d57b5f8 100644 --- a/internal/exporters/html_template_data.go +++ b/internal/exporters/html_template_data.go @@ -21,6 +21,9 @@ const ( 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 @@ -74,7 +77,7 @@ func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleane Description: lesson.Description, } - if lesson.Type != "section" { + if lesson.Type != lessonTypeSection { lessonCounter++ section.Number = lessonCounter section.Items = prepareItems(lesson.Items, htmlCleaner) diff --git a/internal/exporters/markdown.go b/internal/exporters/markdown.go index d66a849..a39a4ab 100644 --- a/internal/exporters/markdown.go +++ b/internal/exporters/markdown.go @@ -40,35 +40,35 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro var buf bytes.Buffer // Write course header - buf.WriteString(fmt.Sprintf("# %s\n\n", course.Course.Title)) + fmt.Fprintf(&buf, "# %s\n\n", course.Course.Title) if course.Course.Description != "" { - buf.WriteString(fmt.Sprintf("%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description))) + fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description)) } // Add metadata buf.WriteString("## Course Information\n\n") - buf.WriteString(fmt.Sprintf("- **Course ID**: %s\n", course.Course.ID)) - buf.WriteString(fmt.Sprintf("- **Share ID**: %s\n", course.ShareID)) - buf.WriteString(fmt.Sprintf("- **Navigation Mode**: %s\n", course.Course.NavigationMode)) + 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) if course.Course.ExportSettings != nil { - buf.WriteString(fmt.Sprintf("- **Export Format**: %s\n", course.Course.ExportSettings.Format)) + fmt.Fprintf(&buf, "- **Export Format**: %s\n", course.Course.ExportSettings.Format) } buf.WriteString("\n---\n\n") // Process lessons lessonCounter := 0 for _, lesson := range course.Course.Lessons { - if lesson.Type == "section" { - buf.WriteString(fmt.Sprintf("# %s\n\n", lesson.Title)) + if lesson.Type == lessonTypeSection { + fmt.Fprintf(&buf, "# %s\n\n", lesson.Title) continue } lessonCounter++ - buf.WriteString(fmt.Sprintf("## Lesson %d: %s\n\n", lessonCounter, lesson.Title)) + fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, lesson.Title) if lesson.Description != "" { - buf.WriteString(fmt.Sprintf("%s\n\n", e.htmlCleaner.CleanHTML(lesson.Description))) + fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(lesson.Description)) } // Process lesson items @@ -100,19 +100,19 @@ func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models. itemType := strings.ToLower(item.Type) switch itemType { - case "text": + case itemTypeText: e.processTextItem(buf, item, headingPrefix) - case "list": + case itemTypeList: e.processListItem(buf, item) - case "multimedia": + case itemTypeMultimedia: e.processMultimediaItem(buf, item, headingPrefix) - case "image": + case itemTypeImage: e.processImageItem(buf, item, headingPrefix) - case "knowledgecheck": + case itemTypeKnowledgeCheck: e.processKnowledgeCheckItem(buf, item, headingPrefix) - case "interactive": + case itemTypeInteractive: e.processInteractiveItem(buf, item, headingPrefix) - case "divider": + case itemTypeDivider: e.processDividerItem(buf) default: e.processUnknownItem(buf, item, headingPrefix) diff --git a/internal/services/parser.go b/internal/services/parser.go index 076848a..12bca80 100644 --- a/internal/services/parser.go +++ b/internal/services/parser.go @@ -15,6 +15,14 @@ import ( "github.com/kjanat/articulate-parser/internal/models" ) +// Default endpoint configuration for the Articulate Rise API. +const ( + // Root URL for the Articulate Rise API. + defaultBaseURL = "https://rise.articulate.com" + // Expected host for Articulate Rise share URLs. + riseHost = "rise.articulate.com" +) + // shareIDRegex is compiled once at package init for extracting share IDs from URIs. var shareIDRegex = regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`) @@ -37,7 +45,7 @@ func NewArticulateParser(logger interfaces.Logger, baseURL string, timeout time. logger = NewNoOpLogger() } if baseURL == "" { - baseURL = "https://rise.articulate.com" + baseURL = defaultBaseURL } if timeout == 0 { timeout = 30 * time.Second @@ -132,7 +140,7 @@ func (p *ArticulateParser) extractShareID(uri string) (string, error) { } // Validate that it's an Articulate Rise domain - if parsedURL.Host != "rise.articulate.com" { + if parsedURL.Host != riseHost { return "", fmt.Errorf("invalid domain for Articulate Rise URI: %s", parsedURL.Host) }