pylearnspec 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pylearnspec-0.4.0/.gitignore +15 -0
- pylearnspec-0.4.0/PKG-INFO +144 -0
- pylearnspec-0.4.0/README.md +118 -0
- pylearnspec-0.4.0/pyproject.toml +45 -0
- pylearnspec-0.4.0/setup.cfg +4 -0
- pylearnspec-0.4.0/src/pylearnspec/__init__.py +50 -0
- pylearnspec-0.4.0/src/pylearnspec/badgemd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/badgemd/models.py +28 -0
- pylearnspec-0.4.0/src/pylearnspec/badgemd/parser.py +67 -0
- pylearnspec-0.4.0/src/pylearnspec/badgemd/validator.py +59 -0
- pylearnspec-0.4.0/src/pylearnspec/certmd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/certmd/models.py +28 -0
- pylearnspec-0.4.0/src/pylearnspec/certmd/parser.py +65 -0
- pylearnspec-0.4.0/src/pylearnspec/certmd/validator.py +72 -0
- pylearnspec-0.4.0/src/pylearnspec/common/__init__.py +0 -0
- pylearnspec-0.4.0/src/pylearnspec/common/directives.py +130 -0
- pylearnspec-0.4.0/src/pylearnspec/common/frontmatter.py +32 -0
- pylearnspec-0.4.0/src/pylearnspec/common/validation.py +22 -0
- pylearnspec-0.4.0/src/pylearnspec/diagrammd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/diagrammd/models.py +43 -0
- pylearnspec-0.4.0/src/pylearnspec/diagrammd/parser.py +94 -0
- pylearnspec-0.4.0/src/pylearnspec/diagrammd/validator.py +42 -0
- pylearnspec-0.4.0/src/pylearnspec/flashmd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/flashmd/models.py +42 -0
- pylearnspec-0.4.0/src/pylearnspec/flashmd/parser.py +74 -0
- pylearnspec-0.4.0/src/pylearnspec/flashmd/validator.py +56 -0
- pylearnspec-0.4.0/src/pylearnspec/glossarymd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/glossarymd/models.py +42 -0
- pylearnspec-0.4.0/src/pylearnspec/glossarymd/parser.py +109 -0
- pylearnspec-0.4.0/src/pylearnspec/glossarymd/validator.py +51 -0
- pylearnspec-0.4.0/src/pylearnspec/learnmd/__init__.py +4 -0
- pylearnspec-0.4.0/src/pylearnspec/learnmd/models.py +84 -0
- pylearnspec-0.4.0/src/pylearnspec/learnmd/parser.py +156 -0
- pylearnspec-0.4.0/src/pylearnspec/learnmd/validator.py +79 -0
- pylearnspec-0.4.0/src/pylearnspec/mediamd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/mediamd/models.py +35 -0
- pylearnspec-0.4.0/src/pylearnspec/mediamd/parser.py +74 -0
- pylearnspec-0.4.0/src/pylearnspec/mediamd/validator.py +62 -0
- pylearnspec-0.4.0/src/pylearnspec/nuggetmd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/nuggetmd/models.py +135 -0
- pylearnspec-0.4.0/src/pylearnspec/nuggetmd/parser.py +272 -0
- pylearnspec-0.4.0/src/pylearnspec/nuggetmd/validator.py +170 -0
- pylearnspec-0.4.0/src/pylearnspec/quizmd/__init__.py +4 -0
- pylearnspec-0.4.0/src/pylearnspec/quizmd/models.py +80 -0
- pylearnspec-0.4.0/src/pylearnspec/quizmd/parser.py +277 -0
- pylearnspec-0.4.0/src/pylearnspec/quizmd/validator.py +82 -0
- pylearnspec-0.4.0/src/pylearnspec/trackmd/__init__.py +6 -0
- pylearnspec-0.4.0/src/pylearnspec/trackmd/models.py +40 -0
- pylearnspec-0.4.0/src/pylearnspec/trackmd/parser.py +66 -0
- pylearnspec-0.4.0/src/pylearnspec/trackmd/validator.py +64 -0
- pylearnspec-0.4.0/src/pylearnspec.egg-info/PKG-INFO +144 -0
- pylearnspec-0.4.0/src/pylearnspec.egg-info/SOURCES.txt +61 -0
- pylearnspec-0.4.0/src/pylearnspec.egg-info/dependency_links.txt +1 -0
- pylearnspec-0.4.0/src/pylearnspec.egg-info/requires.txt +5 -0
- pylearnspec-0.4.0/src/pylearnspec.egg-info/top_level.txt +1 -0
- pylearnspec-0.4.0/tests/__init__.py +0 -0
- pylearnspec-0.4.0/tests/fixtures/sample.learn.md +48 -0
- pylearnspec-0.4.0/tests/fixtures/sample.quiz.md +79 -0
- pylearnspec-0.4.0/tests/test_learnmd.py +100 -0
- pylearnspec-0.4.0/tests/test_nugget_smoke.py +258 -0
- pylearnspec-0.4.0/tests/test_quizmd.py +177 -0
- pylearnspec-0.4.0/tests/test_suite_smoke.py +323 -0
- pylearnspec-0.4.0/tests/test_v04_v03.py +141 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: pylearnspec
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Python parser and validator for the LearnSpec suite (LearnMD, QuizMD, FlashMD, TrackMD, DiagramMD, MediaMD, GlossaryMD, BadgeMD, CertMD, NuggetMD)
|
|
5
|
+
Author-email: learnspec <hello@learnspec.org>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/learnspec/pylearnspec
|
|
8
|
+
Project-URL: Repository, https://github.com/learnspec/pylearnspec
|
|
9
|
+
Keywords: learnspec,quizmd,learnmd,flashmd,trackmd,diagrammd,mediamd,glossarymd,badgemd,certmd,nuggetmd,parser,education,assessment
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Education
|
|
19
|
+
Classifier: Topic :: Text Processing :: Markup :: Markdown
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# pylearnspec
|
|
28
|
+
|
|
29
|
+
Python parser and validator for the [LearnSpec](https://learnspec.org) suite of open standards:
|
|
30
|
+
|
|
31
|
+
| Format | API |
|
|
32
|
+
|---|---|
|
|
33
|
+
| **LearnMD** — instructional content | `parse_learn`, `validate_learn` |
|
|
34
|
+
| **QuizMD** — assessments | `parse_quiz`, `validate_quiz` |
|
|
35
|
+
| **FlashMD** — flashcards | `parse_flash`, `validate_flash` |
|
|
36
|
+
| **NuggetMD** — micro-learning nuggets | `parse_nugget`, `validate_nugget` |
|
|
37
|
+
| **TrackMD** — learning paths | `parse_track`, `validate_track` |
|
|
38
|
+
| **DiagramMD** — diagrams | `parse_diagram`, `validate_diagram` |
|
|
39
|
+
| **MediaMD** — media catalogues | `parse_media`, `validate_media` |
|
|
40
|
+
| **GlossaryMD** — glossaries | `parse_glossary`, `validate_glossary` |
|
|
41
|
+
| **BadgeMD** — micro-credentials | `parse_badge`, `validate_badge` |
|
|
42
|
+
| **CertMD** — macro-credentials | `parse_cert`, `validate_cert` |
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install pylearnspec
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
### Parse a QuizMD file
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pylearnspec import parse_quiz
|
|
56
|
+
|
|
57
|
+
with open("my-quiz.quiz.md") as f:
|
|
58
|
+
quiz = parse_quiz(f.read())
|
|
59
|
+
|
|
60
|
+
print(quiz.title)
|
|
61
|
+
print(f"{len(quiz.questions)} questions")
|
|
62
|
+
|
|
63
|
+
for q in quiz.questions:
|
|
64
|
+
print(f" Q{q.number} ({q.q_type}): {q.title}")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Parse a LearnMD file
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from pylearnspec import parse_learn
|
|
71
|
+
|
|
72
|
+
with open("my-lesson.learn.md") as f:
|
|
73
|
+
lesson = parse_learn(f.read())
|
|
74
|
+
|
|
75
|
+
print(lesson.title)
|
|
76
|
+
for section in lesson.sections:
|
|
77
|
+
print(f" {'#' * section.depth} {section.title}")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Validate
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from pylearnspec import validate_quiz, validate_learn
|
|
84
|
+
|
|
85
|
+
# Lenient mode (default)
|
|
86
|
+
diagnostics = validate_quiz(content)
|
|
87
|
+
|
|
88
|
+
# Strict mode (for CI)
|
|
89
|
+
diagnostics = validate_quiz(content, strict=True)
|
|
90
|
+
|
|
91
|
+
for d in diagnostics:
|
|
92
|
+
print(f"[{d.level}] {d.message}")
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### JSON output
|
|
96
|
+
|
|
97
|
+
Both `Quiz` and `Lesson` objects have a `.to_dict()` method for JSON serialization:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import json
|
|
101
|
+
from pylearnspec import parse_quiz
|
|
102
|
+
|
|
103
|
+
quiz = parse_quiz(content)
|
|
104
|
+
print(json.dumps(quiz.to_dict(), indent=2))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Supported features
|
|
108
|
+
|
|
109
|
+
### QuizMD
|
|
110
|
+
|
|
111
|
+
- Frontmatter (Level 1) and per-question `quiz` blocks (Level 2)
|
|
112
|
+
- Question types: MCQ, multi-select, true/false, open answer, match, order
|
|
113
|
+
- Per-choice and global feedback (`[!correct]`, `[!incorrect]`)
|
|
114
|
+
- `!import` directives
|
|
115
|
+
- Validation (lenient and strict modes)
|
|
116
|
+
|
|
117
|
+
### LearnMD
|
|
118
|
+
|
|
119
|
+
- Frontmatter metadata (lang, estimated_time, tags)
|
|
120
|
+
- Section hierarchy (## modules, ### lessons)
|
|
121
|
+
- Special fenced blocks: `summary`, `example`, `note`, `tip`, `warning`, `quiz`, etc.
|
|
122
|
+
- GFM callout detection (`> [!tip]`, `> [!warning]`, etc.)
|
|
123
|
+
- `!import` directives (`.learn.md` and `.quiz.md`)
|
|
124
|
+
- Validation (lenient and strict modes)
|
|
125
|
+
|
|
126
|
+
### LearnSpec suite (v0.2)
|
|
127
|
+
|
|
128
|
+
Every format follows the same shape:
|
|
129
|
+
|
|
130
|
+
- YAML frontmatter with the universal fields (`lang` required, `license`, `spec_version`, `created`, `updated`)
|
|
131
|
+
- Cross-format directives `!import`, `!ref`, `!checkpoint` (when applicable) — see `pylearnspec.common.directives`
|
|
132
|
+
- `parse_<format>(content)` returns a typed dataclass with a `.to_dict()` method
|
|
133
|
+
- `validate_<format>(content, strict=False)` returns a list of `Diagnostic(level, message)`
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pip install -e ".[dev]"
|
|
139
|
+
pytest
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pylearnspec
|
|
2
|
+
|
|
3
|
+
Python parser and validator for the [LearnSpec](https://learnspec.org) suite of open standards:
|
|
4
|
+
|
|
5
|
+
| Format | API |
|
|
6
|
+
|---|---|
|
|
7
|
+
| **LearnMD** — instructional content | `parse_learn`, `validate_learn` |
|
|
8
|
+
| **QuizMD** — assessments | `parse_quiz`, `validate_quiz` |
|
|
9
|
+
| **FlashMD** — flashcards | `parse_flash`, `validate_flash` |
|
|
10
|
+
| **NuggetMD** — micro-learning nuggets | `parse_nugget`, `validate_nugget` |
|
|
11
|
+
| **TrackMD** — learning paths | `parse_track`, `validate_track` |
|
|
12
|
+
| **DiagramMD** — diagrams | `parse_diagram`, `validate_diagram` |
|
|
13
|
+
| **MediaMD** — media catalogues | `parse_media`, `validate_media` |
|
|
14
|
+
| **GlossaryMD** — glossaries | `parse_glossary`, `validate_glossary` |
|
|
15
|
+
| **BadgeMD** — micro-credentials | `parse_badge`, `validate_badge` |
|
|
16
|
+
| **CertMD** — macro-credentials | `parse_cert`, `validate_cert` |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install pylearnspec
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
### Parse a QuizMD file
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pylearnspec import parse_quiz
|
|
30
|
+
|
|
31
|
+
with open("my-quiz.quiz.md") as f:
|
|
32
|
+
quiz = parse_quiz(f.read())
|
|
33
|
+
|
|
34
|
+
print(quiz.title)
|
|
35
|
+
print(f"{len(quiz.questions)} questions")
|
|
36
|
+
|
|
37
|
+
for q in quiz.questions:
|
|
38
|
+
print(f" Q{q.number} ({q.q_type}): {q.title}")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Parse a LearnMD file
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pylearnspec import parse_learn
|
|
45
|
+
|
|
46
|
+
with open("my-lesson.learn.md") as f:
|
|
47
|
+
lesson = parse_learn(f.read())
|
|
48
|
+
|
|
49
|
+
print(lesson.title)
|
|
50
|
+
for section in lesson.sections:
|
|
51
|
+
print(f" {'#' * section.depth} {section.title}")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Validate
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from pylearnspec import validate_quiz, validate_learn
|
|
58
|
+
|
|
59
|
+
# Lenient mode (default)
|
|
60
|
+
diagnostics = validate_quiz(content)
|
|
61
|
+
|
|
62
|
+
# Strict mode (for CI)
|
|
63
|
+
diagnostics = validate_quiz(content, strict=True)
|
|
64
|
+
|
|
65
|
+
for d in diagnostics:
|
|
66
|
+
print(f"[{d.level}] {d.message}")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### JSON output
|
|
70
|
+
|
|
71
|
+
Both `Quiz` and `Lesson` objects have a `.to_dict()` method for JSON serialization:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import json
|
|
75
|
+
from pylearnspec import parse_quiz
|
|
76
|
+
|
|
77
|
+
quiz = parse_quiz(content)
|
|
78
|
+
print(json.dumps(quiz.to_dict(), indent=2))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Supported features
|
|
82
|
+
|
|
83
|
+
### QuizMD
|
|
84
|
+
|
|
85
|
+
- Frontmatter (Level 1) and per-question `quiz` blocks (Level 2)
|
|
86
|
+
- Question types: MCQ, multi-select, true/false, open answer, match, order
|
|
87
|
+
- Per-choice and global feedback (`[!correct]`, `[!incorrect]`)
|
|
88
|
+
- `!import` directives
|
|
89
|
+
- Validation (lenient and strict modes)
|
|
90
|
+
|
|
91
|
+
### LearnMD
|
|
92
|
+
|
|
93
|
+
- Frontmatter metadata (lang, estimated_time, tags)
|
|
94
|
+
- Section hierarchy (## modules, ### lessons)
|
|
95
|
+
- Special fenced blocks: `summary`, `example`, `note`, `tip`, `warning`, `quiz`, etc.
|
|
96
|
+
- GFM callout detection (`> [!tip]`, `> [!warning]`, etc.)
|
|
97
|
+
- `!import` directives (`.learn.md` and `.quiz.md`)
|
|
98
|
+
- Validation (lenient and strict modes)
|
|
99
|
+
|
|
100
|
+
### LearnSpec suite (v0.2)
|
|
101
|
+
|
|
102
|
+
Every format follows the same shape:
|
|
103
|
+
|
|
104
|
+
- YAML frontmatter with the universal fields (`lang` required, `license`, `spec_version`, `created`, `updated`)
|
|
105
|
+
- Cross-format directives `!import`, `!ref`, `!checkpoint` (when applicable) — see `pylearnspec.common.directives`
|
|
106
|
+
- `parse_<format>(content)` returns a typed dataclass with a `.to_dict()` method
|
|
107
|
+
- `validate_<format>(content, strict=False)` returns a list of `Diagnostic(level, message)`
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
pytest
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64,<77", "setuptools-scm"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pylearnspec"
|
|
7
|
+
version = "0.4.0"
|
|
8
|
+
description = "Python parser and validator for the LearnSpec suite (LearnMD, QuizMD, FlashMD, TrackMD, DiagramMD, MediaMD, GlossaryMD, BadgeMD, CertMD, NuggetMD)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "learnspec", email = "hello@learnspec.org" }]
|
|
13
|
+
keywords = ["learnspec", "quizmd", "learnmd", "flashmd", "trackmd", "diagrammd", "mediamd", "glossarymd", "badgemd", "certmd", "nuggetmd", "parser", "education", "assessment"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Education",
|
|
24
|
+
"Topic :: Text Processing :: Markup :: Markdown",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
"pyyaml>=6.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=8.0",
|
|
34
|
+
"pytest-cov>=5.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/learnspec/pylearnspec"
|
|
39
|
+
Repository = "https://github.com/learnspec/pylearnspec"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""pylearnspec — Python parser and validator for the LearnSpec suite.
|
|
2
|
+
|
|
3
|
+
Supported formats:
|
|
4
|
+
- LearnMD (parse_learn, validate_learn)
|
|
5
|
+
- QuizMD (parse_quiz, validate_quiz)
|
|
6
|
+
- FlashMD (parse_flash, validate_flash)
|
|
7
|
+
- TrackMD (parse_track, validate_track)
|
|
8
|
+
- DiagramMD (parse_diagram, validate_diagram)
|
|
9
|
+
- MediaMD (parse_media, validate_media)
|
|
10
|
+
- GlossaryMD (parse_glossary, validate_glossary)
|
|
11
|
+
- BadgeMD (parse_badge, validate_badge)
|
|
12
|
+
- CertMD (parse_cert, validate_cert)
|
|
13
|
+
- NuggetMD (parse_nugget, validate_nugget)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.4.0"
|
|
17
|
+
|
|
18
|
+
from pylearnspec.badgemd.parser import parse_badge
|
|
19
|
+
from pylearnspec.badgemd.validator import validate_badge
|
|
20
|
+
from pylearnspec.certmd.parser import parse_cert
|
|
21
|
+
from pylearnspec.certmd.validator import validate_cert
|
|
22
|
+
from pylearnspec.diagrammd.parser import parse_diagram
|
|
23
|
+
from pylearnspec.diagrammd.validator import validate_diagram
|
|
24
|
+
from pylearnspec.flashmd.parser import parse_flash
|
|
25
|
+
from pylearnspec.flashmd.validator import validate_flash
|
|
26
|
+
from pylearnspec.glossarymd.parser import parse_glossary
|
|
27
|
+
from pylearnspec.glossarymd.validator import validate_glossary
|
|
28
|
+
from pylearnspec.learnmd.parser import parse_learn
|
|
29
|
+
from pylearnspec.learnmd.validator import validate_learn
|
|
30
|
+
from pylearnspec.mediamd.parser import parse_media
|
|
31
|
+
from pylearnspec.mediamd.validator import validate_media
|
|
32
|
+
from pylearnspec.nuggetmd.parser import parse_nugget
|
|
33
|
+
from pylearnspec.nuggetmd.validator import validate_nugget
|
|
34
|
+
from pylearnspec.quizmd.parser import parse_quiz
|
|
35
|
+
from pylearnspec.quizmd.validator import validate_quiz
|
|
36
|
+
from pylearnspec.trackmd.parser import parse_track
|
|
37
|
+
from pylearnspec.trackmd.validator import validate_track
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"parse_quiz", "validate_quiz",
|
|
41
|
+
"parse_learn", "validate_learn",
|
|
42
|
+
"parse_flash", "validate_flash",
|
|
43
|
+
"parse_track", "validate_track",
|
|
44
|
+
"parse_diagram", "validate_diagram",
|
|
45
|
+
"parse_media", "validate_media",
|
|
46
|
+
"parse_glossary", "validate_glossary",
|
|
47
|
+
"parse_badge", "validate_badge",
|
|
48
|
+
"parse_cert", "validate_cert",
|
|
49
|
+
"parse_nugget", "validate_nugget",
|
|
50
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Data models for BadgeMD parsed output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Badge:
|
|
11
|
+
title: str = ""
|
|
12
|
+
level: str = "0"
|
|
13
|
+
frontmatter: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
image_path: str = "" # from the  line in the body
|
|
15
|
+
body_text: str = "" # narrative criteria
|
|
16
|
+
criteria: list[dict[str, Any]] = field(default_factory=list)
|
|
17
|
+
has_criteria_block: bool = False
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"title": self.title,
|
|
22
|
+
"level": self.level,
|
|
23
|
+
"frontmatter": self.frontmatter,
|
|
24
|
+
"image_path": self.image_path,
|
|
25
|
+
"body_text": self.body_text,
|
|
26
|
+
"criteria": self.criteria,
|
|
27
|
+
"has_criteria_block": self.has_criteria_block,
|
|
28
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""BadgeMD parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from pylearnspec.badgemd.models import Badge
|
|
10
|
+
from pylearnspec.common.frontmatter import extract_frontmatter
|
|
11
|
+
|
|
12
|
+
_H1_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE)
|
|
13
|
+
_IMAGE_LINE_RE = re.compile(r"^!\[[^\]]*\]\(([^)]+)\)", re.MULTILINE)
|
|
14
|
+
_CRITERIA_BLOCK_RE = re.compile(
|
|
15
|
+
r"^```criteria\s*\n(.*?)^```\s*$",
|
|
16
|
+
re.MULTILINE | re.DOTALL,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_badge(content: str) -> Badge:
|
|
21
|
+
frontmatter, body = extract_frontmatter(content)
|
|
22
|
+
|
|
23
|
+
title = frontmatter.get("name", "") or frontmatter.get("title", "")
|
|
24
|
+
if not title:
|
|
25
|
+
m = _H1_RE.search(body)
|
|
26
|
+
if m:
|
|
27
|
+
title = m.group(1).strip()
|
|
28
|
+
|
|
29
|
+
image_path = ""
|
|
30
|
+
if (m := _IMAGE_LINE_RE.search(body)):
|
|
31
|
+
image_path = m.group(1).strip()
|
|
32
|
+
|
|
33
|
+
criteria: list[dict] = []
|
|
34
|
+
has_criteria_block = False
|
|
35
|
+
if (m := _CRITERIA_BLOCK_RE.search(body)):
|
|
36
|
+
has_criteria_block = True
|
|
37
|
+
try:
|
|
38
|
+
loaded = yaml.safe_load(m.group(1))
|
|
39
|
+
if isinstance(loaded, dict):
|
|
40
|
+
criteria = [loaded]
|
|
41
|
+
elif isinstance(loaded, list):
|
|
42
|
+
criteria = [c for c in loaded if isinstance(c, dict)]
|
|
43
|
+
except yaml.YAMLError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
# Strip image and criteria block from body to get narrative text
|
|
47
|
+
body_text = _CRITERIA_BLOCK_RE.sub("", body)
|
|
48
|
+
body_text = _IMAGE_LINE_RE.sub("", body_text, count=1)
|
|
49
|
+
body_text = body_text.strip()
|
|
50
|
+
|
|
51
|
+
has_fm = bool(frontmatter)
|
|
52
|
+
if has_criteria_block:
|
|
53
|
+
level = "2"
|
|
54
|
+
elif has_fm:
|
|
55
|
+
level = "1"
|
|
56
|
+
else:
|
|
57
|
+
level = "0"
|
|
58
|
+
|
|
59
|
+
return Badge(
|
|
60
|
+
title=title,
|
|
61
|
+
level=level,
|
|
62
|
+
frontmatter=frontmatter,
|
|
63
|
+
image_path=image_path,
|
|
64
|
+
body_text=body_text,
|
|
65
|
+
criteria=criteria,
|
|
66
|
+
has_criteria_block=has_criteria_block,
|
|
67
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""BadgeMD validator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from pylearnspec.badgemd.parser import parse_badge
|
|
8
|
+
from pylearnspec.common.validation import Diagnostic, severity_for
|
|
9
|
+
|
|
10
|
+
_DURATION_RE = re.compile(r"^P(?:\d+Y)?(?:\d+M)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$")
|
|
11
|
+
|
|
12
|
+
KNOWN_CRITERIA_TYPES = {"track_complete", "quiz_pass", "checkpoint_reached", "manual"}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def validate_badge(content: str, *, strict: bool = False) -> list[Diagnostic]:
|
|
16
|
+
diags: list[Diagnostic] = []
|
|
17
|
+
try:
|
|
18
|
+
b = parse_badge(content)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
diags.append(Diagnostic(level="error", message=f"Parse error: {e}"))
|
|
21
|
+
return diags
|
|
22
|
+
|
|
23
|
+
sev = severity_for(strict)
|
|
24
|
+
fm = b.frontmatter
|
|
25
|
+
|
|
26
|
+
if not fm.get("lang"):
|
|
27
|
+
diags.append(Diagnostic(level=sev, message="Missing 'lang' in frontmatter"))
|
|
28
|
+
if not fm.get("name"):
|
|
29
|
+
diags.append(Diagnostic(level="error", message="Missing required 'name'"))
|
|
30
|
+
if not fm.get("image"):
|
|
31
|
+
diags.append(Diagnostic(level="error", message="Missing required 'image'"))
|
|
32
|
+
elif not str(fm["image"]).lower().endswith(".svg"):
|
|
33
|
+
diags.append(Diagnostic(level=sev, message="'image' should point to an SVG file"))
|
|
34
|
+
|
|
35
|
+
issuer = fm.get("issuer") or {}
|
|
36
|
+
if not isinstance(issuer, dict) or not issuer.get("name"):
|
|
37
|
+
diags.append(Diagnostic(level="error", message="Missing required 'issuer.name'"))
|
|
38
|
+
if isinstance(issuer, dict) and not issuer.get("url"):
|
|
39
|
+
diags.append(Diagnostic(level=sev, message="Missing 'issuer.url'"))
|
|
40
|
+
|
|
41
|
+
if not b.image_path:
|
|
42
|
+
diags.append(Diagnostic(level=sev, message="No Markdown image line found in the body"))
|
|
43
|
+
|
|
44
|
+
if not b.has_criteria_block:
|
|
45
|
+
diags.append(Diagnostic(level=sev, message="No 'criteria' fenced block — award conditions are narrative only"))
|
|
46
|
+
else:
|
|
47
|
+
for i, c in enumerate(b.criteria, start=1):
|
|
48
|
+
t = c.get("type")
|
|
49
|
+
if t and t not in KNOWN_CRITERIA_TYPES:
|
|
50
|
+
diags.append(Diagnostic(level=sev, message=f"Criterion #{i}: unrecognised type '{t}'"))
|
|
51
|
+
|
|
52
|
+
expires = fm.get("expires")
|
|
53
|
+
if expires is not None and not _DURATION_RE.match(str(expires)):
|
|
54
|
+
diags.append(Diagnostic(
|
|
55
|
+
level="error",
|
|
56
|
+
message=f"'expires' must be an ISO 8601 duration (got {expires!r})",
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
return diags
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Data models for CertMD parsed output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Cert:
|
|
11
|
+
title: str = ""
|
|
12
|
+
level: str = "0"
|
|
13
|
+
frontmatter: dict[str, Any] = field(default_factory=dict)
|
|
14
|
+
image_path: str = ""
|
|
15
|
+
body_text: str = ""
|
|
16
|
+
requirements: list[dict[str, Any]] = field(default_factory=list)
|
|
17
|
+
has_requirements_block: bool = False
|
|
18
|
+
|
|
19
|
+
def to_dict(self) -> dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"title": self.title,
|
|
22
|
+
"level": self.level,
|
|
23
|
+
"frontmatter": self.frontmatter,
|
|
24
|
+
"image_path": self.image_path,
|
|
25
|
+
"body_text": self.body_text,
|
|
26
|
+
"requirements": self.requirements,
|
|
27
|
+
"has_requirements_block": self.has_requirements_block,
|
|
28
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""CertMD parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from pylearnspec.certmd.models import Cert
|
|
10
|
+
from pylearnspec.common.frontmatter import extract_frontmatter
|
|
11
|
+
|
|
12
|
+
_H1_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE)
|
|
13
|
+
_IMAGE_LINE_RE = re.compile(r"^!\[[^\]]*\]\(([^)]+)\)", re.MULTILINE)
|
|
14
|
+
_REQUIREMENTS_BLOCK_RE = re.compile(
|
|
15
|
+
r"^```requirements\s*\n(.*?)^```\s*$",
|
|
16
|
+
re.MULTILINE | re.DOTALL,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse_cert(content: str) -> Cert:
|
|
21
|
+
frontmatter, body = extract_frontmatter(content)
|
|
22
|
+
|
|
23
|
+
title = frontmatter.get("name", "") or frontmatter.get("title", "")
|
|
24
|
+
if not title:
|
|
25
|
+
m = _H1_RE.search(body)
|
|
26
|
+
if m:
|
|
27
|
+
title = m.group(1).strip()
|
|
28
|
+
|
|
29
|
+
image_path = ""
|
|
30
|
+
if (m := _IMAGE_LINE_RE.search(body)):
|
|
31
|
+
image_path = m.group(1).strip()
|
|
32
|
+
|
|
33
|
+
requirements: list[dict] = []
|
|
34
|
+
has_block = False
|
|
35
|
+
if (m := _REQUIREMENTS_BLOCK_RE.search(body)):
|
|
36
|
+
has_block = True
|
|
37
|
+
try:
|
|
38
|
+
loaded = yaml.safe_load(m.group(1))
|
|
39
|
+
if isinstance(loaded, dict):
|
|
40
|
+
requirements = [loaded]
|
|
41
|
+
elif isinstance(loaded, list):
|
|
42
|
+
requirements = [r for r in loaded if isinstance(r, dict)]
|
|
43
|
+
except yaml.YAMLError:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
body_text = _REQUIREMENTS_BLOCK_RE.sub("", body)
|
|
47
|
+
body_text = _IMAGE_LINE_RE.sub("", body_text, count=1).strip()
|
|
48
|
+
|
|
49
|
+
has_fm = bool(frontmatter)
|
|
50
|
+
if has_block:
|
|
51
|
+
level = "2"
|
|
52
|
+
elif has_fm:
|
|
53
|
+
level = "1"
|
|
54
|
+
else:
|
|
55
|
+
level = "0"
|
|
56
|
+
|
|
57
|
+
return Cert(
|
|
58
|
+
title=title,
|
|
59
|
+
level=level,
|
|
60
|
+
frontmatter=frontmatter,
|
|
61
|
+
image_path=image_path,
|
|
62
|
+
body_text=body_text,
|
|
63
|
+
requirements=requirements,
|
|
64
|
+
has_requirements_block=has_block,
|
|
65
|
+
)
|