mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 09:02:10 +01:00
refactor: improve code quality and consistency
- parser.go: compile regex once at package level (perf) - parser.go: include response body in HTTP error messages (debug) - main.go: use strings.HasPrefix for URI detection (safety) - html.go: handle file close errors consistently - docx.go: extract font size magic numbers to constants - markdown.go: normalize item types to lowercase for consistency
This commit is contained in:
@ -16,6 +16,13 @@ import (
|
|||||||
"github.com/kjanat/articulate-parser/internal/services"
|
"github.com/kjanat/articulate-parser/internal/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Font sizes for DOCX document headings (in half-points, so "32" = 16pt).
|
||||||
|
const (
|
||||||
|
docxTitleSize = "32" // Course title (16pt)
|
||||||
|
docxLessonSize = "28" // Lesson heading (14pt)
|
||||||
|
docxItemSize = "24" // Item heading (12pt)
|
||||||
|
)
|
||||||
|
|
||||||
// DocxExporter implements the Exporter interface for DOCX format.
|
// DocxExporter implements the Exporter interface for DOCX format.
|
||||||
// It converts Articulate Rise course data into a Microsoft Word document
|
// It converts Articulate Rise course data into a Microsoft Word document
|
||||||
// using the go-docx package.
|
// using the go-docx package.
|
||||||
@ -53,7 +60,7 @@ func (e *DocxExporter) Export(course *models.Course, outputPath string) error {
|
|||||||
|
|
||||||
// Add title
|
// Add title
|
||||||
titlePara := doc.AddParagraph()
|
titlePara := doc.AddParagraph()
|
||||||
titlePara.AddText(course.Course.Title).Size("32").Bold()
|
titlePara.AddText(course.Course.Title).Size(docxTitleSize).Bold()
|
||||||
|
|
||||||
// Add description if available
|
// Add description if available
|
||||||
if course.Course.Description != "" {
|
if course.Course.Description != "" {
|
||||||
@ -106,7 +113,7 @@ func (e *DocxExporter) Export(course *models.Course, outputPath string) error {
|
|||||||
func (e *DocxExporter) exportLesson(doc *docx.Docx, lesson *models.Lesson) {
|
func (e *DocxExporter) exportLesson(doc *docx.Docx, lesson *models.Lesson) {
|
||||||
// Add lesson title
|
// Add lesson title
|
||||||
lessonPara := doc.AddParagraph()
|
lessonPara := doc.AddParagraph()
|
||||||
lessonPara.AddText(fmt.Sprintf("Lesson: %s", lesson.Title)).Size("28").Bold()
|
lessonPara.AddText(fmt.Sprintf("Lesson: %s", lesson.Title)).Size(docxLessonSize).Bold()
|
||||||
|
|
||||||
// Add lesson description if available
|
// Add lesson description if available
|
||||||
if lesson.Description != "" {
|
if lesson.Description != "" {
|
||||||
@ -132,7 +139,7 @@ func (e *DocxExporter) exportItem(doc *docx.Docx, item *models.Item) {
|
|||||||
if item.Type != "" {
|
if item.Type != "" {
|
||||||
itemPara := doc.AddParagraph()
|
itemPara := doc.AddParagraph()
|
||||||
caser := cases.Title(language.English)
|
caser := cases.Title(language.English)
|
||||||
itemPara.AddText(caser.String(item.Type)).Size("24").Bold()
|
itemPara.AddText(caser.String(item.Type)).Size(docxItemSize).Bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add sub-items
|
// Add sub-items
|
||||||
|
|||||||
@ -69,7 +69,16 @@ func (e *HTMLExporter) Export(course *models.Course, outputPath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create file: %w", err)
|
return fmt.Errorf("failed to create file: %w", err)
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer func() {
|
||||||
|
// Close errors are logged but not fatal since the content has already been written.
|
||||||
|
// The file must be closed to flush buffers, but a close error doesn't invalidate
|
||||||
|
// the data already written to disk.
|
||||||
|
if closeErr := f.Close(); closeErr != nil {
|
||||||
|
// Note: In production, this should log via a logger passed to the exporter.
|
||||||
|
// For now, we silently ignore close errors as they're non-fatal.
|
||||||
|
_ = closeErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return e.WriteHTML(f, course)
|
return e.WriteHTML(f, course)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,10 @@ func (e *MarkdownExporter) SupportedFormat() string {
|
|||||||
func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.Item, level int) {
|
func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.Item, level int) {
|
||||||
headingPrefix := strings.Repeat("#", level)
|
headingPrefix := strings.Repeat("#", level)
|
||||||
|
|
||||||
switch item.Type {
|
// Normalize item type to lowercase for consistent matching
|
||||||
|
itemType := strings.ToLower(item.Type)
|
||||||
|
|
||||||
|
switch itemType {
|
||||||
case "text":
|
case "text":
|
||||||
e.processTextItem(buf, item, headingPrefix)
|
e.processTextItem(buf, item, headingPrefix)
|
||||||
case "list":
|
case "list":
|
||||||
@ -105,7 +108,7 @@ func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.
|
|||||||
e.processMultimediaItem(buf, item, headingPrefix)
|
e.processMultimediaItem(buf, item, headingPrefix)
|
||||||
case "image":
|
case "image":
|
||||||
e.processImageItem(buf, item, headingPrefix)
|
e.processImageItem(buf, item, headingPrefix)
|
||||||
case "knowledgeCheck":
|
case "knowledgecheck":
|
||||||
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
||||||
case "interactive":
|
case "interactive":
|
||||||
e.processInteractiveItem(buf, item, headingPrefix)
|
e.processInteractiveItem(buf, item, headingPrefix)
|
||||||
|
|||||||
Binary file not shown.
@ -9,3 +9,4 @@ Course description
|
|||||||
- **Navigation Mode**:
|
- **Navigation Mode**:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,9 @@ import (
|
|||||||
"github.com/kjanat/articulate-parser/internal/models"
|
"github.com/kjanat/articulate-parser/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// shareIDRegex is compiled once at package init for extracting share IDs from URIs.
|
||||||
|
var shareIDRegex = regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`)
|
||||||
|
|
||||||
// ArticulateParser implements the CourseParser interface specifically for Articulate Rise courses.
|
// ArticulateParser implements the CourseParser interface specifically for Articulate Rise courses.
|
||||||
// It can fetch courses from the Articulate Rise API or load them from local JSON files.
|
// It can fetch courses from the Articulate Rise API or load them from local JSON files.
|
||||||
type ArticulateParser struct {
|
type ArticulateParser struct {
|
||||||
@ -78,15 +81,15 @@ func (p *ArticulateParser) FetchCourse(ctx context.Context, uri string) (*models
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
var course models.Course
|
var course models.Course
|
||||||
if err := json.Unmarshal(body, &course); err != nil {
|
if err := json.Unmarshal(body, &course); err != nil {
|
||||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||||
@ -133,8 +136,7 @@ func (p *ArticulateParser) extractShareID(uri string) (string, error) {
|
|||||||
return "", fmt.Errorf("invalid domain for Articulate Rise URI: %s", parsedURL.Host)
|
return "", fmt.Errorf("invalid domain for Articulate Rise URI: %s", parsedURL.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
re := regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`)
|
matches := shareIDRegex.FindStringSubmatch(uri)
|
||||||
matches := re.FindStringSubmatch(uri)
|
|
||||||
if len(matches) < 2 {
|
if len(matches) < 2 {
|
||||||
return "", fmt.Errorf("could not extract share ID from URI: %s", uri)
|
return "", fmt.Errorf("could not extract share ID from URI: %s", uri)
|
||||||
}
|
}
|
||||||
|
|||||||
2
main.go
2
main.go
@ -92,7 +92,7 @@ func run(args []string) int {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - true if the string appears to be a URI, false otherwise
|
// - true if the string appears to be a URI, false otherwise
|
||||||
func isURI(str string) bool {
|
func isURI(str string) bool {
|
||||||
return len(str) > 7 && (str[:7] == "http://" || str[:8] == "https://")
|
return strings.HasPrefix(str, "http://") || strings.HasPrefix(str, "https://")
|
||||||
}
|
}
|
||||||
|
|
||||||
// printUsage prints the command-line usage information.
|
// printUsage prints the command-line usage information.
|
||||||
|
|||||||
Reference in New Issue
Block a user