3 Commits

Author SHA1 Message Date
94a7924bed 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
2026-01-05 11:54:31 +01:00
90b9d557d8 fix: quote shell variables in CI workflow for shellcheck compliance 2026-01-05 04:17:16 +01:00
33ff267644 fix: restore pre-commit, CGO_ENABLED, gohtml template
- Add CGO_ENABLED=1 to CI test step for race detection
- Fix docker job needs (remove dependency-review, only runs on PRs)
- Restore .pre-commit-config.yaml for local dev safety
- Rename html_template.html to .gohtml (conventional extension)
- Add GitHub URL and default branch info to AGENTS.md
- Add .dprint.jsonc config
- Various formatting normalization
2026-01-05 04:14:56 +01:00
31 changed files with 932 additions and 762 deletions

42
.dprint.jsonc Normal file
View File

@ -0,0 +1,42 @@
{
"typescript": {
},
"json": {
},
"markdown": {
},
"toml": {
},
"dockerfile": {
},
"oxc": {
},
"ruff": {
},
"jupyter": {
},
"malva": {
},
"markup": {
},
"yaml": {
},
"excludes": [
"**/node_modules",
"**/*-lock.json",
],
"plugins": [
"https://plugins.dprint.dev/typescript-0.95.13.wasm",
"https://plugins.dprint.dev/json-0.21.1.wasm",
"https://plugins.dprint.dev/markdown-0.20.0.wasm",
"https://plugins.dprint.dev/toml-0.7.0.wasm",
"https://plugins.dprint.dev/dockerfile-0.3.3.wasm",
"https://plugins.dprint.dev/oxc-0.1.0.wasm",
"https://plugins.dprint.dev/ruff-0.6.11.wasm",
"https://plugins.dprint.dev/jupyter-0.2.1.wasm",
"https://plugins.dprint.dev/g-plane/malva-v0.15.1.wasm",
"https://plugins.dprint.dev/g-plane/markup_fmt-v0.25.3.wasm",
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm",
"https://plugins.dprint.dev/exec-0.6.0.json@a054130d458f124f9b5c91484833828950723a5af3f8ff2bd1523bd47b83b364",
],
}

View File

@ -1,7 +1,7 @@
name: Bug Report name: Bug Report
description: Create a report to help us improve description: Create a report to help us improve
title: '[BUG] ' title: "[BUG] "
labels: ['bug', 'triage'] labels: ["bug", "triage"]
body: body:
- type: markdown - type: markdown
attributes: attributes:

View File

