learningfoundry 0.32.0__tar.gz → 0.34.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.
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/CHANGELOG.md +39 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/PKG-INFO +1 -1
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/pyproject.toml +1 -1
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/__init__.py +1 -1
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/cli.py +19 -0
- learningfoundry-0.34.0/src/learningfoundry/generator.py +193 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/package.json +2 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/app.css +1 -0
- learningfoundry-0.34.0/src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts +54 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0/src/learningfoundry}/sveltekit_template/src/lib/utils/markdown.ts +6 -0
- learningfoundry-0.32.0/src/learningfoundry/generator.py +0 -99
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/.gitignore +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/LICENSE +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/README.md +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/docs/project-guide/README.md +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/__main__.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/config.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/exceptions.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/__init__.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/d3foundry_stub.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/nbfoundry_stub.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/protocols.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/quizazz.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/logging_config.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/parser.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/pipeline.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/py.typed +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/resolver.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/schema_v1.py +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/pnpm-lock.yaml +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/db/progress.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.test.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/lib/types/index.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/routes/+layout.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/routes/+page.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/static/.gitkeep +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/svelte.config.js +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/tsconfig.json +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/sveltekit_template/vite.config.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/app.css +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/app.html +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/ContentBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/ExerciseBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/LessonList.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/LessonView.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/ModuleList.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/Navigation.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/PlaceholderBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/ProgressBar.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/ProgressDashboard.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/QuizBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/TextBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/VideoBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/components/VisualizationBlock.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/db/database.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/db/index.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/db/progress.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/stores/curriculum.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/types/index.ts +0 -0
- {learningfoundry-0.32.0/src/learningfoundry → learningfoundry-0.34.0}/sveltekit_template/src/lib/utils/markdown.ts +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/routes/+layout.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/routes/+page.svelte +0 -0
- {learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/routes/[module]/[lesson]/+page.svelte +0 -0
|
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.34.0] - 2026-04-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`learningfoundry build` now preserves install/build state across rebuilds.** Previously every rebuild wiped the entire output directory, including any existing `node_modules/`, `pnpm-lock.yaml`, `build/`, and `.svelte-kit/` — forcing the user to `pnpm install` after every regen. Now those four paths are moved into the fresh template copy before the swap, so iteration is install → build, then any number of `learningfoundry build` re-runs followed by just `pnpm build` (or `pnpm dev`).
|
|
15
|
+
- `src/learningfoundry/generator.py` — new `_PRESERVED_PATHS` list + `_move_preserved()` helper used by `_atomic_copy()`. Same paths are also passed to `shutil.ignore_patterns` so a stray `node_modules/` in the dev template directory never ships to user output.
|
|
16
|
+
- The "output directory exists" log message changed from `WARNING` ("will be overwritten") to `INFO` ("refreshing template files; preserving …") to reflect the new behaviour.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **Smart post-build next-steps message in the CLI** based on detected dep state:
|
|
21
|
+
- `FIRST_BUILD` (no `node_modules/`) → `Next: cd dist && pnpm install && pnpm build`
|
|
22
|
+
- `CHANGED` (any declared dep missing from `node_modules/`) → `⚠️ Dependencies changed since last install. Run: cd dist && pnpm install && pnpm build`
|
|
23
|
+
- `UNCHANGED` (every declared dep present) → `Next: cd dist && pnpm build`
|
|
24
|
+
- New public API: `learningfoundry.generator.check_dep_state(output_dir)` returning a `DepState` enum, used by the CLI but also callable from third-party tooling.
|
|
25
|
+
|
|
26
|
+
### Added (tests)
|
|
27
|
+
|
|
28
|
+
- `tests/test_generator.py::TestPreserveInstallState` — 5 cases covering preservation of `node_modules/`, `pnpm-lock.yaml`, `build/`, `.svelte-kit/`, and confirmation that template files (e.g. `curriculum.json`) still refresh on rebuild.
|
|
29
|
+
- `tests/test_generator.py::TestCheckDepState` — 4 cases covering first-build, all-deps-installed, missing-dep, and malformed-`package.json` paths.
|
|
30
|
+
|
|
31
|
+
### Performance
|
|
32
|
+
|
|
33
|
+
- Smoke build is ~40% faster (~10s vs ~17s) because the SvelteKit template's leftover dev `node_modules/` (which a developer's local pnpm runs may create in the in-repo template) is no longer copied into every `learningfoundry build` output.
|
|
34
|
+
|
|
35
|
+
## [0.33.0] - 2026-04-29
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
|
|
39
|
+
- **LaTeX math rendering in lesson markdown** via [KaTeX](https://katex.org/). Both inline (`$...$`) and display (`$$...$$`) syntax are supported and rendered to HTML at parse time — no runtime JS overhead per lesson view.
|
|
40
|
+
- `src/learningfoundry/sveltekit_template/package.json` — added `katex ^0.16.11` and `marked-katex-extension ^5.1.4` to dependencies
|
|
41
|
+
- `src/learningfoundry/sveltekit_template/src/lib/utils/markdown.ts` — registered `markedKatex({ throwOnError: false })` so malformed LaTeX renders the source verbatim instead of throwing
|
|
42
|
+
- `src/learningfoundry/sveltekit_template/src/app.css` — `@import 'katex/dist/katex.min.css';` so rendered formulas are styled
|
|
43
|
+
|
|
44
|
+
### Added (tests)
|
|
45
|
+
|
|
46
|
+
- `src/learningfoundry/sveltekit_template/src/lib/utils/markdown.test.ts` — 6 vitest cases covering blank input, headings, fenced code, inline math, display math, and graceful malformed-LaTeX handling
|
|
47
|
+
- `tests/test_smoke_sveltekit.py::test_katex_styles_in_bundled_css` — regression guard asserting `.katex` rules land in the bundled CSS
|
|
48
|
+
|
|
10
49
|
## [0.32.0] - 2026-04-29
|
|
11
50
|
|
|
12
51
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: learningfoundry
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.34.0
|
|
4
4
|
Summary: A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pointmatic/learningfoundry
|
|
6
6
|
Project-URL: Repository, https://github.com/pointmatic/learningfoundry
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "learningfoundry"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.34.0"
|
|
8
8
|
description = "A curriculum engine that turns a YAML curriculum definition into a deployable SvelteKit learning application."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -110,6 +110,25 @@ def build(
|
|
|
110
110
|
|
|
111
111
|
click.echo(f"Build complete → {output_dir}")
|
|
112
112
|
|
|
113
|
+
from learningfoundry.generator import DepState, check_dep_state
|
|
114
|
+
|
|
115
|
+
state = check_dep_state(output_dir)
|
|
116
|
+
if state is DepState.FIRST_BUILD:
|
|
117
|
+
click.echo("")
|
|
118
|
+
click.echo(f"Next: cd {output_dir} && pnpm install && pnpm build")
|
|
119
|
+
click.echo(" (or `pnpm dev` for a live-reloading dev server)")
|
|
120
|
+
elif state is DepState.CHANGED:
|
|
121
|
+
click.echo("")
|
|
122
|
+
click.echo(
|
|
123
|
+
"⚠️ Dependencies changed since last install "
|
|
124
|
+
"(new packages in package.json)."
|
|
125
|
+
)
|
|
126
|
+
click.echo(f" Run: cd {output_dir} && pnpm install && pnpm build")
|
|
127
|
+
else:
|
|
128
|
+
click.echo("")
|
|
129
|
+
click.echo(f"Next: cd {output_dir} && pnpm build")
|
|
130
|
+
click.echo(" (or `pnpm dev` for a live-reloading dev server)")
|
|
131
|
+
|
|
113
132
|
|
|
114
133
|
# ---------------------------------------------------------------------------
|
|
115
134
|
# validate
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Copyright 2026 Pointmatic
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""SvelteKit project generator — copies template and writes curriculum.json."""
|
|
4
|
+
|
|
5
|
+
import dataclasses
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import shutil
|
|
9
|
+
import tempfile
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from learningfoundry.exceptions import GenerationError
|
|
14
|
+
from learningfoundry.resolver import ResolvedCurriculum
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("learningfoundry.generator")
|
|
17
|
+
|
|
18
|
+
_TEMPLATE_DIR = Path(__file__).parent / "sveltekit_template"
|
|
19
|
+
|
|
20
|
+
# Paths that represent install/build state in the generated project. These
|
|
21
|
+
# are preserved across `learningfoundry build` re-runs so the user does not
|
|
22
|
+
# have to `pnpm install` after every regen. Template files (package.json,
|
|
23
|
+
# src/, static/, configs) are still refreshed every time.
|
|
24
|
+
_PRESERVED_PATHS: tuple[str, ...] = (
|
|
25
|
+
"node_modules",
|
|
26
|
+
"pnpm-lock.yaml",
|
|
27
|
+
"build",
|
|
28
|
+
".svelte-kit",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class DepState(StrEnum):
|
|
33
|
+
"""State of installed dependencies relative to the generated package.json."""
|
|
34
|
+
|
|
35
|
+
FIRST_BUILD = "first_build"
|
|
36
|
+
"""No `node_modules/` exists in the output directory."""
|
|
37
|
+
|
|
38
|
+
UNCHANGED = "unchanged"
|
|
39
|
+
"""Every declared dep has a corresponding installed package."""
|
|
40
|
+
|
|
41
|
+
CHANGED = "changed"
|
|
42
|
+
"""`node_modules/` exists but at least one declared dep is missing
|
|
43
|
+
(e.g. user upgraded `learningfoundry` and the template added a new dep)."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_app(
|
|
47
|
+
resolved: ResolvedCurriculum,
|
|
48
|
+
output_dir: Path,
|
|
49
|
+
template_dir: Path | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Generate a SvelteKit project from a resolved curriculum.
|
|
52
|
+
|
|
53
|
+
Copies ``sveltekit_template/`` to ``output_dir`` atomically (write to a
|
|
54
|
+
temp directory, then move into place), then writes ``curriculum.json``
|
|
55
|
+
into ``output_dir/static/``.
|
|
56
|
+
|
|
57
|
+
If ``output_dir`` already exists it is replaced with a warning.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
resolved: Fully resolved curriculum from ``resolve_curriculum()``.
|
|
61
|
+
output_dir: Destination path for the generated SvelteKit project.
|
|
62
|
+
template_dir: Override template source. Defaults to the package's
|
|
63
|
+
``sveltekit_template/`` directory.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
GenerationError: If the template directory does not exist or any
|
|
67
|
+
file operation fails.
|
|
68
|
+
"""
|
|
69
|
+
src = template_dir or _TEMPLATE_DIR
|
|
70
|
+
|
|
71
|
+
if not src.exists():
|
|
72
|
+
raise GenerationError(
|
|
73
|
+
f"SvelteKit template directory not found: `{src}`. "
|
|
74
|
+
"Run `learningfoundry` from the project root, or pass "
|
|
75
|
+
"`template_dir` explicitly."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if output_dir.exists():
|
|
79
|
+
logger.info(
|
|
80
|
+
"Output directory `%s` already exists; refreshing template files "
|
|
81
|
+
"(preserving node_modules/, pnpm-lock.yaml, build/, .svelte-kit/).",
|
|
82
|
+
output_dir,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
_atomic_copy(src, output_dir)
|
|
86
|
+
_write_curriculum_json(resolved, output_dir)
|
|
87
|
+
|
|
88
|
+
logger.info("Generated SvelteKit project at: %s", output_dir)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _atomic_copy(src: Path, dst: Path) -> None:
|
|
92
|
+
"""Copy src tree to dst atomically via a sibling temp directory.
|
|
93
|
+
|
|
94
|
+
If ``dst`` already exists, install/build state listed in
|
|
95
|
+
:data:`_PRESERVED_PATHS` is moved from the existing ``dst`` into the
|
|
96
|
+
fresh template copy before the swap, so users do not have to re-run
|
|
97
|
+
``pnpm install`` after every ``learningfoundry build``.
|
|
98
|
+
"""
|
|
99
|
+
parent = dst.parent
|
|
100
|
+
parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with tempfile.TemporaryDirectory(dir=parent, prefix=".lf_gen_") as tmp_str:
|
|
104
|
+
tmp = Path(tmp_str) / dst.name
|
|
105
|
+
# Never ship user-side artifacts from the template, even if a
|
|
106
|
+
# local dev environment has them (e.g. a stray node_modules/
|
|
107
|
+
# from running pnpm in the template dir during development).
|
|
108
|
+
shutil.copytree(
|
|
109
|
+
src,
|
|
110
|
+
tmp,
|
|
111
|
+
ignore=shutil.ignore_patterns(*_PRESERVED_PATHS),
|
|
112
|
+
)
|
|
113
|
+
if dst.exists():
|
|
114
|
+
_move_preserved(dst, tmp)
|
|
115
|
+
shutil.rmtree(dst)
|
|
116
|
+
tmp.rename(dst)
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
raise GenerationError(
|
|
119
|
+
f"Failed to generate project at `{dst}`: {exc}"
|
|
120
|
+
) from exc
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _move_preserved(existing: Path, fresh: Path) -> None:
|
|
124
|
+
"""Move install/build state from ``existing`` into ``fresh`` in place.
|
|
125
|
+
|
|
126
|
+
Any template-shipped placeholder at the same path inside ``fresh`` is
|
|
127
|
+
removed first so the move never collides.
|
|
128
|
+
"""
|
|
129
|
+
for name in _PRESERVED_PATHS:
|
|
130
|
+
source = existing / name
|
|
131
|
+
if not source.exists() and not source.is_symlink():
|
|
132
|
+
continue
|
|
133
|
+
target = fresh / name
|
|
134
|
+
if target.exists() or target.is_symlink():
|
|
135
|
+
if target.is_dir() and not target.is_symlink():
|
|
136
|
+
shutil.rmtree(target)
|
|
137
|
+
else:
|
|
138
|
+
target.unlink()
|
|
139
|
+
shutil.move(str(source), str(target))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_dep_state(output_dir: Path) -> DepState:
|
|
143
|
+
"""Inspect ``output_dir`` to determine whether `pnpm install` is needed.
|
|
144
|
+
|
|
145
|
+
A dep is considered "installed" if ``node_modules/<name>/package.json``
|
|
146
|
+
exists. We do not check version ranges — that is pnpm's job. This is a
|
|
147
|
+
presence check designed to detect the common case where the generated
|
|
148
|
+
`package.json` adds a new dep (e.g. after a `learningfoundry` upgrade)
|
|
149
|
+
that is not yet in `node_modules/`.
|
|
150
|
+
"""
|
|
151
|
+
node_modules = output_dir / "node_modules"
|
|
152
|
+
if not node_modules.is_dir():
|
|
153
|
+
return DepState.FIRST_BUILD
|
|
154
|
+
|
|
155
|
+
package_json = output_dir / "package.json"
|
|
156
|
+
if not package_json.is_file():
|
|
157
|
+
return DepState.FIRST_BUILD
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
data = json.loads(package_json.read_text(encoding="utf-8"))
|
|
161
|
+
except (OSError, json.JSONDecodeError):
|
|
162
|
+
# Malformed package.json — surface as "changed" so the user is
|
|
163
|
+
# nudged to run pnpm install (which will report the real error).
|
|
164
|
+
return DepState.CHANGED
|
|
165
|
+
|
|
166
|
+
declared: dict[str, str] = {
|
|
167
|
+
**(data.get("dependencies") or {}),
|
|
168
|
+
**(data.get("devDependencies") or {}),
|
|
169
|
+
}
|
|
170
|
+
for name in declared:
|
|
171
|
+
if not (node_modules / name / "package.json").is_file():
|
|
172
|
+
return DepState.CHANGED
|
|
173
|
+
return DepState.UNCHANGED
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _write_curriculum_json(resolved: ResolvedCurriculum, output_dir: Path) -> None:
|
|
177
|
+
"""Serialize ResolvedCurriculum to output_dir/static/curriculum.json."""
|
|
178
|
+
static_dir = output_dir / "static"
|
|
179
|
+
static_dir.mkdir(parents=True, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
curriculum_json = output_dir / "static" / "curriculum.json"
|
|
182
|
+
try:
|
|
183
|
+
data = dataclasses.asdict(resolved)
|
|
184
|
+
curriculum_json.write_text(
|
|
185
|
+
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
186
|
+
encoding="utf-8",
|
|
187
|
+
)
|
|
188
|
+
except OSError as exc:
|
|
189
|
+
raise GenerationError(
|
|
190
|
+
f"Failed to write curriculum.json to `{curriculum_json}`: {exc}"
|
|
191
|
+
) from exc
|
|
192
|
+
|
|
193
|
+
logger.debug("Wrote curriculum.json (%d bytes)", curriculum_json.stat().st_size)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Copyright 2026 Pointmatic
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { renderMarkdown } from './markdown.js';
|
|
5
|
+
|
|
6
|
+
describe('renderMarkdown', () => {
|
|
7
|
+
it('returns empty string for null/undefined/blank input', () => {
|
|
8
|
+
expect(renderMarkdown(null)).toBe('');
|
|
9
|
+
expect(renderMarkdown(undefined)).toBe('');
|
|
10
|
+
expect(renderMarkdown('')).toBe('');
|
|
11
|
+
expect(renderMarkdown(' \n\t')).toBe('');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders headings as <h1>/<h2>/<h3>', () => {
|
|
15
|
+
const html = renderMarkdown('# H1\n\n## H2\n\n### H3');
|
|
16
|
+
expect(html).toContain('<h1');
|
|
17
|
+
expect(html).toContain('<h2');
|
|
18
|
+
expect(html).toContain('<h3');
|
|
19
|
+
expect(html).toContain('H1');
|
|
20
|
+
expect(html).toContain('H2');
|
|
21
|
+
expect(html).toContain('H3');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders fenced code blocks', () => {
|
|
25
|
+
const html = renderMarkdown('```python\nprint("hi")\n```');
|
|
26
|
+
expect(html).toContain('<pre');
|
|
27
|
+
expect(html).toContain('<code');
|
|
28
|
+
expect(html).toContain('print');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders inline math via $...$', () => {
|
|
32
|
+
const html = renderMarkdown('Euler said $e^{i\\pi} + 1 = 0$.');
|
|
33
|
+
// KaTeX inline output wraps the formula in <span class="katex">…</span>
|
|
34
|
+
// (without the `katex-display` wrapper used for block-level math).
|
|
35
|
+
expect(html).toContain('class="katex"');
|
|
36
|
+
// The literal `$...$` delimiters should be consumed by the parser.
|
|
37
|
+
expect(html).not.toContain('$e^{i\\pi}');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders display math via $$...$$', () => {
|
|
41
|
+
const md = '$$\n\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}\n$$';
|
|
42
|
+
const html = renderMarkdown(md);
|
|
43
|
+
// Display math gets the `katex-display` wrapper. KaTeX preserves the
|
|
44
|
+
// source LaTeX inside a MathML <annotation> tag for accessibility,
|
|
45
|
+
// so we don't assert that the source is absent — only that the
|
|
46
|
+
// rendered HTML structure is present.
|
|
47
|
+
expect(html).toContain('katex-display');
|
|
48
|
+
expect(html).toContain('class="katex"');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('does not throw on malformed LaTeX (throwOnError: false)', () => {
|
|
52
|
+
expect(() => renderMarkdown('$\\unknownmacro{x}$')).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// Copyright 2026 Pointmatic
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { marked } from 'marked';
|
|
4
|
+
import markedKatex from 'marked-katex-extension';
|
|
5
|
+
|
|
6
|
+
// Register the KaTeX extension once at module load. Supports both inline
|
|
7
|
+
// `$...$` and display `$$...$$` math, rendered to HTML at parse time using
|
|
8
|
+
// the KaTeX engine. Stylesheet is imported in `src/app.css`.
|
|
9
|
+
marked.use(markedKatex({ throwOnError: false }));
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Convert a markdown string to sanitised HTML.
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
# Copyright 2026 Pointmatic
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
"""SvelteKit project generator — copies template and writes curriculum.json."""
|
|
4
|
-
|
|
5
|
-
import dataclasses
|
|
6
|
-
import json
|
|
7
|
-
import logging
|
|
8
|
-
import shutil
|
|
9
|
-
import tempfile
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from learningfoundry.exceptions import GenerationError
|
|
13
|
-
from learningfoundry.resolver import ResolvedCurriculum
|
|
14
|
-
|
|
15
|
-
logger = logging.getLogger("learningfoundry.generator")
|
|
16
|
-
|
|
17
|
-
_TEMPLATE_DIR = Path(__file__).parent / "sveltekit_template"
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def generate_app(
|
|
21
|
-
resolved: ResolvedCurriculum,
|
|
22
|
-
output_dir: Path,
|
|
23
|
-
template_dir: Path | None = None,
|
|
24
|
-
) -> None:
|
|
25
|
-
"""Generate a SvelteKit project from a resolved curriculum.
|
|
26
|
-
|
|
27
|
-
Copies ``sveltekit_template/`` to ``output_dir`` atomically (write to a
|
|
28
|
-
temp directory, then move into place), then writes ``curriculum.json``
|
|
29
|
-
into ``output_dir/static/``.
|
|
30
|
-
|
|
31
|
-
If ``output_dir`` already exists it is replaced with a warning.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
resolved: Fully resolved curriculum from ``resolve_curriculum()``.
|
|
35
|
-
output_dir: Destination path for the generated SvelteKit project.
|
|
36
|
-
template_dir: Override template source. Defaults to the package's
|
|
37
|
-
``sveltekit_template/`` directory.
|
|
38
|
-
|
|
39
|
-
Raises:
|
|
40
|
-
GenerationError: If the template directory does not exist or any
|
|
41
|
-
file operation fails.
|
|
42
|
-
"""
|
|
43
|
-
src = template_dir or _TEMPLATE_DIR
|
|
44
|
-
|
|
45
|
-
if not src.exists():
|
|
46
|
-
raise GenerationError(
|
|
47
|
-
f"SvelteKit template directory not found: `{src}`. "
|
|
48
|
-
"Run `learningfoundry` from the project root, or pass "
|
|
49
|
-
"`template_dir` explicitly."
|
|
50
|
-
)
|
|
51
|
-
|
|
52
|
-
if output_dir.exists():
|
|
53
|
-
logger.warning(
|
|
54
|
-
"Output directory `%s` already exists and will be overwritten.",
|
|
55
|
-
output_dir,
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
_atomic_copy(src, output_dir)
|
|
59
|
-
_write_curriculum_json(resolved, output_dir)
|
|
60
|
-
|
|
61
|
-
logger.info("Generated SvelteKit project at: %s", output_dir)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def _atomic_copy(src: Path, dst: Path) -> None:
|
|
65
|
-
"""Copy src tree to dst atomically via a sibling temp directory."""
|
|
66
|
-
parent = dst.parent
|
|
67
|
-
parent.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
with tempfile.TemporaryDirectory(dir=parent, prefix=".lf_gen_") as tmp_str:
|
|
71
|
-
tmp = Path(tmp_str) / dst.name
|
|
72
|
-
shutil.copytree(src, tmp)
|
|
73
|
-
if dst.exists():
|
|
74
|
-
shutil.rmtree(dst)
|
|
75
|
-
tmp.rename(dst)
|
|
76
|
-
except OSError as exc:
|
|
77
|
-
raise GenerationError(
|
|
78
|
-
f"Failed to generate project at `{dst}`: {exc}"
|
|
79
|
-
) from exc
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _write_curriculum_json(resolved: ResolvedCurriculum, output_dir: Path) -> None:
|
|
83
|
-
"""Serialize ResolvedCurriculum to output_dir/static/curriculum.json."""
|
|
84
|
-
static_dir = output_dir / "static"
|
|
85
|
-
static_dir.mkdir(parents=True, exist_ok=True)
|
|
86
|
-
|
|
87
|
-
curriculum_json = output_dir / "static" / "curriculum.json"
|
|
88
|
-
try:
|
|
89
|
-
data = dataclasses.asdict(resolved)
|
|
90
|
-
curriculum_json.write_text(
|
|
91
|
-
json.dumps(data, indent=2, ensure_ascii=False) + "\n",
|
|
92
|
-
encoding="utf-8",
|
|
93
|
-
)
|
|
94
|
-
except OSError as exc:
|
|
95
|
-
raise GenerationError(
|
|
96
|
-
f"Failed to write curriculum.json to `{curriculum_json}`: {exc}"
|
|
97
|
-
) from exc
|
|
98
|
-
|
|
99
|
-
logger.debug("Wrote curriculum.json (%d bytes)", curriculum_json.stat().st_size)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/__init__.py
RENAMED
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/d3foundry_stub.py
RENAMED
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/nbfoundry_stub.py
RENAMED
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/protocols.py
RENAMED
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/src/learningfoundry/integrations/quizazz.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/lib/stores/curriculum.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/routes/+layout.svelte
RENAMED
|
File without changes
|
{learningfoundry-0.32.0 → learningfoundry-0.34.0}/sveltekit_template/src/routes/+page.svelte
RENAMED
|
File without changes
|
|
File without changes
|