documentation
← Retour au playground

HTSL — documentation

HTSL (HyperText Structured Language) est un langage de balisage léger, pensé pour les mathématiciens et physiciens : une syntaxe concise qui compile en HTML, avec des formules (KaTeX), des graphes et des scènes 2D/3D animées (Plotly, Three.js), des composants réutilisables — le tout déclaratif et rendu par un runtime unique.

{h1:Théorème de Pythagore}
{p:Dans un triangle rectangle : {@mti: a^2 + b^2 = c^2}.}
{@plot[fn="sin(x)/x", xrange="(-15,15)", title="sinc"]/}

Philosophie

Trois principes guident tout le moteur :

  • Tout est objet structuré. Balises, formules, géométrie, composants partagent le même AST. Les objets {@…} sont décrits dans un registre introspectable : l'autocomplétion et la palette du playground en sont générées — rien n'est codé en dur.
  • Sortie 100 % déclarative. Le rendu ne produit jamais de <script> exécutable : chaque élément dynamique est un nœud de données (class="htsl-<type>" data-htsl-<type>="…").
  • Un runtime unique. Une seule couche JS interprète ces données : elle charge les dépendances (KaTeX/Plotly/Three.js) une fois, hydrate de façon idempotente, et nettoie les ressources.

Pourquoi ces choix (motivations)

Déclaratif + runtime

Émettre des <script> impératifs causait trois bugs structurels : code lancé avant le CDN, ré-exécuté aux rerenders, ou avant que le DOM soit prêt. Un runtime idempotent les élimine par construction : il attend ses dépendances, ne redessine que ce qui change (react), et purge ce qui disparaît.

Zéro JS depuis le contenu

Le contenu HTSL ne peut pas produire de JavaScript exécutable (un {script: …} inline est rendu inerte). On évite toute une classe de problèmes de sécurité et de cycle de vie ; le dynamisme passe par des objets déclaratifs.

Introspection

Le registre d'objets est la source unique de vérité. Palette, autocomplétion, aide contextuelle, et même ce catalogue sont générés depuis registry.list()/describe() — donc toujours à jour.

Iframe isolée

Le rendu vit dans une iframe : on peut y charger Tailwind, Bootstrap, KaTeX, Plotly… sans jamais polluer l'interface, et le morphing chirurgical (data-htsl-hash + morphdom) ne touche que les nœuds réellement modifiés.

Les outils derrière

Chaque dépendance est chargée à la demande, une seule fois, par le runtime — uniquement quand un document en a besoin.

KaTeX Formules

Rendu typographique des mathématiques (rapide, sans serveur). Repli sur le LaTeX brut si absent.

Plotly Graphes & géométrie

Graphes de fonctions 2D et scènes géométriques 2D/3D (repères, cercles, sphères, surfaces mathématiques). Mise à jour en place via Plotly.react.

Three.js 3D animé (WebGL)

Scènes 3D libres : formes, vecteurs, trajectoires, surfaces z=f(x,y), labels, et une timeline d'animations (dont un vrai morph de géométrie).

CodeMirror 6 Éditeur

Coloration HTSL (et JS dans {script}), autocomplétion contextuelle, indentation, linter — réutilisables via le paquet @noah-medra/htsl-codemirror.

morphdom Rendu chirurgical

Seuls les nœuds réellement changés sont mis à jour ; un bloc au hash inchangé n'est jamais re-rendu (ni re-KaTeX, ni re-Plotly).

Tailwind / Bootstrap Mise en page

