7 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
33673d661b fix: set go 1.24.0 minimum with toolchain 1.25.5 2026-01-05 03:31:09 +01:00
41f3f5c4e2 [autofix.ci] apply automated fixes 2026-01-05 02:26:28 +00:00
d644094999 chore: enable CGO for race detection, update deps, drop old Go versions 2026-01-05 03:24:49 +01:00
71d1429048 chore: update actions/checkout to v6, improve AGENTS.md 2026-01-05 03:17:26 +01:00
24 changed files with 634 additions and 789 deletions

42
.dprint.jsonc Normal file
View File

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

View File

@ -45,23 +45,23 @@ Enhancement suggestions are welcome! Please use the feature request template and
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

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

View File

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

View File

@ -2,7 +2,7 @@ name: autofix.ci
on: on:
pull_request: pull_request:
push: push:
branches: [ "master" ] branches: ["master"]
permissions: permissions:
contents: read contents: read
@ -11,18 +11,18 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Install Task - name: Install Task
uses: go-task/setup-task@v1 uses: go-task/setup-task@v1
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: { go-version-file: 'go.mod' } with: { go-version-file: "go.mod" }
- name: Setup go deps - name: Setup go deps
run: | run: |
# Install golangci-lint # Install golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b "$(go env GOPATH)/bin" curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin
# Install go-task dependencies # Install go-task dependencies
go install golang.org/x/tools/cmd/goimports@latest go install golang.org/x/tools/cmd/goimports@latest

View File

@ -2,7 +2,7 @@ name: CI
on: on:
push: push:
branches: ['master', 'develop'] branches: ["master", "develop"]
pull_request: pull_request:
env: env:
@ -21,12 +21,12 @@ jobs:
contents: read contents: read
pull-requests: read pull-requests: read
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version: stable go-version: stable
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v9
with: { version: latest } with: { version: latest }
test: test:
@ -42,7 +42,7 @@ jobs:
- 1.25.x - 1.25.x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v6
- name: Set up Go ${{ matrix.go }} - name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@ -83,9 +83,9 @@ jobs:
# Extract test results for summary # Extract test results for summary
TEST_STATUS=$? TEST_STATUS=$?
TOTAL_TESTS=$(grep -c "=== RUN" test-output.log || echo "0") TOTAL_TESTS=$(grep -c "=== RUN" test-output.log || echo "0")
PASSED_TESTS=$(grep -c -- "--- PASS:" test-output.log || echo "0") PASSED_TESTS=$(grep -c "--- PASS:" test-output.log || echo "0")
FAILED_TESTS=$(grep -c -- "--- FAIL:" test-output.log || echo "0") FAILED_TESTS=$(grep -c "--- FAIL:" test-output.log || echo "0")
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
{ {
@ -106,31 +106,31 @@ 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 |" 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 |" echo "| $pkg | ❌ FAIL |"
fi fi
done done
echo "" 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 << 'EOF' cat << 'EOF'
### ❌ Failed Tests Details ### ❌ Failed Tests Details
``` ```
EOF EOF
grep -A 10 -- "--- FAIL:" test-output.log | head -100 grep -A 10 -- "--- FAIL:" test-output.log | head -100
cat << 'EOF' cat << 'EOF'
``` ```
EOF EOF
fi fi
} >> "$GITHUB_STEP_SUMMARY" } >> "$GITHUB_STEP_SUMMARY"
# Set outputs for other steps # Set outputs for other steps
@ -211,7 +211,7 @@ jobs:
- name: Upload test artifacts - name: Upload test artifacts
if: failure() if: failure()
uses: actions/upload-artifact@v5 uses: actions/upload-artifact@v6
with: with:
name: test-results-go-${{ matrix.go }} name: test-results-go-${{ matrix.go }}
path: | path: |
@ -221,7 +221,6 @@ jobs:
- name: Run linters - name: Run linters
run: | run: |
# Initialize summary
{ {
cat << EOF cat << EOF
## 🔍 Static Analysis (Go ${{ matrix.go }}) ## 🔍 Static Analysis (Go ${{ matrix.go }})
@ -304,7 +303,7 @@ jobs:
contents: read contents: read
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
@ -351,10 +350,10 @@ jobs:
contents: read contents: read
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
steps: steps:
- name: 'Checkout Repository' - name: "Checkout Repository"
uses: actions/checkout@v5 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
@ -366,14 +365,14 @@ jobs:
permissions: permissions:
contents: read contents: read
packages: write packages: write
needs: [test] 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' ||
startsWith(github.ref, 'refs/heads/feature/docker')) startsWith(github.ref, 'refs/heads/feature/docker'))
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3

