244 lines
9.1 KiB
Python
244 lines
9.1 KiB
Python
import html
|
|
import os
|
|
import re
|
|
import shutil
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
LESSON_RELATIVE = Path(
|
|
"contenus_pedagogiques/cycle_3/mathematiques/cm1/"
|
|
"01_nombres_calcul_resolution/01A_nombres_entiers"
|
|
)
|
|
MIRROR_RELATIVE = Path(
|
|
"Programme/contenus_pedagogiques/cycle_3/mathematiques/cm1/"
|
|
"01_nombres_calcul_resolution/01A_nombres_entiers"
|
|
)
|
|
LESSON_DIRS = [ROOT / "backend" / LESSON_RELATIVE, ROOT / MIRROR_RELATIVE]
|
|
|
|
SOURCE_BLOCK = (
|
|
"\n\n## Sources\n\n"
|
|
"- Source principale locale : `Programme/pdf_collecteur/state/synthese_cycle3_cm1_mathematiques.md`.\n"
|
|
"- Documents officiels de reference : PDF du programme de mathematiques cycle 3 conserves dans `Programme/Cycle 3/Math/`.\n"
|
|
"- Niveau : cycle 3, CM1, mathematiques, nombres/calcul/resolution, nombres entiers.\n"
|
|
)
|
|
|
|
|
|
def title_from_slug(slug: str) -> str:
|
|
parts = slug.split("_")
|
|
if parts and parts[0][:2].isdigit():
|
|
parts = parts[1:]
|
|
return " ".join(part.capitalize() for part in parts if part)
|
|
|
|
|
|
def svg_attr(attrs: str, name: str, default: float | None = None) -> float | None:
|
|
match = re.search(rf'\b{name}="(?P<value>[-\d.]+)"', attrs)
|
|
if match:
|
|
return float(match.group("value"))
|
|
return default
|
|
|
|
|
|
def extract_svg_sections(svg_path: Path) -> list[dict]:
|
|
text = svg_path.read_text(encoding="utf-8", errors="ignore")
|
|
sections: list[dict] = []
|
|
pattern = re.compile(
|
|
r'<g\s+transform="translate\((?P<x>[-\d.]+)\s+(?P<y>[-\d.]+)\)">\s*'
|
|
r'<rect\b(?P<attrs>[^>]*)/?>'
|
|
r'(?P<body>.*?)</g>',
|
|
re.DOTALL,
|
|
)
|
|
for match in pattern.finditer(text):
|
|
attrs = match.group("attrs")
|
|
rect_x = svg_attr(attrs, "x", 0)
|
|
rect_y = svg_attr(attrs, "y", 0)
|
|
width = svg_attr(attrs, "width")
|
|
height = svg_attr(attrs, "height")
|
|
if rect_x != 0 or rect_y != 0 or width is None or height is None:
|
|
continue
|
|
if width < 250 or height < 100:
|
|
continue
|
|
title_match = re.search(r'<text[^>]*>(?P<title>.*?)</text>', match.group("body"), re.DOTALL)
|
|
raw_title = re.sub(r"<[^>]+>", "", title_match.group("title") if title_match else "")
|
|
sections.append(
|
|
{
|
|
"index": len(sections),
|
|
"title": html.unescape(raw_title).strip() or f"Card {len(sections) + 1}",
|
|
"view_box": [
|
|
max(float(match.group("x")) - 28, 0),
|
|
max(float(match.group("y")) - 28, 0),
|
|
width + 56,
|
|
height + 56,
|
|
],
|
|
}
|
|
)
|
|
return sections
|
|
|
|
|
|
def render_svg_section(svg_path: Path, section: dict) -> str:
|
|
x, y, width, height = section["view_box"]
|
|
text = svg_path.read_text(encoding="utf-8", errors="ignore")
|
|
text = re.sub(r'\swidth="[^"]+"', f' width="{int(width)}"', text, count=1)
|
|
text = re.sub(r'\sheight="[^"]+"', f' height="{int(height)}"', text, count=1)
|
|
text = re.sub(r'\sviewBox="[^"]+"', f' viewBox="{x:g} {y:g} {width:g} {height:g}"', text, count=1)
|
|
return text
|
|
|
|
|
|
def move_file(source: Path, target: Path) -> None:
|
|
if not source.exists():
|
|
return
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
if target.exists():
|
|
target.unlink()
|
|
shutil.move(str(source), str(target))
|
|
|
|
|
|
def ensure_source_block(path: Path) -> None:
|
|
if not path.exists():
|
|
return
|
|
text = path.read_text(encoding="utf-8", errors="ignore").rstrip()
|
|
if "## Sources" not in text and "# Sources" not in text:
|
|
path.write_text(text + SOURCE_BLOCK + "\n", encoding="utf-8")
|
|
|
|
|
|
def write_if_missing(path: Path, text: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not path.exists():
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
|
|
def migrate_fiches_and_cards(lesson_dir: Path) -> list[str]:
|
|
old_svg_dir = lesson_dir / "svg"
|
|
old_card_dir = lesson_dir / "card_contexts"
|
|
fiche_slugs = sorted({path.stem for path in list(lesson_dir.glob("[0-9][0-9]_*.md")) + list(old_svg_dir.glob("*.svg"))})
|
|
|
|
for index, slug in enumerate(fiche_slugs, start=1):
|
|
fiche_dir = lesson_dir / "fiches" / slug
|
|
move_file(lesson_dir / f"{slug}.md", fiche_dir / "fiche.md")
|
|
move_file(old_svg_dir / f"{slug}.svg", fiche_dir / "fiche.svg")
|
|
ensure_source_block(fiche_dir / "fiche.md")
|
|
|
|
fiche_svg = fiche_dir / "fiche.svg"
|
|
if not fiche_svg.exists():
|
|
continue
|
|
|
|
card_dir = lesson_dir / "cards" / slug
|
|
legacy_context_dir = old_card_dir / slug
|
|
sections = extract_svg_sections(fiche_svg)
|
|
if not sections:
|
|
sections = [{"index": 0, "title": title_from_slug(slug), "view_box": [0, 0, 1600, 900]}]
|
|
|
|
for section in sections:
|
|
card_number = section["index"] + 1
|
|
target_md = card_dir / f"card_{card_number:02d}.md"
|
|
for legacy_name in [
|
|
legacy_context_dir / f"Fiche{index}Card{card_number}.md",
|
|
legacy_context_dir / f"card_{card_number:02d}.md",
|
|
]:
|
|
if legacy_name.exists():
|
|
move_file(legacy_name, target_md)
|
|
break
|
|
write_if_missing(
|
|
target_md,
|
|
(
|
|
f"# {section['title']}\n\n"
|
|
f"Contexte de card pour la fiche `{slug}`.\n\n"
|
|
"## Intention\n\n"
|
|
"Faire verbaliser le point cle de la zone affichee avant de passer a la suite.\n"
|
|
+ SOURCE_BLOCK
|
|
),
|
|
)
|
|
ensure_source_block(target_md)
|
|
target_svg = card_dir / f"card_{card_number:02d}.svg"
|
|
target_svg.write_text(render_svg_section(fiche_svg, section), encoding="utf-8")
|
|
|
|
return fiche_slugs
|
|
|
|
|
|
def migrate_adaptive_content(lesson_dir: Path) -> None:
|
|
kit_dir = lesson_dir / "01_lecture_ecriture_nombres_entiers_kit_adaptatif"
|
|
if not kit_dir.exists():
|
|
return
|
|
|
|
tests_dir = lesson_dir / "tests"
|
|
for md_path in sorted((kit_dir / "test_diagnostic").glob("*.md")):
|
|
if md_path.name == "README.md":
|
|
continue
|
|
move_file(md_path, tests_dir / md_path.name)
|
|
ensure_source_block(tests_dir / md_path.name)
|
|
for support_dir in [kit_dir / "svg" / "tests" / "questions", kit_dir / "svg" / "tests" / "reponses"]:
|
|
for svg_path in sorted(support_dir.glob("*.svg")):
|
|
move_file(svg_path, tests_dir / svg_path.name)
|
|
for md_name in ["00_principe_adaptatif.md", "NOTES_SESSION_2026-04-24.md"]:
|
|
move_file(kit_dir / md_name, tests_dir / md_name)
|
|
ensure_source_block(tests_dir / md_name)
|
|
|
|
parcours_dir = lesson_dir / "exercices" / "parcours_adaptes"
|
|
for exercise_md in sorted((kit_dir / "exercices").glob("*/exercices.md")):
|
|
target_dir = parcours_dir / exercise_md.parent.name
|
|
move_file(exercise_md, target_dir / "exercices.md")
|
|
ensure_source_block(target_dir / "exercices.md")
|
|
svg_path = kit_dir / "svg" / "exercices" / "profils" / f"{exercise_md.parent.name}.svg"
|
|
move_file(svg_path, target_dir / "exercices.svg")
|
|
|
|
|
|
def build_lesson_markdown(lesson_dir: Path) -> None:
|
|
parts = []
|
|
source_lesson = lesson_dir / "sources" / "lecon_originale.md"
|
|
kit_lesson = lesson_dir / "01_lecture_ecriture_nombres_entiers_kit_adaptatif" / "lecon" / "lecon.md"
|
|
if kit_lesson.exists():
|
|
source_lesson.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.move(str(kit_lesson), str(source_lesson))
|
|
ensure_source_block(source_lesson)
|
|
parts.append(source_lesson.read_text(encoding="utf-8", errors="ignore").strip())
|
|
|
|
readme = lesson_dir / "README.md"
|
|
if readme.exists():
|
|
parts.append(readme.read_text(encoding="utf-8", errors="ignore").strip())
|
|
for fiche_md in sorted((lesson_dir / "fiches").glob("*/fiche.md")):
|
|
parts.append(fiche_md.read_text(encoding="utf-8", errors="ignore").strip())
|
|
|
|
body = "\n\n---\n\n".join(part for part in parts if part)
|
|
text = (
|
|
"# Lecon globale - CM1 - Nombres entiers\n\n"
|
|
"Cette lecon globale sert de contexte general a Professeur TOP pour la lecon nombres entiers.\n"
|
|
+ SOURCE_BLOCK
|
|
+ "\n## Contenu consolide\n\n"
|
|
+ body
|
|
+ "\n"
|
|
)
|
|
(lesson_dir / "lecon.md").write_text(text, encoding="utf-8")
|
|
|
|
|
|
def cleanup_legacy_dirs(lesson_dir: Path) -> None:
|
|
def handle_remove_error(function, path, exc_info) -> None:
|
|
os.chmod(path, stat.S_IWRITE)
|
|
function(path)
|
|
|
|
for legacy_dir in [
|
|
lesson_dir / "svg",
|
|
lesson_dir / "card_contexts",
|
|
lesson_dir / "01_lecture_ecriture_nombres_entiers_kit_adaptatif",
|
|
]:
|
|
if legacy_dir.exists():
|
|
shutil.rmtree(legacy_dir, onerror=handle_remove_error)
|
|
|
|
|
|
def migrate_lesson(lesson_dir: Path) -> None:
|
|
if not lesson_dir.exists():
|
|
return
|
|
fiche_slugs = migrate_fiches_and_cards(lesson_dir)
|
|
migrate_adaptive_content(lesson_dir)
|
|
build_lesson_markdown(lesson_dir)
|
|
cleanup_legacy_dirs(lesson_dir)
|
|
print(f"migrated: {lesson_dir} ({len(fiche_slugs)} fiches)")
|
|
|
|
|
|
def main() -> None:
|
|
for lesson_dir in LESSON_DIRS:
|
|
migrate_lesson(lesson_dir)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|