@ -5,11 +5,13 @@
## Related Issue ## Related Issue
<!-- Link to the issue this PR addresses using the syntax: Fixes #issue_number --> <!-- Link to the issue this PR addresses using the syntax: Fixes #issue_number -->
Fixes # Fixes #
## Type of Change ## Type of Change
<!-- Mark the appropriate option with an "x" --> <!-- Mark the appropriate option with an "x" -->
- [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
@ -21,6 +23,7 @@ Fixes #
## Checklist ## Checklist
<!-- Mark the items you've completed with an "x" --> <!-- Mark the items you've completed with an "x" -->
- [ ] My code follows the style guidelines of this project - [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code - [ ] I have performed a self-review of my code
- [ ] I have added comments to complex logic - [ ] I have added comments to complex logic
@ -42,6 +45,7 @@ Fixes #
## Testing Instructions ## Testing Instructions
<!-- Provide steps to test the changes, if applicable --> <!-- Provide steps to test the changes, if applicable -->
1. 1.
2. 2.
3. 3.

View File

@ -1,86 +1,86 @@
version: 2 version: 2
updates: updates:
# Check for updates to GitHub Actions # Check for updates to GitHub Actions
- package-ecosystem: 'github-actions' - package-ecosystem: "github-actions"
directory: '/' directory: "/"
schedule: schedule:
interval: 'weekly' interval: "weekly"
day: 'monday' day: "monday"
time: '07:00' time: "07:00"
timezone: 'Europe/Amsterdam' timezone: "Europe/Amsterdam"
open-pull-requests-limit: 2 open-pull-requests-limit: 2
labels: labels:
- 'dependencies' - "dependencies"
- 'dependencies/github-actions' - "dependencies/github-actions"
commit-message: commit-message:
prefix: 'ci' prefix: "ci"
include: 'scope' include: "scope"
# Check for updates to Docker # Check for updates to Docker
- package-ecosystem: 'docker' - package-ecosystem: "docker"
directory: '/' directory: "/"
schedule: schedule:
interval: 'weekly' interval: "weekly"
day: 'monday' day: "monday"
time: '07:00' time: "07:00"
timezone: 'Europe/Amsterdam' timezone: "Europe/Amsterdam"
open-pull-requests-limit: 2 open-pull-requests-limit: 2
labels: labels:
- 'dependencies' - "dependencies"
- 'dependencies/docker' - "dependencies/docker"
commit-message: commit-message:
prefix: 'docker' prefix: "docker"
include: 'scope' include: "scope"
groups: groups:
docker: docker:
patterns: patterns:
- '*' - "*"
update-types: update-types:
- 'minor' - "minor"
- 'patch' - "patch"
# Check for updates to Docker Compose # Check for updates to Docker Compose
- package-ecosystem: 'docker-compose' - package-ecosystem: "docker-compose"
directory: '/' directory: "/"
schedule: schedule:
interval: 'weekly' interval: "weekly"
day: 'monday' day: "monday"
time: '07:00' time: "07:00"
timezone: 'Europe/Amsterdam' timezone: "Europe/Amsterdam"
open-pull-requests-limit: 2 open-pull-requests-limit: 2
labels: labels:
- 'dependencies' - "dependencies"
- 'dependencies/docker-compose' - "dependencies/docker-compose"
commit-message: commit-message:
prefix: 'docker' prefix: "docker"
include: 'scope' include: "scope"
groups: groups:
docker: docker:
patterns: patterns:
- '*' - "*"
update-types: update-types:
- 'minor' - "minor"
- 'patch' - "patch"
# Check for updates to Go modules # Check for updates to Go modules
- package-ecosystem: 'gomod' - package-ecosystem: "gomod"
directory: '/' directory: "/"
schedule: schedule:
interval: 'weekly' interval: "weekly"
day: 'monday' day: "monday"
time: '07:00' time: "07:00"
timezone: 'Europe/Amsterdam' timezone: "Europe/Amsterdam"
open-pull-requests-limit: 2 open-pull-requests-limit: 2
labels: labels:
- 'dependencies' - "dependencies"
- 'dependencies/go' - "dependencies/go"
commit-message: commit-message:
prefix: 'deps' prefix: "deps"
include: 'scope' include: "scope"
groups: groups:
go-modules: go-modules:
patterns: patterns:
- '*' - "*"
update-types: update-types:
- 'minor' - "minor"
- 'patch' - "patch"

View File

@ -64,14 +64,18 @@ jobs:
- name: Run tests with enhanced reporting - name: Run tests with enhanced reporting
id: test id: test
env:
CGO_ENABLED: 1
run: | run: |
cat >> $GITHUB_STEP_SUMMARY << EOF {
cat << EOF
## 🔧 Test Environment ## 🔧 Test Environment
- **Go Version:** ${{ matrix.go }} - **Go Version:** ${{ matrix.go }}
- **OS:** ubuntu-latest - **OS:** ubuntu-latest
- **Timestamp:** $(date -u) - **Timestamp:** $(date -u)
EOF EOF
} >> "$GITHUB_STEP_SUMMARY"
echo "Running tests with coverage..." echo "Running tests with coverage..."
task test:coverage 2>&1 | tee test-output.log task test:coverage 2>&1 | tee test-output.log
@ -84,16 +88,17 @@ jobs:
SKIPPED_TESTS=$(grep -c "--- SKIP:" test-output.log || echo "0") SKIPPED_TESTS=$(grep -c "--- SKIP:" test-output.log || echo "0")
# Generate test summary # Generate test summary
cat >> $GITHUB_STEP_SUMMARY << EOF {
cat << EOF
## 🧪 Test Results (Go ${{ matrix.go }}) ## 🧪 Test Results (Go ${{ matrix.go }})
| Metric | Value | | Metric | Value |
| ----------- | ----------------------------------------------------------- | | ----------- | ------------------------------------------------------------- |
| Total Tests | $TOTAL_TESTS | | Total Tests | $TOTAL_TESTS |
| Passed | $PASSED_TESTS | | Passed | $PASSED_TESTS |
| Failed | $FAILED_TESTS | | Failed | $FAILED_TESTS |
| Skipped | $SKIPPED_TESTS | | Skipped | $SKIPPED_TESTS |
| Status | $([ $TEST_STATUS -eq 0 ] && echo "PASSED" || echo "FAILED") | | Status | $([ "$TEST_STATUS" -eq 0 ] && echo "PASSED" || echo "FAILED") |
### 📦 Package Test Results ### 📦 Package Test Results
@ -105,38 +110,39 @@ jobs:
grep "^ok\|^FAIL" test-output.log | while read -r line; do grep "^ok\|^FAIL" test-output.log | while read -r line; do
if [[ $line == ok* ]]; then if [[ $line == ok* ]]; then
pkg=$(echo "$line" | awk '{print $2}') pkg=$(echo "$line" | awk '{print $2}')
echo "| $pkg | ✅ PASS |" >> $GITHUB_STEP_SUMMARY echo "| $pkg | ✅ PASS |"
elif [[ $line == FAIL* ]]; then elif [[ $line == FAIL* ]]; then
pkg=$(echo "$line" | awk '{print $2}') pkg=$(echo "$line" | awk '{print $2}')
echo "| $pkg | ❌ FAIL |" >> $GITHUB_STEP_SUMMARY echo "| $pkg | ❌ FAIL |"
fi fi
done done
echo "" >> $GITHUB_STEP_SUMMARY echo ""
# Add detailed results if tests failed # Add detailed results if tests failed
if [ $TEST_STATUS -ne 0 ]; then if [ "$TEST_STATUS" -ne 0 ]; then
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat << 'EOF'
### ❌ Failed Tests Details ### ❌ Failed Tests Details
``` ```
EOF EOF
grep -A 10 "--- FAIL:" test-output.log | head -100 >> $GITHUB_STEP_SUMMARY grep -A 10 -- "--- FAIL:" test-output.log | head -100
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat << 'EOF'
``` ```
EOF EOF
fi fi
} >> "$GITHUB_STEP_SUMMARY"
# Set outputs for other steps # Set outputs for other steps
cat >> $GITHUB_OUTPUT << EOF {
test-status=$TEST_STATUS echo "test-status=$TEST_STATUS"
total-tests=$TOTAL_TESTS echo "total-tests=$TOTAL_TESTS"
passed-tests=$PASSED_TESTS echo "passed-tests=$PASSED_TESTS"
failed-tests=$FAILED_TESTS echo "failed-tests=$FAILED_TESTS"
EOF } >> "$GITHUB_OUTPUT"
# Exit with the original test status # Exit with the original test status
exit $TEST_STATUS exit "$TEST_STATUS"
- name: Generate coverage report - name: Generate coverage report
if: always() if: always()
@ -144,7 +150,8 @@ jobs:
if [ -f coverage/coverage.out ]; then if [ -f coverage/coverage.out ]; then
COVERAGE=$(go tool cover -func=coverage/coverage.out | grep total | awk '{print $3}') COVERAGE=$(go tool cover -func=coverage/coverage.out | grep total | awk '{print $3}')
cat >> $GITHUB_STEP_SUMMARY << EOF {
cat << EOF
## 📊 Code Coverage (Go ${{ matrix.go }}) ## 📊 Code Coverage (Go ${{ matrix.go }})
**Total Coverage: $COVERAGE** **Total Coverage: $COVERAGE**
@ -184,17 +191,18 @@ jobs:
avg = packages[pkg] / counts[pkg] avg = packages[pkg] / counts[pkg]
printf "| %s | %.1f%% |\n", pkg, avg printf "| %s | %.1f%% |\n", pkg, avg
} }
}' "$temp_coverage" | sort >> $GITHUB_STEP_SUMMARY }' "$temp_coverage" | sort
rm -f "$temp_coverage" rm -f "$temp_coverage"
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat << 'EOF'
</details> </details>
EOF EOF
} >> "$GITHUB_STEP_SUMMARY"
else else
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
## ⚠️ Coverage Report ## ⚠️ Coverage Report
No coverage file generated No coverage file generated
@ -213,8 +221,8 @@ jobs:
- name: Run linters - name: Run linters
run: | run: |
# Initialize summary {
cat >> $GITHUB_STEP_SUMMARY << EOF cat << EOF
## 🔍 Static Analysis (Go ${{ matrix.go }}) ## 🔍 Static Analysis (Go ${{ matrix.go }})
EOF EOF
@ -223,42 +231,43 @@ jobs:
VET_OUTPUT=$(task lint:vet 2>&1 || echo "") VET_OUTPUT=$(task lint:vet 2>&1 || echo "")
VET_STATUS=$? VET_STATUS=$?
if [ $VET_STATUS -eq 0 ]; then if [ "$VET_STATUS" -eq 0 ]; then
echo "✅ **go vet:** No issues found" >> $GITHUB_STEP_SUMMARY echo "✅ **go vet:** No issues found"
else else
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat << 'EOF'
❌ **go vet:** Issues found ❌ **go vet:** Issues found
``` ```
EOF EOF
echo "$VET_OUTPUT" >> $GITHUB_STEP_SUMMARY echo "$VET_OUTPUT"
echo '```' >> $GITHUB_STEP_SUMMARY echo '```'
fi fi
echo "" >> $GITHUB_STEP_SUMMARY echo ""
# Run go fmt check # Run go fmt check
FMT_OUTPUT=$(task lint:fmt 2>&1 || echo "") FMT_OUTPUT=$(task lint:fmt 2>&1 || echo "")
FMT_STATUS=$? FMT_STATUS=$?
if [ $FMT_STATUS -eq 0 ]; then if [ "$FMT_STATUS" -eq 0 ]; then
echo "✅ **go fmt:** All files properly formatted" >> $GITHUB_STEP_SUMMARY echo "✅ **go fmt:** All files properly formatted"
else else
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat << 'EOF'
❌ **go fmt:** Files need formatting ❌ **go fmt:** Files need formatting
``` ```
EOF EOF
echo "$FMT_OUTPUT" >> $GITHUB_STEP_SUMMARY echo "$FMT_OUTPUT"
echo '```' >> $GITHUB_STEP_SUMMARY echo '```'
fi fi
} >> "$GITHUB_STEP_SUMMARY"
# Exit with error if any linter failed # Exit with error if any linter failed
[ $VET_STATUS -eq 0 ] && [ $FMT_STATUS -eq 0 ] || exit 1 [ "$VET_STATUS" -eq 0 ] && [ "$FMT_STATUS" -eq 0 ] || exit 1
- name: Job Summary - name: Job Summary
if: always() if: always()
run: | run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
## 📋 Job Summary (Go ${{ matrix.go }}) ## 📋 Job Summary (Go ${{ matrix.go }})
| Step | Status | | Step | Status |
@ -313,7 +322,8 @@ jobs:
- name: Test Docker image using Task - name: Test Docker image using Task
run: | run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF' {
cat << 'EOF'
## 🧪 Docker Image Tests ## 🧪 Docker Image Tests
EOF EOF
@ -321,16 +331,17 @@ jobs:
# Run Task docker test # Run Task docker test
task docker:test task docker:test
echo "**Testing help command:**" >> $GITHUB_STEP_SUMMARY echo "**Testing help command:**"
echo '```terminaloutput' >> $GITHUB_STEP_SUMMARY echo '```terminaloutput'
docker run --rm articulate-parser:latest --help >> $GITHUB_STEP_SUMMARY docker run --rm articulate-parser:latest --help
echo '```' >> $GITHUB_STEP_SUMMARY echo '```'
echo "" >> $GITHUB_STEP_SUMMARY echo ""
# Test image size # Test image size
IMAGE_SIZE=$(docker image inspect articulate-parser:latest --format='{{.Size}}' | numfmt --to=iec-i --suffix=B) IMAGE_SIZE=$(docker image inspect articulate-parser:latest --format='{{.Size}}' | numfmt --to=iec-i --suffix=B)
echo "**Image size:** $IMAGE_SIZE" >> $GITHUB_STEP_SUMMARY echo "**Image size:** $IMAGE_SIZE"
echo "" >> $GITHUB_STEP_SUMMARY echo ""
} >> "$GITHUB_STEP_SUMMARY"
dependency-review: dependency-review:
name: Dependency Review name: Dependency Review
@ -354,7 +365,7 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
needs: [test, docker-test, dependency-review] needs: [test, docker-test]
if: | if: |
github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.event_name == 'push' && (github.ref == 'refs/heads/master' ||
github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/develop' ||
@ -433,7 +444,7 @@ jobs:
- name: Generate Docker summary - name: Generate Docker summary
run: | run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF' cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
## 🐳 Docker Build Summary ## 🐳 Docker Build Summary
**Image:** `ghcr.io/${{ github.repository }}` **Image:** `ghcr.io/${{ github.repository }}`

View File

@ -17,7 +17,7 @@ name: "CodeQL"
on: on:
workflow_call: workflow_call:
schedule: schedule:
- cron: '44 16 * * 6' - cron: "44 16 * * 6"
# push: # push:
# branches: [ "master" ] # branches: [ "master" ]
# pull_request: # pull_request:

View File

@ -16,10 +16,10 @@ jobs:
dependency-review: dependency-review:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: 'Checkout Repository' - name: "Checkout Repository"
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: 'Dependency Review' - name: "Dependency Review"
uses: actions/dependency-review-action@v4 uses: actions/dependency-review-action@v4
with: with:
fail-on-severity: moderate fail-on-severity: moderate

View File

@ -82,7 +82,7 @@ jobs:
docker: docker:
name: Docker Build & Push name: Docker Build & Push
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: ['release'] needs: ["release"]
permissions: permissions:
contents: read contents: read
packages: write packages: write

View File

@ -349,7 +349,15 @@ linters:
# stylecheck settings # stylecheck settings
staticcheck: staticcheck:
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] checks: [
"all",
"-ST1000",
"-ST1003",
"-ST1016",
"-ST1020",
"-ST1021",
"-ST1022",
]
# maintidx settings # maintidx settings
maintidx: maintidx:

