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",
],
}

2
.github/CODEOWNERS vendored
View File

@ -1,5 +1,5 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will # the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request. # be requested for review when someone opens a pull request.
* @kjanat * @kjanat

View File

@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our Examples of behavior that contributes to a positive environment for our
community include: community include:
- Demonstrating empathy and kindness toward other people - Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences - Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback - Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes, - Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience and learning from the experience
- Focusing on what is best not just for us as individuals, but for the - Focusing on what is best not just for us as individuals, but for the
overall community overall community
Examples of unacceptable behavior include: Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or - The use of sexualized language or imagery, and sexual attention or
advances of any kind advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks - Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment - Public or private harassment
- Publishing others' private information, such as a physical or email - Publishing others' private information, such as a physical or email
address, without their explicit permission address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a - Other conduct which could reasonably be considered inappropriate in a
professional setting professional setting
## Enforcement Responsibilities ## Enforcement Responsibilities

View File

@ -12,73 +12,73 @@ This project and everyone participating in it is governed by our Code of Conduct
Before creating bug reports, please check existing issues as you might find that the issue has already been reported. When creating a bug report, include as many details as possible: Before creating bug reports, please check existing issues as you might find that the issue has already been reported. When creating a bug report, include as many details as possible:
- Use the bug report template - Use the bug report template
- Include sample Articulate Rise content that reproduces the issue - Include sample Articulate Rise content that reproduces the issue
- Provide your environment details (OS, Go version, etc.) - Provide your environment details (OS, Go version, etc.)
- Include error messages and stack traces - Include error messages and stack traces
### Suggesting Enhancements ### Suggesting Enhancements
Enhancement suggestions are welcome! Please use the feature request template and include: Enhancement suggestions are welcome! Please use the feature request template and include:
- A clear description of the enhancement - A clear description of the enhancement
- Your use case and why this would be valuable - Your use case and why this would be valuable
- Any implementation ideas you might have - Any implementation ideas you might have
### Pull Requests ### Pull Requests
1. **Fork the repository** and create your branch from `master` 1. **Fork the repository** and create your branch from `master`
2. **Make your changes** following our coding standards 2. **Make your changes** following our coding standards
3. **Add tests** for any new functionality 3. **Add tests** for any new functionality
4. **Ensure all tests pass** by running `go test ./...` 4. **Ensure all tests pass** by running `go test ./...`
5. **Run `go fmt`** to format your code 5. **Run `go fmt`** to format your code
6. **Run `go vet`** to check for common issues 6. **Run `go vet`** to check for common issues
7. **Update documentation** if needed 7. **Update documentation** if needed
8. **Create a pull request** with a clear title and description 8. **Create a pull request** with a clear title and description
## Development Setup ## Development Setup
1. **Prerequisites:** 1. **Prerequisites:**
- Go 1.21 or later - Go 1.21 or later
- Git - Git
2. **Clone and setup:** 2. **Clone and setup:**
```bash ```bash
git clone https://github.com/your-username/articulate-parser.git git clone https://github.com/your-username/articulate-parser.git
cd articulate-parser cd articulate-parser
go mod download go mod download
``` ```
3. **Run tests:** 3. **Run tests:**
```bash ```bash
go test -v ./... go test -v ./...
``` ```
4. **Build:** 4. **Build:**
```bash ```bash
go build main.go go build main.go
``` ```
## Coding Standards ## Coding Standards
### Go Style Guide ### Go Style Guide
- Follow the [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments) - Follow the [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- Use `gofmt` to format your code - Use `gofmt` to format your code
- Use meaningful variable and function names - Use meaningful variable and function names
- Add comments for exported functions and types - Add comments for exported functions and types
- Keep functions focused and small - Keep functions focused and small
### Testing ### Testing
- Write tests for new functionality - Write tests for new functionality
- Use table-driven tests where appropriate - Use table-driven tests where appropriate
- Aim for good test coverage - Aim for good test coverage
- Test error cases and edge conditions - Test error cases and edge conditions
### Commit Messages ### Commit Messages
@ -112,19 +112,19 @@ articulate-parser/
### New Content Types ### New Content Types
1. Add the content type definition to `types/` 1. Add the content type definition to `types/`
2. Implement parsing logic in `parser/` 2. Implement parsing logic in `parser/`
3. Add export handling in `exporters/` 3. Add export handling in `exporters/`
4. Write comprehensive tests 4. Write comprehensive tests
5. Update documentation 5. Update documentation
### New Export Formats ### New Export Formats
1. Create a new exporter in `exporters/` 1. Create a new exporter in `exporters/`
2. Implement the `Exporter` interface 2. Implement the `Exporter` interface
3. Add CLI support in `main.go` 3. Add CLI support in `main.go`
4. Add tests with sample output 4. Add tests with sample output
5. Update README with usage examples 5. Update README with usage examples
## Testing ## Testing
@ -146,31 +146,31 @@ go test -run TestSpecificFunction ./...
### Test Data ### Test Data
- Add sample Articulate Rise JSON files to `tests/data/` - Add sample Articulate Rise JSON files to `tests/data/`
- Include both simple and complex content examples - Include both simple and complex content examples
- Test edge cases and error conditions - Test edge cases and error conditions
## Documentation ## Documentation
- Update the README for user-facing changes - Update the README for user-facing changes
- Add inline code comments for complex logic - Add inline code comments for complex logic
- Update examples when adding new features - Update examples when adding new features
- Keep the feature list current - Keep the feature list current
## Release Process ## Release Process
Releases are handled by maintainers: Releases are handled by maintainers:
1. Version bumping follows semantic versioning 1. Version bumping follows semantic versioning
2. Releases are created from the `master` branch 2. Releases are created from the `master` branch
3. GitHub Actions automatically builds and publishes releases 3. GitHub Actions automatically builds and publishes releases
4. Release notes are auto-generated from commits 4. Release notes are auto-generated from commits
## Questions? ## Questions?
- Open a discussion for general questions - Open a discussion for general questions
- Use the question issue template for specific help - Use the question issue template for specific help
- Check existing issues and documentation first - Check existing issues and documentation first
## Recognition ## Recognition

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:
@ -27,9 +27,9 @@ body:
2. Parse file '...' 2. Parse file '...'
3. See error 3. See error
value: | value: |
1. 1.
2. 2.
3. 3.
validations: validations:
required: true required: true

View File

@ -5,31 +5,34 @@
## 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)
- [ ] New feature (non-breaking change which adds functionality) - [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Documentation update - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Performance improvement - [ ] Documentation update
- [ ] Code refactoring (no functional changes) - [ ] Performance improvement
- [ ] Test updates - [ ] Code refactoring (no functional changes)
- [ ] Test updates
## 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
- [ ] I have performed a self-review of my code - [ ] My code follows the style guidelines of this project
- [ ] I have added comments to complex logic - [ ] I have performed a self-review of my code
- [ ] I have updated the documentation - [ ] I have added comments to complex logic
- [ ] I have added tests that prove my fix is effective or that my feature works - [ ] I have updated the documentation
- [ ] New and existing unit tests pass locally with my changes - [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have checked for potential breaking changes - [ ] New and existing unit tests pass locally with my changes
- [ ] No new warnings are generated - [ ] I have checked for potential breaking changes
- [ ] The commit message follows our guidelines - [ ] No new warnings are generated
- [ ] The commit message follows our guidelines
## Screenshots (if appropriate) ## Screenshots (if appropriate)
@ -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.

32
.github/SECURITY.md vendored
View File

@ -13,32 +13,32 @@ Currently, the following versions of Articulate Rise Parser are supported with s
We take the security of Articulate Rise Parser seriously. If you believe you have found a security vulnerability, please follow these steps: We take the security of Articulate Rise Parser seriously. If you believe you have found a security vulnerability, please follow these steps:
1. **Do not disclose the vulnerability publicly** - Please do not create a public GitHub issue for security vulnerabilities. 1. **Do not disclose the vulnerability publicly** - Please do not create a public GitHub issue for security vulnerabilities.
2. **Email the details to [security+articulate-parser@kjanat.com]** - Include as much information as possible about the vulnerability. 2. **Email the details to [security+articulate-parser@kjanat.com]** - Include as much information as possible about the vulnerability.
3. **Wait for a response** - We will acknowledge your email within 48 hours and provide an estimated timeline for a fix. 3. **Wait for a response** - We will acknowledge your email within 48 hours and provide an estimated timeline for a fix.
4. **Work with us** - We may ask for additional information to help us understand and address the issue. 4. **Work with us** - We may ask for additional information to help us understand and address the issue.
## What to Include in a Report ## What to Include in a Report
When reporting a vulnerability, please include: When reporting a vulnerability, please include:
- A clear description of the issue - A clear description of the issue
- Steps to reproduce the vulnerability - Steps to reproduce the vulnerability
- The potential impact of the vulnerability - The potential impact of the vulnerability
- Any possible mitigations you've identified - Any possible mitigations you've identified
## What to Expect ## What to Expect
- We will acknowledge receipt of your vulnerability report within 48 hours. - We will acknowledge receipt of your vulnerability report within 48 hours.
- We will provide regular updates about our progress. - We will provide regular updates about our progress.
- We will notify you when the vulnerability is fixed. - We will notify you when the vulnerability is fixed.
- With your permission, we will include your name in the acknowledgments. - With your permission, we will include your name in the acknowledgments.
## Security Measures ## Security Measures
This project follows these security practices: This project follows these security practices:
- Regular dependency updates via Dependabot - Regular dependency updates via Dependabot
- CodeQL security scanning - CodeQL security scanning
- Automated testing for each pull request - Automated testing for each pull request
- Code review requirements for all changes - Code review requirements for all changes

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
@ -101,42 +106,43 @@ jobs:
|---------|--------| |---------|--------|
EOF EOF
# Extract package results # Extract package results
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**
@ -156,45 +163,46 @@ jobs:
| ------- | -------- | | ------- | -------- |
EOF EOF
# Create temporary file for package coverage aggregation # Create temporary file for package coverage aggregation
temp_coverage=$(mktemp) temp_coverage=$(mktemp)
# Extract package-level coverage data # Extract package-level coverage data
go tool cover -func=coverage/coverage.out | grep -v total | while read -r line; do go tool cover -func=coverage/coverage.out | grep -v total | while read -r line; do
if [[ $line == *".go:"* ]]; then if [[ $line == *".go:"* ]]; then
# Extract package path from file path (everything before the filename) # Extract package path from file path (everything before the filename)
filepath=$(echo "$line" | awk '{print $1}') filepath=$(echo "$line" | awk '{print $1}')
pkg_path=$(dirname "$filepath" | sed 's|github.com/kjanat/articulate-parser/||; s|^\./||') pkg_path=$(dirname "$filepath" | sed 's|github.com/kjanat/articulate-parser/||; s|^\./||')
coverage=$(echo "$line" | awk '{print $3}' | sed 's/%//') coverage=$(echo "$line" | awk '{print $3}' | sed 's/%//')
# Use root package if no subdirectory # Use root package if no subdirectory
[[ "$pkg_path" == "." || "$pkg_path" == "" ]] && pkg_path="root" [[ "$pkg_path" == "." || "$pkg_path" == "" ]] && pkg_path="root"
echo "$pkg_path $coverage" >> "$temp_coverage" echo "$pkg_path $coverage" >> "$temp_coverage"
fi fi
done done
# Aggregate coverage by package (average) # Aggregate coverage by package (average)
awk '{ awk '{
packages[$1] += $2 packages[$1] += $2
counts[$1]++ counts[$1]++
}
END {
for (pkg in packages) {
avg = packages[pkg] / counts[pkg]
printf "| %s | %.1f%% |\n", pkg, avg
} }
}' "$temp_coverage" | sort >> $GITHUB_STEP_SUMMARY END {
for (pkg in packages) {
avg = packages[pkg] / counts[pkg]
printf "| %s | %.1f%% |\n", pkg, avg
}
}' "$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,52 +221,53 @@ 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
# Run go vet # Run go vet
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,24 +322,26 @@ 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
# 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

@ -1,104 +1,104 @@
# For most projects, this workflow file will not need changing; you simply need # For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository. # to commit it to your repository.
# #
# You may wish to alter this file to override the set of languages analyzed, # You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic. # or to provide custom queries or build logic.
# #
# ******** NOTE ******** # ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check # We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of # the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages. # supported CodeQL languages.
# #
name: "CodeQL" name: "CodeQL"
# This workflow is configured to be called by other workflows for more controlled execution # This workflow is configured to be called by other workflows for more controlled execution
# This allows integration with the main CI pipeline and avoids redundant runs # This allows integration with the main CI pipeline and avoids redundant runs
# To enable automatic execution, uncomment the triggers below: # To enable automatic execution, uncomment the triggers below:
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:
# branches: [ "master" ] # branches: [ "master" ]
jobs: jobs:
analyze: analyze:
name: Analyze (${{ matrix.language }}) name: Analyze (${{ matrix.language }})
# Runner size impacts CodeQL analysis time. To learn more, please see: # Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners (GitHub.com only) # - https://gh.io/using-larger-runners (GitHub.com only)
# Consider using larger runners or machines with greater resources for possible analysis time improvements. # Consider using larger runners or machines with greater resources for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
permissions: permissions:
# required for all workflows # required for all workflows
security-events: write security-events: write
# required to fetch internal or private CodeQL packs # required to fetch internal or private CodeQL packs
packages: read packages: read
# only required for workflows in private repositories # only required for workflows in private repositories
actions: read actions: read
contents: read contents: read
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
- language: actions - language: actions
build-mode: none build-mode: none
- language: go - language: go
build-mode: autobuild build-mode: autobuild
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
# Use `c-cpp` to analyze code written in C, C++ or both # Use `c-cpp` to analyze code written in C, C++ or both
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
# Add any setup steps before running the `github/codeql-action/init` action. # Add any setup steps before running the `github/codeql-action/init` action.
# This includes steps like installing compilers or runtimes (`actions/setup-node` # This includes steps like installing compilers or runtimes (`actions/setup-node`
# or others). This is typically only required for manual builds. # or others). This is typically only required for manual builds.
# - name: Setup runtime (example) # - name: Setup runtime (example)
# uses: actions/setup-example@v1 # uses: actions/setup-example@v1
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v4
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# If the analyze step fails for one of the languages you are analyzing with # If the analyze step fails for one of the languages you are analyzing with
# "We were unable to automatically build your code", modify the matrix above # "We were unable to automatically build your code", modify the matrix above
# to set the build mode to "manual" for that language. Then modify this step # to set the build mode to "manual" for that language. Then modify this step
# to build your code. # to build your code.
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
- if: matrix.build-mode == 'manual' - if: matrix.build-mode == 'manual'
shell: bash shell: bash
run: | run: |
echo 'If you are using a "manual" build mode for one or more of the' \ echo 'If you are using a "manual" build mode for one or more of the' \
'languages you are analyzing, replace this with the commands to build' \ 'languages you are analyzing, replace this with the commands to build' \
'your code, for example:' 'your code, for example:'
echo ' make bootstrap' echo ' make bootstrap'
echo ' make release' echo ' make release'
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v4
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

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

158
.gitignore vendored
View File

@ -1,79 +1,79 @@
# Created by https://www.toptal.com/developers/gitignore/api/go # Created by https://www.toptal.com/developers/gitignore/api/go
# Edit at https://www.toptal.com/developers/gitignore?templates=go # Edit at https://www.toptal.com/developers/gitignore?templates=go
### Go ### ### Go ###
# If you prefer the allow list template instead of the deny list, see community template: # If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
# #
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
*.dylib *.dylib
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
# Go workspace file # Go workspace file
go.work go.work
# End of https://www.toptal.com/developers/gitignore/api/go # End of https://www.toptal.com/developers/gitignore/api/go
# Shit # Shit
.github/TODO .github/TODO
# Local test files # Local test files
output/ output/
outputs/ outputs/
articulate-sample.json articulate-sample.json
test-output.* test-output.*
go-os-arch-matrix.csv go-os-arch-matrix.csv
test_godocx.go test_godocx.go
test_input.json test_input.json
# Build artifacts # Build artifacts
build/ build/
# Old workflows # Old workflows
.github/workflows/ci-old.yml .github/workflows/ci-old.yml
.github/workflows/ci-enhanced.yml .github/workflows/ci-enhanced.yml
# Test coverage files # Test coverage files
coverage.out coverage.out
coverage.txt coverage.txt
coverage.html coverage.html
coverage.* coverage.*
coverage coverage
*.cover *.cover
*.coverprofile *.coverprofile
main_coverage main_coverage
# Other common exclusions # Other common exclusions
*.exe *.exe
*.exe~ *.exe~
*.dll *.dll
*.so *.so
*.dylib *.dylib
*.test *.test
*.out *.out
/tmp/ /tmp/
.github/copilot-instructions.md .github/copilot-instructions.md
# Editors # Editors
.vscode/ .vscode/
.idea/ .idea/
.task/ .task/
**/*.local.* **/*.local.*
.claude/ .claude/
NUL NUL

View File

@ -79,68 +79,68 @@ linters:
# Enable specific linters # Enable specific linters
enable: enable:
# Default/standard linters # Default/standard linters
- errcheck # Check for unchecked errors - errcheck # Check for unchecked errors
- govet # Go vet - govet # Go vet
- ineffassign # Detect ineffectual assignments - ineffassign # Detect ineffectual assignments
- staticcheck # Staticcheck - staticcheck # Staticcheck
- unused # Find unused code - unused # Find unused code
# Code quality # Code quality
- revive # Fast, configurable linter - revive # Fast, configurable linter
- gocritic # Opinionated Go source code linter - gocritic # Opinionated Go source code linter
- godot # Check comment periods - godot # Check comment periods
- godox # Detect TODO/FIXME comments - godox # Detect TODO/FIXME comments
- gocognit # Cognitive complexity - gocognit # Cognitive complexity
- gocyclo # Cyclomatic complexity - gocyclo # Cyclomatic complexity
- funlen # Function length - funlen # Function length
- maintidx # Maintainability index - maintidx # Maintainability index
# Security # Security
- gosec # Security problems - gosec # Security problems
# Performance # Performance
- prealloc # Find slice preallocation opportunities - prealloc # Find slice preallocation opportunities
- bodyclose # Check HTTP response body closed - bodyclose # Check HTTP response body closed
# Style and formatting # Style and formatting
- goconst # Find repeated strings - goconst # Find repeated strings
- misspell # Find misspellings - misspell # Find misspellings
- whitespace # Find unnecessary blank lines - whitespace # Find unnecessary blank lines
- unconvert # Remove unnecessary type conversions - unconvert # Remove unnecessary type conversions
- dupword # Check for duplicate words - dupword # Check for duplicate words
# Error handling # Error handling
- errorlint # Error handling improvements - errorlint # Error handling improvements
- wrapcheck # Check error wrapping - wrapcheck # Check error wrapping
# Testing # Testing
- testifylint # Testify usage - testifylint # Testify usage
- tparallel # Detect improper t.Parallel() usage - tparallel # Detect improper t.Parallel() usage
- thelper # Detect test helpers without t.Helper() - thelper # Detect test helpers without t.Helper()
# Best practices # Best practices
- exhaustive # Check exhaustiveness of enum switches - exhaustive # Check exhaustiveness of enum switches
- nolintlint # Check nolint directives - nolintlint # Check nolint directives
- nakedret # Find naked returns - nakedret # Find naked returns
- nilnil # Check for redundant nil checks - nilnil # Check for redundant nil checks
- noctx # Check sending HTTP requests without context - noctx # Check sending HTTP requests without context
- contextcheck # Check context propagation - contextcheck # Check context propagation
- asciicheck # Check for non-ASCII identifiers - asciicheck # Check for non-ASCII identifiers
- bidichk # Check for dangerous unicode sequences - bidichk # Check for dangerous unicode sequences
- durationcheck # Check for multiplied durations - durationcheck # Check for multiplied durations
- makezero # Find slice declarations with non-zero length - makezero # Find slice declarations with non-zero length
- nilerr # Find code returning nil with non-nil error - nilerr # Find code returning nil with non-nil error
- predeclared # Find code shadowing predeclared identifiers - predeclared # Find code shadowing predeclared identifiers
- promlinter # Check Prometheus metrics naming - promlinter # Check Prometheus metrics naming
- reassign # Check reassignment of package variables - reassign # Check reassignment of package variables
- usestdlibvars # Use variables from stdlib - usestdlibvars # Use variables from stdlib
- wastedassign # Find wasted assignments - wastedassign # Find wasted assignments
# Documentation # Documentation
- godoclint # Check godoc comments - godoclint # Check godoc comments
# New # New
- modernize # Suggest simplifications using new Go features - modernize # Suggest simplifications using new Go features
# Exclusion rules for linters # Exclusion rules for linters
exclusions: exclusions:
@ -221,8 +221,8 @@ linters:
govet: govet:
enable-all: true enable-all: true
disable: disable:
- fieldalignment # Too many false positives - fieldalignment # Too many false positives
- shadow # Can be noisy - shadow # Can be noisy
# goconst settings # goconst settings
goconst: goconst:
@ -286,8 +286,8 @@ linters:
severity: medium severity: medium
confidence: medium confidence: medium
excludes: excludes:
- G104 # Handled by errcheck - G104 # Handled by errcheck
- G304 # File path provided as taint input - G304 # File path provided as taint input
# revive settings # revive settings
revive: revive:
@ -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

@ -49,17 +49,17 @@ docker run --rm ghcr.io/kjanat/articulate-parser:latest --version
## Available Tags ## Available Tags
- `latest` - Latest stable release - `latest` - Latest stable release
- `v1.x.x` - Specific version tags - `v1.x.x` - Specific version tags
- `main` - Latest development build - `main` - Latest development build
## Image Details ## Image Details
- **Base Image**: `scratch` (minimal attack surface) - **Base Image**: `scratch` (minimal attack surface)
- **Architecture**: Multi-arch support (amd64, arm64) - **Architecture**: Multi-arch support (amd64, arm64)
- **Size**: < 10MB (optimized binary) - **Size**: < 10MB (optimized binary)
- **Security**: Runs as non-root user - **Security**: Runs as non-root user
- **Features**: SBOM and provenance attestation included - **Features**: SBOM and provenance attestation included
## Development ## Development
@ -77,6 +77,6 @@ docker-compose up --build
## Repository ## Repository
- **Source**: [github.com/kjanat/articulate-parser](https://github.com/kjanat/articulate-parser) - **Source**: [github.com/kjanat/articulate-parser](https://github.com/kjanat/articulate-parser)
- **Issues**: [Report bugs or request features](https://github.com/kjanat/articulate-parser/issues) - **Issues**: [Report bugs or request features](https://github.com/kjanat/articulate-parser/issues)
- **License**: See repository for license details - **License**: See repository for license details

View File

@ -27,7 +27,7 @@ ARG BUILD_TIME
ARG GIT_COMMIT ARG GIT_COMMIT
# Docker buildx automatically provides these for multi-platform builds # Docker buildx automatically provides these for multi-platform builds
ARG BUILDPLATFORM ARG BUILDPLATFORM
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT

View File

@ -30,7 +30,7 @@ ARG BUILD_TIME
ARG GIT_COMMIT ARG GIT_COMMIT
# Docker buildx automatically provides these for multi-platform builds # Docker buildx automatically provides these for multi-platform builds
ARG BUILDPLATFORM ARG BUILDPLATFORM
ARG TARGETPLATFORM ARG TARGETPLATFORM
ARG TARGETOS ARG TARGETOS
ARG TARGETARCH ARG TARGETARCH
ARG TARGETVARIANT ARG TARGETVARIANT

214
README.md
View File

@ -20,36 +20,36 @@ A Go-based parser that converts Articulate Rise e-learning content to various fo
flowchart TD flowchart TD
%% User Input %% User Input
CLI[Command Line Interface<br/>main.go] --> APP{App Service<br/>services/app.go} CLI[Command Line Interface<br/>main.go] --> APP{App Service<br/>services/app.go}
%% Core Application Logic %% Core Application Logic
APP --> |"ProcessCourseFromURI"| PARSER[Course Parser<br/>services/parser.go] APP --> |"ProcessCourseFromURI"| PARSER[Course Parser<br/>services/parser.go]
APP --> |"ProcessCourseFromFile"| PARSER APP --> |"ProcessCourseFromFile"| PARSER
APP --> |"exportCourse"| FACTORY[Exporter Factory<br/>exporters/factory.go] APP --> |"exportCourse"| FACTORY[Exporter Factory<br/>exporters/factory.go]
%% Data Sources %% Data Sources
PARSER --> |"FetchCourse"| API[Articulate Rise API<br/>rise.articulate.com] PARSER --> |"FetchCourse"| API[Articulate Rise API<br/>rise.articulate.com]
PARSER --> |"LoadCourseFromFile"| FILE[Local JSON File<br/>*.json] PARSER --> |"LoadCourseFromFile"| FILE[Local JSON File<br/>*.json]
%% Data Models %% Data Models
API --> MODELS[Data Models<br/>models/course.go] API --> MODELS[Data Models<br/>models/course.go]
FILE --> MODELS FILE --> MODELS
MODELS --> |Course, Lesson, Item| APP MODELS --> |Course, Lesson, Item| APP
%% Export Factory Pattern %% Export Factory Pattern
FACTORY --> |"CreateExporter"| MARKDOWN[Markdown Exporter<br/>exporters/markdown.go] FACTORY --> |"CreateExporter"| MARKDOWN[Markdown Exporter<br/>exporters/markdown.go]
FACTORY --> |"CreateExporter"| HTML[HTML Exporter<br/>exporters/html.go] FACTORY --> |"CreateExporter"| HTML[HTML Exporter<br/>exporters/html.go]
FACTORY --> |"CreateExporter"| DOCX[DOCX Exporter<br/>exporters/docx.go] FACTORY --> |"CreateExporter"| DOCX[DOCX Exporter<br/>exporters/docx.go]
%% HTML Cleaning Service %% HTML Cleaning Service
CLEANER[HTML Cleaner<br/>services/html_cleaner.go] --> MARKDOWN CLEANER[HTML Cleaner<br/>services/html_cleaner.go] --> MARKDOWN
CLEANER --> HTML CLEANER --> HTML
CLEANER --> DOCX CLEANER --> DOCX
%% Output Files %% Output Files
MARKDOWN --> |"Export"| MD_OUT[Markdown Files<br/>*.md] MARKDOWN --> |"Export"| MD_OUT[Markdown Files<br/>*.md]
HTML --> |"Export"| HTML_OUT[HTML Files<br/>*.html] HTML --> |"Export"| HTML_OUT[HTML Files<br/>*.html]
DOCX --> |"Export"| DOCX_OUT[Word Documents<br/>*.docx] DOCX --> |"Export"| DOCX_OUT[Word Documents<br/>*.docx]
%% Interfaces (Contracts) %% Interfaces (Contracts)
IPARSER[CourseParser Interface<br/>interfaces/parser.go] -.-> PARSER IPARSER[CourseParser Interface<br/>interfaces/parser.go] -.-> PARSER
IEXPORTER[Exporter Interface<br/>interfaces/exporter.go] -.-> MARKDOWN IEXPORTER[Exporter Interface<br/>interfaces/exporter.go] -.-> MARKDOWN
@ -64,7 +64,7 @@ flowchart TD
classDef output fill:#fce7f3,stroke:#be185d,stroke-width:2px,color:#be185d classDef output fill:#fce7f3,stroke:#be185d,stroke-width:2px,color:#be185d
classDef interface fill:#ecfdf5,stroke:#16a34a,stroke-width:1px,stroke-dasharray: 5 5,color:#16a34a classDef interface fill:#ecfdf5,stroke:#16a34a,stroke-width:1px,stroke-dasharray: 5 5,color:#16a34a
classDef service fill:#cffafe,stroke:#0891b2,stroke-width:2px,color:#0891b2 classDef service fill:#cffafe,stroke:#0891b2,stroke-width:2px,color:#0891b2
class CLI userInput class CLI userInput
class APP,FACTORY coreLogic class APP,FACTORY coreLogic
class API,FILE,MODELS dataSource class API,FILE,MODELS dataSource
@ -78,32 +78,32 @@ flowchart TD
The system follows **Clean Architecture** principles with clear separation of concerns: The system follows **Clean Architecture** principles with clear separation of concerns:
- **🎯 Entry Point**: Command-line interface handles user input and coordinates operations - **🎯 Entry Point**: Command-line interface handles user input and coordinates operations
- **🏗️ Application Layer**: Core business logic with dependency injection - **🏗️ Application Layer**: Core business logic with dependency injection
- **📋 Interface Layer**: Contracts defining behavior without implementation details - **📋 Interface Layer**: Contracts defining behavior without implementation details
- **🔧 Service Layer**: Concrete implementations of parsing and utility services - **🔧 Service Layer**: Concrete implementations of parsing and utility services
- **📤 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
## Features ## Features
- Parse Articulate Rise JSON data from URLs or local files - Parse Articulate Rise JSON data from URLs or local files
- Export to Markdown (.md) format - Export to Markdown (.md) format
- Export to HTML (.html) format with professional styling - Export to HTML (.html) format with professional styling
- Export to Word Document (.docx) format - Export to Word Document (.docx) format
- Support for various content types: - Support for various content types:
- Text content with headings and paragraphs - Text content with headings and paragraphs
- Lists and bullet points - Lists and bullet points
- Multimedia content (videos and images) - Multimedia content (videos and images)
- Knowledge checks and quizzes - Knowledge checks and quizzes
- Interactive content (flashcards) - Interactive content (flashcards)
- Course structure and metadata - Course structure and metadata
## Installation ## Installation
### Prerequisites ### Prerequisites
- Go, I don't know the version, but I have [![Go version](https://img.shields.io/github/go-mod/go-version/kjanat/articulate-parser?label=)][gomod] configured right now, and it works, see the [CI][Build] workflow where it is tested. - Go, I don't know the version, but I have [![Go version](https://img.shields.io/github/go-mod/go-version/kjanat/articulate-parser?label=)][gomod] configured right now, and it works, see the [CI][Build] workflow where it is tested.
### Install from source ### Install from source
@ -124,7 +124,7 @@ go install github.com/kjanat/articulate-parser@latest
The parser uses the following external library: The parser uses the following external library:
- `github.com/fumiama/go-docx` - For creating Word documents (MIT license) - `github.com/fumiama/go-docx` - For creating Word documents (MIT license)
## Testing ## Testing
@ -164,25 +164,25 @@ go run main.go <input_uri_or_file> <output_format> [output_path]
#### Examples #### Examples
1. **Parse from URL and export to Markdown:** 1. **Parse from URL and export to Markdown:**
```bash ```bash
go run main.go "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/" md go run main.go "https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/" md
``` ```
2. **Parse from local file and export to Word:** 2. **Parse from local file and export to Word:**
```bash ```bash
go run main.go "articulate-sample.json" docx "my-course.docx" go run main.go "articulate-sample.json" docx "my-course.docx"
``` ```
3. **Parse from local file and export to HTML:** 3. **Parse from local file and export to HTML:**
```bash ```bash
go run main.go "articulate-sample.json" html "output.html" go run main.go "articulate-sample.json" html "output.html"
``` ```
4. **Parse from local file and export to Markdown:** 4. **Parse from local file and export to Markdown:**
```bash ```bash
go run main.go "articulate-sample.json" md "output.md" go run main.go "articulate-sample.json" md "output.md"
@ -225,14 +225,14 @@ 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 |
| `develop` | Development branch builds | Development/testing | | `develop` | Development branch builds | Development/testing |
| `feature/docker-ghcr` | Feature branch builds | Feature testing | | `feature/docker-ghcr` | Feature branch builds | Feature testing |
| `master` | Latest master branch build | Continuous integration | | `master` | Latest master branch build | Continuous integration |
### Usage Examples ### Usage Examples
@ -313,11 +313,11 @@ 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 |
### Docker Security ### Docker Security
@ -332,88 +332,88 @@ The Docker image supports the following build-time arguments:
The project maintains high code quality standards: The project maintains high code quality standards:
- Cyclomatic complexity ≤ 15 (checked with [gocyclo](https://github.com/fzipp/gocyclo)) - Cyclomatic complexity ≤ 15 (checked with [gocyclo](https://github.com/fzipp/gocyclo))
- Race condition detection enabled - Race condition detection enabled
- Comprehensive test coverage - Comprehensive test coverage
- Code formatting with `gofmt` - Code formatting with `gofmt`
- Static analysis with `go vet` - Static analysis with `go vet`
### Contributing ### Contributing
1. Fork the repository 1. Fork the repository
2. Create a feature branch 2. Create a feature branch
3. Make your changes 3. Make your changes
4. Run tests: `go test ./...` 4. Run tests: `go test ./...`
5. Submit a pull request 5. Submit a pull request
## Output Formats ## Output Formats
### Markdown (`.md`) ### Markdown (`.md`)
- Hierarchical structure with proper heading levels - Hierarchical structure with proper heading levels
- Clean text content with HTML tags removed - Clean text content with HTML tags removed
- Lists and bullet points preserved - Lists and bullet points preserved
- Quiz questions with correct answers marked - Quiz questions with correct answers marked
- Media references included - Media references included
- Course metadata at the top - Course metadata at the top
### HTML (`.html`) ### HTML (`.html`)
- Professional styling with embedded CSS - Professional styling with embedded CSS
- Interactive and visually appealing layout - Interactive and visually appealing layout
- Proper HTML structure with semantic elements - Proper HTML structure with semantic elements
- Responsive design for different screen sizes - Responsive design for different screen sizes
- All content types beautifully formatted - All content types beautifully formatted
- Maintains course hierarchy and organization - Maintains course hierarchy and organization
### Word Document (`.docx`) ### Word Document (`.docx`)
- Professional document formatting - Professional document formatting
- Bold headings and proper typography - Bold headings and proper typography
- Bulleted lists - Bulleted lists
- Quiz questions with answers - Quiz questions with answers
- Media content references - Media content references
- Maintains course structure - Maintains course structure
## Supported Content Types ## Supported Content Types
The parser handles the following Articulate Rise content types: The parser handles the following Articulate Rise content types:
- **Text blocks**: Headings and paragraphs - **Text blocks**: Headings and paragraphs
- **Lists**: Bullet points and numbered lists - **Lists**: Bullet points and numbered lists
- **Multimedia**: Videos and images (references only) - **Multimedia**: Videos and images (references only)
- **Knowledge Checks**: Multiple choice, multiple response, fill-in-the-blank, matching - **Knowledge Checks**: Multiple choice, multiple response, fill-in-the-blank, matching
- **Interactive Content**: Flashcards and interactive scenarios - **Interactive Content**: Flashcards and interactive scenarios
- **Dividers**: Section breaks - **Dividers**: Section breaks
- **Sections**: Course organization - **Sections**: Course organization
## Data Structure ## Data Structure
The parser works with the standard Articulate Rise JSON format which includes: The parser works with the standard Articulate Rise JSON format which includes:
- Course metadata (title, description, settings) - Course metadata (title, description, settings)
- Lesson structure - Lesson structure
- Content items with various types - Content items with various types
- Media references - Media references
- Quiz/assessment data - Quiz/assessment data
- Styling and layout information - Styling and layout information
## URL Pattern Recognition ## URL Pattern Recognition
The parser automatically extracts share IDs from Articulate Rise URLs: The parser automatically extracts share IDs from Articulate Rise URLs:
- Input: `https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/` - Input: `https://rise.articulate.com/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO#/`
- API URL: `https://rise.articulate.com/api/rise-runtime/boot/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO` - API URL: `https://rise.articulate.com/api/rise-runtime/boot/share/N_APNg40Vr2CSH2xNz-ZLATM5kNviDIO`
## Error Handling ## Error Handling
The parser includes error handling for: The parser includes error handling for:
- Invalid URLs or share IDs - Invalid URLs or share IDs
- Network connection issues - Network connection issues
- Malformed JSON data - Malformed JSON data
- File I/O errors - File I/O errors
- Unsupported content types - Unsupported content types
<!-- ## Code coverage <!-- ## Code coverage
@ -425,28 +425,28 @@ The parser includes error handling for:
## Limitations ## Limitations
- Media files (videos, images) are referenced but not downloaded - Media files (videos, images) are referenced but not downloaded
- Complex interactive elements may be simplified in export - Complex interactive elements may be simplified in export
- Styling and visual formatting is not preserved - Styling and visual formatting is not preserved
- Assessment logic and interactivity is lost in static exports - Assessment logic and interactivity is lost in static exports
## Performance ## Performance
- Lightweight with minimal dependencies - Lightweight with minimal dependencies
- Fast JSON parsing and export - Fast JSON parsing and export
- Memory efficient processing - Memory efficient processing
- No external license requirements - No external license requirements
## Future Enhancements ## Future Enhancements
Potential improvements could include: Potential improvements could include:
- [ ] PDF export support - [ ] PDF export support
- [ ] Media file downloading - [ ] Media file downloading
- [x] ~~HTML export with preserved styling~~ - [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
@ -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

@ -210,13 +210,13 @@ tasks:
sh: gofmt -s -l . sh: gofmt -s -l .
cmds: cmds:
- | - |
{{if ne .UNFORMATTED ""}} {{if ne .UNFORMATTED ""}}
echo "❌ The following files need formatting:" echo "❌ The following files need formatting:"
echo "{{.UNFORMATTED}}" echo "{{.UNFORMATTED}}"
exit 1 exit 1
{{else}} {{else}}
echo "All files are properly formatted" echo "All files are properly formatted"
{{end}} {{end}}
lint:staticcheck: lint:staticcheck:
desc: Run staticcheck (install if needed) desc: Run staticcheck (install if needed)
@ -304,22 +304,22 @@ tasks:
aliases: [db] aliases: [db]
cmds: cmds:
- | - |
docker build \ docker build \
--build-arg VERSION={{.VERSION}} \ --build-arg VERSION={{.VERSION}} \
--build-arg BUILD_TIME={{.BUILD_TIME}} \ --build-arg BUILD_TIME={{.BUILD_TIME}} \
--build-arg GIT_COMMIT={{.GIT_COMMIT}} \ --build-arg GIT_COMMIT={{.GIT_COMMIT}} \
-t {{.APP_NAME}}:{{.VERSION}} \ -t {{.APP_NAME}}:{{.VERSION}} \
-t {{.APP_NAME}}:latest \ -t {{.APP_NAME}}:latest \
. .
- > - >
echo "Docker image built: {{.APP_NAME}}:{{.VERSION}}" echo "Docker image built: {{.APP_NAME}}:{{.VERSION}}"
docker:build:dev: docker:build:dev:
desc: Build development Docker image desc: Build development Docker image
cmds: cmds:
- docker build -f Dockerfile.dev -t {{.APP_NAME}}:dev . - docker build -f Dockerfile.dev -t {{.APP_NAME}}:dev .
- > - >
echo "Development Docker image built: {{.APP_NAME}}:dev" echo "Development Docker image built: {{.APP_NAME}}:dev"
docker:run: docker:run:
desc: Run Docker container desc: Run Docker container
@ -426,7 +426,7 @@ tasks:
- git tag -a v{{.VERSION}} -m "Release v{{.VERSION}}" - git tag -a v{{.VERSION}} -m "Release v{{.VERSION}}"
- echo "Tagged v{{.VERSION}}" - echo "Tagged v{{.VERSION}}"
- > - >
echo "Push with: git push origin v{{.VERSION}}" echo "Push with: git push origin v{{.VERSION}}"
# Documentation tasks # Documentation tasks
docs:serve: docs:serve:

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,173 +1,175 @@
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; font-family:
line-height: 1.6; -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
color: #333; Cantarell, sans-serif;
max-width: 800px; line-height: 1.6;
margin: 0 auto; color: #333;
padding: 20px; max-width: 800px;
background-color: #f9f9f9; margin: 0 auto;
padding: 20px;
background-color: #f9f9f9;
} }
header { header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white; color: white;
padding: 2rem; padding: 2rem;
border-radius: 10px; border-radius: 10px;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
header h1 { header h1 {
margin: 0; margin: 0;
font-size: 2.5rem; font-size: 2.5rem;
font-weight: 300; font-weight: 300;
} }
.course-description { .course-description {
margin-top: 1rem; margin-top: 1rem;
font-size: 1.1rem; font-size: 1.1rem;
opacity: 0.9; opacity: 0.9;
} }
.course-info { .course-info {
background: white; background: white;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
margin-bottom: 2rem; margin-bottom: 2rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.course-info h2 { .course-info h2 {
margin-top: 0; margin-top: 0;
color: #4a5568; color: #4a5568;
border-bottom: 2px solid #e2e8f0; border-bottom: 2px solid #e2e8f0;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.course-info ul { .course-info ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
} }
.course-info li { .course-info li {
margin: 0.5rem 0; margin: 0.5rem 0;
padding: 0.5rem; padding: 0.5rem;
background: #f7fafc; background: #f7fafc;
border-radius: 4px; border-radius: 4px;
} }
.course-section { .course-section {
background: #4299e1; background: #4299e1;
color: white; color: white;
padding: 1.5rem; padding: 1.5rem;
border-radius: 8px; border-radius: 8px;
margin: 2rem 0; margin: 2rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.course-section h2 { .course-section h2 {
margin: 0; margin: 0;
font-weight: 400; font-weight: 400;
} }
.lesson { .lesson {
background: white; background: white;
padding: 2rem; padding: 2rem;
border-radius: 8px; border-radius: 8px;
margin: 2rem 0; margin: 2rem 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-left: 4px solid #4299e1; border-left: 4px solid #4299e1;
} }
.lesson h3 { .lesson h3 {
margin-top: 0; margin-top: 0;
color: #2d3748; color: #2d3748;
font-size: 1.5rem; font-size: 1.5rem;
} }
.lesson-description { .lesson-description {
margin: 1rem 0; margin: 1rem 0;
padding: 1rem; padding: 1rem;
background: #f7fafc; background: #f7fafc;
border-radius: 4px; border-radius: 4px;
border-left: 3px solid #4299e1; border-left: 3px solid #4299e1;
} }
.item { .item {
margin: 1.5rem 0; margin: 1.5rem 0;
padding: 1rem; padding: 1rem;
border-radius: 6px; border-radius: 6px;
background: #fafafa; background: #fafafa;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
} }
.item h4 { .item h4 {
margin-top: 0; margin-top: 0;
color: #4a5568; color: #4a5568;
font-size: 1.2rem; font-size: 1.2rem;
text-transform: capitalize; text-transform: capitalize;
} }
.text-item { .text-item {
background: #f0fff4; background: #f0fff4;
border-left: 3px solid #48bb78; border-left: 3px solid #48bb78;
} }
.list-item { .list-item {
background: #fffaf0; background: #fffaf0;
border-left: 3px solid #ed8936; border-left: 3px solid #ed8936;
} }
.knowledge-check { .knowledge-check {
background: #e6fffa; background: #e6fffa;
border-left: 3px solid #38b2ac; border-left: 3px solid #38b2ac;
} }
.multimedia-item { .multimedia-item {
background: #faf5ff; background: #faf5ff;
border-left: 3px solid #9f7aea; border-left: 3px solid #9f7aea;
} }
.interactive-item { .interactive-item {
background: #fff5f5; background: #fff5f5;
border-left: 3px solid #f56565; border-left: 3px solid #f56565;
} }
.unknown-item { .unknown-item {
background: #f7fafc; background: #f7fafc;
border-left: 3px solid #a0aec0; border-left: 3px solid #a0aec0;
} }
.answers { .answers {
margin: 1rem 0; margin: 1rem 0;
} }
.answers h5 { .answers h5 {
margin: 0.5rem 0; margin: 0.5rem 0;
color: #4a5568; color: #4a5568;
} }
.answers ol { .answers ol {
margin: 0.5rem 0; margin: 0.5rem 0;
padding-left: 1.5rem; padding-left: 1.5rem;
} }
.answers li { .answers li {
margin: 0.3rem 0; margin: 0.3rem 0;
padding: 0.3rem; padding: 0.3rem;
} }
.correct-answer { .correct-answer {
background: #c6f6d5; background: #c6f6d5;
border-radius: 3px; border-radius: 3px;
font-weight: bold; font-weight: bold;
} }
.correct-answer::after { .correct-answer::after {
content: " ✓"; content: " ✓";
color: #38a169; color: #38a169;
} }
.feedback { .feedback {
margin: 1rem 0; margin: 1rem 0;
padding: 1rem; padding: 1rem;
background: #edf2f7; background: #edf2f7;
border-radius: 4px; border-radius: 4px;
border-left: 3px solid #4299e1; border-left: 3px solid #4299e1;
font-style: italic; font-style: italic;
} }
.media-info { .media-info {
background: #edf2f7; background: #edf2f7;
padding: 1rem; padding: 1rem;
border-radius: 4px; border-radius: 4px;
margin: 0.5rem 0; margin: 0.5rem 0;
} }
.media-info strong { .media-info strong {
color: #4a5568; color: #4a5568;
} }
hr { hr {
border: none; border: none;
height: 2px; height: 2px;
background: linear-gradient(to right, #667eea, #764ba2); background: linear-gradient(to right, #667eea, #764ba2);
margin: 2rem 0; margin: 2rem 0;
border-radius: 1px; border-radius: 1px;
} }
ul { ul {
padding-left: 1.5rem; padding-left: 1.5rem;
} }
li { li {
margin: 0.5rem 0; margin: 0.5rem 0;
} }

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.

View File

@ -137,7 +137,7 @@ try {
# Show targets and exit if requested # Show targets and exit if requested
if ($ShowTargets) { if ($ShowTargets) {
Write-Host 'Available build targets:' -ForegroundColor Cyan Write-Host 'Available build targets:' -ForegroundColor Cyan
# Get available platforms and architectures from Go toolchain # Get available platforms and architectures from Go toolchain
try { try {
$GoTargets = @(go tool dist list 2>$null) $GoTargets = @(go tool dist list 2>$null)
@ -148,7 +148,7 @@ try {
Write-Host '⚠️ Could not retrieve targets from Go. Using default targets.' -ForegroundColor Yellow Write-Host '⚠️ Could not retrieve targets from Go. Using default targets.' -ForegroundColor Yellow
$PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() } $PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() }
$ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() } $ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() }
foreach ($platform in $PlatformList) { foreach ($platform in $PlatformList) {
foreach ($arch in $ArchList) { foreach ($arch in $ArchList) {
$BinaryName = "articulate-parser-$platform-$arch" $BinaryName = "articulate-parser-$platform-$arch"
@ -163,12 +163,12 @@ try {
$SelectedTargets = @() $SelectedTargets = @()
$PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() } $PlatformList = $Platforms.Split(',') | ForEach-Object { $_.Trim() }
$ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() } $ArchList = $Architectures.Split(',') | ForEach-Object { $_.Trim() }
foreach ($target in $GoTargets) { foreach ($target in $GoTargets) {
$parts = $target.Split('/') $parts = $target.Split('/')
$platform = $parts[0] $platform = $parts[0]
$arch = $parts[1] $arch = $parts[1]
if ($PlatformList -contains $platform -and $ArchList -contains $arch) { if ($PlatformList -contains $platform -and $ArchList -contains $arch) {
$SelectedTargets += @{ $SelectedTargets += @{
Platform = $platform Platform = $platform
@ -177,14 +177,14 @@ try {
} }
} }
} }
# Display filtered targets # Display filtered targets
foreach ($target in $SelectedTargets) { foreach ($target in $SelectedTargets) {
$BinaryName = "articulate-parser-$($target.Platform)-$($target.Arch)" $BinaryName = "articulate-parser-$($target.Platform)-$($target.Arch)"
if ($target.Platform -eq 'windows') { $BinaryName += '.exe' } if ($target.Platform -eq 'windows') { $BinaryName += '.exe' }
Write-Host " $($target.Original) -> $BinaryName" -ForegroundColor Gray Write-Host " $($target.Original) -> $BinaryName" -ForegroundColor Gray
} }
# Show all available targets if verbose # Show all available targets if verbose
if ($VerboseOutput) { if ($VerboseOutput) {
Write-Host "`nAll Go targets available on this system:" -ForegroundColor Cyan Write-Host "`nAll Go targets available on this system:" -ForegroundColor Cyan
@ -404,13 +404,13 @@ try {
} }
$BuildArgs += '-o' $BuildArgs += '-o'
$BuildArgs += $Target.Path $BuildArgs += $Target.Path
# If using custom entry point that's not main.go # If using custom entry point that's not main.go
# we need to use the file explicitly to avoid duplicate declarations # we need to use the file explicitly to avoid duplicate declarations
$EntryPointPath = Join-Path $ProjectRoot $EntryPoint $EntryPointPath = Join-Path $ProjectRoot $EntryPoint
$EntryPointFile = Split-Path $EntryPointPath -Leaf $EntryPointFile = Split-Path $EntryPointPath -Leaf
$IsCustomEntryPoint = ($EntryPointFile -ne 'main.go') $IsCustomEntryPoint = ($EntryPointFile -ne 'main.go')
if ($IsCustomEntryPoint) { if ($IsCustomEntryPoint) {
# When using custom entry point, compile only that file # When using custom entry point, compile only that file
$BuildArgs += $EntryPointPath $BuildArgs += $EntryPointPath
@ -419,7 +419,7 @@ try {
$PackagePath = Split-Path $EntryPointPath -Parent $PackagePath = Split-Path $EntryPointPath -Parent
$BuildArgs += $PackagePath $BuildArgs += $PackagePath
} }
# For verbose output, show the command that will be executed # For verbose output, show the command that will be executed
if ($VerboseOutput) { if ($VerboseOutput) {
Write-Host "Command: go $($BuildArgs -join ' ')" -ForegroundColor DarkCyan Write-Host "Command: go $($BuildArgs -join ' ')" -ForegroundColor DarkCyan

View File

@ -73,7 +73,7 @@ EXAMPLES:
DEFAULT TARGETS: DEFAULT TARGETS:
Operating Systems: darwin, freebsd, linux, windows Operating Systems: darwin, freebsd, linux, windows
Architectures: amd64, arm64 Architectures: amd64, arm64
This creates 8 binaries total (4 OS × 2 ARCH) This creates 8 binaries total (4 OS × 2 ARCH)
GO BUILD FLAGS: GO BUILD FLAGS: