mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-08-05 18:04:07 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b673992fd |
+22
-28
@@ -35,24 +35,21 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
go:
|
||||
- 1.24.x
|
||||
- 1.25.x
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
go-version: ${{ matrix.go }}
|
||||
check-latest: true
|
||||
|
||||
# Expose the resolved Go version as an env var so it is never
|
||||
# interpolated directly into a shell script (avoids script injection).
|
||||
- name: Export Go version
|
||||
env:
|
||||
GO_VERSION: ${{ steps.setup-go.outputs.go-version }}
|
||||
run: echo "GO_VERSION=$GO_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
|
||||
@@ -73,7 +70,7 @@ jobs:
|
||||
{
|
||||
cat << EOF
|
||||
## 🔧 Test Environment
|
||||
- **Go Version:** $GO_VERSION
|
||||
- **Go Version:** ${{ matrix.go }}
|
||||
- **OS:** ubuntu-latest
|
||||
- **Timestamp:** $(date -u)
|
||||
|
||||
@@ -93,7 +90,7 @@ jobs:
|
||||
# Generate test summary
|
||||
{
|
||||
cat << EOF
|
||||
## 🧪 Test Results (Go $GO_VERSION)
|
||||
## 🧪 Test Results (Go ${{ matrix.go }})
|
||||
|
||||
| Metric | Value |
|
||||
| ----------- | ------------------------------------------------------------- |
|
||||
@@ -155,7 +152,7 @@ jobs:
|
||||
|
||||
{
|
||||
cat << EOF
|
||||
## 📊 Code Coverage (Go $GO_VERSION)
|
||||
## 📊 Code Coverage (Go ${{ matrix.go }})
|
||||
|
||||
**Total Coverage: $COVERAGE**
|
||||
|
||||
@@ -214,9 +211,9 @@ jobs:
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: test-results-go-${{ env.GO_VERSION }}
|
||||
name: test-results-go-${{ matrix.go }}
|
||||
path: |
|
||||
test-output.log
|
||||
coverage/
|
||||
@@ -226,7 +223,7 @@ jobs:
|
||||
run: |
|
||||
{
|
||||
cat << EOF
|
||||
## 🔍 Static Analysis (Go $GO_VERSION)
|
||||
## 🔍 Static Analysis (Go ${{ matrix.go }})
|
||||
|
||||
EOF
|
||||
|
||||
@@ -270,10 +267,9 @@ jobs:
|
||||
- name: Job Summary
|
||||
if: always()
|
||||
run: |
|
||||
{
|
||||
echo "## 📋 Job Summary (Go $GO_VERSION)"
|
||||
echo ""
|
||||
cat << 'EOF'
|
||||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||||
## 📋 Job Summary (Go ${{ matrix.go }})
|
||||
|
||||
| Step | Status |
|
||||
| --------------- | --------------------------------------------------------------- |
|
||||
| Dependencies | Success |
|
||||
@@ -283,13 +279,12 @@ jobs:
|
||||
| Static Analysis | ${{ job.status == 'success' && 'Clean' || 'Issues' }} |
|
||||
| Code Formatting | ${{ job.status == 'success' && 'Clean' || 'Issues' }} |
|
||||
EOF
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
files: ./coverage/coverage.out
|
||||
flags: Go ${{ env.GO_VERSION }}
|
||||
flags: Go ${{ matrix.go }}
|
||||
slug: kjanat/articulate-parser
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
@@ -297,7 +292,7 @@ jobs:
|
||||
if: ${{ !cancelled() }}
|
||||
uses: codecov/test-results-action@v1
|
||||
with:
|
||||
flags: Go ${{ env.GO_VERSION }}
|
||||
flags: Go ${{ matrix.go }}
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
docker-test:
|
||||
@@ -353,14 +348,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v5
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
comment-summary-in-pr: always
|
||||
@@ -381,20 +375,20 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v5
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
comment-summary-in-pr: always
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
build/articulate-parser-linux-amd64
|
||||
@@ -91,20 +91,20 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v4
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -8,6 +8,11 @@ run:
|
||||
# Timeout for total work
|
||||
timeout: 5m
|
||||
|
||||
# Skip directories (not allowed in config v2, will use issues exclude instead)
|
||||
|
||||
# Go version
|
||||
go: "1.24"
|
||||
|
||||
# Include test files
|
||||
tests: true
|
||||
|
||||
|
||||
@@ -78,12 +78,12 @@ flowchart TD
|
||||
|
||||
The system follows **Clean Architecture** principles with clear separation of concerns:
|
||||
|
||||
- **Entry Point**: Command-line interface handles user input and coordinates operations
|
||||
- **Application Layer**: Core business logic with dependency injection
|
||||
- **Interface Layer**: Contracts defining behavior without implementation details
|
||||
- **Service Layer**: Concrete implementations of parsing and utility services
|
||||
- **Export Layer**: Factory pattern for format-specific exporters
|
||||
- **Data Layer**: Domain models representing course structure
|
||||
- **🎯 Entry Point**: Command-line interface handles user input and coordinates operations
|
||||
- **🏗️ Application Layer**: Core business logic with dependency injection
|
||||
- **📋 Interface Layer**: Contracts defining behavior without implementation details
|
||||
- **🔧 Service Layer**: Concrete implementations of parsing and utility services
|
||||
- **📤 Export Layer**: Factory pattern for format-specific exporters
|
||||
- **📊 Data Layer**: Domain models representing course structure
|
||||
|
||||
## Features
|
||||
|
||||
@@ -206,7 +206,7 @@ Then run:
|
||||
|
||||
The application is available as a Docker image from GitHub Container Registry.
|
||||
|
||||
### Docker Image Information
|
||||
### 🐳 Docker Image Information
|
||||
|
||||
- **Registry**: `ghcr.io/kjanat/articulate-parser`
|
||||
- **Platforms**: linux/amd64, linux/arm64
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
module github.com/kjanat/articulate-parser
|
||||
|
||||
go 1.26.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.5
|
||||
|
||||
require (
|
||||
github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/text v0.38.0
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/text v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fumiama/imgsz v0.0.4 // indirect
|
||||
golang.org/x/image v0.42.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,9 +2,9 @@ github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b h1:/mxSugRc4SgN7Xg
|
||||
github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b/go.mod h1:ssRF0IaB1hCcKIObp3FkZOsjTcAHpgii70JelNb4H8M=
|
||||
github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI=
|
||||
github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E=
|
||||
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
|
||||
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
|
||||
@@ -13,11 +13,6 @@ const (
|
||||
FormatMarkdown = "markdown"
|
||||
FormatDocx = "docx"
|
||||
FormatHTML = "html"
|
||||
|
||||
// Format aliases accepted by CreateExporter.
|
||||
formatAliasMarkdown = "md"
|
||||
formatAliasDocx = "word"
|
||||
formatAliasHTML = "htm"
|
||||
)
|
||||
|
||||
// Factory implements the ExporterFactory interface.
|
||||
@@ -46,11 +41,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, formatAliasMarkdown:
|
||||
case FormatMarkdown, "md":
|
||||
return NewMarkdownExporter(f.htmlCleaner), nil
|
||||
case FormatDocx, formatAliasDocx:
|
||||
case FormatDocx, "word":
|
||||
return NewDocxExporter(f.htmlCleaner), nil
|
||||
case FormatHTML, formatAliasHTML:
|
||||
case FormatHTML, "htm":
|
||||
return NewHTMLExporter(f.htmlCleaner), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported export format: %s", format)
|
||||
@@ -60,9 +55,5 @@ 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, formatAliasMarkdown,
|
||||
FormatDocx, formatAliasDocx,
|
||||
FormatHTML, formatAliasHTML,
|
||||
}
|
||||
return []string{FormatMarkdown, "md", FormatDocx, "word", FormatHTML, "htm"}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,6 @@ 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
|
||||
@@ -77,7 +74,7 @@ func prepareTemplateData(course *models.Course, htmlCleaner *services.HTMLCleane
|
||||
Description: lesson.Description,
|
||||
}
|
||||
|
||||
if lesson.Type != lessonTypeSection {
|
||||
if lesson.Type != "section" {
|
||||
lessonCounter++
|
||||
section.Number = lessonCounter
|
||||
section.Items = prepareItems(lesson.Items, htmlCleaner)
|
||||
|
||||
@@ -40,35 +40,35 @@ func (e *MarkdownExporter) Export(course *models.Course, outputPath string) erro
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Write course header
|
||||
fmt.Fprintf(&buf, "# %s\n\n", course.Course.Title)
|
||||
buf.WriteString(fmt.Sprintf("# %s\n\n", course.Course.Title))
|
||||
|
||||
if course.Course.Description != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description))
|
||||
buf.WriteString(fmt.Sprintf("%s\n\n", e.htmlCleaner.CleanHTML(course.Course.Description)))
|
||||
}
|
||||
|
||||
// Add metadata
|
||||
buf.WriteString("## Course Information\n\n")
|
||||
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)
|
||||
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))
|
||||
if course.Course.ExportSettings != nil {
|
||||
fmt.Fprintf(&buf, "- **Export Format**: %s\n", course.Course.ExportSettings.Format)
|
||||
buf.WriteString(fmt.Sprintf("- **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 == lessonTypeSection {
|
||||
fmt.Fprintf(&buf, "# %s\n\n", lesson.Title)
|
||||
if lesson.Type == "section" {
|
||||
buf.WriteString(fmt.Sprintf("# %s\n\n", lesson.Title))
|
||||
continue
|
||||
}
|
||||
|
||||
lessonCounter++
|
||||
fmt.Fprintf(&buf, "## Lesson %d: %s\n\n", lessonCounter, lesson.Title)
|
||||
buf.WriteString(fmt.Sprintf("## Lesson %d: %s\n\n", lessonCounter, lesson.Title))
|
||||
|
||||
if lesson.Description != "" {
|
||||
fmt.Fprintf(&buf, "%s\n\n", e.htmlCleaner.CleanHTML(lesson.Description))
|
||||
buf.WriteString(fmt.Sprintf("%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 itemTypeText:
|
||||
case "text":
|
||||
e.processTextItem(buf, item, headingPrefix)
|
||||
case itemTypeList:
|
||||
case "list":
|
||||
e.processListItem(buf, item)
|
||||
case itemTypeMultimedia:
|
||||
case "multimedia":
|
||||
e.processMultimediaItem(buf, item, headingPrefix)
|
||||
case itemTypeImage:
|
||||
case "image":
|
||||
e.processImageItem(buf, item, headingPrefix)
|
||||
case itemTypeKnowledgeCheck:
|
||||
case "knowledgecheck":
|
||||
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
||||
case itemTypeInteractive:
|
||||
case "interactive":
|
||||
e.processInteractiveItem(buf, item, headingPrefix)
|
||||
case itemTypeDivider:
|
||||
case "divider":
|
||||
e.processDividerItem(buf)
|
||||
default:
|
||||
e.processUnknownItem(buf, item, headingPrefix)
|
||||
|
||||
Binary file not shown.
@@ -15,12 +15,6 @@ import (
|
||||
"github.com/kjanat/articulate-parser/internal/models"
|
||||
)
|
||||
|
||||
// Default endpoint configuration for the Articulate Rise API.
|
||||
const (
|
||||
riseHost = "rise.articulate.com"
|
||||
defaultBaseURL = "https://" + riseHost
|
||||
)
|
||||
|
||||
// shareIDRegex is compiled once at package init for extracting share IDs from URIs.
|
||||
var shareIDRegex = regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`)
|
||||
|
||||
@@ -43,7 +37,7 @@ func NewArticulateParser(logger interfaces.Logger, baseURL string, timeout time.
|
||||
logger = NewNoOpLogger()
|
||||
}
|
||||
if baseURL == "" {
|
||||
baseURL = defaultBaseURL
|
||||
baseURL = "https://rise.articulate.com"
|
||||
}
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
@@ -138,7 +132,7 @@ func (p *ArticulateParser) extractShareID(uri string) (string, error) {
|
||||
}
|
||||
|
||||
// Validate that it's an Articulate Rise domain
|
||||
if parsedURL.Host != riseHost {
|
||||
if parsedURL.Host != "rise.articulate.com" {
|
||||
return "", fmt.Errorf("invalid domain for Articulate Rise URI: %s", parsedURL.Host)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user