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.}
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.
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.
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.