View File

@ -17,7 +17,7 @@ name: "CodeQL"
on: on:
workflow_call: workflow_call:
schedule: schedule:
- cron: '44 16 * * 6' - cron: "44 16 * * 6"
# push: # push:
# branches: [ "master" ] # branches: [ "master" ]
# pull_request: # pull_request:
@ -47,58 +47,58 @@ jobs:
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@v5 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@v5 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

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
@ -82,13 +82,13 @@ 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
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3

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:

View File

@ -9,7 +9,7 @@ repos:
exclude: '^\.github/ISSUE_TEMPLATE/.*\.yml$' exclude: '^\.github/ISSUE_TEMPLATE/.*\.yml$'
- id: end-of-file-fixer - id: end-of-file-fixer
- id: mixed-line-ending - id: mixed-line-ending
args: ['--fix=lf'] args: ["--fix=lf"]
# File validation # File validation
- id: check-yaml - id: check-yaml
@ -23,7 +23,7 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-case-conflict - id: check-case-conflict
- id: no-commit-to-branch - id: no-commit-to-branch
args: ['--branch=master', '--branch=main'] args: ["--branch=master", "--branch=main"]
# File structure # File structure
- id: check-added-large-files - id: check-added-large-files
@ -31,16 +31,16 @@ repos:
- id: check-executables-have-shebangs - id: check-executables-have-shebangs
- repo: local - repo: local
hooks: hooks:
- id: actionlint - id: actionlint
name: Lint GitHub Actions workflow files name: Lint GitHub Actions workflow files
description: Runs actionlint to lint GitHub Actions workflow files description: Runs actionlint to lint GitHub Actions workflow files
language: golang language: golang
types: ["yaml"] types: ["yaml"]
files: ^\.github/workflows/ files: ^\.github/workflows/
entry: actionlint entry: actionlint
minimum_pre_commit_version: 3.0.0 minimum_pre_commit_version: 3.0.0
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.6.1 rev: v2.7.2
hooks: hooks:
- id: golangci-lint - id: golangci-lint
name: golangci-lint name: golangci-lint

502
AGENTS.md
View File

