mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 15:42:11 +01:00
Compare commits
2 Commits
v0.3.0
...
a0003983c4
| Author | SHA1 | Date | |
|---|---|---|---|
| a0003983c4 | |||
|
1c1460ff04
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -48,9 +48,12 @@ build/
|
|||||||
# Test coverage files
|
# Test coverage files
|
||||||
coverage.out
|
coverage.out
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
coverage.html
|
||||||
|
coverage.*
|
||||||
coverage
|
coverage
|
||||||
*.cover
|
*.cover
|
||||||
*.coverprofile
|
*.coverprofile
|
||||||
|
main_coverage
|
||||||
|
|
||||||
# Other common exclusions
|
# Other common exclusions
|
||||||
*.exe
|
*.exe
|
||||||
|
|||||||
34
README.md
34
README.md
@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
A Go-based parser that converts Articulate Rise e-learning content to various formats including Markdown, HTML, and Word documents.
|
A Go-based parser that converts Articulate Rise e-learning content to various formats including Markdown, HTML, and Word documents.
|
||||||
|
|
||||||
|
[][gomod]
|
||||||
|
[][Package documentation]
|
||||||
|
[][Go report]
|
||||||
|
[][Tags] <!-- [][Latest release] -->
|
||||||
|
[][MIT License] <!-- [][Commits] -->
|
||||||
|
[][Commits]
|
||||||
|
[][Issues]
|
||||||
|
[][Build]
|
||||||
|
[][Codecov]
|
||||||
|
|
||||||
## System Architecture
|
## System Architecture
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
@ -73,16 +83,6 @@ The system follows **Clean Architecture** principles with clear separation of co
|
|||||||
- **📤 Export Layer**: Factory pattern for format-specific exporters
|
- **📤 Export Layer**: Factory pattern for format-specific exporters
|
||||||
- **📊 Data Layer**: Domain models representing course structure
|
- **📊 Data Layer**: Domain models representing course structure
|
||||||
|
|
||||||
[][gomod]
|
|
||||||
[][Package documentation]
|
|
||||||
[][Go report]
|
|
||||||
[][Tags] <!-- [][Latest release] -->
|
|
||||||
[][MIT License] <!-- [][Commits] -->
|
|
||||||
[][Commits]
|
|
||||||
[][Issues]
|
|
||||||
[][Build]
|
|
||||||
[][Codecov]
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Parse Articulate Rise JSON data from URLs or local files
|
- Parse Articulate Rise JSON data from URLs or local files
|
||||||
@ -291,7 +291,7 @@ The parser includes error handling for:
|
|||||||
|
|
||||||
<!-- ## Code coverage
|
<!-- ## Code coverage
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -315,12 +315,12 @@ The parser includes error handling for:
|
|||||||
|
|
||||||
Potential improvements could include:
|
Potential improvements could include:
|
||||||
|
|
||||||
- PDF export support
|
- [ ] PDF export support
|
||||||
- Media file downloading
|
- [ ] Media file downloading
|
||||||
- ~~HTML export with preserved styling~~ ✅ **Completed**
|
- [x] ~~HTML export with preserved styling~~
|
||||||
- SCORM package support
|
- [ ] SCORM package support
|
||||||
- Batch processing capabilities
|
- [ ] Batch processing capabilities
|
||||||
- Custom template support
|
- [ ] Custom template support
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ package version
|
|||||||
// Version information.
|
// Version information.
|
||||||
var (
|
var (
|
||||||
// Version is the current version of the application.
|
// Version is the current version of the application.
|
||||||
Version = "0.3.0"
|
Version = "0.3.1"
|
||||||
|
|
||||||
// BuildTime is the time the binary was built.
|
// BuildTime is the time the binary was built.
|
||||||
BuildTime = "unknown"
|
BuildTime = "unknown"
|
||||||
|
|||||||
26
main.go
26
main.go
@ -16,6 +16,12 @@ import (
|
|||||||
// It handles command-line arguments, sets up dependencies,
|
// It handles command-line arguments, sets up dependencies,
|
||||||
// and coordinates the parsing and exporting of courses.
|
// and coordinates the parsing and exporting of courses.
|
||||||
func main() {
|
func main() {
|
||||||
|
os.Exit(run(os.Args))
|
||||||
|
}
|
||||||
|
|
||||||
|
// run contains the main application logic and returns an exit code.
|
||||||
|
// This function is testable as it doesn't call os.Exit directly.
|
||||||
|
func run(args []string) int {
|
||||||
// Dependency injection setup
|
// Dependency injection setup
|
||||||
htmlCleaner := services.NewHTMLCleaner()
|
htmlCleaner := services.NewHTMLCleaner()
|
||||||
parser := services.NewArticulateParser()
|
parser := services.NewArticulateParser()
|
||||||
@ -23,20 +29,20 @@ func main() {
|
|||||||
app := services.NewApp(parser, exporterFactory)
|
app := services.NewApp(parser, exporterFactory)
|
||||||
|
|
||||||
// Check for required command-line arguments
|
// Check for required command-line arguments
|
||||||
if len(os.Args) < 4 {
|
if len(args) < 4 {
|
||||||
fmt.Printf("Usage: %s <source> <format> <output>\n", os.Args[0])
|
fmt.Printf("Usage: %s <source> <format> <output>\n", args[0])
|
||||||
fmt.Printf(" source: URI or file path to the course\n")
|
fmt.Printf(" source: URI or file path to the course\n")
|
||||||
fmt.Printf(" format: export format (%s)\n", joinStrings(app.GetSupportedFormats(), ", "))
|
fmt.Printf(" format: export format (%s)\n", joinStrings(app.GetSupportedFormats(), ", "))
|
||||||
fmt.Printf(" output: output file path\n")
|
fmt.Printf(" output: output file path\n")
|
||||||
fmt.Println("\nExample:")
|
fmt.Println("\nExample:")
|
||||||
fmt.Printf(" %s articulate-sample.json markdown output.md\n", os.Args[0])
|
fmt.Printf(" %s articulate-sample.json markdown output.md\n", args[0])
|
||||||
fmt.Printf(" %s https://rise.articulate.com/share/xyz docx output.docx\n", os.Args[0])
|
fmt.Printf(" %s https://rise.articulate.com/share/xyz docx output.docx\n", args[0])
|
||||||
os.Exit(1)
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
source := os.Args[1]
|
source := args[1]
|
||||||
format := os.Args[2]
|
format := args[2]
|
||||||
output := os.Args[3]
|
output := args[3]
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
@ -48,10 +54,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error processing course: %v", err)
|
log.Printf("Error processing course: %v", err)
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Successfully exported course to %s\n", output)
|
fmt.Printf("Successfully exported course to %s\n", output)
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// isURI checks if a string is a URI by looking for http:// or https:// prefixes.
|
// isURI checks if a string is a URI by looking for http:// or https:// prefixes.
|
||||||
|
|||||||
303
main_test.go
303
main_test.go
@ -2,6 +2,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -173,3 +179,300 @@ func BenchmarkJoinStrings(b *testing.B) {
|
|||||||
joinStrings(strs, separator)
|
joinStrings(strs, separator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRunWithInsufficientArgs tests the run function with insufficient command-line arguments.
|
||||||
|
func TestRunWithInsufficientArgs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no arguments",
|
||||||
|
args: []string{"articulate-parser"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one argument",
|
||||||
|
args: []string{"articulate-parser", "source"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "two arguments",
|
||||||
|
args: []string{"articulate-parser", "source", "format"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Capture stdout
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
// Run the function
|
||||||
|
exitCode := run(tt.args)
|
||||||
|
|
||||||
|
// Restore stdout
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
|
||||||
|
// Read captured output
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// Verify exit code
|
||||||
|
if exitCode != 1 {
|
||||||
|
t.Errorf("Expected exit code 1, got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify usage message is displayed
|
||||||
|
if !strings.Contains(output, "Usage:") {
|
||||||
|
t.Errorf("Expected usage message in output, got: %s", output)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(output, "export format") {
|
||||||
|
t.Errorf("Expected format information in output, got: %s", output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunWithInvalidFile tests the run function with a non-existent file.
|
||||||
|
func TestRunWithInvalidFile(t *testing.T) {
|
||||||
|
// Capture stdout and stderr
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
|
||||||
|
stdoutR, stdoutW, _ := os.Pipe()
|
||||||
|
stderrR, stderrW, _ := os.Pipe()
|
||||||
|
|
||||||
|
os.Stdout = stdoutW
|
||||||
|
os.Stderr = stderrW
|
||||||
|
|
||||||
|
// Also need to redirect log output
|
||||||
|
oldLogOutput := log.Writer()
|
||||||
|
log.SetOutput(stderrW)
|
||||||
|
|
||||||
|
// Run with non-existent file
|
||||||
|
args := []string{"articulate-parser", "nonexistent-file.json", "markdown", "output.md"}
|
||||||
|
exitCode := run(args)
|
||||||
|
|
||||||
|
// Restore stdout/stderr and log output
|
||||||
|
stdoutW.Close()
|
||||||
|
stderrW.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
os.Stderr = oldStderr
|
||||||
|
log.SetOutput(oldLogOutput)
|
||||||
|
|
||||||
|
// Read captured output
|
||||||
|
var stdoutBuf, stderrBuf bytes.Buffer
|
||||||
|
io.Copy(&stdoutBuf, stdoutR)
|
||||||
|
io.Copy(&stderrBuf, stderrR)
|
||||||
|
|
||||||
|
stdoutR.Close()
|
||||||
|
stderrR.Close()
|
||||||
|
|
||||||
|
// Verify exit code
|
||||||
|
if exitCode != 1 {
|
||||||
|
t.Errorf("Expected exit code 1 for non-existent file, got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have error output
|
||||||
|
errorOutput := stderrBuf.String()
|
||||||
|
if !strings.Contains(errorOutput, "Error processing course") {
|
||||||
|
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunWithInvalidURI tests the run function with an invalid URI.
|
||||||
|
func TestRunWithInvalidURI(t *testing.T) {
|
||||||
|
// Capture stdout and stderr
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
oldStderr := os.Stderr
|
||||||
|
|
||||||
|
stdoutR, stdoutW, _ := os.Pipe()
|
||||||
|
stderrR, stderrW, _ := os.Pipe()
|
||||||
|
|
||||||
|
os.Stdout = stdoutW
|
||||||
|
os.Stderr = stderrW
|
||||||
|
|
||||||
|
// Also need to redirect log output
|
||||||
|
oldLogOutput := log.Writer()
|
||||||
|
log.SetOutput(stderrW)
|
||||||
|
|
||||||
|
// Run with invalid URI (will fail because we can't actually fetch)
|
||||||
|
args := []string{"articulate-parser", "https://example.com/invalid", "markdown", "output.md"}
|
||||||
|
exitCode := run(args)
|
||||||
|
|
||||||
|
// Restore stdout/stderr and log output
|
||||||
|
stdoutW.Close()
|
||||||
|
stderrW.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
os.Stderr = oldStderr
|
||||||
|
log.SetOutput(oldLogOutput)
|
||||||
|
|
||||||
|
// Read captured output
|
||||||
|
var stdoutBuf, stderrBuf bytes.Buffer
|
||||||
|
io.Copy(&stdoutBuf, stdoutR)
|
||||||
|
io.Copy(&stderrBuf, stderrR)
|
||||||
|
|
||||||
|
stdoutR.Close()
|
||||||
|
stderrR.Close()
|
||||||
|
|
||||||
|
// Should fail because the URI is invalid/unreachable
|
||||||
|
if exitCode != 1 {
|
||||||
|
t.Errorf("Expected failure (exit code 1) for invalid URI, got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have error output
|
||||||
|
errorOutput := stderrBuf.String()
|
||||||
|
if !strings.Contains(errorOutput, "Error processing course") {
|
||||||
|
t.Errorf("Expected error message about processing course, got: %s", errorOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunWithValidJSONFile tests the run function with a valid JSON file.
|
||||||
|
func TestRunWithValidJSONFile(t *testing.T) {
|
||||||
|
// Create a temporary test JSON file
|
||||||
|
testContent := `{
|
||||||
|
"title": "Test Course",
|
||||||
|
"lessons": [
|
||||||
|
{
|
||||||
|
"id": "lesson1",
|
||||||
|
"title": "Test Lesson",
|
||||||
|
"blocks": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"id": "block1",
|
||||||
|
"data": {
|
||||||
|
"text": "Test content"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
tmpFile, err := os.CreateTemp("", "test-course-*.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpFile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpFile.WriteString(testContent); err != nil {
|
||||||
|
t.Fatalf("Failed to write test content: %v", err)
|
||||||
|
}
|
||||||
|
tmpFile.Close()
|
||||||
|
|
||||||
|
// Test successful run with valid file
|
||||||
|
outputFile := "test-output.md"
|
||||||
|
defer os.Remove(outputFile)
|
||||||
|
|
||||||
|
// Save original stdout
|
||||||
|
originalStdout := os.Stdout
|
||||||
|
defer func() { os.Stdout = originalStdout }()
|
||||||
|
|
||||||
|
// Capture stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
args := []string{"articulate-parser", tmpFile.Name(), "markdown", outputFile}
|
||||||
|
exitCode := run(args)
|
||||||
|
|
||||||
|
// Close write end and restore stdout
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = originalStdout
|
||||||
|
|
||||||
|
// Read captured output
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// Verify successful execution
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
expectedMsg := fmt.Sprintf("Successfully exported course to %s", outputFile)
|
||||||
|
if !strings.Contains(output, expectedMsg) {
|
||||||
|
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify output file was created
|
||||||
|
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Expected output file %s to be created", outputFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunIntegration tests the run function with different output formats using sample file.
|
||||||
|
func TestRunIntegration(t *testing.T) {
|
||||||
|
// Skip if sample file doesn't exist
|
||||||
|
if _, err := os.Stat("articulate-sample.json"); os.IsNotExist(err) {
|
||||||
|
t.Skip("Skipping integration test: articulate-sample.json not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
formats := []struct {
|
||||||
|
format string
|
||||||
|
output string
|
||||||
|
}{
|
||||||
|
{"markdown", "test-output.md"},
|
||||||
|
{"html", "test-output.html"},
|
||||||
|
{"docx", "test-output.docx"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, format := range formats {
|
||||||
|
t.Run("format_"+format.format, func(t *testing.T) {
|
||||||
|
// Capture stdout
|
||||||
|
oldStdout := os.Stdout
|
||||||
|
r, w, _ := os.Pipe()
|
||||||
|
os.Stdout = w
|
||||||
|
|
||||||
|
// Run the function
|
||||||
|
args := []string{"articulate-parser", "articulate-sample.json", format.format, format.output}
|
||||||
|
exitCode := run(args)
|
||||||
|
|
||||||
|
// Restore stdout
|
||||||
|
w.Close()
|
||||||
|
os.Stdout = oldStdout
|
||||||
|
|
||||||
|
// Read captured output
|
||||||
|
var buf bytes.Buffer
|
||||||
|
io.Copy(&buf, r)
|
||||||
|
output := buf.String()
|
||||||
|
|
||||||
|
// Clean up test file
|
||||||
|
defer os.Remove(format.output)
|
||||||
|
|
||||||
|
// Verify successful execution
|
||||||
|
if exitCode != 0 {
|
||||||
|
t.Errorf("Expected successful execution (exit code 0), got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify success message
|
||||||
|
expectedMsg := "Successfully exported course to " + format.output
|
||||||
|
if !strings.Contains(output, expectedMsg) {
|
||||||
|
t.Errorf("Expected success message '%s' in output, got: %s", expectedMsg, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify output file was created
|
||||||
|
if _, err := os.Stat(format.output); os.IsNotExist(err) {
|
||||||
|
t.Errorf("Expected output file %s to be created", format.output)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMainFunction tests that the main function exists and is properly structured.
|
||||||
|
// We can't test os.Exit behavior directly, but we can verify the main function
|
||||||
|
// calls the run function correctly by testing run function behavior.
|
||||||
|
func TestMainFunction(t *testing.T) {
|
||||||
|
// Test that insufficient args return exit code 1
|
||||||
|
exitCode := run([]string{"program"})
|
||||||
|
if exitCode != 1 {
|
||||||
|
t.Errorf("Expected run to return exit code 1 for insufficient args, got %d", exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that main function exists (this is mainly for coverage)
|
||||||
|
// The main function just calls os.Exit(run(os.Args)), which we can't test directly
|
||||||
|
// but we've tested the run function thoroughly above.
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user