75
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,75 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
# File quality
- id: trailing-whitespace
exclude: '^\.github/ISSUE_TEMPLATE/.*\.yml$'
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
# File validation
- id: check-yaml
- id: check-json
- id: check-toml
# Security
- id: detect-private-key
# Git safety
- id: check-merge-conflict
- id: check-case-conflict
- id: no-commit-to-branch
args: ["--branch=master", "--branch=main"]
# File structure
- id: check-added-large-files
- id: check-symlinks
- id: check-executables-have-shebangs
- repo: local
hooks:
- id: actionlint
name: Lint GitHub Actions workflow files
description: Runs actionlint to lint GitHub Actions workflow files
language: golang
types: ["yaml"]
files: ^\.github/workflows/
entry: actionlint
minimum_pre_commit_version: 3.0.0
- repo: https://github.com/golangci/golangci-lint
rev: v2.7.2
hooks:
- id: golangci-lint
name: golangci-lint
description: Fast linters runner for Go. Note that only modified files are linted, so linters like 'unused' that need to scan all files won't work as expected.
entry: golangci-lint run --new-from-rev HEAD --fix
types: [go]
language: golang
require_serial: true
pass_filenames: false
# - id: golangci-lint-full
# name: golangci-lint-full
# description: Fast linters runner for Go. Runs on all files in the module. Use this hook if you use pre-commit in CI.
# entry: golangci-lint run --fix
# types: [go]
# language: golang
# require_serial: true
# pass_filenames: false
- id: golangci-lint-fmt
name: golangci-lint-fmt
description: Fast linters runner for Go. Formats all files in the repo.
entry: golangci-lint fmt
types: [go]
language: golang
require_serial: true
pass_filenames: false
- id: golangci-lint-config-verify
name: golangci-lint-config-verify
description: Verifies the configuration file
entry: golangci-lint config verify
files: '\.golangci\.(?:yml|yaml|toml|json)'
language: golang
pass_filenames: false