@ -1,36 +1,87 @@
# Agent Guidelines for articulate-parser # 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/Test Commands
- **Build**: `task build` or `go build -o bin/articulate-parser main.go` ### Primary Commands (using Taskfile)
- **Run tests**: `task test` or `go test -race -timeout 5m ./...`
- **Run single test**: `go test -v -race -run ^TestName$ ./path/to/package` ```bash
- **Test with coverage**: task build # Build binary to bin/articulate-parser
- `task test:coverage` or task test # Run all tests with race detection
- `go test -race -coverprofile=coverage/coverage.out -covermode=atomic ./...` task lint # Run all linters (vet, fmt, staticcheck, golangci-lint)
- **Lint**: `task lint` (runs vet, fmt check, staticcheck, golangci-lint) task fmt # Format all Go files
- **Format**: `task fmt` or `gofmt -s -w .` task ci # Full CI pipeline: deps, lint, test with coverage, build
- **CI checks**: `task ci` (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 ## Code Style Guidelines
### Imports ### Imports
- Use `goimports` with local prefix: `github.com/kjanat/articulate-parser` - Use `goimports` with local prefix: `github.com/kjanat/articulate-parser`
- Order: stdlib, external, internal packages - Order: stdlib, blank line, external packages, blank line, internal packages
- Group related imports together
```go
import (
"context"
"fmt"
"github.com/fumiama/go-docx"
"github.com/kjanat/articulate-parser/internal/interfaces"
)
```
### Formatting ### Formatting
- Use `gofmt -s` (simplify) and `gofumpt` with extra rules - Use `gofmt -s` (simplify) and `gofumpt` with extra rules
- Function length: max 100 lines, 50 statements - Function length: max 100 lines, 50 statements
- Cyclomatic complexity: max 15 - Cyclomatic complexity: max 15; Cognitive complexity: max 20
- Cognitive complexity: max 20
### Types & Naming ### Types & Naming
- Use interface-based design (see `internal/interfaces/`) - 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` - Use descriptive names: `ArticulateParser`, `MarkdownExporter`
- Receiver names: short (1-2 chars), consistent per type - Receiver names: short (1-2 chars), consistent per type
@ -41,6 +92,20 @@
- Check all error returns (enforced by `errcheck`) - Check all error returns (enforced by `errcheck`)
- Document error handling rationale in defer blocks when ignoring close errors - 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 ### Comments
- All exported types/functions require godoc comments - All exported types/functions require godoc comments
@ -49,379 +114,70 @@
### Security ### Security
- Use `#nosec` with justification for deliberate security exceptions (G304 for CLI file paths, G306 for export file permissions) - Use `#nosec` with justification for deliberate security exceptions
- Run `gosec` and `govulncheck` for security audits - 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 ### Testing
- Enable race detection: `-race` flag - Enable race detection: `-race` flag always
- Use table-driven tests where applicable - Use table-driven tests where applicable
- Mark test helpers with `t.Helper()` - Mark test helpers with `t.Helper()`
- Use `t.TempDir()` for temporary files
- Benchmarks in `*_bench_test.go`, examples in `*_example_test.go` - 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 ### 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 - Run `task deps:tidy` after adding/removing dependencies
- CGO disabled by default (`CGO_ENABLED=0`)
--- ## Project Structure
## Go 1.24 & 1.25 New Features Reference ```
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
```
This project uses Go 1.24+. Below is a comprehensive summary of new features and changes in Go 1.24 and 1.25 that may be relevant for development and maintenance. ## Common Patterns
### Go 1.24 Major Changes (Released February 2025) ### Creating a new exporter
#### Language Features 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
**Generic Type Aliases** ### Adding configuration options
- Type aliases can now be parameterized with type parameters 1. Add field to `Config` struct in `internal/config/config.go`
- Example: `type List[T any] = []T` 2. Load from environment variable with sensible default
- Can be disabled via `GOEXPERIMENT=noaliastypeparams` (removed in 1.25) 3. Document in config struct comments
#### Tooling Enhancements
**Module Tool Dependencies**
- New `tool` directive in go.mod tracks executable dependencies
- Use `go get -tool <package>` to add tool dependencies
- Use `go install tool` and `go get tool` to manage them
- Eliminates need for blank imports in `tools.go` files
**Build Output Formatting**
- Both `go build` and `go test` support `-json` flag for structured JSON output
- New action types distinguish build output from test results
**Authentication**
- New `GOAUTH` environment variable provides flexible authentication for private modules
**Automatic Version Tracking**
- `go build` automatically sets main module version in binaries based on VCS tags
- Adds `+dirty` suffix for uncommitted changes
**Cgo Performance Improvements**
- New `#cgo noescape` annotation: Prevents escape analysis overhead for C function calls
- New `#cgo nocallback` annotation: Indicates C function won't call back to Go
**Toolchain Tracing**
- `GODEBUG=toolchaintrace=1` enables tracing of toolchain selection
#### Runtime & Performance
**Performance Improvements**
- **2-3% CPU overhead reduction** across benchmark suites
- New Swiss Tables-based map implementation (faster lookups)
- Disable via `GOEXPERIMENT=noswissmap`
- More efficient small object allocation
- Redesigned runtime-internal mutexes
- Disable via `GOEXPERIMENT=nospinbitmutex`
#### Compiler & Linker
**Method Receiver Restrictions**
- Methods on cgo-generated types now prevented (both directly and through aliases)
**Build IDs**
- Linkers generate GNU build IDs (ELF) and UUIDs (macOS) by default
- Disable via `-B none` flag
#### Standard Library Additions
**File System Safety - `os.Root`**
- New `os.Root` type enables directory-limited operations
- Prevents path escape and symlink breakouts
- Essential for sandboxed file operations
**Cryptography Expansion**
- `crypto/mlkem`: ML-KEM-768/1024 post-quantum key exchange (FIPS 203)
- `crypto/hkdf`: HMAC-based Extract-and-Expand KDF (RFC 5869)
- `crypto/pbkdf2`: Password-based key derivation (RFC 8018)
- `crypto/sha3`: SHA-3 and SHAKE functions (FIPS 202)
**FIPS 140-3 Support**
- New `GOFIPS140` environment variable enables FIPS mode
- New `fips140` GODEBUG setting for cryptographic module compliance
**Weak References - `weak` Package**
- New `weak` package provides low-level weak pointers
- Enables memory-efficient structures like weak maps and caches
- Useful for preventing memory leaks in cache implementations
**Testing Improvements**
- `testing.B.Loop()`: Cleaner syntax replacing manual `b.N` iteration
- Prevents compiler from optimizing away benchmarked code
- New `testing/synctest` package (experimental) for testing concurrent code with fake clocks
**Iterator Support**
- Multiple packages now offer iterator-returning variants:
- `bytes`: Iterator-based functions
- `strings`: Iterator-based functions
- `go/types`: Iterator support
#### Security Enhancements
**TLS Post-Quantum Cryptography**
- `X25519MLKEM768` hybrid key exchange enabled by default in TLS
- Provides quantum-resistant security
**Encrypted Client Hello (ECH)**
- TLS servers can enable ECH via `Config.EncryptedClientHelloKeys`
- Protects client identity during TLS handshake
**RSA Key Validation**
- Keys smaller than 1024 bits now rejected by default
- Use `GODEBUG=rsa1024min=0` to revert (testing only)
**Constant-Time Execution**
- New `crypto/subtle.WithDataIndependentTiming()` enables architecture-specific timing guarantees
- Helps prevent timing attacks
#### Deprecations & Removals
- `runtime.GOROOT()`: Deprecated; use system path instead
- `crypto/cipher` OFB/CFB modes: Deprecated (unauthenticated encryption)
- `x509sha1` GODEBUG: Removed; SHA-1 certificates no longer verified
- Experimental `X25519Kyber768Draft00`: Removed
#### Platform Changes
- **Linux**: Now requires kernel 3.2+ (enforced)
- **macOS**: Go 1.24 is final release supporting Big Sur
- **Windows/ARM 32-bit**: Marked broken
- **WebAssembly**:
- New `go:wasmexport` directive
- Reactor/library builds supported via `-buildmode=c-shared`
#### Bootstrap Requirements
- Go 1.24 requires Go 1.22.6+ for bootstrapping
- Go 1.26 will require Go 1.24+
---
### Go 1.25 Major Changes (Released August 2025)
#### Language Changes
- No breaking language changes
- "Core types" concept removed from specification (replaced with clearer prose)
#### Tooling Improvements
**Go Command Enhancements**
- `go build -asan`: Now defaults to leak detection at program exit
- New `go.mod ignore` directive: Specify directories for go command to ignore
- `go doc -http`: Starts documentation server and opens in browser
- `go version -m -json`: Prints JSON-encoded BuildInfo structures
- Module path resolution now supports subdirectories using `<meta>` syntax
- New `work` package pattern matches all packages in work/workspace modules
- Removed automatic toolchain line additions when updating `go` version
**Vet Analyzers**
- **"waitgroup"**: Detects misplaced `sync.WaitGroup.Add` calls
- **"hostport"**: Warns against using `fmt.Sprintf` for constructing addresses
- Recommends `net.JoinHostPort` instead
#### Runtime Enhancements
**Container-Aware GOMAXPROCS**
- Linux now respects cgroup CPU bandwidth limits
- All OSes periodically update GOMAXPROCS if CPU availability changes
- Disable via environment variables or GODEBUG settings
- Critical for containerized applications
**New Garbage Collector - "Green Tea GC"**
- Experimental `GOEXPERIMENT=greenteagc` enables new GC
- **10-40% reduction in garbage collection overhead**
- Significant for GC-sensitive applications
**Trace Flight Recorder**
- New `runtime/trace.FlightRecorder` API
- Captures execution traces in in-memory ring buffer
- Essential for debugging rare events and production issues
**Other Runtime Changes**
- Simplified unhandled panic output
- VMA names on Linux identify memory purpose (debugging aid)
- New `SetDefaultGOMAXPROCS` function resets GOMAXPROCS to defaults
#### Compiler Fixes & Improvements
**Critical Nil Pointer Bug Fix**
- Fixed Go 1.21 regression where nil pointer checks were incorrectly delayed
- ⚠️ **May cause previously passing code to now panic** (correct behavior)
- Review code for assumptions about delayed nil checks
**DWARF5 Support**
- Debug information now uses DWARF version 5
- Reduces binary size and linking time
- Better debugging experience
**Faster Slices**
- Expanded stack allocation for slice backing stores
- Improved slice performance
#### Linker
- New `-funcalign=N` option specifies function entry alignment
#### Standard Library Highlights
**New Packages**
1. **`testing/synctest`** (Promoted from Experimental)
- Concurrent code testing with virtualized time
- Control time progression in tests
- Essential for testing time-dependent concurrent code
2. **`encoding/json/v2`** (Experimental)
- **Substantially better decoding performance**
- Improved API design
- Backward compatible with v1
**Major Package Updates**
| Package | Key Changes |
|---------|------------|
| `crypto` | New `MessageSigner` interface and `SignMessage` function |
| `crypto/ecdsa` | New raw key parsing/serialization functions |
| `crypto/rsa` | **Key generation now 3x faster** |
| `crypto/sha1` | **Hashing 2x faster on amd64 with SHA-NI** |
| `crypto/tls` | New `CurveID` field; SHA-1 algorithms disallowed in TLS 1.2 |
| `net` | Windows now supports file-to-connection conversion; IPv6 multicast improvements |
| `net/http` | **New `CrossOriginProtection` middleware for CSRF defense** |
| `os` | Windows async I/O support; `Root` type expanded with 12 new methods |
| `sync` | **New `WaitGroup.Go` method for convenient goroutine creation** |
| `testing` | New `Attr`, `Output` methods; `AllocsPerRun` panics with parallel tests |
| `unique` | More eager and parallel reclamation of interned values |
#### Performance Notes
**Performance Improvements**
- ECDSA and Ed25519 signing **4x faster** in FIPS 140-3 mode
- SHA3 hashing **2x faster** on Apple M processors
- AMD64 fused multiply-add instructions in v3+ mode
- ⚠️ **Changes floating-point results** (within IEEE 754 spec)
**Performance Regressions**
- SHA-1, SHA-256, SHA-512 slower without AVX2
- Most servers post-2015 support AVX2
#### Platform Changes
- **macOS**: Requires version 12 Monterey or later
- **Windows**: 32-bit windows/arm port marked for removal in Go 1.26
- **Loong64**: Race detector now supported
- **RISC-V**:
- Plugin build mode support
- New `GORISCV64=rva23u64` environment variable value
#### Deprecations
- `go/ast` functions: `FilterPackage`, `PackageExports`, `MergePackageFiles`
- `go/parser.ParseDir` function
- Old `testing/synctest` API (when `GOEXPERIMENT=synctest` set)
---
### Actionable Recommendations for This Project
#### Immediate Opportunities
1. **Replace `sync.WaitGroup` patterns with `WaitGroup.Go()`** (Go 1.25)
```go
// Old pattern
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
// New pattern (Go 1.25)
wg.Go(func() {
// work
})
```
2. **Use `testing.B.Loop()` in benchmarks** (Go 1.24)
```go
// Old pattern
for i := 0; i < b.N; i++ {
// benchmark code
}
// New pattern (Go 1.24)
for b.Loop() {
// benchmark code
}
```
3. **Consider `os.Root` for file operations** (Go 1.24)
- Prevents path traversal vulnerabilities
- Safer for user-provided file paths
4. **Enable Green Tea GC for testing** (Go 1.25)
- Test with `GOEXPERIMENT=greenteagc`
- May reduce GC overhead by 10-40%
5. **Leverage container-aware GOMAXPROCS** (Go 1.25)
- No changes needed; automatic in containers
- Improves resource utilization
6. **Review floating-point operations** (Go 1.25)
- AMD64 v3+ uses FMA instructions
- May change floating-point results (within spec)
7. **Watch nil pointer checks** (Go 1.25)
- Compiler bug fix may expose latent nil pointer bugs
- Review crash reports carefully
#### Future Considerations
1. **Evaluate `encoding/json/v2`** when stable
- Better performance for JSON operations
- Currently experimental in Go 1.25
2. **Adopt tool directives** in go.mod
- Cleaner dependency management for build tools
- Remove `tools.go` if present
3. **Enable FIPS mode if required**
- Use `GOFIPS140=1` for compliance
- Performance improvements in Go 1.25 (4x faster signing)
4. **Use `runtime/trace.FlightRecorder`** for production debugging
- Capture traces of rare events
- Minimal overhead when not triggered

