mirror of
https://github.com/kjanat/articulate-parser.git
synced 2026-01-16 08:22:09 +01:00
Compare commits
8 Commits
bd308e4dfc
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
| 02941138ee | |||
|
94a7924bed
|
|||
|
90b9d557d8
|
|||
|
33ff267644
|
|||
|
33673d661b
|
|||
| 41f3f5c4e2 | |||
|
d644094999
|
|||
|
71d1429048
|
42
.dprint.jsonc
Normal file
42
.dprint.jsonc
Normal 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",
|
||||
],
|
||||
}
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,7 +1,7 @@
|
||||
name: Bug Report
|
||||
description: Create a report to help us improve
|
||||
title: '[BUG] '
|
||||
labels: ['bug', 'triage']
|
||||
title: "[BUG] "
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -5,11 +5,13 @@
|
||||
## Related Issue
|
||||
|
||||
<!-- Link to the issue this PR addresses using the syntax: Fixes #issue_number -->
|
||||
|
||||
Fixes #
|
||||
|
||||
## Type of Change
|
||||
|
||||
<!-- Mark the appropriate option with an "x" -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
@ -21,6 +23,7 @@ Fixes #
|
||||
## Checklist
|
||||
|
||||
<!-- 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
|
||||
- [ ] I have added comments to complex logic
|
||||
@ -42,6 +45,7 @@ Fixes #
|
||||
## Testing Instructions
|
||||
|
||||
<!-- Provide steps to test the changes, if applicable -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
98
.github/dependabot.yml
vendored
98
.github/dependabot.yml
vendored
@ -1,86 +1,86 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Check for updates to GitHub Actions
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
time: '07:00'
|
||||
timezone: 'Europe/Amsterdam'
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "07:00"
|
||||
timezone: "Europe/Amsterdam"
|
||||
open-pull-requests-limit: 2
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'dependencies/github-actions'
|
||||
- "dependencies"
|
||||
- "dependencies/github-actions"
|
||||
commit-message:
|
||||
prefix: 'ci'
|
||||
include: 'scope'
|
||||
prefix: "ci"
|
||||
include: "scope"
|
||||
|
||||
# Check for updates to Docker
|
||||
- package-ecosystem: 'docker'
|
||||
directory: '/'
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
time: '07:00'
|
||||
timezone: 'Europe/Amsterdam'
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "07:00"
|
||||
timezone: "Europe/Amsterdam"
|
||||
open-pull-requests-limit: 2
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'dependencies/docker'
|
||||
- "dependencies"
|
||||
- "dependencies/docker"
|
||||
commit-message:
|
||||
prefix: 'docker'
|
||||
include: 'scope'
|
||||
prefix: "docker"
|
||||
include: "scope"
|
||||
groups:
|
||||
docker:
|
||||
patterns:
|
||||
- '*'
|
||||
- "*"
|
||||
update-types:
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Check for updates to Docker Compose
|
||||
- package-ecosystem: 'docker-compose'
|
||||
directory: '/'
|
||||
- package-ecosystem: "docker-compose"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
time: '07:00'
|
||||
timezone: 'Europe/Amsterdam'
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "07:00"
|
||||
timezone: "Europe/Amsterdam"
|
||||
open-pull-requests-limit: 2
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'dependencies/docker-compose'
|
||||
- "dependencies"
|
||||
- "dependencies/docker-compose"
|
||||
commit-message:
|
||||
prefix: 'docker'
|
||||
include: 'scope'
|
||||
prefix: "docker"
|
||||
include: "scope"
|
||||
groups:
|
||||
docker:
|
||||
patterns:
|
||||
- '*'
|
||||
- "*"
|
||||
update-types:
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
# Check for updates to Go modules
|
||||
- package-ecosystem: 'gomod'
|
||||
directory: '/'
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
day: 'monday'
|
||||
time: '07:00'
|
||||
timezone: 'Europe/Amsterdam'
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "07:00"
|
||||
timezone: "Europe/Amsterdam"
|
||||
open-pull-requests-limit: 2
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'dependencies/go'
|
||||
- "dependencies"
|
||||
- "dependencies/go"
|
||||
commit-message:
|
||||
prefix: 'deps'
|
||||
include: 'scope'
|
||||
prefix: "deps"
|
||||
include: "scope"
|
||||
groups:
|
||||
go-modules:
|
||||
patterns:
|
||||
- '*'
|
||||
- "*"
|
||||
update-types:
|
||||
- 'minor'
|
||||
- 'patch'
|
||||
- "minor"
|
||||
- "patch"
|
||||
|
||||
6
.github/workflows/autofix.yml
vendored
6
.github/workflows/autofix.yml
vendored
@ -11,13 +11,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
|
||||
- uses: actions/setup-go@v6
|
||||
with: { go-version-file: 'go.mod' }
|
||||
with: { go-version-file: "go.mod" }
|
||||
|
||||
- name: Setup go deps
|
||||
run: |
|
||||
@ -34,7 +34,7 @@ jobs:
|
||||
run: golangci-lint run --fix
|
||||
|
||||
- name: Run golangci-lint format
|
||||
run: golangci-lint format
|
||||
run: golangci-lint fmt
|
||||
|
||||
- name: Run go mod tidy
|
||||
run: go mod tidy
|
||||
|
||||
128
.github/workflows/ci.yml
vendored
128
.github/workflows/ci.yml
vendored
@ -2,7 +2,7 @@ name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['master', 'develop']
|
||||
branches: ["master", "develop"]
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
@ -21,12 +21,12 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: stable
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with: { version: latest }
|
||||
|
||||
test:
|
||||
@ -38,14 +38,11 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
go:
|
||||
- 1.21.x
|
||||
- 1.22.x
|
||||
- 1.23.x
|
||||
- 1.24.x
|
||||
- 1.25.x
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ matrix.go }}
|
||||
uses: actions/setup-go@v6
|
||||
@ -67,14 +64,18 @@ jobs:
|
||||
|
||||
- name: Run tests with enhanced reporting
|
||||
id: test
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
{
|
||||
cat << EOF
|
||||
## 🔧 Test Environment
|
||||
- **Go Version:** ${{ matrix.go }}
|
||||
- **OS:** ubuntu-latest
|
||||
- **Timestamp:** $(date -u)
|
||||
|
||||
EOF
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "Running tests with coverage..."
|
||||
task test:coverage 2>&1 | tee test-output.log
|
||||
@ -87,16 +88,17 @@ jobs:
|
||||
SKIPPED_TESTS=$(grep -c "--- SKIP:" test-output.log || echo "0")
|
||||
|
||||
# Generate test summary
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
{
|
||||
cat << EOF
|
||||
## 🧪 Test Results (Go ${{ matrix.go }})
|
||||
|
||||
| Metric | Value |
|
||||
| ----------- | ----------------------------------------------------------- |
|
||||
| ----------- | ------------------------------------------------------------- |
|
||||
| Total Tests | $TOTAL_TESTS |
|
||||
| Passed | $PASSED_TESTS |
|
||||
| Failed | $FAILED_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
|
||||
|
||||
@ -108,38 +110,39 @@ jobs:
|
||||
grep "^ok\|^FAIL" test-output.log | while read -r line; do
|
||||
if [[ $line == ok* ]]; then
|
||||
pkg=$(echo "$line" | awk '{print $2}')
|
||||
echo "| $pkg | ✅ PASS |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| $pkg | ✅ PASS |"
|
||||
elif [[ $line == FAIL* ]]; then
|
||||
pkg=$(echo "$line" | awk '{print $2}')
|
||||
echo "| $pkg | ❌ FAIL |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| $pkg | ❌ FAIL |"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""
|
||||
|
||||
# Add detailed results if tests failed
|
||||
if [ $TEST_STATUS -ne 0 ]; then
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
if [ "$TEST_STATUS" -ne 0 ]; then
|
||||
cat << 'EOF'
|
||||
### ❌ Failed Tests Details
|
||||
```
|
||||
EOF
|
||||
grep -A 10 "--- FAIL:" test-output.log | head -100 >> $GITHUB_STEP_SUMMARY
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
grep -A 10 -- "--- FAIL:" test-output.log | head -100
|
||||
cat << 'EOF'
|
||||
```
|
||||
|
||||
EOF
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# Set outputs for other steps
|
||||
cat >> $GITHUB_OUTPUT << EOF
|
||||
test-status=$TEST_STATUS
|
||||
total-tests=$TOTAL_TESTS
|
||||
passed-tests=$PASSED_TESTS
|
||||
failed-tests=$FAILED_TESTS
|
||||
EOF
|
||||
{
|
||||
echo "test-status=$TEST_STATUS"
|
||||
echo "total-tests=$TOTAL_TESTS"
|
||||
echo "passed-tests=$PASSED_TESTS"
|
||||
echo "failed-tests=$FAILED_TESTS"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Exit with the original test status
|
||||
exit $TEST_STATUS
|
||||
exit "$TEST_STATUS"
|
||||
|
||||
- name: Generate coverage report
|
||||
if: always()
|
||||
@ -147,7 +150,8 @@ jobs:
|
||||
if [ -f coverage/coverage.out ]; then
|
||||
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 }})
|
||||
|
||||
**Total Coverage: $COVERAGE**
|
||||
@ -187,17 +191,18 @@ jobs:
|
||||
avg = packages[pkg] / counts[pkg]
|
||||
printf "| %s | %.1f%% |\n", pkg, avg
|
||||
}
|
||||
}' "$temp_coverage" | sort >> $GITHUB_STEP_SUMMARY
|
||||
}' "$temp_coverage" | sort
|
||||
|
||||
rm -f "$temp_coverage"
|
||||
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat << 'EOF'
|
||||
|
||||
</details>
|
||||
|
||||
EOF
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||||
## ⚠️ Coverage Report
|
||||
No coverage file generated
|
||||
|
||||
@ -206,7 +211,7 @@ jobs:
|
||||
|
||||
- name: Upload test artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: test-results-go-${{ matrix.go }}
|
||||
path: |
|
||||
@ -216,8 +221,8 @@ jobs:
|
||||
|
||||
- name: Run linters
|
||||
run: |
|
||||
# Initialize summary
|
||||
cat >> $GITHUB_STEP_SUMMARY << EOF
|
||||
{
|
||||
cat << EOF
|
||||
## 🔍 Static Analysis (Go ${{ matrix.go }})
|
||||
|
||||
EOF
|
||||
@ -226,42 +231,43 @@ jobs:
|
||||
VET_OUTPUT=$(task lint:vet 2>&1 || echo "")
|
||||
VET_STATUS=$?
|
||||
|
||||
if [ $VET_STATUS -eq 0 ]; then
|
||||
echo "✅ **go vet:** No issues found" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$VET_STATUS" -eq 0 ]; then
|
||||
echo "✅ **go vet:** No issues found"
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat << 'EOF'
|
||||
❌ **go vet:** Issues found
|
||||
|
||||
```
|
||||
EOF
|
||||
echo "$VET_OUTPUT" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$VET_OUTPUT"
|
||||
echo '```'
|
||||
fi
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo ""
|
||||
|
||||
# Run go fmt check
|
||||
FMT_OUTPUT=$(task lint:fmt 2>&1 || echo "")
|
||||
FMT_STATUS=$?
|
||||
|
||||
if [ $FMT_STATUS -eq 0 ]; then
|
||||
echo "✅ **go fmt:** All files properly formatted" >> $GITHUB_STEP_SUMMARY
|
||||
if [ "$FMT_STATUS" -eq 0 ]; then
|
||||
echo "✅ **go fmt:** All files properly formatted"
|
||||
else
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat << 'EOF'
|
||||
❌ **go fmt:** Files need formatting
|
||||
|
||||
```
|
||||
EOF
|
||||
echo "$FMT_OUTPUT" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "$FMT_OUTPUT"
|
||||
echo '```'
|
||||
fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# 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
|
||||
if: always()
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||||
## 📋 Job Summary (Go ${{ matrix.go }})
|
||||
|
||||
| Step | Status |
|
||||
@ -297,7 +303,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@ -316,7 +322,8 @@ jobs:
|
||||
|
||||
- name: Test Docker image using Task
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
{
|
||||
cat << 'EOF'
|
||||
## 🧪 Docker Image Tests
|
||||
|
||||
EOF
|
||||
@ -324,16 +331,17 @@ jobs:
|
||||
# Run Task docker test
|
||||
task docker:test
|
||||
|
||||
echo "**Testing help command:**" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```terminaloutput' >> $GITHUB_STEP_SUMMARY
|
||||
docker run --rm articulate-parser:latest --help >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Testing help command:**"
|
||||
echo '```terminaloutput'
|
||||
docker run --rm articulate-parser:latest --help
|
||||
echo '```'
|
||||
echo ""
|
||||
|
||||
# Test image size
|
||||
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 "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "**Image size:** $IMAGE_SIZE"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
@ -342,10 +350,10 @@ jobs:
|
||||
contents: read
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 'Dependency Review'
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
@ -357,14 +365,14 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: [test, docker-test, dependency-review]
|
||||
needs: [test, docker-test]
|
||||
if: |
|
||||
github.event_name == 'push' && (github.ref == 'refs/heads/master' ||
|
||||
github.ref == 'refs/heads/develop' ||
|
||||
startsWith(github.ref, 'refs/heads/feature/docker'))
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
@ -436,7 +444,7 @@ jobs:
|
||||
|
||||
- name: Generate Docker summary
|
||||
run: |
|
||||
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
|
||||
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
|
||||
## 🐳 Docker Build Summary
|
||||
|
||||
**Image:** `ghcr.io/${{ github.repository }}`
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@ -17,7 +17,7 @@ name: "CodeQL"
|
||||
on:
|
||||
workflow_call:
|
||||
schedule:
|
||||
- cron: '44 16 * * 6'
|
||||
- cron: "44 16 * * 6"
|
||||
# push:
|
||||
# branches: [ "master" ]
|
||||
# pull_request:
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
# 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:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
|
||||
6
.github/workflows/dependency-review.yml
vendored
6
.github/workflows/dependency-review.yml
vendored
@ -16,10 +16,10 @@ jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 'Dependency Review'
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: moderate
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -82,13 +82,13 @@ jobs:
|
||||
docker:
|
||||
name: Docker Build & Push
|
||||
runs-on: ubuntu-latest
|
||||
needs: ['release']
|
||||
needs: ["release"]
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@ -349,7 +349,15 @@ linters:
|
||||
|
||||
# stylecheck settings
|
||||
staticcheck:
|
||||
checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"]
|
||||
checks: [
|
||||
"all",
|
||||
"-ST1000",
|
||||
"-ST1003",
|
||||
"-ST1016",
|
||||
"-ST1020",
|
||||
"-ST1021",
|
||||
"-ST1022",
|
||||
]
|
||||
|
||||
# maintidx settings
|
||||
maintidx:
|
||||
|
||||
75
.pre-commit-config.yaml
Normal file
75
.pre-commit-config.yaml
Normal 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
|
||||
163
AGENTS.md
163
AGENTS.md
@ -1,56 +1,183 @@
|
||||
# Agent Guidelines for articulate-parser
|
||||
|
||||
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**: `task build` or `go build -o bin/articulate-parser main.go`
|
||||
- **Run tests**: `task test` or `go test -race -timeout 5m ./...`
|
||||
- **Run single test**: `go test -v -race -run ^TestName$ ./path/to/package`
|
||||
- **Test with coverage**:
|
||||
- `task test:coverage` or
|
||||
- `go test -race -coverprofile=coverage/coverage.out -covermode=atomic ./...`
|
||||
- **Lint**: `task lint` (runs vet, fmt check, staticcheck, golangci-lint)
|
||||
- **Format**: `task fmt` or `gofmt -s -w .`
|
||||
- **CI checks**: `task ci` (deps, lint, test with coverage, build)
|
||||
|
||||
### Primary Commands (using Taskfile)
|
||||
|
||||
```bash
|
||||
task build # Build binary to bin/articulate-parser
|
||||
task test # Run all tests with race detection
|
||||
task lint # Run all linters (vet, fmt, staticcheck, golangci-lint)
|
||||
task fmt # Format all Go files
|
||||
task ci # Full CI pipeline: deps, lint, test with coverage, build
|
||||
task qa # Quick QA: fmt + lint + test
|
||||
```
|
||||
|
||||
### Direct Go Commands
|
||||
|
||||
```bash
|
||||
# Build
|
||||
go build -o bin/articulate-parser main.go
|
||||
|
||||
# Run all tests
|
||||
go test -race -timeout 5m ./...
|
||||
|
||||
# Run single test by name
|
||||
go test -v -race -run ^TestMarkdownExporter_Export$ ./internal/exporters
|
||||
|
||||
# Run tests in specific package
|
||||
go test -v -race ./internal/services
|
||||
|
||||
# Run tests matching pattern
|
||||
go test -v -race -run "TestParser" ./...
|
||||
|
||||
# Test with coverage
|
||||
go test -race -coverprofile=coverage/coverage.out -covermode=atomic ./...
|
||||
go tool cover -html=coverage/coverage.out -o coverage/coverage.html
|
||||
|
||||
# Benchmarks
|
||||
go test -bench=. -benchmem ./...
|
||||
go test -bench=BenchmarkMarkdownExporter ./internal/exporters
|
||||
```
|
||||
|
||||
### Security & Auditing
|
||||
|
||||
```bash
|
||||
task security:check # Run gosec security scanner
|
||||
task security:audit # Run govulncheck for vulnerabilities
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Imports
|
||||
|
||||
- Use `goimports` with local prefix: `github.com/kjanat/articulate-parser`
|
||||
- Order: stdlib, external, internal packages
|
||||
- Group related imports together
|
||||
- Order: stdlib, blank line, external packages, blank line, internal packages
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/fumiama/go-docx"
|
||||
|
||||
"github.com/kjanat/articulate-parser/internal/interfaces"
|
||||
)
|
||||
```
|
||||
|
||||
### Formatting
|
||||
|
||||
- Use `gofmt -s` (simplify) and `gofumpt` with extra rules
|
||||
- Function length: max 100 lines, 50 statements
|
||||
- Cyclomatic complexity: max 15
|
||||
- Cognitive complexity: max 20
|
||||
- Cyclomatic complexity: max 15; Cognitive complexity: max 20
|
||||
|
||||
### Types & Naming
|
||||
|
||||
- Use interface-based design (see `internal/interfaces/`)
|
||||
- Export types/functions with clear godoc comments ending with period
|
||||
- Exported types/functions require godoc comments ending with period
|
||||
- Use descriptive names: `ArticulateParser`, `MarkdownExporter`
|
||||
- Receiver names: short (1-2 chars), consistent per type
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Always wrap errors with context: `fmt.Errorf("operation failed: %w", err)`
|
||||
- Use `%w` verb for error wrapping to preserve error chain
|
||||
- Check all error returns (enforced by `errcheck`)
|
||||
- Document error handling rationale in defer blocks when ignoring close errors
|
||||
|
||||
```go
|
||||
// Good: Error wrapping with context
|
||||
if err := json.Unmarshal(body, &course); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
// Good: Documented defer with error handling
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
p.Logger.Warn("failed to close response body", "error", err)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
- All exported types/functions require godoc comments
|
||||
- End sentences with periods (`godot` linter enforced)
|
||||
- Mark known issues with TODO/FIXME/HACK/BUG/XXX
|
||||
|
||||
### Security
|
||||
- Use `#nosec` with justification for deliberate security exceptions (G304 for CLI file paths, G306 for export file permissions)
|
||||
- Run `gosec` and `govulncheck` for security audits
|
||||
|
||||
- Use `#nosec` with justification for deliberate security exceptions
|
||||
- G304: File paths from CLI args; G306: Export file permissions
|
||||
|
||||
```go
|
||||
// #nosec G304 - File path provided by user via CLI argument
|
||||
data, err := os.ReadFile(filePath)
|
||||
```
|
||||
|
||||
### Testing
|
||||
- Enable race detection: `-race` flag
|
||||
|
||||
- Enable race detection: `-race` flag always
|
||||
- Use table-driven tests where applicable
|
||||
- Mark test helpers with `t.Helper()`
|
||||
- Use `t.TempDir()` for temporary files
|
||||
- Benchmarks in `*_bench_test.go`, examples in `*_example_test.go`
|
||||
- Test naming: `Test<Type>_<Method>` or `Test<Function>`
|
||||
|
||||
```go
|
||||
func TestMarkdownExporter_ProcessItemToMarkdown_AllTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, itemType, expectedText string
|
||||
}{
|
||||
{"text item", "text", ""},
|
||||
{"divider item", "divider", "---"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// test implementation
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
- Minimal external dependencies (currently: go-docx, golang.org/x/net, golang.org/x/text)
|
||||
|
||||
- Minimal external dependencies (go-docx, golang.org/x/net, golang.org/x/text)
|
||||
- Run `task deps:tidy` after adding/removing dependencies
|
||||
- CGO disabled by default (`CGO_ENABLED=0`)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
articulate-parser/
|
||||
internal/
|
||||
config/ # Configuration loading
|
||||
exporters/ # Export implementations (markdown, html, docx)
|
||||
interfaces/ # Core interfaces (Exporter, CourseParser, Logger)
|
||||
models/ # Data models (Course, Lesson, Item, Media)
|
||||
services/ # Core services (parser, html cleaner, app, logger)
|
||||
version/ # Version information
|
||||
main.go # Application entry point
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Creating a new exporter
|
||||
|
||||
1. Implement `interfaces.Exporter` interface
|
||||
2. Add factory method to `internal/exporters/factory.go`
|
||||
3. Register format in `NewFactory()`
|
||||
4. Add tests following existing patterns
|
||||
|
||||
### Adding configuration options
|
||||
|
||||
1. Add field to `Config` struct in `internal/config/config.go`
|
||||
2. Load from environment variable with sensible default
|
||||
3. Document in config struct comments
|
||||
|
||||
@ -226,7 +226,7 @@ docker run --rm ghcr.io/kjanat/articulate-parser:latest --help
|
||||
### Available Tags
|
||||
|
||||
| Tag | Description | Use Case |
|
||||
|-----|-------------|----------|
|
||||
| --------------------- | ------------------------------------------- | ---------------------- |
|
||||
| `latest` | Latest stable release from master branch | Production use |
|
||||
| `edge` | Latest development build from master branch | Testing new features |
|
||||
| `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:
|
||||
|
||||
| Argument | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| ------------ | ------------------------------------- | -------------- |
|
||||
| `VERSION` | Version string embedded in the binary | `dev` |
|
||||
| `BUILD_TIME` | Build timestamp | Current time |
|
||||
| `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
|
||||
[gomod]: go.mod
|
||||
[Issues]: https://github.com/kjanat/articulate-parser/issues
|
||||
|
||||
<!-- [Latest release]: https://github.com/kjanat/articulate-parser/releases/latest -->
|
||||
|
||||
[MIT License]: LICENSE
|
||||
[Package documentation]: https://godoc.org/github.com/kjanat/articulate-parser
|
||||
[Tags]: https://github.com/kjanat/articulate-parser/tags
|
||||
|
||||
70
Taskfile.yml
70
Taskfile.yml
@ -1,7 +1,7 @@
|
||||
# yaml-language-server: $schema=https://taskfile.dev/schema.json
|
||||
# Articulate Parser - Task Automation
|
||||
# https://taskfile.dev
|
||||
version: '3'
|
||||
version: "3"
|
||||
|
||||
# Global output settings
|
||||
output: prefixed
|
||||
@ -47,11 +47,11 @@ vars:
|
||||
|
||||
# Environment variables
|
||||
env:
|
||||
CGO_ENABLED: '{{.CGO_ENABLED}}'
|
||||
CGO_ENABLED: "{{.CGO_ENABLED}}"
|
||||
GO111MODULE: on
|
||||
|
||||
# Load .env files if present
|
||||
dotenv: ['.env', '.env.local']
|
||||
dotenv: [".env", ".env.local"]
|
||||
|
||||
# Task definitions
|
||||
tasks:
|
||||
@ -69,12 +69,12 @@ tasks:
|
||||
interactive: true
|
||||
watch: true
|
||||
sources:
|
||||
- '**/*.go'
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
cmds:
|
||||
- task: build
|
||||
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} --help'
|
||||
- "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} --help"
|
||||
|
||||
# Build tasks
|
||||
build:
|
||||
@ -82,14 +82,14 @@ tasks:
|
||||
aliases: [b]
|
||||
deps: [clean-bin]
|
||||
sources:
|
||||
- '**/*.go'
|
||||
- "**/*.go"
|
||||
- go.mod
|
||||
- go.sum
|
||||
generates:
|
||||
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}}'
|
||||
- "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}}"
|
||||
cmds:
|
||||
- task: mkdir
|
||||
vars: { DIR: '{{.OUTPUT_DIR}}' }
|
||||
vars: { DIR: "{{.OUTPUT_DIR}}" }
|
||||
- go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o {{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} {{.MAIN_FILE}}
|
||||
method: checksum
|
||||
|
||||
@ -99,25 +99,25 @@ tasks:
|
||||
deps: [clean-bin]
|
||||
cmds:
|
||||
- task: mkdir
|
||||
vars: { DIR: '{{.OUTPUT_DIR}}' }
|
||||
vars: { DIR: "{{.OUTPUT_DIR}}" }
|
||||
- for:
|
||||
matrix:
|
||||
GOOS: [linux, darwin, windows]
|
||||
GOARCH: [amd64, arm64]
|
||||
task: build:platform
|
||||
vars:
|
||||
TARGET_GOOS: '{{.ITEM.GOOS}}'
|
||||
TARGET_GOARCH: '{{.ITEM.GOARCH}}'
|
||||
TARGET_GOOS: "{{.ITEM.GOOS}}"
|
||||
TARGET_GOARCH: "{{.ITEM.GOARCH}}"
|
||||
- echo "Built binaries for all platforms in {{.OUTPUT_DIR}}/"
|
||||
|
||||
build:platform:
|
||||
internal: true
|
||||
vars:
|
||||
TARGET_EXT: '{{if eq .TARGET_GOOS "windows"}}.exe{{end}}'
|
||||
OUTPUT_FILE: '{{.OUTPUT_DIR}}/{{.APP_NAME}}-{{.TARGET_GOOS}}-{{.TARGET_GOARCH}}{{.TARGET_EXT}}'
|
||||
OUTPUT_FILE: "{{.OUTPUT_DIR}}/{{.APP_NAME}}-{{.TARGET_GOOS}}-{{.TARGET_GOARCH}}{{.TARGET_EXT}}"
|
||||
env:
|
||||
GOOS: '{{.TARGET_GOOS}}'
|
||||
GOARCH: '{{.TARGET_GOARCH}}'
|
||||
GOOS: "{{.TARGET_GOOS}}"
|
||||
GOARCH: "{{.TARGET_GOARCH}}"
|
||||
cmds:
|
||||
- echo "Building {{.OUTPUT_FILE}}..."
|
||||
- go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o "{{.OUTPUT_FILE}}" {{.MAIN_FILE}}
|
||||
@ -134,6 +134,8 @@ tasks:
|
||||
test:
|
||||
desc: Run all tests
|
||||
aliases: [t]
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
cmds:
|
||||
- go test {{.GO_FLAGS}} -race -timeout {{.TEST_TIMEOUT}} ./...
|
||||
|
||||
@ -141,9 +143,11 @@ tasks:
|
||||
desc: Run tests with coverage report
|
||||
aliases: [cover, cov]
|
||||
deps: [clean-coverage]
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
cmds:
|
||||
- task: mkdir
|
||||
vars: { DIR: '{{.COVERAGE_DIR}}' }
|
||||
vars: { DIR: "{{.COVERAGE_DIR}}" }
|
||||
- go test {{.GO_FLAGS}} -race -coverprofile={{.COVERAGE_DIR}}/coverage.out -covermode=atomic -timeout {{.TEST_TIMEOUT}} ./...
|
||||
- go tool cover -html={{.COVERAGE_DIR}}/coverage.out -o {{.COVERAGE_DIR}}/coverage.html
|
||||
- go tool cover -func={{.COVERAGE_DIR}}/coverage.out
|
||||
@ -152,6 +156,8 @@ tasks:
|
||||
test:verbose:
|
||||
desc: Run tests with verbose output
|
||||
aliases: [tv]
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
cmds:
|
||||
- go test -v -race -timeout {{.TEST_TIMEOUT}} ./...
|
||||
|
||||
@ -160,7 +166,7 @@ tasks:
|
||||
aliases: [tw]
|
||||
watch: true
|
||||
sources:
|
||||
- '**/*.go'
|
||||
- "**/*.go"
|
||||
cmds:
|
||||
- task: test
|
||||
|
||||
@ -172,6 +178,8 @@ tasks:
|
||||
|
||||
test:integration:
|
||||
desc: Run integration tests
|
||||
env:
|
||||
CGO_ENABLED: 1
|
||||
status:
|
||||
- '{{if eq OS "windows"}}if not exist "main_test.go" exit 1{{else}}test ! -f "main_test.go"{{end}}'
|
||||
cmds:
|
||||
@ -352,14 +360,14 @@ tasks:
|
||||
internal: true
|
||||
cmds:
|
||||
- task: rmdir
|
||||
vars: { DIR: '{{.OUTPUT_DIR}}' }
|
||||
vars: { DIR: "{{.OUTPUT_DIR}}" }
|
||||
|
||||
clean-coverage:
|
||||
desc: Remove coverage files
|
||||
internal: true
|
||||
cmds:
|
||||
- task: rmdir
|
||||
vars: { DIR: '{{.COVERAGE_DIR}}' }
|
||||
vars: { DIR: "{{.COVERAGE_DIR}}" }
|
||||
|
||||
clean-cache:
|
||||
desc: Clean Go build and test cache
|
||||
@ -410,10 +418,10 @@ tasks:
|
||||
requires:
|
||||
vars: [VERSION]
|
||||
preconditions:
|
||||
- sh: 'git diff --exit-code'
|
||||
msg: 'Working directory is not clean'
|
||||
- sh: 'git diff --cached --exit-code'
|
||||
msg: 'Staging area is not clean'
|
||||
- sh: "git diff --exit-code"
|
||||
msg: "Working directory is not clean"
|
||||
- sh: "git diff --cached --exit-code"
|
||||
msg: "Staging area is not clean"
|
||||
cmds:
|
||||
- git tag -a v{{.VERSION}} -m "Release v{{.VERSION}}"
|
||||
- echo "Tagged v{{.VERSION}}"
|
||||
@ -498,11 +506,11 @@ tasks:
|
||||
- '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
|
||||
deps: [build]
|
||||
cmds:
|
||||
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json md output-demo.md'
|
||||
- "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json md output-demo.md"
|
||||
- echo "Demo Markdown created{{:}} output-demo.md"
|
||||
- defer:
|
||||
task: rmfile
|
||||
vars: { FILE: 'output-demo.md' }
|
||||
vars: { FILE: "output-demo.md" }
|
||||
|
||||
demo:html:
|
||||
desc: Demo - Convert sample to HTML
|
||||
@ -510,11 +518,11 @@ tasks:
|
||||
- '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
|
||||
deps: [build]
|
||||
cmds:
|
||||
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json html output-demo.html'
|
||||
- "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json html output-demo.html"
|
||||
- echo "Demo HTML created{{:}} output-demo.html"
|
||||
- defer:
|
||||
task: rmfile
|
||||
vars: { FILE: 'output-demo.html' }
|
||||
vars: { FILE: "output-demo.html" }
|
||||
|
||||
demo:docx:
|
||||
desc: Demo - Convert sample to DOCX
|
||||
@ -522,11 +530,11 @@ tasks:
|
||||
- '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
|
||||
deps: [build]
|
||||
cmds:
|
||||
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json docx output-demo.docx'
|
||||
- "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} articulate-sample.json docx output-demo.docx"
|
||||
- echo "Demo DOCX created{{:}} output-demo.docx"
|
||||
- defer:
|
||||
task: rmfile
|
||||
vars: { FILE: 'output-demo.docx' }
|
||||
vars: { FILE: "output-demo.docx" }
|
||||
|
||||
# Performance profiling
|
||||
profile:cpu:
|
||||
@ -536,7 +544,7 @@ tasks:
|
||||
- go tool pprof -http=:8080 cpu.prof
|
||||
- defer:
|
||||
task: rmfile
|
||||
vars: { FILE: 'cpu.prof' }
|
||||
vars: { FILE: "cpu.prof" }
|
||||
|
||||
profile:mem:
|
||||
desc: Run memory profiling
|
||||
@ -545,14 +553,14 @@ tasks:
|
||||
- go tool pprof -http=:8080 mem.prof
|
||||
- defer:
|
||||
task: rmfile
|
||||
vars: { FILE: 'mem.prof' }
|
||||
vars: { FILE: "mem.prof" }
|
||||
|
||||
# Git hooks
|
||||
hooks:install:
|
||||
desc: Install git hooks
|
||||
cmds:
|
||||
- task: mkdir
|
||||
vars: { DIR: '.git/hooks' }
|
||||
vars: { DIR: ".git/hooks" }
|
||||
- '{{if eq OS "windows"}}echo "#!/bin/sh" > .git/hooks/pre-commit && echo "task lint:fmt" >> .git/hooks/pre-commit{{else}}cat > .git/hooks/pre-commit << ''EOF''{{printf "\n"}}#!/bin/sh{{printf "\n"}}task lint:fmt{{printf "\n"}}EOF{{printf "\n"}}chmod +x .git/hooks/pre-commit{{end}}'
|
||||
- echo "Git hooks installed"
|
||||
|
||||
|
||||
8
go.mod
8
go.mod
@ -2,13 +2,15 @@ module github.com/kjanat/articulate-parser
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.25.5
|
||||
|
||||
require (
|
||||
github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b
|
||||
golang.org/x/net v0.46.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/text v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fumiama/imgsz v0.0.4 // indirect
|
||||
golang.org/x/image v0.32.0 // indirect
|
||||
golang.org/x/image v0.34.0 // indirect
|
||||
)
|
||||
|
||||
12
go.sum
12
go.sum
@ -2,9 +2,9 @@ github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b h1:/mxSugRc4SgN7Xg
|
||||
github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b/go.mod h1:ssRF0IaB1hCcKIObp3FkZOsjTcAHpgii70JelNb4H8M=
|
||||
github.com/fumiama/imgsz v0.0.4 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI=
|
||||
github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E=
|
||||
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
|
||||
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
|
||||
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
|
||||
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
|
||||
@ -16,6 +16,13 @@ import (
|
||||
"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.
|
||||
// It converts Articulate Rise course data into a Microsoft Word document
|
||||
// using the go-docx package.
|
||||
@ -53,7 +60,7 @@ func (e *DocxExporter) Export(course *models.Course, outputPath string) error {
|
||||
|
||||
// Add title
|
||||
titlePara := doc.AddParagraph()
|
||||
titlePara.AddText(course.Course.Title).Size("32").Bold()
|
||||
titlePara.AddText(course.Course.Title).Size(docxTitleSize).Bold()
|
||||
|
||||
// Add description if available
|
||||
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) {
|
||||
// Add lesson title
|
||||
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
|
||||
if lesson.Description != "" {
|
||||
@ -132,7 +139,7 @@ func (e *DocxExporter) exportItem(doc *docx.Docx, item *models.Item) {
|
||||
if item.Type != "" {
|
||||
itemPara := doc.AddParagraph()
|
||||
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
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
//go:embed html_styles.css
|
||||
var defaultCSS string
|
||||
|
||||
//go:embed html_template.html
|
||||
//go:embed html_template.gohtml
|
||||
var htmlTemplate string
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
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;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
|
||||
@ -96,7 +96,10 @@ func (e *MarkdownExporter) SupportedFormat() string {
|
||||
func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.Item, level int) {
|
||||
headingPrefix := strings.Repeat("#", level)
|
||||
|
||||
switch item.Type {
|
||||
// Normalize item type to lowercase for consistent matching
|
||||
itemType := strings.ToLower(item.Type)
|
||||
|
||||
switch itemType {
|
||||
case "text":
|
||||
e.processTextItem(buf, item, headingPrefix)
|
||||
case "list":
|
||||
@ -105,7 +108,7 @@ func (e *MarkdownExporter) processItemToMarkdown(buf *bytes.Buffer, item models.
|
||||
e.processMultimediaItem(buf, item, headingPrefix)
|
||||
case "image":
|
||||
e.processImageItem(buf, item, headingPrefix)
|
||||
case "knowledgeCheck":
|
||||
case "knowledgecheck":
|
||||
e.processKnowledgeCheckItem(buf, item, headingPrefix)
|
||||
case "interactive":
|
||||
e.processInteractiveItem(buf, item, headingPrefix)
|
||||
|
||||
@ -15,6 +15,9 @@ import (
|
||||
"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.
|
||||
// It can fetch courses from the Articulate Rise API or load them from local JSON files.
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
if err := json.Unmarshal(body, &course); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`/share/([a-zA-Z0-9_-]+)`)
|
||||
matches := re.FindStringSubmatch(uri)
|
||||
matches := shareIDRegex.FindStringSubmatch(uri)
|
||||
if len(matches) < 2 {
|
||||
return "", fmt.Errorf("could not extract share ID from URI: %s", uri)
|
||||
}
|
||||
|
||||
2
main.go
2
main.go
@ -92,7 +92,7 @@ func run(args []string) int {
|
||||
// Returns:
|
||||
// - true if the string appears to be a URI, false otherwise
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user