commit 02cf82447b3ad8e53fcba49d12e38f0bab0a42a9 Author: Cédrix Date: Sat Oct 10 21:44:32 2026 +0200 Créer le site des présentations d'Alpinux Un dossier par présentation en Markdown, converti en diaporama HTML autonome par Marp CLI, et une page d'accueil générée qui les liste toutes. - scripts/build.mjs : build strict (fiche validée, images vérifiées, aucune ressource externe hormis le logo), page d'accueil filtrable, presentations.json et .htaccess pour l'en-tête CORS - theme/alpinux.css : thème commun pour la vidéoprojection, classes titre et demo - slides/_modele : modèle commenté, non publié - slides/pdf-signer : « Lire, remplir et signer ses PDF », avec notes d'orateur - scripts/deploy-presentations.sh et deploy/ : déploiement par webhook Gitea, sur le modèle du wiki et de www Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M713yNBL8cssBA1Zri5QLT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c34a43f --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Dépendances et sortie du build : tout se régénère +node_modules/ +public/ +.build-*/ + +# Exports faits à la main +*.pdf + +# Jetons et secrets : jamais versionnés +.gitea-token +*.secret +.env diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/CONTRIBUER.md b/CONTRIBUER.md new file mode 100644 index 0000000..c11ecf8 --- /dev/null +++ b/CONTRIBUER.md @@ -0,0 +1,196 @@ +# Proposer une présentation + +Vous présentez un sujet à une réunion d'Alpinux ? Votre diaporama a sa place ici. Il +s'écrit dans un simple fichier texte, en Markdown : pas de logiciel de présentation à +maîtriser, et le résultat s'affiche dans n'importe quel navigateur. + +Vous n'avez jamais utilisé Git ? Ce n'est pas un obstacle : la [méthode sans rien +installer](#la-méthode-sans-rien-installer) se fait entièrement depuis le site de la +forge. Et si vous bloquez, demandez de l'aide en réunion ou sur la liste de diffusion — +quelqu'un mettra votre fichier en ligne avec vous. + +--- + +## En bref + +1. **Copier** le dossier `slides/_modele` sous un nouveau nom. +2. **Écrire** vos diapos dans `slides.md`. +3. **Prévisualiser** sur votre ordinateur. +4. **Ouvrir une pull request** : un mainteneur relit, fusionne, et le site se met à + jour tout seul. + +--- + +## 1. Copier le modèle + +Chaque présentation vit dans son dossier, sous `slides/`. Le nom du dossier devient +l'adresse du diaporama : `slides/pdf-signer/` est publié sur +`presentations.alpinux.org/pdf-signer/`. + +Choisissez un nom court, en **minuscules, sans accent ni espace**, avec des tirets : +`sauvegardes`, `nextcloud-quotidien`, `kicad-debuter`. + +```bash +cp -r slides/_modele slides/mon-sujet +``` + +Vous obtenez : + +``` +slides/mon-sujet/ +├── slides.md votre diaporama +└── images/ vos images (supprimez exemple.svg) +``` + +--- + +## 2. Écrire + +Ouvrez `slides/mon-sujet/slides.md` dans un éditeur de texte. Le modèle est commenté : +il montre une diapo de titre, une liste, du code, un tableau, une image et une diapo de +démonstration. + +### La fiche, en haut du fichier + +Le bloc entre les deux lignes `---` décrit votre présentation. C'est lui qui alimente +la page d'accueil du site. + +| Champ | Obligatoire | Ce qu'on y met | +|---------------|:-----------:|-------------------------------------------------------------| +| `title` | oui | Le titre | +| `description` | oui | Une phrase : ce qu'on saura faire en sortant | +| `auteur` | oui | Votre nom, ou un pseudonyme | +| `date` | oui | La date de la séance, `AAAA-MM-JJ` — par exemple `2026-10-15` | +| `niveau` | oui | `debutant`, `intermediaire` ou `confirme` | +| `format` | oui | `eclair`, `court` ou `long` | +| `tags` | oui | Des mots-clés : `[pdf, bureautique]` | +| `wiki` | non | L'adresse de la fiche du wiki, s'il y en a une | +| `brouillon` | non | `true` tant que le diaporama n'est pas prêt à être publié | + +Laissez tels quels `marp`, `theme` et `paginate`. Et pensez à **retirer +`brouillon: true`** quand vous avez fini : le modèle le contient, et une présentation +en brouillon n'est pas publiée. + +### Les diapos + +- Trois tirets seuls sur une ligne (`---`) séparent deux diapos. +- `## Un titre` donne le titre de la diapo. +- Un commentaire `` est une **note d'orateur** : invisible à l'écran, + affichée dans la vue présentateur. +- `` en tête d'une diapo lui donne la mise en page de titre ; + `` annonce une démonstration. + +### Quelques conseils pour la vidéoprojection + +- **Peu de texte.** Trois à cinq lignes par diapo. Ce que vous direz va dans les notes. +- **Pas plus de six ou sept lignes de code** par diapo. +- **Des tableaux étroits** : deux ou trois colonnes. +- **Des images à vous**, ou sous licence libre, rangées dans `images/`. Une image + chargée depuis un autre site est refusée : le diaporama doit fonctionner sans réseau + dans la salle. +- **Pas de données personnelles** dans les captures d'écran : adresse, nom de + machine, courriel. + +--- + +## 3. Prévisualiser + +Il faut [Node.js](https://nodejs.org/fr) (version 22 ou plus récente). Une seule fois, +à la racine du dépôt : + +```bash +npm ci +``` + +Puis, à chaque séance de travail : + +```bash +npm run dev +``` + +Ouvrez , cliquez sur votre dossier puis sur `slides.md`. La page +se **recharge toute seule** à chaque enregistrement du fichier. Ctrl + +C dans le terminal arrête l'aperçu. + +Dans le diaporama : les flèches pour avancer, F pour le plein écran, +P pour la vue présentateur avec vos notes. + +Avant de proposer votre travail, lancez le build complet : + +```bash +npm run build +``` + +Il vérifie votre fiche et vos images, et dit précisément ce qui ne va pas : + +``` +ÉCHEC du build — 1 erreur(s) dans les sources : + - slides/mon-sujet/slides.md : « niveau » invalide : « expert » — attendu debutant | intermediaire | confirme +``` + +Le site complet est alors dans `public/` : ouvrez `public/index.html` pour voir votre +présentation dans la liste. Si elle n'y est pas, c'est qu'il reste `brouillon: true`. + +> Vous utilisez VS Code ou VSCodium ? L'extension *Marp for VS Code* affiche l'aperçu +> à côté du texte. Indiquez-lui le thème dans les réglages +> (`markdown.marp.themes` : `./theme/alpinux.css`). + +--- + +## 4. Ouvrir une pull request + +Une *pull request* est une proposition de modification : vous ne touchez pas +directement au site, un mainteneur relit d'abord. + +1. Créez un compte sur si vous n'en avez pas. +2. Sur la page du dépôt `alpinux-presentations`, cliquez sur **Bifurcation** (*Fork*) : + vous obtenez votre copie du dépôt. +3. Récupérez-la, et créez une branche pour votre présentation : + + ```bash + git clone https://gitea.alpinux.org/VOTRE-COMPTE/alpinux-presentations.git + cd alpinux-presentations + git switch -c mon-sujet + ``` + +4. Faites votre travail (étapes 1 à 3), puis enregistrez-le et envoyez-le : + + ```bash + git add slides/mon-sujet + git commit -m "Ajouter la présentation « Mon sujet »" + git push origin mon-sujet + ``` + +5. Retournez sur la forge : un bandeau propose **Nouvelle demande d'ajout**. Validez, + en visant la branche `main` du dépôt d'Alpinux. + +Un mainteneur relit, vous répond dans la pull request si quelque chose est à +reprendre, puis fusionne. Le site est à jour dans la minute qui suit. + +Le guide [Git chez Alpinux](https://wiki.alpinux.org/guides/git-alpinux/) du wiki +détaille chacune de ces étapes. + +### La méthode sans rien installer + +Tout peut se faire depuis le navigateur, sur : + +1. Faites une **bifurcation** du dépôt. +2. Dans votre copie, ouvrez `slides/_modele/slides.md`, copiez son contenu. +3. **Ajouter un fichier → Nouveau fichier**, nommez-le `slides/mon-sujet/slides.md` + (taper les `/` crée les dossiers), collez, adaptez. +4. Pour les images : **Ajouter un fichier → Téléverser**, dans + `slides/mon-sujet/images/`. +5. En bas de la page, choisissez « Créer une nouvelle branche », puis ouvrez la + **demande d'ajout**. + +Vous n'aurez pas d'aperçu : dites-le dans la pull request, un mainteneur vérifiera le +rendu pour vous. + +--- + +## Corriger une présentation existante + +Une faute, un lien mort, une commande qui a changé ? Même chemin : modifiez le +`slides.md` concerné et ouvrez une pull request. Sur la forge, le crayon en haut du +fichier suffit pour une petite correction. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bab669 --- /dev/null +++ b/README.md @@ -0,0 +1,226 @@ +# Présentations d'Alpinux + +Les diaporamas des réunions d'[Alpinux](https://alpinux.org), le groupe d'utilisateurs +de logiciels libres de Savoie — publiés sur et +projetés depuis ce site le soir de la réunion. + +Chaque présentation est un fichier Markdown. [Marp](https://marp.app) en fait un +diaporama HTML autonome, et le build génère la page d'accueil qui les liste toutes. + +**Vous voulez proposer une présentation ?** Tout est dans +[CONTRIBUER.md](CONTRIBUER.md). + +--- + +## Je veux… + +| Ce que vous voulez faire | Ce que vous faites | +|-------------------------------------------|-----------------------------------------------------------------| +| Proposer une présentation | Suivre [CONTRIBUER.md](CONTRIBUER.md) | +| Voir mes diapos pendant que j'écris | `npm run dev`, puis | +| Vérifier que tout est bon avant de pousser | `npm run build` | +| Obtenir un PDF d'un diaporama | `npm run pdf -- pdf-signer` | +| Mettre en ligne | Fusionner sur `main` — le déploiement suit, en une minute | +| Comprendre pourquoi ce n'est pas en ligne | `tail -30 /var/log/presentations-deploy.log` sur le serveur | +| Installer ou réparer le déploiement | Lire [deploy/webhook.md](deploy/webhook.md) | + +--- + +## Installation + +Il faut **Node.js 24 LTS** — c'est la version du serveur, indiquée dans `.nvmrc`. La +22 LTS convient aussi. Rien d'autre : ni Python, ni navigateur pour le build. + +```bash +git clone git@gitea.alpinux.org:alpinux.cedrica5l/alpinux-presentations.git +cd alpinux-presentations +npm ci +``` + +Deux dépendances, aux versions figées dans `package.json` et `package-lock.json` : + +- [`@marp-team/marp-cli`](https://github.com/marp-team/marp-cli) — convertit le + Markdown en diaporama ; +- [`js-yaml`](https://github.com/nodeca/js-yaml) — lit la fiche de chaque + présentation. Marp l'embarque déjà : elle n'ajoute rien à l'installation. + +--- + +## Commandes + +### `npm run dev` — aperçu local + +Sert `slides/` sur avec le thème commun, et recharge la page à +chaque enregistrement. Les brouillons et le modèle y sont visibles. + +### `npm run build` — build complet + +```bash +npm run build # vers public/ +npm run build -- /un/chemin # vers un autre répertoire +``` + +1. Parcourt `slides/*/slides.md`, en ignorant les dossiers qui commencent par `_` et + les présentations marquées `brouillon: true`. +2. Valide la fiche de chacune. +3. Génère `//index.html` et copie son dossier `images/`. +4. Génère `/index.html`, la page d'accueil. +5. Génère `/presentations.json` et `/.htaccess`. + +Le build est **strict**, comme `mkdocs build --strict` pour le wiki. Il échoue, avec un +message qui nomme le fichier et le défaut, sur : + +- un champ obligatoire manquant, une valeur invalide, un champ inconnu (une faute de + frappe sur `brouillon` publierait sinon un diaporama inachevé) ; +- une image introuvable, ou rangée ailleurs que dans `images/` ; +- une ressource chargée depuis un autre site — image, police, script, feuille de style. + +En cas d'échec, le répertoire de sortie n'est pas touché : tout est construit à part, +puis mis en place d'un seul coup. + +### `npm run pdf -- ` — export PDF + +```bash +npm run pdf -- pdf-signer # écrit public/pdf-signer/pdf-signer.pdf +``` + +À lancer sur un **poste de travail** : Marp pilote un navigateur installé (Chromium, +Chrome ou Firefox) pour imprimer le diaporama, et le serveur n'en a pas. Le PDF n'est +ni versionné ni publié par le déploiement. + +--- + +## Organisation du dépôt + +``` +alpinux-presentations/ +├── README.md +├── CONTRIBUER.md le guide du contributeur +├── package.json dépendances, versions figées +├── theme/alpinux.css le thème Marp commun +├── slides/ +│ ├── _modele/ le modèle à copier — jamais publié +│ └── pdf-signer/ +│ ├── slides.md le diaporama +│ └── images/ ses images +├── scripts/ +│ ├── build.mjs le build +│ └── deploy-presentations.sh ce que le serveur exécute après un push +├── deploy/ la chaîne de déploiement, versionnée avec le site +│ ├── webhook.md mise en place côté serveur +│ ├── installer.sh pose tout sur le serveur (root) +│ ├── webhook.py service d'écoute, 127.0.0.1:9878 +│ ├── presentations-webhook.service +│ └── apache-vhost.exemple.conf +└── public/ sortie locale du build, ignorée par git +``` + +--- + +## La fiche d'une présentation + +Chaque `slides.md` commence par un front matter : les réglages de Marp, puis la fiche. + +```yaml +--- +marp: true +theme: alpinux +paginate: true + +title: Lire, remplir et signer ses PDF +description: Remplir, signer, assembler et alléger un dossier PDF sous Linux Mint. +auteur: Alpinux +date: 2026-10-15 # date de la séance +niveau: debutant # debutant | intermediaire | confirme +format: court # eclair | court | long +tags: [pdf, bureautique] +wiki: https://wiki.alpinux.org/… # facultatif +brouillon: true # facultatif : exclut de la publication +--- +``` + +--- + +## Le thème + +`theme/alpinux.css` est pensé pour la vidéoprojection : gros corps de texte, fort +contraste, logo et numéro de page en pied de diapo. Les polices sont celles du +système ; rien n'est téléchargé. + +Deux classes, à poser sur une diapo avec `` : + +| Classe | Usage | +|---------|--------------------------------------------------------------| +| `titre` | La diapo d'ouverture, les intercalaires, la diapo de fin | +| `demo` | L'annonce d'une démonstration en direct | + +Les citations (`> …`) servent d'encadrés « à retenir » ou « piège », et `` dessine +une touche de clavier. + +--- + +## Ce que le build publie pour alpinux.org + +`presentations.json`, à la racine du site, liste toutes les présentations publiées, de +la plus récente à la plus ancienne : + +```json +{ + "genere": "2026-10-10T19:39:31.061Z", + "site": "https://presentations.alpinux.org/", + "presentations": [ + { + "id": "pdf-signer", + "titre": "Lire, remplir et signer ses PDF", + "description": "…", + "auteur": "Alpinux", + "date": "2026-10-15", + "niveau": "debutant", + "format": "court", + "tags": ["pdf", "bureautique"], + "url": "https://presentations.alpinux.org/pdf-signer/", + "wiki": null + } + ] +} +``` + +Le `.htaccess` généré pose `Access-Control-Allow-Origin` sur **ce seul fichier**, pour +qu'alpinux.org puisse le lire depuis le navigateur du visiteur — comme le wiki le fait +avec `derniers-articles.json`. + +--- + +## Vie privée et autonomie + +Aucun traceur, aucune police ni aucun script chargés depuis un service tiers. Une fois +la page chargée, un diaporama fonctionne **sans réseau** : son style et son code sont +dans le fichier HTML, ses images à côté. + +La seule ressource prise ailleurs est le logo, servi par `static.alpinux.org` — un +serveur de l'association. Le build refuse toute autre adresse externe. + +--- + +## Déploiement + +``` +git push (sur main) + │ +Gitea ── webhook ──▶ https://presentations.alpinux.org/deploy + │ service d'écoute, signature HMAC vérifiée + ▼ +deploy-presentations.sh : git pull → npm ci → build dans un staging → rsync + ▼ + https://presentations.alpinux.org +``` + +Les contributions arrivent par pull request sur `main`. À la fusion, le serveur +construit le site dans un répertoire de staging et ne le copie vers le DocumentRoot +que si le build a réussi : une fiche invalide ne casse pas le site en ligne. + +Le journal est dans `/var/log/presentations-deploy.log` ; une exécution réussie y est +encadrée par `Deploy started` et `Deploy done`. + +L'installation côté serveur, la déclaration du webhook et les pièges à connaître sont +décrits dans [deploy/webhook.md](deploy/webhook.md). diff --git a/deploy/apache-vhost.exemple.conf b/deploy/apache-vhost.exemple.conf new file mode 100644 index 0000000..c428ab1 --- /dev/null +++ b/deploy/apache-vhost.exemple.conf @@ -0,0 +1,23 @@ +# Relais Apache du webhook de presentations.alpinux.org. +# +# Deux lignes, à placer dans le VirtualHost *:443 du site. Elles transmettent +# https://presentations.alpinux.org/deploy au service d'écoute, qui ne répond que sur +# la boucle locale (voir webhook.py et presentations-webhook.service). +# +# Où les écrire : dans ISPConfig, Sites → presentations.alpinux.org → Options → +# « Directives Apache ». ISPConfig régénère le fichier +# /etc/apache2/sites-available/presentations.alpinux.org.vhost à chaque modification +# du site : une ligne ajoutée à la main dans ce fichier disparaît ce jour-là, alors +# qu'une directive saisie dans ISPConfig est réécrite avec le reste. +# +# installer.sh les pose directement dans le vhost pour que le webhook marche tout de +# suite ; les recopier dans ISPConfig les rend durables. +# +# Modules requis : proxy et proxy_http (déjà actifs sur owni, le wiki s'en sert). + +ProxyPass /deploy http://127.0.0.1:9878/deploy +ProxyPassReverse /deploy http://127.0.0.1:9878/deploy + +# Le reste du site n'a besoin de rien ici : le .htaccess généré par le build pose +# l'en-tête Access-Control-Allow-Origin sur presentations.json, ce qui suppose +# « AllowOverride All » (réglage par défaut d'ISPConfig) et le module headers. diff --git a/deploy/installer.sh b/deploy/installer.sh new file mode 100755 index 0000000..03fa570 --- /dev/null +++ b/deploy/installer.sh @@ -0,0 +1,161 @@ +#!/bin/bash +# Installe la chaîne de déploiement de presentations.alpinux.org sur owni. À lancer en +# root — relancer ne casse rien, chaque étape vérifie d'abord ce qui existe. +# +# sudo bash deploy/installer.sh +# +# Ce que ça met en place : Node.js dans /opt/presentations-alpinux/node, une clé de +# déploiement en lecture seule, un clone du dépôt, le service d'écoute du webhook sur +# 127.0.0.1:9878, le relais Apache /deploy, et le droit d'écriture de l'utilisateur du +# service sur le DocumentRoot. +# +# Le script s'arrête à l'étape 3 tant que la clé de déploiement n'est pas déclarée +# dans Gitea : la déclarer, puis relancer. +set -euo pipefail + +BASE="/opt/presentations-alpinux" +REPO_DIR="$BASE/repo" +UTILISATEUR="abonnelc" +CIBLE="/var/www/clients/client1/web22/web" +VHOST="/etc/apache2/sites-available/presentations.alpinux.org.vhost" +SECRET_FICHIER="/etc/default/presentations-webhook" +JOURNAL="/var/log/presentations-deploy.log" +UNITE="/etc/systemd/system/presentations-webhook.service" +PORT="9878" + +# Node.js : la version LTS, figée. Pour en changer, modifier ce numéro et relancer. +NODE_VERSION="v24.21.0" +NODE_ARCHIVE="node-$NODE_VERSION-linux-x64.tar.xz" +NODE_URL="https://nodejs.org/dist/$NODE_VERSION" + +# Le dépôt est tiré par SSH, avec une clé qui ne sert qu'à lui. +CLE="/home/$UTILISATEUR/.ssh/id_ed25519_presentations_deploy" +ALIAS_SSH="gitea-presentations" +DEPOT_URL="$ALIAS_SSH:alpinux.cedrica5l/alpinux-presentations.git" + +SOURCE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +[ "$(id -u)" -eq 0 ] || { echo "À lancer en root : sudo bash $0" >&2; exit 1; } + +echo "==> 1/7 Node.js $NODE_VERSION dans $BASE/node" +install -d -o "$UTILISATEUR" -g "$UTILISATEUR" "$BASE" +if [ -x "$BASE/node/bin/node" ] && [ "$("$BASE/node/bin/node" --version)" = "$NODE_VERSION" ]; then + echo " (déjà là)" +else + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/$NODE_ARCHIVE" "$NODE_URL/$NODE_ARCHIVE" + curl -fsSL -o "$tmp/SHASUMS256.txt" "$NODE_URL/SHASUMS256.txt" + (cd "$tmp" && grep " $NODE_ARCHIVE\$" SHASUMS256.txt | sha256sum -c -) + rm -rf "$BASE/node" + install -d "$BASE/node" + tar -xJf "$tmp/$NODE_ARCHIVE" -C "$BASE/node" --strip-components=1 --no-same-owner + rm -rf "$tmp" + echo " $("$BASE/node/bin/node" --version) installé" +fi + +echo "==> 2/7 Clé de déploiement" +if [ ! -f "$CLE" ]; then + sudo -u "$UTILISATEUR" ssh-keygen -q -t ed25519 -N "" -C "deploy@presentations.alpinux.org" -f "$CLE" + echo " clé créée" +fi +CONFIG_SSH="/home/$UTILISATEUR/.ssh/config" +if ! grep -q "^Host $ALIAS_SSH\$" "$CONFIG_SSH" 2>/dev/null; then + sudo -u "$UTILISATEUR" tee -a "$CONFIG_SSH" > /dev/null < 3/7 Dépôt dans $REPO_DIR" +if [ -d "$REPO_DIR/.git" ]; then + sudo -u "$UTILISATEUR" git -C "$REPO_DIR" pull --ff-only origin main +elif ! sudo -u "$UTILISATEUR" git clone "$DEPOT_URL" "$REPO_DIR"; then + echo + echo "Le serveur n'a pas pu cloner le dépôt. Déclarer cette clé publique dans Gitea :" >&2 + echo " dépôt alpinux-presentations → Paramètres → Clés de déploiement → Ajouter" >&2 + echo " (sans cocher l'accès en écriture)" >&2 + echo + cat "$CLE.pub" + echo + echo "Puis relancer : sudo bash $0" >&2 + exit 1 +fi + +echo "==> 4/7 Scripts et journal" +# Le serveur exécute ces copies, pas les fichiers du dépôt : une modification de +# deploy/ ou de scripts/deploy-presentations.sh ne prend effet qu'en relançant +# installer.sh. Un push ne peut donc pas changer ce que le service exécute. +install -o "$UTILISATEUR" -g "$UTILISATEUR" -m 755 "$SOURCE/scripts/deploy-presentations.sh" "$BASE/deploy-presentations.sh" +install -o "$UTILISATEUR" -g "$UTILISATEUR" -m 755 "$SOURCE/deploy/webhook.py" "$BASE/webhook.py" +touch "$JOURNAL" && chown "$UTILISATEUR":"$UTILISATEUR" "$JOURNAL" + +echo "==> 5/7 Droit d'écriture sur $CIBLE" +# Le DocumentRoot appartient à web22 ; une ACL suffit à y laisser écrire le service, +# sans toucher au propriétaire ni aux droits qu'attend ISPConfig. +setfacl -m "u:$UTILISATEUR:rwx" "$CIBLE" + +echo "==> 6/7 Secret du webhook et service systemd" +if [ -f "$SECRET_FICHIER" ]; then + echo " (secret déjà là, inchangé)" +else + (umask 077; printf 'PRESENTATIONS_WEBHOOK_SECRET=%s\n' "$(openssl rand -hex 24)" > "$SECRET_FICHIER") +fi +chmod 600 "$SECRET_FICHIER" +install -m 644 "$SOURCE/deploy/presentations-webhook.service" "$UNITE" +systemctl daemon-reload +systemctl enable --now presentations-webhook +systemctl restart presentations-webhook +systemctl is-active --quiet presentations-webhook && echo " service actif" + +echo "==> 7/7 Relais Apache /deploy" +if grep -q "127.0.0.1:$PORT" "$VHOST"; then + echo " déjà relayé" +else + cp -a "$VHOST" "$VHOST.bak-$(date +%Y%m%d%H%M)" + awk -v port="$PORT" ' + { print } + !fait && /^[[:space:]]*]*:443>/ { + print "\t\tProxyPass /deploy http://127.0.0.1:" port "/deploy" + print "\t\tProxyPassReverse /deploy http://127.0.0.1:" port "/deploy" + fait = 1 + } + ' "$VHOST" > "$VHOST.tmp" + # Recopié dans le fichier plutôt que déplacé par-dessus : un « mv » remplacerait + # l'original par un fichier neuf, avec d'autres droits. + cat "$VHOST.tmp" > "$VHOST" && rm -f "$VHOST.tmp" + grep -q "127.0.0.1:$PORT" "$VHOST" || { echo " ÉCHEC : relais non posé dans $VHOST" >&2; exit 1; } + echo " relais ajouté" +fi +if apache2ctl configtest; then + systemctl reload apache2 +else + echo "Configuration Apache invalide : rien n'a été rechargé." >&2 + echo "La sauvegarde .bak-* est à côté du vhost." >&2 + exit 1 +fi + +echo +echo "────────────────────────────────────────────────────────────" +echo "Reste à faire à la main :" +echo +echo "1. Déclarer le webhook dans Gitea" +echo " dépôt alpinux-presentations → Paramètres → Webhooks → Ajouter → Gitea" +echo " URL : https://presentations.alpinux.org/deploy" +echo " Méthode : POST, type de contenu : application/json" +echo " Secret : sudo cat $SECRET_FICHIER" +echo " Événement : Push seulement, filtre de branche : main" +echo +echo "2. Recopier le relais dans ISPConfig pour qu'il survive à une régénération" +echo " du vhost : Sites → presentations.alpinux.org → Options → Directives Apache" +echo " (les deux lignes de deploy/apache-vhost.exemple.conf)" +echo +echo "Première mise en ligne, sans attendre un push :" +echo " sudo -u $UTILISATEUR $BASE/deploy-presentations.sh; tail -5 $JOURNAL" +echo "────────────────────────────────────────────────────────────" diff --git a/deploy/presentations-webhook.service b/deploy/presentations-webhook.service new file mode 100644 index 0000000..8b7427c --- /dev/null +++ b/deploy/presentations-webhook.service @@ -0,0 +1,17 @@ +[Unit] +Description=presentations.alpinux.org webhook deploy server +After=network.target + +[Service] +Type=simple +User=abonnelc +Group=abonnelc +WorkingDirectory=/opt/presentations-alpinux +ExecStart=/usr/bin/python3 /opt/presentations-alpinux/webhook.py +# Le secret vit hors du dépôt : ce fichier est écrit par installer.sh, en 0600. +EnvironmentFile=/etc/default/presentations-webhook +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/deploy/webhook.md b/deploy/webhook.md new file mode 100644 index 0000000..01df415 --- /dev/null +++ b/deploy/webhook.md @@ -0,0 +1,181 @@ +# Déploiement automatique : mise en place côté serveur + +Cette page s'adresse aux mainteneurs qui ont un accès SSH à `alpinux.org`. Les +contributeurs n'ont rien à faire ici : leur travail s'arrête à la pull request. + +La chaîne est la même que celle du wiki et de www.alpinux.org : + +``` +push sur main → webhook Gitea → https://presentations.alpinux.org/deploy + │ Apache relaie vers 127.0.0.1:9878 + ▼ + service d'écoute (signature HMAC vérifiée) + ▼ +deploy-presentations.sh : git pull → npm ci → build dans le staging → rsync + ▼ + https://presentations.alpinux.org +``` + +Si le build échoue — front matter invalide, image manquante — rien n'est copié : le +site en ligne reste celui du dernier build réussi. + +--- + +## Un second service plutôt qu'une nouvelle route + +Le service d'écoute du wiki (`/opt/wiki-mkdocs/webhook.py`) ne connaît qu'un secret et +qu'un script. Y ajouter une route aurait demandé de le réécrire, de faire cohabiter +deux secrets dans la même unité systemd, et de redémarrer le déploiement du wiki à +chaque retouche de celui des présentations. + +Ce dépôt apporte donc **son propre service**, comme www.alpinux.org l'a fait avant +lui. Les trois sont des copies du même petit programme : + +| Site | Service systemd | Port | Secret | +|----------------|--------------------------|-------|-----------------------------------------| +| wiki | `wiki-webhook` | 9876 | dans l'unité systemd | +| www | `www-webhook` | 9877 | `/etc/default/www-webhook` | +| présentations | `presentations-webhook` | 9878 | `/etc/default/presentations-webhook` | + +Chaque dépôt a **son** secret : celui des présentations n'ouvre pas le déploiement du +wiki, et inversement. + +--- + +## Ce qui est installé, et où + +| Quoi | Où | +|-----------------------------------|----------------------------------------------------------| +| Clone du dépôt | `/opt/presentations-alpinux/repo` | +| Répertoire de staging | `/opt/presentations-alpinux/staging` | +| Node.js (LTS, version figée) | `/opt/presentations-alpinux/node` | +| Script de déploiement (copie) | `/opt/presentations-alpinux/deploy-presentations.sh` | +| Service d'écoute (copie) | `/opt/presentations-alpinux/webhook.py` | +| Unité systemd | `/etc/systemd/system/presentations-webhook.service` | +| Secret HMAC (`0600`, root) | `/etc/default/presentations-webhook` | +| Clé de déploiement, lecture seule | `~abonnelc/.ssh/id_ed25519_presentations_deploy` | +| DocumentRoot | `/var/www/clients/client1/web22/web` | +| Journal | `/var/log/presentations-deploy.log` | + +Fichiers fournis par ce dossier : [`installer.sh`](installer.sh), +[`webhook.py`](webhook.py), [`presentations-webhook.service`](presentations-webhook.service), +[`apache-vhost.exemple.conf`](apache-vhost.exemple.conf), et +[`../scripts/deploy-presentations.sh`](../scripts/deploy-presentations.sh). + +**Aucun secret n'est dans le dépôt.** Le secret du webhook est tiré au hasard sur le +serveur par `installer.sh` ; la clé privée de déploiement y est créée et n'en sort pas. + +--- + +## Installation + +Tout tient dans `installer.sh`, à lancer en root. Il est sans danger de le relancer : +chaque étape vérifie d'abord ce qui existe. + +```bash +sudo bash deploy/installer.sh +``` + +1. **Node.js** — téléchargé depuis nodejs.org, somme SHA-256 vérifiée, déposé dans + `/opt/presentations-alpinux/node`. Rien n'est ajouté aux dépôts apt du serveur, et + pas de Chromium : le build HTML de Marp n'en a pas besoin. +2. **Clé de déploiement** — une clé SSH propre à ce dépôt, et un alias + `gitea-presentations` dans `~abonnelc/.ssh/config`. +3. **Clone du dépôt.** La première fois, le script s'arrête ici et affiche la clé + publique : la déclarer dans Gitea, *Paramètres → Clés de déploiement* du dépôt, + **sans** l'accès en écriture, puis relancer. +4. **Scripts** — copie de `deploy-presentations.sh` et de `webhook.py` dans + `/opt/presentations-alpinux`, création du journal. +5. **Droit d'écriture** sur le DocumentRoot, par une ACL pour `abonnelc`. +6. **Secret et service** — secret tiré au hasard s'il n'existe pas, unité systemd + installée et démarrée. +7. **Relais Apache** — les deux lignes `ProxyPass` dans le vhost, puis rechargement + d'Apache si la configuration est valide. + +### Déclarer le webhook dans Gitea + +Dans le dépôt `alpinux-presentations` : *Paramètres → Webhooks → Ajouter un webhook → +Gitea*. + +| Champ | Valeur | +|---------------------|-----------------------------------------------------| +| URL cible | `https://presentations.alpinux.org/deploy` | +| Méthode HTTP | `POST` | +| Type de contenu | `application/json` | +| Secret | la valeur lue par `sudo cat /etc/default/presentations-webhook` | +| Déclencheur | *Push* seulement | +| Filtre de branche | `main` | + +Le filtre de branche évite des appels inutiles ; le service ignore de toute façon tout +ce qui ne vise pas `refs/heads/main`. + +### Rendre le relais Apache durable + +Le vhost est écrit par ISPConfig, qui le **régénère** à chaque modification du site +(certificat, alias, version de PHP…). Les deux lignes posées par `installer.sh` +disparaissent ce jour-là. + +Pour qu'elles tiennent : dans ISPConfig, *Sites → presentations.alpinux.org → Options → +Directives Apache*, coller les deux lignes de +[`apache-vhost.exemple.conf`](apache-vhost.exemple.conf). ISPConfig les réécrira alors +avec le reste du vhost. + +--- + +## Vérifier + +```bash +# Le service tourne et n'écoute que sur la boucle locale +systemctl status presentations-webhook +ss -ltn | grep 9878 + +# Sans signature valable, le relais répond 403 — c'est la bonne réponse +curl -s -o /dev/null -w '%{http_code}\n' -X POST -d '{}' https://presentations.alpinux.org/deploy + +# Le dernier déploiement +tail -20 /var/log/presentations-deploy.log +``` + +Une exécution réussie encadre le build par une ligne `Deploy started` et une ligne +`Deploy done`. Une ligne `ABANDON` signale un échec : la cause est juste au-dessus, et +le site en ligne n'a pas bougé. + +Dans Gitea, la page du webhook liste les envois récents avec la réponse reçue, et son +bouton *Tester l'envoi* rejoue un push. + +--- + +## Au quotidien + +| Je veux… | Je fais… | +|--------------------------------------------|--------------------------------------------------------------------------| +| Savoir pourquoi ce n'est pas en ligne | `tail -30 /var/log/presentations-deploy.log` | +| Republier sans nouveau commit | `sudo -u abonnelc /opt/presentations-alpinux/deploy-presentations.sh` | +| Appliquer une modification de `deploy/` | `sudo bash /opt/presentations-alpinux/repo/deploy/installer.sh` | +| Changer de version de Node.js | modifier `NODE_VERSION` dans `installer.sh`, pousser, relancer `installer.sh` | +| Changer le secret | supprimer `/etc/default/presentations-webhook`, relancer `installer.sh`, mettre à jour le webhook dans Gitea | + +--- + +## Les pièges à connaître + +**Le serveur exécute des copies.** `deploy-presentations.sh` et `webhook.py` sont +copiés dans `/opt/presentations-alpinux` par `installer.sh`. Les modifier dans le dépôt +ne change rien tant que `installer.sh` n'a pas été relancé. C'est voulu : une pull +request fusionnée ne peut pas, à elle seule, changer ce que le service exécute. + +**Mais le build, lui, exécute le dépôt.** `scripts/build.mjs` et les dépendances de +`package-lock.json` tournent sur le serveur à chaque push, sous le compte du service. +Une pull request qui touche à `scripts/`, à `package.json` ou à `package-lock.json` se +relit donc comme du code qui s'exécutera sur le serveur — pas comme une diapo. + +**`rsync --delete` épargne deux dossiers.** `error/` et `stats/` appartiennent à +ISPConfig et sont exclus de la synchronisation. Tout autre fichier déposé à la main +dans le DocumentRoot est supprimé au déploiement suivant. + +**Le droit d'écriture tient à une ACL.** Si ISPConfig réinitialise les droits du site, +le déploiement échoue sur une erreur de permission : relancer `installer.sh` la repose. + +**Node.js ne se met pas à jour tout seul.** Il est installé hors d'apt. Suivre les +versions LTS sur et changer +`NODE_VERSION` de temps en temps. diff --git a/deploy/webhook.py b/deploy/webhook.py new file mode 100644 index 0000000..0860fd5 --- /dev/null +++ b/deploy/webhook.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Service d'écoute du webhook Gitea pour presentations.alpinux.org. + +N'écoute que sur la boucle locale : c'est Apache qui lui transmet les requêtes reçues +sur https://presentations.alpinux.org/deploy. La signature HMAC envoyée par Gitea est +vérifiée avant de lancer quoi que ce soit, et seuls les push sur main déclenchent un +déploiement. + +Calqué sur les services du wiki et de www (/opt/wiki-mkdocs/webhook.py, +/opt/www-alpinux/webhook.py), à trois différences près : le port, le script appelé, +et un secret qui lui est propre, lu dans un fichier d'environnement — ce dépôt est +public. +""" +import hashlib +import hmac +import http.server +import json +import os +import subprocess +import threading + +SECRET = os.environ.get("PRESENTATIONS_WEBHOOK_SECRET", "") +DEPLOY_SCRIPT = "/opt/presentations-alpinux/deploy-presentations.sh" +LOG = "/var/log/presentations-deploy.log" +PORT = 9878 + + +class Handler(http.server.BaseHTTPRequestHandler): + def do_POST(self): + if self.path != "/deploy": + self.send_response(404); self.end_headers(); return + + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + signature = self.headers.get("X-Gitea-Signature", "") + + attendue = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest() + if not SECRET or not hmac.compare_digest(attendue, signature): + self.send_response(403); self.end_headers() + self.wfile.write(b"Forbidden"); return + + try: + ref = json.loads(body).get("ref", "") + except (ValueError, AttributeError): + self.send_response(400); self.end_headers() + self.wfile.write(b"Bad request"); return + if ref != "refs/heads/main": + self.send_response(200); self.end_headers() + self.wfile.write(b"Skipped: not main"); return + + # Déploiement en tâche de fond : Gitea a sa réponse tout de suite. + threading.Thread( + target=subprocess.run, + args=([DEPLOY_SCRIPT],), + kwargs={"capture_output": True}, + daemon=True, + ).start() + + self.send_response(200); self.end_headers() + self.wfile.write(b"Deploy triggered") + + def log_message(self, fmt, *args): + with open(LOG, "a") as journal: + journal.write(self.log_date_time_string() + " " + fmt % args + "\n") + + +if __name__ == "__main__": + http.server.HTTPServer(("127.0.0.1", PORT), Handler).serve_forever() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..04c1029 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1818 @@ +{ + "name": "alpinux-presentations", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "alpinux-presentations", + "version": "1.0.0", + "dependencies": { + "@marp-team/marp-cli": "4.5.1", + "js-yaml": "5.4.3" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@marp-team/marp-cli": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@marp-team/marp-cli/-/marp-cli-4.5.1.tgz", + "integrity": "sha512-KwjfXZpABRjSivsHOD0pIsOmODxgQFwc3AHjlnFUKCsSZX5U4erHmm4GWd1K7l2NATLvtrGw29At89Tar/cS4g==", + "license": "MIT", + "dependencies": { + "@marp-team/marp-core": "^4.4.0", + "@marp-team/marpit": "^3.2.2", + "chokidar": "^4.0.3", + "cosmiconfig": "^9.0.2", + "puppeteer-core": "^24.43.1", + "serve-index": "^1.9.2", + "tmp": "^0.2.7", + "ws": "^8.21.3" + }, + "bin": { + "marp": "marp-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@marp-team/marp-core": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@marp-team/marp-core/-/marp-core-4.4.0.tgz", + "integrity": "sha512-YTiS0oIo25KiqjfMtDchTMc/uuFhR48weKNVw4QPPAM/9UQ71QI0JnDkC1qenJSmFPMlXQK72wQ8vlhmgQS3Cg==", + "license": "MIT", + "dependencies": { + "@marp-team/marpit": "^3.2.2", + "@marp-team/marpit-svg-polyfill": "^2.1.0", + "highlight.js": "^11.11.1", + "katex": "^0.17.0", + "mathjax-full": "^3.2.2", + "postcss-selector-parser": "^7.1.4", + "xss": "^1.0.15" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@marp-team/marpit": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@marp-team/marpit/-/marpit-3.2.3.tgz", + "integrity": "sha512-YMSS+LaYM3/cCpB18gtmgHHRcu4R1FHSSj8l5biPTP68ML5pdiQNvgSADmnNGJI0uaW4msmPiVMI1zRJ4VquJw==", + "license": "MIT", + "dependencies": { + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "cssesc": "^3.0.0", + "js-yaml": "^5.4.1", + "lodash.kebabcase": "^4.1.1", + "markdown-it": "^14.3.1", + "markdown-it-front-matter": "^0.2.4", + "postcss": "^8.5.26", + "postcss-nesting": "^13.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@marp-team/marpit-svg-polyfill": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@marp-team/marpit-svg-polyfill/-/marpit-svg-polyfill-2.1.0.tgz", + "integrity": "sha512-VqCoAKwv1HJdzZp36dDPxznz2JZgRjkVSSPHpCzk72G2N753F0HPKXjevdjxmzN6gir9bUGBgMD1SguWJIi11A==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@marp-team/marpit": ">=0.5.0" + }, + "peerDependenciesMeta": { + "@marp-team/marpit": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.2.tgz", + "integrity": "sha512-5EUZSUIc37H6aIXyWO0Z4y8NlF8NnjgmqeQgOGiswAU7pY0HOo16ho4+alIWmSfdZnjqBRawMsP3I5YqLSn6kw==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.5.tgz", + "integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "deprecated": "this version has critical issues, please update to the latest version", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.9.0.tgz", + "integrity": "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.2.tgz", + "integrity": "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.8.4.tgz", + "integrity": "sha512-MPC2eyrpbk3A873sK4y6rk+Iv4o2enxTjHZ8zH3HO0mjrpm5XjnguY2XmfidIsDV6vnjpbMBKUAJpV0ISD2SsQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.28.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-path": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.1.2.tgz", + "integrity": "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw==", + "license": "Apache-2.0" + }, + "node_modules/bare-stream": { + "version": "2.13.4", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.4.tgz", + "integrity": "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.8.1", + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.5.4.tgz", + "integrity": "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1608973", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1608973.tgz", + "integrity": "sha512-Tpm17fxYzt+J7VrGdc1k8YdRqS3YV7se/M6KeemEqvUbq/n7At1rWVuXMxQgpWkdwSdIEKYbU//Bve+Shm4YNQ==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/highlight.js": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.4.3.tgz", + "integrity": "sha512-yzzNtgczwgoVHNigCyKTXcQJU4Y8t/ikU4UxXW0BvrLfHTGWZhsNcOT2s/a7vn81/4KqVnNmHjaUEPlvWnNwVw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.mjs" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/katex": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.17.0.tgz", + "integrity": "sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-it": { + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it-front-matter": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/markdown-it-front-matter/-/markdown-it-front-matter-0.2.4.tgz", + "integrity": "sha512-25GUs0yjS2hLl8zAemVndeEzThB1p42yxuDEKbd4JlL3jiz+jsm6e56Ya8B0VREOkNxLYB4TTwaoPJ3ElMmW+w==", + "license": "MIT" + }, + "node_modules/mathjax-full": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz", + "integrity": "sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==", + "deprecated": "Version 4 replaces this package with the scoped package @mathjax/src", + "license": "Apache-2.0", + "dependencies": { + "esm": "^3.2.25", + "mhchemparser": "^4.1.0", + "mj-context-menu": "^0.6.1", + "speech-rule-engine": "^4.0.6" + } + }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "license": "MIT" + }, + "node_modules/mhchemparser": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.2.1.tgz", + "integrity": "sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==", + "license": "Apache-2.0" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mj-context-menu": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz", + "integrity": "sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==", + "license": "Apache-2.0" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.43.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.43.1.tgz", + "integrity": "sha512-T5ScUMAsmhdNbgDR41AGESYeS6V9MSgetkSnVhhW+gXvzC42VesKCn5ld87gAZDJ6vLHL9GkRvY9WtQWSnwFbw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.2", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1608973", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.20.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.10", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.10.tgz", + "integrity": "sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speech-rule-engine": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.1.4.tgz", + "integrity": "sha512-i/VCLG1fvRc95pMHRqG4aQNscv+9aIsqA2oI7ZQS51sTdUcDHYX6cpT8/tqZ+enjs1tKVwbRBWgxut9SWn+f9g==", + "license": "Apache-2.0", + "dependencies": { + "@xmldom/xmldom": "0.9.10", + "commander": "13.1.0", + "wicked-good-xpath": "1.3.0" + }, + "bin": { + "sre": "bin/sre" + } + }, + "node_modules/speech-rule-engine/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamx": { + "version": "2.28.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.1.tgz", + "integrity": "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.3.tgz", + "integrity": "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.2.tgz", + "integrity": "sha512-+8NeqHRjQWH9nYlwo2gamAMImZCVzI4UoEgDpWorBt9OEfppiZn+uSkskzQKPWIZyji/C8fpWO7u69G0DX0tbg==", + "license": "MIT", + "dependencies": { + "b4a": "^1.9.0", + "bare-fs": "^4.8.2", + "fast-fifo": "^1.3.2", + "streamx": "^2.28.1" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.3", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.3.tgz", + "integrity": "sha512-/dtuMZyutHLlrxWhhC1uib00ELUr2dZ8C9B8YzZYsJmeE3V56OxRblrSM2UaF142pcMtak7ehZ6+poWGeehNIg==", + "license": "MIT" + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT", + "optional": true + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "license": "Apache-2.0" + }, + "node_modules/wicked-good-xpath": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz", + "integrity": "sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3dc74e3 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "alpinux-presentations", + "version": "1.0.0", + "description": "Diaporamas des réunions d'Alpinux, écrits en Markdown et publiés sur presentations.alpinux.org", + "private": true, + "type": "module", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "node scripts/build.mjs", + "dev": "marp --server --theme-set theme/alpinux.css -- slides", + "pdf": "node scripts/build.mjs --pdf" + }, + "dependencies": { + "@marp-team/marp-cli": "4.5.1", + "js-yaml": "5.4.3" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..f0cd972 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,599 @@ +#!/usr/bin/env node +// Construit le site des présentations d'Alpinux. +// +// node scripts/build.mjs [sortie] build complet (sortie : public/ par défaut) +// node scripts/build.mjs --pdf [sortie] +// un PDF, sur un poste qui a un navigateur +// +// Le build est strict, comme « mkdocs build --strict » pour le wiki : un front matter +// incomplet, une image introuvable ou une ressource chargée depuis un autre site le +// font échouer avant d'avoir écrit quoi que ce soit dans le répertoire de sortie. + +import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs' +import { dirname, join, relative, resolve, sep } from 'node:path' +import { fileURLToPath } from 'node:url' +import { marpCli } from '@marp-team/marp-cli' +import { CORE_SCHEMA, load } from 'js-yaml' + +const RACINE = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const SOURCES = join(RACINE, 'slides') +const THEME = join(RACINE, 'theme', 'alpinux.css') +const NOM_DU_THEME = 'alpinux' + +// Adresse publique du site : elle ne sert qu'aux URL absolues de presentations.json. +const URL_SITE = (process.env.URL_SITE ?? 'https://presentations.alpinux.org').replace(/\/+$/, '') +const URL_LOGO = 'https://static.alpinux.org/logo/alpinux-logo-512.png' + +// Seules adresses qu'une page générée a le droit de charger depuis un autre site. +const RESSOURCES_EXTERNES_ADMISES = new Set([URL_LOGO]) + +const NIVEAUX = { debutant: 'Débutant', intermediaire: 'Intermédiaire', confirme: 'Confirmé' } +const FORMATS = { eclair: 'Éclair', court: 'Court', long: 'Long' } + +const CHAMPS_OBLIGATOIRES = ['title', 'description', 'auteur', 'date', 'niveau', 'format', 'tags'] +const CHAMPS_FACULTATIFS = ['wiki', 'brouillon'] +// Directives de Marp admises dans le front matter. Toute autre clé est refusée : une +// faute de frappe sur « brouillon » publierait sinon une présentation inachevée. +const DIRECTIVES_MARP = [ + 'marp', 'theme', 'paginate', 'header', 'footer', 'class', 'size', 'style', 'lang', 'math', + 'headingDivider', 'transition', 'color', 'backgroundColor', 'backgroundImage', + 'backgroundPosition', 'backgroundRepeat', 'backgroundSize', +] +const CLES_CONNUES = new Set([...CHAMPS_OBLIGATOIRES, ...CHAMPS_FACULTATIFS, ...DIRECTIVES_MARP]) + +const FORME_IDENTIFIANT = /^[a-z0-9]+(-[a-z0-9]+)*$/ + +class ErreurDeBuild extends Error {} + +// ── Lecture et validation des sources ────────────────────────────────────── + +function lireFrontMatter(texte) { + const trouve = /^?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(\r?\n|$)/.exec(texte) + if (!trouve) { + throw new ErreurDeBuild('pas de front matter : le fichier doit commencer par un bloc encadré de « --- »') + } + let donnees + try { + // CORE_SCHEMA : les dates restent des chaînes, telles qu'elles sont écrites. + donnees = load(trouve[1], { schema: CORE_SCHEMA }) + } catch (erreur) { + throw new ErreurDeBuild(`front matter illisible (YAML) : ${erreur.reason ?? erreur.message}` + + (erreur.mark ? `, ligne ${erreur.mark.line + 2}` : '')) + } + if (donnees === null || typeof donnees !== 'object' || Array.isArray(donnees)) { + throw new ErreurDeBuild('front matter vide ou mal formé : une liste de « clé: valeur » est attendue') + } + return donnees +} + +const estTexte = (valeur) => typeof valeur === 'string' && valeur.trim() !== '' + +function estUneDate(valeur) { + if (typeof valeur !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(valeur)) return false + const date = new Date(`${valeur}T00:00:00Z`) + return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === valeur +} + +function estUneAdresseWeb(valeur) { + if (typeof valeur !== 'string') return false + try { + return ['http:', 'https:'].includes(new URL(valeur).protocol) + } catch { + return false + } +} + +/** Renvoie la liste des erreurs du front matter — vide s'il est bon. */ +function validerFrontMatter(fm) { + const erreurs = [] + const choix = (valeurs) => Object.keys(valeurs).join(' | ') + + for (const champ of CHAMPS_OBLIGATOIRES) { + if (fm[champ] === undefined || fm[champ] === null || fm[champ] === '') { + erreurs.push(`champ obligatoire manquant : « ${champ} »`) + } + } + for (const cle of Object.keys(fm)) { + if (!CLES_CONNUES.has(cle)) { + erreurs.push(`champ inconnu : « ${cle} » (faute de frappe ? champs admis : ${[...CHAMPS_OBLIGATOIRES, ...CHAMPS_FACULTATIFS].join(', ')})`) + } + } + + const present = (champ) => fm[champ] !== undefined && fm[champ] !== null && fm[champ] !== '' + for (const champ of ['title', 'description', 'auteur']) { + if (present(champ) && !estTexte(fm[champ])) erreurs.push(`« ${champ} » doit être un texte`) + } + if (present('date') && !estUneDate(fm.date)) { + erreurs.push(`« date » invalide : « ${fm.date} » — attendu AAAA-MM-JJ, la date de la séance (exemple : 2026-10-15)`) + } + if (present('niveau') && !Object.hasOwn(NIVEAUX, fm.niveau)) { + erreurs.push(`« niveau » invalide : « ${fm.niveau} » — attendu ${choix(NIVEAUX)}`) + } + if (present('format') && !Object.hasOwn(FORMATS, fm.format)) { + erreurs.push(`« format » invalide : « ${fm.format} » — attendu ${choix(FORMATS)}`) + } + if (present('tags')) { + if (!Array.isArray(fm.tags) || fm.tags.length === 0 || !fm.tags.every(estTexte)) { + erreurs.push('« tags » doit être une liste d\'au moins un mot-clé (exemple : tags: [pdf, bureautique])') + } + } + if (present('wiki') && !estUneAdresseWeb(fm.wiki)) { + erreurs.push(`« wiki » invalide : « ${fm.wiki} » — attendu une adresse complète, en https://`) + } + if (fm.theme !== undefined && fm.theme !== NOM_DU_THEME) { + erreurs.push(`« theme » invalide : « ${fm.theme} » — seul le thème commun « ${NOM_DU_THEME} » est publié`) + } + return erreurs +} + +/** + * Parcourt slides/ et renvoie les présentations à publier, de la plus récente à la + * plus ancienne. Lève une ErreurDeBuild qui détaille tous les défauts trouvés. + */ +function collecterLesPresentations() { + if (!existsSync(SOURCES)) throw new ErreurDeBuild(`répertoire introuvable : ${SOURCES}`) + + const presentations = [] + const brouillons = [] + const defauts = [] + + const dossiers = readdirSync(SOURCES, { withFileTypes: true }) + .filter((entree) => entree.isDirectory() && !entree.name.startsWith('_') && !entree.name.startsWith('.')) + .map((entree) => entree.name) + .sort() + + for (const id of dossiers) { + const source = join(SOURCES, id, 'slides.md') + const chemin = relative(RACINE, source) + const signaler = (...messages) => defauts.push(...messages.map((message) => `${chemin} : ${message}`)) + + if (!FORME_IDENTIFIANT.test(id)) { + signaler(`nom de dossier « ${id} » invalide — minuscules sans accent, chiffres et tirets seulement (exemple : pdf-signer)`) + continue + } + if (!existsSync(source)) { + signaler('fichier absent — chaque dossier de slides/ doit contenir un slides.md') + continue + } + + let fm + try { + fm = lireFrontMatter(readFileSync(source, 'utf8')) + } catch (erreur) { + if (!(erreur instanceof ErreurDeBuild)) throw erreur + signaler(erreur.message) + continue + } + + if (fm.brouillon !== undefined && typeof fm.brouillon !== 'boolean') { + signaler(`« brouillon » invalide : « ${fm.brouillon} » — attendu true ou false`) + continue + } + if (fm.brouillon === true) { + brouillons.push(id) + continue + } + + const erreurs = validerFrontMatter(fm) + if (erreurs.length > 0) { + signaler(...erreurs) + continue + } + + presentations.push({ + id, + source, + titre: fm.title.trim(), + description: fm.description.trim(), + auteur: fm.auteur.trim(), + date: fm.date, + niveau: fm.niveau, + format: fm.format, + tags: fm.tags.map((tag) => tag.trim()), + wiki: fm.wiki ?? null, + langue: typeof fm.lang === 'string' ? fm.lang : 'fr', + }) + } + + if (defauts.length > 0) { + throw new ErreurDeBuild(`${defauts.length} erreur(s) dans les sources :\n` + defauts.map((defaut) => ` - ${defaut}`).join('\n')) + } + + presentations.sort((a, b) => b.date.localeCompare(a.date) || a.titre.localeCompare(b.titre, 'fr')) + return { presentations, brouillons } +} + +// ── Diaporamas ───────────────────────────────────────────────────────────── + +/** Lance Marp CLI en gardant sa sortie pour nous : elle n'est montrée qu'en cas d'échec. */ +async function lancerMarp(arguments_) { + const journal = [] + const originaux = { log: console.log, info: console.info, warn: console.warn, error: console.error } + for (const methode of Object.keys(originaux)) { + console[methode] = (...morceaux) => journal.push(morceaux.join(' ')) + } + let code + try { + code = await marpCli(['--no-stdin', '--no-config-file', ...arguments_]) + } catch (erreur) { + journal.push(String(erreur?.message ?? erreur)) + code = 1 + } finally { + Object.assign(console, originaux) + } + return { code, journal: journal.join('\n').trim() } +} + +const adresses = (html, motif) => [...html.matchAll(motif)].map((trouve) => trouve[1].replaceAll('&', '&')) + +/** + * Vérifie ce que le diaporama généré charge : rien d'un autre site hormis le logo, et + * des images locales qui existent bien dans le dossier images/ de la présentation. + */ +function controlerLesRessources(html, presentation) { + const erreurs = [] + const chargees = new Set([ + ...adresses(html, /<(?:img|script|source|video|audio|iframe|embed|track)\b[^>]*?\ssrc=["']([^"']+)["']/gi), + ...adresses(html, /]*?\shref=["']([^"']+)["']/gi), + ...adresses(html, /url\(\s*(?:"|["'])?([^"')&]+)/gi), + ...adresses(html, /@import\s+(?:url\()?\s*(?:"|["'])([^"')&]+)/gi), + ]) + + for (const adresse of chargees) { + if (adresse.startsWith('data:') || adresse.startsWith('#')) continue + if (/^([a-z][a-z0-9+.-]*:)?\/\//i.test(adresse)) { + if (!RESSOURCES_EXTERNES_ADMISES.has(adresse)) { + erreurs.push(`ressource externe interdite : ${adresse} — copiez le fichier dans images/ et citez-le par son chemin relatif`) + } + continue + } + const chemin = decodeURI(adresse.split(/[?#]/)[0]) + if (!chemin.startsWith('images/') || chemin.split('/').includes('..')) { + erreurs.push(`fichier hors du dossier images/ : ${adresse} — seul images/ est publié avec le diaporama`) + } else if (!existsSync(join(dirname(presentation.source), chemin))) { + erreurs.push(`image introuvable : ${adresse}`) + } + } + return erreurs +} + +async function genererLeDiaporama(presentation, sortie) { + const chemin = relative(RACINE, presentation.source) + const dossier = join(sortie, presentation.id) + const page = join(dossier, 'index.html') + mkdirSync(dossier, { recursive: true }) + + const { code, journal } = await lancerMarp([ + presentation.source, '-o', page, '--theme-set', THEME, '--theme', NOM_DU_THEME, + ]) + if (code !== 0 || !existsSync(page)) { + throw new ErreurDeBuild(`${chemin} : Marp n'a pas pu générer le diaporama.\n${journal}`) + } + + let html = readFileSync(page, 'utf8') + const erreurs = controlerLesRessources(html, presentation) + if (erreurs.length > 0) { + throw new ErreurDeBuild(erreurs.map((erreur) => `${chemin} : ${erreur}`).join('\n')) + } + + // Marp écrit lang="en" faute de directive : le contenu est en français. + html = html.replace(/]*>/i, (balise) => /\slang=/.test(balise) + ? balise.replace(/\slang=["'][^"']*["']/, ` lang="${presentation.langue}"`) + : balise.replace(/ String(texte) + .replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') + .replaceAll('"', '"').replaceAll("'", ''') + +/** Minuscules sans accents : « Débutant » et « debutant » se retrouvent. */ +const normaliser = (texte) => texte.normalize('NFD').replace(/\p{M}/gu, '').toLowerCase() + +const dateLisible = (date) => new Intl.DateTimeFormat('fr-FR', { dateStyle: 'long', timeZone: 'UTC' }) + .format(new Date(`${date}T00:00:00Z`)) + +function carte(presentation) { + const p = presentation + const recherche = normaliser([p.titre, ...p.tags].join(' ')) + const tags = p.tags.map((tag) => `
  • ${echapper(tag)}
  • `).join('') + const lienWiki = p.wiki + ? `\n Fiche sur le wiki : ${echapper(p.titre)}` + : '' + return `
  • + +
  • ` +} + +const options = (valeurs) => Object.entries(valeurs) + .map(([valeur, libelle]) => ``).join('') + +function pageAccueil(presentations) { + const total = presentations.length + const compte = total === 0 ? 'Aucune présentation' : total === 1 ? '1 présentation' : `${total} présentations` + return ` + + + + +Présentations d'Alpinux + + + + + + + +Aller à la liste des présentations +
    +
    + +
    +

    Présentations d'Alpinux

    +

    Les diaporamas des réunions du groupe d'utilisateurs de logiciels libres de Savoie.

    +
    +
    +
    +
    +
    + +

    ${compte}, de la plus récente à la plus ancienne.

    +
      +${presentations.map(carte).join('\n')} +
    +

    ${total === 0 ? 'Aucune présentation n\'est encore publiée.' : 'Aucune présentation ne correspond à ces critères.'}

    +
    +
    + + + + +` +} + +function fichierJson(presentations) { + return JSON.stringify({ + genere: new Date().toISOString(), + site: `${URL_SITE}/`, + presentations: presentations.map((p) => ({ + id: p.id, + titre: p.titre, + description: p.description, + auteur: p.auteur, + date: p.date, + niveau: p.niveau, + format: p.format, + tags: p.tags, + url: `${URL_SITE}/${p.id}/`, + wiki: p.wiki, + })), + }, null, 2) + '\n' +} + +const HTACCESS = `# Généré par scripts/build.mjs — ne pas modifier sur le serveur, le prochain +# déploiement l'écraserait. + +AddDefaultCharset utf-8 + +# presentations.json est lu par alpinux.org depuis le navigateur du visiteur : sans cet +# en-tête, un autre domaine n'a pas le droit de le lire. Rien d'autre n'est ouvert. + + + Header set Access-Control-Allow-Origin "*" + + +` + +// ── Enchaînement ─────────────────────────────────────────────────────────── + +/** Refuse un répertoire de sortie dont le nettoyage emporterait les sources. */ +function verifierLaSortie(sortie) { + const contient = (parent, enfant) => enfant === parent || enfant.startsWith(parent + sep) + if (contient(sortie, RACINE) || ['slides', 'theme', 'scripts', 'deploy', 'node_modules', '.git'] + .some((dossier) => contient(join(RACINE, dossier), sortie))) { + throw new ErreurDeBuild(`répertoire de sortie refusé : ${sortie} — il serait vidé alors qu'il contient des sources`) + } +} + +async function construire(sortie) { + verifierLaSortie(sortie) + const { presentations, brouillons } = collecterLesPresentations() + for (const id of brouillons) console.log(` (brouillon, non publié) ${id}`) + + // Tout est généré à part, puis mis en place d'un coup : un échec en cours de route + // laisse le répertoire de sortie tel qu'il était. + mkdirSync(dirname(sortie), { recursive: true }) + const chantier = mkdtempSync(join(dirname(sortie), '.build-')) + chmodSync(chantier, 0o755) + try { + for (const presentation of presentations) { + await genererLeDiaporama(presentation, chantier) + console.log(` diaporama ${presentation.id}/index.html`) + } + writeFileSync(join(chantier, 'index.html'), pageAccueil(presentations)) + writeFileSync(join(chantier, 'presentations.json'), fichierJson(presentations)) + writeFileSync(join(chantier, '.htaccess'), HTACCESS) + console.log(' accueil index.html\n données presentations.json\n serveur .htaccess') + + rmSync(sortie, { recursive: true, force: true }) + renameSync(chantier, sortie) + } finally { + rmSync(chantier, { recursive: true, force: true }) + } + console.log(`Build réussi : ${presentations.length} présentation(s) dans ${relative(process.cwd(), sortie) || '.'}/`) +} + +async function exporterEnPdf(id, sortie) { + if (!id) throw new ErreurDeBuild('identifiant manquant — usage : npm run pdf -- ') + const source = join(SOURCES, id, 'slides.md') + if (!FORME_IDENTIFIANT.test(id) && id !== '_modele' || !existsSync(source)) { + throw new ErreurDeBuild(`présentation introuvable : slides/${id}/slides.md`) + } + const pdf = join(sortie, id, `${id}.pdf`) + mkdirSync(dirname(pdf), { recursive: true }) + // Marp pilote un navigateur déjà installé (Chromium, Chrome, Edge ou Firefox) pour + // imprimer le diaporama : c'est pourquoi cet export ne se fait pas sur le serveur. + const { code, journal } = await lancerMarp([ + source, '-o', pdf, '--pdf', '--allow-local-files', '--theme-set', THEME, '--theme', NOM_DU_THEME, + ]) + if (code !== 0 || !existsSync(pdf)) { + throw new ErreurDeBuild(`l'export PDF de « ${id} » a échoué. Un navigateur (Chromium ou Firefox) ` + + `doit être installé sur ce poste.\n${journal}`) + } + console.log(`PDF généré : ${relative(process.cwd(), pdf)}`) +} + +const args = process.argv.slice(2) +try { + if (args[0] === '--pdf') { + await exporterEnPdf(args[1], resolve(args[2] ?? join(RACINE, 'public'))) + } else if (args[0]?.startsWith('-')) { + throw new ErreurDeBuild(`option inconnue : ${args[0]} — usage : node scripts/build.mjs [sortie] | --pdf [sortie]`) + } else { + await construire(resolve(args[0] ?? join(RACINE, 'public'))) + } +} catch (erreur) { + if (!(erreur instanceof ErreurDeBuild)) throw erreur + console.error(`\nÉCHEC du build — ${erreur.message}\n`) + process.exit(1) +} diff --git a/scripts/deploy-presentations.sh b/scripts/deploy-presentations.sh new file mode 100755 index 0000000..138b120 --- /dev/null +++ b/scripts/deploy-presentations.sh @@ -0,0 +1,40 @@ +#!/bin/bash +# Met en ligne presentations.alpinux.org depuis le dépôt alpinux-presentations. +# Appelé par le service webhook après chaque push sur main, ou à la main en secours. +# Calqué sur deploy-wiki.sh : le serveur exécute sa copie, posée par deploy/installer.sh. +set -e + +REPO_DIR="/opt/presentations-alpinux/repo" +STAGING="/opt/presentations-alpinux/staging" +TARGET="/var/www/clients/client1/web22/web" +NODE_BIN="/opt/presentations-alpinux/node/bin" +LOG="/var/log/presentations-deploy.log" + +export PATH="$NODE_BIN:$PATH" + +# Deux push rapprochés : le second déploiement attend la fin du premier au lieu de +# construire dans le même staging. +exec 9> "/tmp/presentations-deploy.lock" +flock 9 + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy started" >> "$LOG" +trap 'echo "[$(date "+%Y-%m-%d %H:%M:%S")] ABANDON : le site en ligne est inchangé" >> "$LOG"' ERR + +cd "$REPO_DIR" +git pull origin main >> "$LOG" 2>&1 + +# --ignore-scripts : aucune dépendance n'a besoin d'exécuter du code à l'installation, +# et aucune n'en a donc le droit. +npm ci --ignore-scripts --no-audit --no-fund >> "$LOG" 2>&1 + +# Build dans un répertoire de staging : le site en ligne reste intact si le build +# échoue (front matter invalide, image manquante, ressource externe). +node scripts/build.mjs "$STAGING" >> "$LOG" 2>&1 + +# Bascule vers le DocumentRoot une fois le build valide. Les deux dossiers exclus +# appartiennent à ISPConfig (pages d'erreur, statistiques) : --delete les épargne. +rsync -rltD --no-perms --omit-dir-times --delete \ + --exclude=/error/ --exclude=/stats/ \ + "$STAGING/" "$TARGET/" >> "$LOG" 2>&1 + +echo "[$(date '+%Y-%m-%d %H:%M:%S')] Deploy done" >> "$LOG" diff --git a/slides/_modele/images/exemple.svg b/slides/_modele/images/exemple.svg new file mode 100644 index 0000000..5bba219 --- /dev/null +++ b/slides/_modele/images/exemple.svg @@ -0,0 +1,4 @@ + + + images/exemple.svg + diff --git a/slides/_modele/slides.md b/slides/_modele/slides.md new file mode 100644 index 0000000..b554e16 --- /dev/null +++ b/slides/_modele/slides.md @@ -0,0 +1,126 @@ +--- +# ── Réglages de Marp : à laisser tels quels ──────────────────────────────── +marp: true +theme: alpinux +paginate: true + +# ── Fiche de la présentation : à remplir ─────────────────────────────────── +# Tous ces champs sont obligatoires, sauf « wiki » et « brouillon ». +# Le build refuse un champ manquant, mal orthographié ou une valeur inattendue. + +title: Le titre de votre présentation +description: Une phrase qui dit ce qu'on saura faire en sortant de la séance. +auteur: Prénom Nom +date: 2026-01-15 # date de la séance, au format AAAA-MM-JJ +niveau: debutant # debutant | intermediaire | confirme +format: court # eclair | court | long +tags: [exemple, modele] # mots-clés, en minuscules ; ils servent à la recherche + +# Adresse de la fiche correspondante sur le wiki — supprimez la ligne s'il n'y en a pas. +wiki: https://wiki.alpinux.org/ + +# Tant que cette ligne vaut « true », la présentation n'est pas publiée. +# Supprimez-la (ou passez-la à « false ») quand le diaporama est prêt. +brouillon: true +--- + + + + +# Le titre de votre présentation + +## Un sous-titre, si besoin + +Prénom Nom — Alpinux, jeudi 15 janvier 2026 + + + +--- + +## Une diapo ordinaire + +Trois tirets seuls sur une ligne séparent deux diapos. + +- Une idée par ligne, pas une phrase entière +- **Du gras** pour le mot qui compte +- Trois à cinq lignes : au-delà, on ne lit plus depuis le fond de la salle + +> Une citation fait un encadré : pratique pour « à retenir » ou « piège ». + + + +--- + +## Du code et des raccourcis + +Une commande courte se lit bien dans un bloc : + +```bash +sudo apt install xournalpp +``` + +Dans une phrase : `du -h fichier.pdf`, ou une touche Ctrl + S. + + + +--- + +## Un tableau + +| Je veux… | J'utilise… | +|---------------------|-------------| +| Lire un document | Xreader | +| Signer un document | Xournal++ | + + + +--- + +## Une image + +Rangez vos images dans le dossier `images/`, à côté de ce fichier : + +![Description de l'image, pour qui ne la voit pas](images/exemple.svg) + + + +--- + + + +## Ce que je vais montrer + +1. Première manipulation +2. Deuxième manipulation +3. Ce qu'il faut regarder à l'écran + + + +--- + + + +# Merci ! + +## Des questions ? + +Diaporama : presentations.alpinux.org diff --git a/slides/pdf-signer/images/parcours.svg b/slides/pdf-signer/images/parcours.svg new file mode 100644 index 0000000..b5ae630 --- /dev/null +++ b/slides/pdf-signer/images/parcours.svg @@ -0,0 +1,37 @@ + + Le parcours du dossier : remplir, signer, assembler, alléger, envoyer + + + + + + + + + + + + + + + + + + + + + Remplir + Signer + Assembler + Alléger + + Envoyer + + Okular + Xournal++ + PDF Arranger + Ghostscript + + un seul PDF + + diff --git a/slides/pdf-signer/slides.md b/slides/pdf-signer/slides.md new file mode 100644 index 0000000..d2bcb43 --- /dev/null +++ b/slides/pdf-signer/slides.md @@ -0,0 +1,477 @@ +--- +marp: true +theme: alpinux +paginate: true + +title: Lire, remplir et signer ses PDF +description: Remplir, signer, assembler et alléger un dossier PDF sous Linux Mint, sans imprimante, sans scanner et sans service en ligne. +auteur: Alpinux +date: 2026-10-15 +niveau: debutant +format: court +tags: [pdf, bureautique, linux-mint, xreader, okular, xournalpp, pdfarranger, libreoffice] +--- + + + + +# Lire, remplir et signer ses PDF + +## Sans imprimante, sans scanner, sans service en ligne + +Alpinux — sous Linux Mint, 20 minutes + + + +--- + +## Le fil rouge : un dossier d'inscription + +![Le parcours du dossier : recevoir le formulaire, le remplir, le signer, joindre le justificatif, alléger, envoyer](images/parcours.svg) + +- Un **formulaire** à remplir et à signer +- Une **pièce justificative** à joindre +- Le tout en **un seul PDF léger**, envoyé par mail + + + +--- + +## Cinq outils, tous libres + +| Étape | Outil | Dans Linux Mint ? | +|----------------------|------------------|--------------------| +| Lire, surligner | **Xreader** | Déjà installé | +| Remplir, annoter | **Okular** | À installer | +| Signer | **Xournal++** | À installer | +| Assembler | **PDF Arranger** | À installer | +| Créer, alléger | **LibreOffice** | Déjà installé | + +```bash +sudo apt install okular xournalpp pdfarranger +``` + + + +--- + +## 1. Xreader : lire, chercher, surligner + +Le lecteur de Linux Mint : un double-clic sur le PDF suffit. + +- **Panneau latéral** : vignettes des pages, sommaire +- **Chercher** un mot : Ctrl + F +- **Surligner** et ajouter une note : panneau *Annotations* +- **Remplir** un champ de formulaire simple, directement + +> Pour lire, et pour un formulaire simple : **rien à installer**. + + + +--- + +## Xreader : le piège de la fermeture + +Xreader **n'enregistre pas** dans le fichier ouvert. + +- *Fichier → Enregistrer une copie…* +- Donner un **nouveau nom** : `inscription-remplie.pdf` +- Rouvrir la copie pour **vérifier** + +> **Piège** — fermer la fenêtre sans « Enregistrer une copie », c'est perdre les +> annotations et tout ce qu'on a saisi. + + + +--- + + + +## Xreader + +1. Ouvrir `inscription.pdf` par double-clic +2. Ctrl + F : chercher « justificatif » +3. Surligner la liste des pièces demandées +4. *Enregistrer une copie…*, puis rouvrir la copie + + + +--- + +## 2. Okular : remplir un formulaire + +- Bandeau **« Afficher les formulaires »** en haut du document +- Saisie des **champs**, des **cases à cocher**, des listes +- **Annotations** : F6 — surligneur, note, trait à main levée +- Enregistrer : Ctrl + S — le texte est **dans** le PDF + +> S'il ne fallait installer qu'un seul outil, ce serait celui-ci. + + + +--- + +## Okular : écrire là où il n'y a pas de champ + +Un formulaire **scanné** n'a pas de champs : c'est une image. + +- Barre d'annotations (F6) → **Machine à écrire** +- Cliquer à l'endroit voulu, taper le texte +- Déplacer et ajuster le texte ensuite, si besoin +- Cocher une case : taper un **X** au bon endroit + +> **Piège** — un PDF protégé peut refuser la saisie. +> Dans ce cas : passer à Xournal++. + + + +--- + + + +## Okular + +1. Ouvrir le formulaire **à champs** : afficher les formulaires, remplir +2. Ouvrir le formulaire **scanné** : machine à écrire +3. *Enregistrer sous* `inscription-remplie.pdf` +4. Rouvrir dans Xreader : le texte est bien là + + + +--- + +## 3. Xournal++ : signer + +- *Fichier → Annoter un PDF* : ouvrir le formulaire rempli +- Outil **Image** : insérer sa signature en **PNG** +- La **redimensionner**, la placer sur la ligne +- Ajouter la **date** avec l'outil Texte + +> Une signature en image convient à beaucoup de démarches courantes. +> Ce n'est **pas** une signature électronique qualifiée. + + + +--- + +## Xournal++ : l'erreur numéro un + +| Commande | Produit | Le destinataire… | +|------------------------------|-----------------|-------------------------| +| *Fichier → Enregistrer* | un `.xopp` | **ne peut pas l'ouvrir** | +| *Fichier → Exporter en PDF* | un `.pdf` | le lit normalement | + +> **Piège** — « Enregistrer » ne modifie pas le PDF. Pour l'envoyer, il faut +> **exporter en PDF**. + + + +--- + +## 4. PDF Arranger : assembler + +- **Glisser** le formulaire signé et le justificatif dans la fenêtre +- **Réordonner** les pages à la souris +- **Pivoter** une page scannée de travers +- **Supprimer** une page blanche +- **Extraire** : sélectionner des pages → *Exporter la sélection* + +> Il accepte aussi les **images** : une photo devient une page du PDF. + + + +--- + + + +## Signer, puis assembler + +1. **Xournal++** : *Annoter un PDF* → `inscription-remplie.pdf` +2. Insérer la signature, ajouter la date +3. *Exporter en PDF* → `inscription-signee.pdf` +4. **PDF Arranger** : glisser le PDF signé et la photo du justificatif +5. Pivoter la photo, *Enregistrer sous* `dossier-inscription.pdf` + + + +--- + +## 5. LibreOffice : créer un PDF léger + +*Fichier → Exporter vers → Exporter au format PDF…* + +- **Compression JPEG** : une qualité de 80 % suffit pour l'écran +- **Réduire la résolution des images** : 150 DPI +- Comparer le poids **avant et après** dans le gestionnaire de fichiers + +> Option **PDF hybride** : le PDF embarque le document d'origine et reste +> modifiable dans LibreOffice. + + + +--- + +## Alléger un scan : Ghostscript + +Le PDF existe déjà et il est trop lourd pour le mail : + +```bash +gs -sDEVICE=pdfwrite -dPDFSETTINGS=/ebook \ + -dNOPAUSE -dBATCH \ + -sOutputFile=leger.pdf lourd.pdf +``` + +- `/ebook` : images à 150 DPI — le bon compromis +- `/screen` : encore plus léger, souvent **illisible** + + + +--- + +## Et les sites de compression en ligne ? + +Ils fonctionnent. Mais on y envoie, à un inconnu : + +- sa **pièce d'identité** +- son **avis d'imposition** +- son **RIB**, sa **signature** + +> Tout ce qui a été montré ce soir **reste sur l'ordinateur**. + + + +--- + +## Mémo : je veux… j'utilise… + +| Je veux… | J'utilise… | +|---------------------------------------------|------------------| +| Lire, chercher, surligner | **Xreader** | +| Remplir un formulaire, écrire sur un PDF | **Okular** | +| Signer | **Xournal++** | +| Fusionner, pivoter, extraire des pages | **PDF Arranger** | +| Créer un PDF léger | **LibreOffice** | +| Alléger un scan | **Ghostscript** | + + + +--- + + + +# Merci ! + +## Des questions ? + +Diaporama et mémo : **presentations.alpinux.org** + + diff --git a/theme/alpinux.css b/theme/alpinux.css new file mode 100644 index 0000000..46134d4 --- /dev/null +++ b/theme/alpinux.css @@ -0,0 +1,297 @@ +/* @theme alpinux */ +/* @auto-scaling true */ +/* @size 16:9 1280px 720px */ +/* @size 4:3 960px 720px */ + +/* + * Thème Marp des présentations d'Alpinux. + * + * Pensé pour la vidéoprojection : gros corps de texte, fort contraste, aucune + * ressource externe hormis le logo, servi par static.alpinux.org. Les polices sont + * celles du système — rien n'est téléchargé. + * + * Deux classes à poser sur une diapo avec : + * titre la diapo d'ouverture (et les intercalaires de partie) ; + * demo une diapo qui annonce une démonstration en direct. + */ + +:root { + --bleu: #1a6bbf; + --bleu-fonce: #0f4e8f; + --nuit: #12263f; + --encre: #1a1a2e; + --encre-douce: #4a5568; + --papier: #ffffff; + --papier-teinte: #f3f6fb; + --trait: #c9d6e8; + --alerte: #b45309; + --logo: url("https://static.alpinux.org/logo/alpinux-logo-512.png"); +} + +/* ── La diapo ─────────────────────────────────────────────────────────── */ +section { + width: 1280px; + height: 720px; + box-sizing: border-box; + padding: 48px 72px 92px; + display: flex; + flex-direction: column; + justify-content: flex-start; + background: var(--papier); + color: var(--encre); + font-family: "Inter", "Noto Sans", "Ubuntu", "DejaVu Sans", "Segoe UI", system-ui, sans-serif; + font-size: 32px; + line-height: 1.4; + letter-spacing: 0.005em; +} + +section > *:first-child { margin-top: 0; } +section > *:last-child { margin-bottom: 0; } + +/* ── Titres ───────────────────────────────────────────────────────────── */ +h1, h2, h3, h4 { + margin: 0 0 0.55em; + line-height: 1.15; + font-weight: 700; + color: var(--bleu-fonce); +} + +h1 { font-size: 1.75em; } + +h2 { + font-size: 1.4em; + padding-bottom: 0.25em; + border-bottom: 4px solid var(--bleu); +} + +h3 { font-size: 1.1em; color: var(--encre); } +h4 { font-size: 1em; color: var(--encre-douce); } + +/* ── Texte ────────────────────────────────────────────────────────────── */ +p, ul, ol, blockquote, pre, table { margin: 0 0 0.6em; } + +ul, ol { padding-left: 1.2em; } +li { margin-bottom: 0.25em; } +li > ul, li > ol { margin: 0.2em 0 0; font-size: 0.9em; } +ul > li::marker { color: var(--bleu); } +ol > li::marker { color: var(--bleu); font-weight: 700; } + +strong { color: var(--bleu-fonce); font-weight: 700; } +em { font-style: italic; } + +a { color: var(--bleu); text-decoration: underline; text-underline-offset: 0.15em; } + +mark { background: #ffe066; color: var(--encre); padding: 0 0.15em; border-radius: 4px; } + +/* Une citation sert d'encadré « à retenir » ou « piège ». */ +blockquote { + padding: 0.5em 0.9em; + border-left: 10px solid var(--bleu); + background: var(--papier-teinte); + border-radius: 0 10px 10px 0; + font-size: 0.92em; +} + +blockquote > *:last-child { margin-bottom: 0; } + +kbd { + display: inline-block; + padding: 0.05em 0.4em; + border: 2px solid var(--encre-douce); + border-bottom-width: 4px; + border-radius: 8px; + background: var(--papier); + font-family: inherit; + font-size: 0.82em; + line-height: 1.3; + white-space: nowrap; +} + +img { max-width: 100%; max-height: 100%; } + +/* ── Code ─────────────────────────────────────────────────────────────── */ +code { + font-family: "JetBrains Mono", "DejaVu Sans Mono", "Ubuntu Mono", "Liberation Mono", monospace; + font-size: 0.86em; + padding: 0.08em 0.3em; + border-radius: 6px; + background: #e4ecf7; + color: #0b2e55; +} + +pre { + padding: 0.7em 0.9em; + border-radius: 12px; + border-left: 10px solid var(--bleu); + background: var(--nuit); + color: #f1f5fb; + font-size: 0.8em; + line-height: 1.45; + overflow: hidden; +} + +pre code { + padding: 0; + background: none; + color: inherit; + font-size: 1em; +} + +/* Coloration syntaxique (highlight.js) — teintes claires sur fond nuit, toutes + au-dessus de 7:1 de contraste. */ +.hljs-comment, .hljs-quote { color: #a8b8cc; font-style: italic; } +.hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-name, .hljs-tag { color: #8cc4ff; } +.hljs-string, .hljs-attr, .hljs-symbol, .hljs-bullet, .hljs-addition { color: #a5e3a0; } +.hljs-number, .hljs-literal, .hljs-variable, .hljs-template-variable { color: #ffc98a; } +.hljs-title, .hljs-section, .hljs-type, .hljs-meta { color: #f7e08a; } +.hljs-deletion { color: #ffb0a8; } +.hljs-emphasis { font-style: italic; } +.hljs-strong { font-weight: 700; } + +/* ── Tableaux ─────────────────────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + font-size: 0.86em; +} + +th, td { + padding: 0.32em 0.7em; + text-align: left; + vertical-align: top; + border-bottom: 2px solid var(--trait); +} + +thead th { + background: var(--bleu-fonce); + color: #fff; + font-weight: 700; + border-bottom: none; +} + +thead th:first-child { border-radius: 10px 0 0 0; } +thead th:last-child { border-radius: 0 10px 0 0; } +tbody tr:nth-child(even) { background: var(--papier-teinte); } +tbody td:first-child { font-weight: 600; } + +/* ── Pied de diapo : logo, texte libre, numéro de page ────────────────── */ +section::before { + content: ""; + position: absolute; + left: 40px; + bottom: 22px; + width: 52px; + height: 52px; + background: var(--logo) center / contain no-repeat; +} + +header, footer { + position: absolute; + left: 108px; + right: 160px; + font-size: 0.55em; + color: var(--encre-douce); +} + +header { top: 22px; left: 72px; } +footer { bottom: 36px; } + +section::after { + content: attr(data-marpit-pagination) " / " attr(data-marpit-pagination-total); + position: absolute; + right: 44px; + bottom: 34px; + /* Marpit donne à ce pseudo-élément le remplissage de la diapo : on l'annule. */ + padding: 0; + font-size: 0.6em; + font-weight: 600; + font-variant-numeric: tabular-nums; + color: var(--encre-douce); +} + +/* ── Classe « titre » : ouverture et intercalaires ────────────────────── */ +section.titre { + justify-content: center; + padding: 72px 96px 120px; + background: var(--bleu); + color: #fff; +} + +section.titre h1, +section.titre h2, +section.titre h3, +section.titre strong { color: #fff; } + +section.titre h1 { font-size: 2.3em; margin-bottom: 0.3em; } + +section.titre h2 { + border-bottom: none; + padding-bottom: 0; + font-size: 1.25em; + font-weight: 400; +} + +section.titre p { font-size: 0.82em; color: #e6f0fb; } +section.titre a { color: #fff; } +section.titre code { background: var(--bleu-fonce); color: #fff; } +section.titre footer, +section.titre header, +section.titre::after { color: #e6f0fb; } + +/* Le logo est bleu : sur fond bleu, on le pose sur une pastille blanche. */ +section.titre::before { + width: 72px; + height: 72px; + left: 96px; + bottom: 40px; + background-color: #fff; + background-size: 84%; + border-radius: 14px; +} + +section.titre footer { left: 190px; bottom: 62px; } + +/* ── Classe « demo » : on quitte le diaporama pour montrer ────────────── */ +section.demo { + justify-content: center; + background: var(--nuit); + color: #f1f5fb; +} + +section.demo h1, +section.demo h2, +section.demo h3 { color: #fff; } + +section.demo h2 { border-bottom-color: #ffc857; } +section.demo strong { color: #ffc857; } +section.demo a { color: #9fd0ff; } +section.demo ul > li::marker, +section.demo ol > li::marker { color: #ffc857; } +section.demo code { background: #24456e; color: #fff; } +section.demo pre { background: #0a1728; border-left-color: #ffc857; } +section.demo blockquote { background: #1c3a5e; border-left-color: #ffc857; color: #f1f5fb; } +section.demo kbd { background: #1c3a5e; border-color: #9fb3cc; color: #fff; } +section.demo footer, +section.demo header, +section.demo::after { color: #c5d3e6; } + +section.demo h2::before { + content: "Démonstration"; + display: block; + width: fit-content; + margin-bottom: 0.5em; + padding: 0.15em 0.7em; + border-radius: 999px; + background: #ffc857; + color: var(--nuit); + font-size: 0.5em; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +section.demo::before { + background-color: #fff; + background-size: 84%; + border-radius: 10px; +}