View File

@ -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
@ -441,12 +441,12 @@ The parser includes error handling for:
Potential improvements could include: Potential improvements could include:
- [ ] PDF export support - [ ] PDF export support
- [ ] Media file downloading - [ ] Media file downloading
- [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

@ -1,7 +1,7 @@
# yaml-language-server: $schema=https://taskfile.dev/schema.json # yaml-language-server: $schema=https://taskfile.dev/schema.json
# Articulate Parser - Task Automation # Articulate Parser - Task Automation
# https://taskfile.dev # https://taskfile.dev
version: '3' version: "3"
# Global output settings # Global output settings
output: prefixed output: prefixed
@ -47,11 +47,11 @@ vars:
# Environment variables # Environment variables
env: env:
CGO_ENABLED: '{{.CGO_ENABLED}}' CGO_ENABLED: "{{.CGO_ENABLED}}"
GO111MODULE: on GO111MODULE: on
# Load .env files if present # Load .env files if present
dotenv: ['.env', '.env.local'] dotenv: [".env", ".env.local"]
# Task definitions # Task definitions
tasks: tasks:
@ -69,12 +69,12 @@ tasks:
interactive: true interactive: true
watch: true watch: true
sources: sources:
- '**/*.go' - "**/*.go"
- go.mod - go.mod
- go.sum - go.sum
cmds: cmds:
- task: build - task: build
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} --help' - "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} --help"
# Build tasks # Build tasks
build: build:
@ -82,14 +82,14 @@ tasks:
aliases: [b] aliases: [b]
deps: [clean-bin] deps: [clean-bin]
sources: sources:
- '**/*.go' - "**/*.go"
- go.mod - go.mod
- go.sum - go.sum
generates: generates:
- '{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}}' - "{{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}}"
cmds: cmds:
- task: mkdir - 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}} - go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o {{.OUTPUT_DIR}}/{{.APP_NAME}}{{.EXE_EXT}} {{.MAIN_FILE}}
method: checksum method: checksum
@ -99,25 +99,25 @@ tasks:
deps: [clean-bin] deps: [clean-bin]
cmds: cmds:
- task: mkdir - task: mkdir
vars: { DIR: '{{.OUTPUT_DIR}}' } vars: { DIR: "{{.OUTPUT_DIR}}" }
- for: - for:
matrix: matrix:
GOOS: [linux, darwin, windows] GOOS: [linux, darwin, windows]
GOARCH: [amd64, arm64] GOARCH: [amd64, arm64]
task: build:platform task: build:platform
vars: vars:
TARGET_GOOS: '{{.ITEM.GOOS}}' TARGET_GOOS: "{{.ITEM.GOOS}}"
TARGET_GOARCH: '{{.ITEM.GOARCH}}' TARGET_GOARCH: "{{.ITEM.GOARCH}}"
- echo "Built binaries for all platforms in {{.OUTPUT_DIR}}/" - echo "Built binaries for all platforms in {{.OUTPUT_DIR}}/"
build:platform: build:platform:
internal: true internal: true
vars: vars:
TARGET_EXT: '{{if eq .TARGET_GOOS "windows"}}.exe{{end}}' 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: env:
GOOS: '{{.TARGET_GOOS}}' GOOS: "{{.TARGET_GOOS}}"
GOARCH: '{{.TARGET_GOARCH}}' GOARCH: "{{.TARGET_GOARCH}}"
cmds: cmds:
- echo "Building {{.OUTPUT_FILE}}..." - echo "Building {{.OUTPUT_FILE}}..."
- go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o "{{.OUTPUT_FILE}}" {{.MAIN_FILE}} - go build {{.GO_FLAGS}} -ldflags="{{.LDFLAGS}}" -o "{{.OUTPUT_FILE}}" {{.MAIN_FILE}}
@ -134,6 +134,8 @@ tasks:
test: test:
desc: Run all tests desc: Run all tests
aliases: [t] aliases: [t]
env:
CGO_ENABLED: 1
cmds: cmds:
- go test {{.GO_FLAGS}} -race -timeout {{.TEST_TIMEOUT}} ./... - go test {{.GO_FLAGS}} -race -timeout {{.TEST_TIMEOUT}} ./...
@ -141,9 +143,11 @@ tasks:
desc: Run tests with coverage report desc: Run tests with coverage report
aliases: [cover, cov] aliases: [cover, cov]
deps: [clean-coverage] deps: [clean-coverage]
env:
CGO_ENABLED: 1
cmds: cmds:
- task: mkdir - 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 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 -html={{.COVERAGE_DIR}}/coverage.out -o {{.COVERAGE_DIR}}/coverage.html
- go tool cover -func={{.COVERAGE_DIR}}/coverage.out - go tool cover -func={{.COVERAGE_DIR}}/coverage.out
@ -152,6 +156,8 @@ tasks:
test:verbose: test:verbose:
desc: Run tests with verbose output desc: Run tests with verbose output
aliases: [tv] aliases: [tv]
env:
CGO_ENABLED: 1
cmds: cmds:
- go test -v -race -timeout {{.TEST_TIMEOUT}} ./... - go test -v -race -timeout {{.TEST_TIMEOUT}} ./...
@ -160,7 +166,7 @@ tasks:
aliases: [tw] aliases: [tw]
watch: true watch: true
sources: sources:
- '**/*.go' - "**/*.go"
cmds: cmds:
- task: test - task: test
@ -172,6 +178,8 @@ tasks:
test:integration: test:integration:
desc: Run integration tests desc: Run integration tests
env:
CGO_ENABLED: 1
status: status:
- '{{if eq OS "windows"}}if not exist "main_test.go" exit 1{{else}}test ! -f "main_test.go"{{end}}' - '{{if eq OS "windows"}}if not exist "main_test.go" exit 1{{else}}test ! -f "main_test.go"{{end}}'
cmds: cmds:
@ -202,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)
@ -296,33 +304,33 @@ 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
aliases: [dr] aliases: [dr]
deps: ['docker:build'] deps: [docker:build]
cmds: cmds:
- docker run --rm {{.APP_NAME}}:{{.VERSION}} --help - docker run --rm {{.APP_NAME}}:{{.VERSION}} --help
docker:test: docker:test:
desc: Test Docker image desc: Test Docker image
deps: ['docker:build'] deps: [docker:build]
cmds: cmds:
- docker run --rm {{.APP_NAME}}:{{.VERSION}} --version - docker run --rm {{.APP_NAME}}:{{.VERSION}} --version
- echo "Docker image tested successfully" - echo "Docker image tested successfully"
@ -352,14 +360,14 @@ tasks:
internal: true internal: true
cmds: cmds:
- task: rmdir - task: rmdir
vars: { DIR: '{{.OUTPUT_DIR}}' } vars: { DIR: "{{.OUTPUT_DIR}}" }
clean-coverage: clean-coverage:
desc: Remove coverage files desc: Remove coverage files
internal: true internal: true
cmds: cmds:
- task: rmdir - task: rmdir
vars: { DIR: '{{.COVERAGE_DIR}}' } vars: { DIR: "{{.COVERAGE_DIR}}" }
clean-cache: clean-cache:
desc: Clean Go build and test cache desc: Clean Go build and test cache
@ -410,15 +418,15 @@ tasks:
requires: requires:
vars: [VERSION] vars: [VERSION]
preconditions: preconditions:
- sh: 'git diff --exit-code' - sh: "git diff --exit-code"
msg: 'Working directory is not clean' msg: "Working directory is not clean"
- sh: 'git diff --cached --exit-code' - sh: "git diff --cached --exit-code"
msg: 'Staging area is not clean' msg: "Staging area is not clean"
cmds: cmds:
- 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:
@ -434,7 +442,7 @@ tasks:
docs:coverage: docs:coverage:
desc: Open coverage report in browser desc: Open coverage report in browser
deps: ['test:coverage'] deps: [test:coverage]
cmds: cmds:
- '{{if eq OS "darwin"}}open {{.COVERAGE_DIR}}/coverage.html{{else if eq OS "windows"}}start {{.COVERAGE_DIR}}/coverage.html{{else}}xdg-open {{.COVERAGE_DIR}}/coverage.html 2>/dev/null || echo "Please open {{.COVERAGE_DIR}}/coverage.html in your browser"{{end}}' - '{{if eq OS "darwin"}}open {{.COVERAGE_DIR}}/coverage.html{{else if eq OS "windows"}}start {{.COVERAGE_DIR}}/coverage.html{{else}}xdg-open {{.COVERAGE_DIR}}/coverage.html 2>/dev/null || echo "Please open {{.COVERAGE_DIR}}/coverage.html in your browser"{{end}}'
@ -498,11 +506,11 @@ tasks:
- '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}' - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
deps: [build] deps: [build]
cmds: 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" - echo "Demo Markdown created{{:}} output-demo.md"
- defer: - defer:
task: rmfile task: rmfile
vars: { FILE: 'output-demo.md' } vars: { FILE: "output-demo.md" }
demo:html: demo:html:
desc: Demo - Convert sample to 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}}' - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
deps: [build] deps: [build]
cmds: 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" - echo "Demo HTML created{{:}} output-demo.html"
- defer: - defer:
task: rmfile task: rmfile
vars: { FILE: 'output-demo.html' } vars: { FILE: "output-demo.html" }
demo:docx: demo:docx:
desc: Demo - Convert sample to 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}}' - '{{if eq OS "windows"}}if not exist "articulate-sample.json" exit 1{{else}}test ! -f "articulate-sample.json"{{end}}'
deps: [build] deps: [build]
cmds: 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" - echo "Demo DOCX created{{:}} output-demo.docx"
- defer: - defer:
task: rmfile task: rmfile
vars: { FILE: 'output-demo.docx' } vars: { FILE: "output-demo.docx" }
# Performance profiling # Performance profiling
profile:cpu: profile:cpu:
@ -536,7 +544,7 @@ tasks:
- go tool pprof -http=:8080 cpu.prof - go tool pprof -http=:8080 cpu.prof
- defer: - defer:
task: rmfile task: rmfile
vars: { FILE: 'cpu.prof' } vars: { FILE: "cpu.prof" }
profile:mem: profile:mem:
desc: Run memory profiling desc: Run memory profiling
@ -545,14 +553,14 @@ tasks:
- go tool pprof -http=:8080 mem.prof - go tool pprof -http=:8080 mem.prof
- defer: - defer:
task: rmfile task: rmfile
vars: { FILE: 'mem.prof' } vars: { FILE: "mem.prof" }
# Git hooks # Git hooks
hooks:install: hooks:install:
desc: Install git hooks desc: Install git hooks
cmds: cmds:
- task: mkdir - 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}}' - '{{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" - echo "Git hooks installed"

8
go.mod
View File

@ -2,13 +2,15 @@ module github.com/kjanat/articulate-parser
go 1.24.0 go 1.24.0
toolchain go1.25.5
require ( require (
github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b github.com/fumiama/go-docx v0.0.0-20250506085032-0c30fd09304b
golang.org/x/net v0.46.0 golang.org/x/net v0.48.0
golang.org/x/text v0.30.0 golang.org/x/text v0.32.0
) )
require ( require (
github.com/fumiama/imgsz v0.0.4 // indirect 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
View File

@ -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/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 h1:Lsasu2hdSSFS+vnD+nvR1UkiRMK7hcpyYCC0FzgSMFI=
github.com/fumiama/imgsz v0.0.4/go.mod h1:bISOQVTlw9sRytPwe8ir7tAaEmyz9hSNj9n8mXMBG0E= 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.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=

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

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

View File

@ -4,8 +4,9 @@ Course description
## Course Information ## Course Information
- **Course ID**: - **Course ID**:
- **Share ID**: example-id - **Share ID**: example-id
- **Navigation Mode**: - **Navigation Mode**:
--- ---

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.