Chargés depuis le document HTSL (CDN dans l'iframe). Le style appartient au document, pas au moteur.

Syntaxe de base

  • Élément : {tag: contenu} · classes {tag.box: …} · id {tag#main: …}
  • Attributs entre crochets, séparés par des virgules, chaînes entre guillemets doubles : {a[href="/x", target="_blank"]: lien}
  • Auto-fermant : {img[src="a.png"]/}
  • Commentaire : {!-- ignoré --} · Échappements : \{ \} \: \$
{div.card[class="p-4 rounded-xl bg-white"]:
  {h2:Titre}
  {p:Un paragraphe avec du {strong:texte fort}.}
}

Mathématiques

  • En ligne : {@mti: a^2+b^2} ou $a^2+b^2$ — Bloc : {@mtb: …} ou $$…$$
  • Équation numérotée : {@mte[label=euler]: e^{i\pi}+1=0} — Référence : {@mtr[to=euler]/}
  • Aligné / cas / système : {@mta: {line:…}}, {@mtc[intro="…"]: {case:…}}, {@mts: {line:…}} · Fraction {@mof:{num:1}{den:2}}
{@mte[label=ham]: H = \tfrac{1}{2}\big(p^2 + \omega^2 q^2\big)}
{p:Le hamiltonien {@mtr[to=ham]/} décrit l'énergie totale.}

Encadrés sémantiques (théorèmes, définitions…)

Des cadres stylés et auto-numérotés par type, pour structurer un cours (l'équivalent de amsthm en LaTeX). Un encadré peut contenir n'importe quel HTSL (formules, listes, scènes…).

  • Numérotés : {@theorem:…} (alias theoreme, thm), {@definition:…} (def), {@property:…} (propriete), {@example:…} (exemple, ex).
  • Non numérotés : {@proof:…} (preuve), {@remark:…} (remarque), {@warning:…} (attention).
  • Attributs optionnels : title="…" (affiché après le numéro) et label=… pour un renvoi cliquable {@ref[to=label]/} → « Théorème N ».
{@theorem[title="Pythagore", label=pyth]:
  {p:Dans un triangle rectangle d'hypoténuse $c$ : $$a^2 + b^2 = c^2$$}
}
{@example: {p:Avec $a=3$, $b=4$ : $c=5$.}}
{@proof: {p:On découpe le carré de côté $a+b$ de deux façons.}}
{p:Ce résultat est {@ref[to=pyth]/}.}

Tableaux de variations & de signes

Le fameux tableau de variations (et le tableau de signes), central au lycée et en prépa — rendu en grille CSS, valeurs en KaTeX.

  • {@variations[var="x", fn="f(x)"]:…} : un {pt[x="…", y="…"]/} par point (abscisse + valeur), séparés par {up/} ou {down/}. Les positions (haut = maximum, bas = minimum) sont déduites des flèches. Utilisez \infty pour l'infini.
  • {@signs[var="x", fn="…"]:…} : des {pt[x="…"]/} ({pt[x="…", zero=true]/} pour une racine) séparés par {s: +} ou {s: -}.
{@variations[var="x", fn="f(x)"]:
  {pt[x="-\infty", y="+\infty"]/} {down/}
  {pt[x="1", y="-1"]/} {up/}
  {pt[x="+\infty", y="+\infty"]/}
}
{@signs[var="x", fn="f'(x)"]:
  {pt[x="-\infty"]/} {s: -} {pt[x="1", zero=true]/} {s: +} {pt[x="+\infty"]/}
}

Correction cachée & onglets

Deux objets parfaits pour les exercices corrigés :

  • {@reveal[title="Solution"]: …} — un bloc dépliable (clic pour révéler la correction). C'est un <details> natif : aucun JavaScript. Option open=true pour l'ouvrir d'emblée.
  • {@tabs:} avec des {@tabs.tab[title="…"]: …} — des onglets (« Énoncé / Indice / Solution », « Méthode 1 / 2 »…). La navigation est gérée par le runtime (clic sur un onglet) ; sans le runtime, le premier onglet reste visible.
{@tabs:
  {@tabs.tab[title="Énoncé"]: {p:Calculer $c$ pour $a=3$, $b=4$.}}
  {@tabs.tab[title="Indice"]: {p:Utiliser $a^2+b^2=c^2$.}}
  {@tabs.tab[title="Solution"]: {@reveal[title="Voir le détail"]: {p:$c=5$.}}}
}

Auto-évaluation : QCM & cartes

  • {@quiz:} — une question à choix : {q:…} (énoncé), des {opt:…} (la bonne réponse est {opt[correct=true]:…}) et un {explain:…} optionnel. Au clic sur une option : feedback ✓ / ✗, la bonne réponse est révélée et l'explication s'affiche (géré par le runtime).
  • {@flashcard:} — une carte à retourner : {front:…} et {back:…}. Un clic la retourne — c'est du CSS pur (zéro JS).
{@quiz:
  {q: Quelle est l'hypoténuse d'un triangle rectangle ?}
  {opt[correct=true]: Le côté opposé à l'angle droit}
  {opt: Le plus petit côté}
  {explain: C'est toujours le plus long côté.}
}
{@flashcard: {front: $e^{i\pi} + 1$} {back: $= 0$}}

Composants & variables

{!define carte[titre, couleur=indigo]:
  {div[class="card text-{$couleur}-600"]:
    {h3:{$titre}}
    {div.body:{$children}}
  }
}
{@carte[titre="Bonjour"]: contenu injecté}

{!set vitesse: 9.81}
{!set H: "\tfrac{1}{2}\big(p^2 + \omega^2 q^2\big)"}   {!-- LaTeX verbatim (guillemets) --}
{p:g = {$vitesse} m/s², {@mtb: H = {$H}}}

Astuce. Une valeur de {!set} entre guillemets est prise verbatim : ses { } et \ ne sont pas interprétés — idéal pour réutiliser des macros LaTeX (hamiltoniens, opérateurs…).

Graphes 2D

Une fonction : {@plot[fn="sin(x)/x", xrange="(-15,15)", title="…"]/}. Plusieurs courbes avec légende : {@plot} devient un conteneur.

{@plot[xrange="(-6.28,6.28)", title="Trigonométrie"]:
  {@plot.curve[fn="sin(x)", label="sin"]/}
  {@plot.curve[fn="cos(x)", label="cos"]/}
}

Les expressions (sin(x)/x, cos(x)…) sont évaluées par un interpréteur sûr (pas de eval) : opérateurs + - * / % ^, fonctions usuelles, constantes pi e tau phi.

Graphiques de données

Au-delà des fonctions, {@chart} trace de vraies données (Plotly) : barres, camembert, ligne ou histogramme. Idéal pour les statistiques.

  • Chaque valeur est un {pt[x="…", y=…]/} (x = catégorie/abscisse, y = valeur ; color optionnel).
  • Pour un histogramme, fournir les valeurs brutes via values="a,b,c,…" (et bins=N optionnel), sans {pt}.
  • Titres : title, xtitle, ytitle.
{@chart[type="bar", title="Notes au DS", ytitle="Élèves"]:
  {pt[x="0-5", y=2]/}  {pt[x="5-10", y=6]/}
  {pt[x="10-15", y=11]/}  {pt[x="15-20", y=5]/}
}
{@chart[type="pie"]: {pt[x="AB", y=10]/} {pt[x="TB", y=3]/}}
{@chart[type="histogram", values="160,165,168,170,170,175", bins=6]/}

Paramètre interactif

Le « moment Desmos » : un {@param} ajoute un curseur, et un {@plot} dont la fonction utilise ce paramètre se met à jour en direct quand on le bouge — recalculé par le runtime (interpréteur sûr, jamais eval). Aucun JavaScript dans le contenu.

  • {@param[name="a", min=…, max=…, step=…, value=…]/} — le name est le nom utilisé dans une fn de {@plot}. Plusieurs paramètres possibles.
{@param[name="a", min=-3, max=3, step=0.1, value=1]/}
{@param[name="b", min=0.5, max=4, step=0.1, value=1]/}
{@plot[fn="a*sin(b*x)", xrange="(-6.28,6.28)", title="y = a·sin(b·x)"]/}

Géométrie (Plotly)

{@mg2.scene:
  {@mg2.frame[grid=true]/}
  {@mg2.circle[center="(0,0)", radius=2]/}
  {@mg2.point[x=0, y=0, name=O]/}
}
{@mg3.scene:
  {@mg3.space[grid=true]/}
  {@mg3.sphere[center="(0,0,0)", radius=2]/}
}

Scènes 3D animées (Three.js)

Formes, vecteurs, lignes/trajectoires, surfaces z=f(x,y), labels, repère/grille, contrôles souris — et des animations par id.

{@s3.scene[height=440, controls=true, distance=9]:
  {@s3.axes[size=3]/}  {@s3.grid[size=12]/}
  {@s3.box[id="A", color="#f472b6"]/}
  {@s3.torus[id="B", x=99, color="#34d399"]/}     {!-- gabarit hors champ --}
  {@s3.surface[z="sin(x)*cos(y)", xrange="(-3,3)", yrange="(-3,3)"]/}
  {@s3.vector[from="(0,0,0)", to="(2,2,1)"]/}
  {@s3.animate[target="A", action="move", to="(2,2,0)", duration=2]/}
  {@s3.animate[target="A", action="transform", to="B", duration=2]/}   {!-- vrai morph de forme --}
}

Actions : move, rotate, scale, color, fade, transform (morph de géométrie réel + couleur). Options : duration, delay, at, easing ; la scène loope par défaut.

Présentations

Un diaporama navigable : le conteneur {@slider:…} et un {@slider.slide:…} par slide. Boutons ⟵ / ⟶, flèches clavier ←/→, plein écran, compteur et barre de progression — le tout géré par le runtime du moteur. Aucun JavaScript n'est émis depuis le contenu : un slide reste un nœud déclaratif (voir Sécurité).

{@slider:
  {@slider.slide: {h1:Titre de la présentation}}
  {@slider.slide: {h2:Une équation} {@mte: e^{i\pi} + 1 = 0}}
  {@slider.slide: {h2:Un graphe} {@plot[fn="sin(x)/x", xrange="(-15,15)"]/}}
}
  • Un slide peut contenir n'importe quel HTSL : titres, texte, formules, scènes 2D/3D, graphes…
  • Seuls les {@slider.slide:…} deviennent des slides ; les autres enfants sont ignorés.
  • À l'export PDF, chaque slide devient une page (la barre de navigation est masquée) — pratique pour distribuer un cours.
  • Dégradation gracieuse : même sans le runtime, le premier slide s'affiche.

Frameworks CSS

Le rendu est une iframe isolée : chargez n'importe quel framework depuis votre document. Les classes passent par [class="…"] (les variantes hover:, md:, w-1/2 ne rentrent pas dans le raccourci .classe).

{link[rel="stylesheet", href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"]/}
{script[src="https://cdn.tailwindcss.com"]/}

{div[class="bg-white ring-1 ring-slate-200 rounded-xl p-4 hover:shadow-md"]:Carte Tailwind}

Éditer depuis le rendu

  • Texte : cliquez un texte pour le corriger en place (sans syntaxe).
  • Bloc : double-cliquez un bloc parent → un vrai éditeur HTSL (coloration, autocomplétion) s'ouvre translucide par-dessus. ⌘/Ctrl+Entrée valide, Échap annule.

Sécurité

  • Échappement HTML par défaut de tout texte et de toute valeur d'attribut (prévention XSS).
  • Le contenu HTSL ne produit jamais de JS exécutable : un {script: …code…} inline est rendu inerte (type="text/plain"). Seul {script[src=…]/} (CDN choisi par l'auteur) charge du code.

Intégration — utiliser HTSL hors du playground

Le moteur est distribué en deux paquets npm publics — @noah-medra/htsl-core (le moteur + le runtime) et @noah-medra/htsl-codemirror (l'éditeur, optionnel) — plus un bundle CDN. L'architecture est toujours la même : compile() produit du HTML inerte (des nœuds data-htsl-*, jamais de <script> exécutable), puis un runtime unique hydrate la page (KaTeX/Plotly/Three.js chargés à la demande). Trois façons de l'exploiter, du plus simple au plus intégré.

1 · Sans rien installer — balise <script> (CDN)

Idéal pour une page statique, un blog, un cours. Un seul script : il expose le global htsl_engine et installe le runtime (window.HTSL), qui hydrate automatiquement ce que tu injectes.

<script src="https://unpkg.com/@noah-medra/htsl-core/dist-min/htsl.auto.global.js"></script>
<div id="out"></div>
<script>
  document.getElementById("out").innerHTML =
    htsl_engine.compile("{h1:Bonjour} {@mte: E = mc^2} {@plot[fn=\"sin(x)/x\", xrange=\"(-15,15)\"]/}");
  // le runtime (MutationObserver) hydrate la formule et le graphe tout seul
</script>

Variantes disponibles dans dist-min/ : htsl.global.js (même global, sans auto-runtime — tu appelles htsl_engine.installHtslRuntime() toi-même) et htsl.min.js (ESM minifié pour un import par URL). Remplace unpkg.com par cdn.jsdelivr.net/npm si tu préfères jsDelivr.

2 · Dans une application — npm

npm install @noah-medra/htsl-core
import { compile, installHtslRuntime } from "@noah-medra/htsl-core";

// 1) compiler du HTSL en HTML (nœuds de données, inertes)
document.querySelector("#doc").innerHTML = compile(
  "{h2:Trajectoire} {@plot[fn=\"cos(x)*exp(-x*x/20)\", xrange=\"(-10,10)\"]/}"
);

// 2) installer le runtime une fois : il hydrate les data-htsl-* présents
//    (et à venir), en chargeant KaTeX/Plotly/Three uniquement si nécessaire
installHtslRuntime();

API utile : parse() (HTSL → AST), render(ast, opts) (AST → HTML, options prettyPrint/allowedTags), compile() (les deux), mathCss (styles des équations), registry (introspection des objets), HTSLError. Le paquet est en ESM, typé (TypeScript), sans dépendance dans le cœur.

3 · Côté serveur / build — HTML statique

Pour générer des fichiers (polycopiés, site statique, CI), compile au build et sers le HTML. Ajoute le runtime côté client uniquement si le document contient du dynamique (formules, graphes, scènes).

import { compile, mathCss } from "@noah-medra/htsl-core";
import { writeFileSync } from "node:fs";

const body = compile(sourceHtsl, { prettyPrint: true });
writeFileSync("sortie.html", `<!doctype html><html><head>
  <style>${mathCss}</style>
</head><body>${body}
  <script src="https://unpkg.com/@noah-medra/htsl-core/dist-min/htsl.auto.global.js"></script>
</body></html>`);

Bonus · un éditeur HTSL dans ton app

Le même éditeur que ce playground (coloration, autocomplétion, indentation, linter, pliage) est réutilisable via @noah-medra/htsl-codemirror sur CodeMirror 6.

npm install @noah-medra/htsl-codemirror @noah-medra/htsl-core codemirror
import { EditorView, basicSetup } from "codemirror";
import { autocompletion } from "@codemirror/autocomplete";
import { parse, registry } from "@noah-medra/htsl-core";
import { htslLanguage, htslCompletion, htslLinter } from "@noah-medra/htsl-codemirror";

new EditorView({
  parent: document.querySelector("#editor"),
  doc: "{div.box:Bonjour {@mti: x^2}}",
  extensions: [
    basicSetup,
    htslLanguage(),
    autocompletion({ override: [htslCompletion(registry)] }),
    htslLinter(parse),
  ],
});

Paquets & liens. @noah-medra/htsl-core · @noah-medra/htsl-codemirror · code source sur GitHub. Sécurité : où qu'il s'exécute, le moteur n'émet jamais de JS depuis le contenu (voir Sécurité).

Catalogue d'objets

Généré en direct depuis l'introspection du moteur (registry.list()) — donc toujours à jour.

Prompt pour une IA

Collez ce prompt à n'importe quel modèle pour qu'il sache rédiger des documents HTSL automatiquement. Il inclut la syntaxe, les contraintes et le catalogue complet.