View File

@ -2,6 +2,11 @@
A Go CLI tool that parses Articulate Rise courses from URLs or local JSON files and exports them to Markdown, HTML, or DOCX formats. A Go CLI tool that parses Articulate Rise courses from URLs or local JSON files and exports them to Markdown, HTML, or DOCX formats.
## Repository Info
- **GitHub**: https://github.com/kjanat/articulate-parser
- **Default branch**: `master` (not `main`)
## Build/Test Commands ## Build/Test Commands
### Primary Commands (using Taskfile) ### Primary Commands (using Taskfile)

View File

@ -226,7 +226,7 @@ docker run --rm ghcr.io/kjanat/articulate-parser:latest --help
### Available Tags ### Available Tags
| Tag | Description | Use Case | | Tag | Description | Use Case |
|-----|-------------|----------| | --------------------- | ------------------------------------------- | ---------------------- |
| `latest` | Latest stable release from master branch | Production use | | `latest` | Latest stable release from master branch | Production use |
| `edge` | Latest development build from master branch | Testing new features | | `edge` | Latest development build from master branch | Testing new features |
| `v1.x.x` | Specific version releases | Production pinning | | `v1.x.x` | Specific version releases | Production pinning |
@ -314,7 +314,7 @@ docker build --build-arg VERSION=local --build-arg BUILD_TIME=$(date -u +%Y-%m-%
The Docker image supports the following build-time arguments: The Docker image supports the following build-time arguments:
| Argument | Description | Default | | Argument | Description | Default |
|----------|-------------|---------| | ------------ | ------------------------------------- | -------------- |
| `VERSION` | Version string embedded in the binary | `dev` | | `VERSION` | Version string embedded in the binary | `dev` |
| `BUILD_TIME` | Build timestamp | Current time | | `BUILD_TIME` | Build timestamp | Current time |
| `GIT_COMMIT` | Git commit hash | Current commit | | `GIT_COMMIT` | Git commit hash | Current commit |
@ -460,7 +460,9 @@ This is a utility tool for educational content conversion. Please ensure you hav
[Go report]: https://goreportcard.com/report/github.com/kjanat/articulate-parser [Go report]: https://goreportcard.com/report/github.com/kjanat/articulate-parser
[gomod]: go.mod [gomod]: go.mod
[Issues]: https://github.com/kjanat/articulate-parser/issues [Issues]: https://github.com/kjanat/articulate-parser/issues
<!-- [Latest release]: https://github.com/kjanat/articulate-parser/releases/latest --> <!-- [Latest release]: https://github.com/kjanat/articulate-parser/releases/latest -->
[MIT License]: LICENSE [MIT License]: LICENSE
[Package documentation]: https://godoc.org/github.com/kjanat/articulate-parser [Package documentation]: https://godoc.org/github.com/kjanat/articulate-parser
[Tags]: https://github.com/kjanat/articulate-parser/tags [Tags]: https://github.com/kjanat/articulate-parser/tags

View File

@ -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

View File

@ -15,7 +15,7 @@ import (
//go:embed html_styles.css //go:embed html_styles.css
var defaultCSS string var defaultCSS string
//go:embed html_template.html //go:embed html_template.gohtml
var htmlTemplate string var htmlTemplate string
// HTMLExporter implements the Exporter interface for HTML format. // HTMLExporter implements the Exporter interface for HTML format.
@ -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)
} }

View File

@ -1,5 +1,7 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
Cantarell, sans-serif;
line-height: 1.6; line-height: 1.6;
color: #333; color: #333;
max-width: 800px; max-width: 800px;

View File

@ -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.

View File

@ -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)
} }

View File

@ -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.