Compare commits
10 Commits
feat/ancho
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e894e7a1f | |||
| d87d8c0a8f | |||
| 5c00593e67 | |||
| b2b3d5621b | |||
| 4c7b6a772c | |||
| 11e45eb9d0 | |||
| 266b364195 | |||
| cb4cd01409 | |||
| a09dbca800 | |||
| 874c630a2a |
46
README.md
46
README.md
@@ -1,47 +1,3 @@
|
|||||||
# Astro Starter Kit: Minimal
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm create astro@latest -- --template minimal
|
|
||||||
```
|
|
||||||
|
|
||||||
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
|
|
||||||
|
|
||||||
## 🚀 Project Structure
|
|
||||||
|
|
||||||
Inside of your Astro project, you'll see the following folders and files:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/
|
|
||||||
├── public/
|
|
||||||
├── src/
|
|
||||||
│ └── pages/
|
|
||||||
│ └── index.astro
|
|
||||||
└── package.json
|
|
||||||
```
|
|
||||||
|
|
||||||
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
|
||||||
|
|
||||||
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
|
||||||
|
|
||||||
Any static assets, like images, can be placed in the `public/` directory.
|
|
||||||
|
|
||||||
## 🧞 Commands
|
|
||||||
|
|
||||||
All commands are run from the root of the project, from a terminal:
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| :------------------------ | :----------------------------------------------- |
|
|
||||||
| `npm install` | Installs dependencies |
|
|
||||||
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
|
||||||
| `npm run build` | Build your production site to `./dist/` |
|
|
||||||
| `npm run preview` | Preview your build locally, before deploying |
|
|
||||||
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
|
||||||
| `npm run astro -- --help` | Get help using the Astro CLI |
|
|
||||||
|
|
||||||
## 👀 Want to learn more?
|
|
||||||
|
|
||||||
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).
|
|
||||||
|
|
||||||
# Archicratie — Web Edition (Atelier éditorial)
|
# Archicratie — Web Edition (Atelier éditorial)
|
||||||
|
|
||||||
Ce repo contient le site Astro “Archicratie – Web Edition” et sa machine éditoriale :
|
Ce repo contient le site Astro “Archicratie – Web Edition” et sa machine éditoriale :
|
||||||
@@ -58,6 +14,7 @@ Ce repo contient le site Astro “Archicratie – Web Edition” et sa machine
|
|||||||
- **Quickstart (10 min)** : `docs/QUICKSTART.md`
|
- **Quickstart (10 min)** : `docs/QUICKSTART.md`
|
||||||
- **Manuel de référence** : `docs/MANUEL_REFERENCE.md`
|
- **Manuel de référence** : `docs/MANUEL_REFERENCE.md`
|
||||||
- **Contrat de tickets** : `docs/CONTRAT_TICKETS.md`
|
- **Contrat de tickets** : `docs/CONTRAT_TICKETS.md`
|
||||||
|
- **Topo logique métier** : `docs/TOPO_LOGIQUE_METIER.md`
|
||||||
|
|
||||||
## Commandes clés
|
## Commandes clés
|
||||||
- Tests complets : `npm test`
|
- Tests complets : `npm test`
|
||||||
@@ -65,3 +22,4 @@ Ce repo contient le site Astro “Archicratie – Web Edition” et sa machine
|
|||||||
- Appliquer un ticket :
|
- Appliquer un ticket :
|
||||||
- `node scripts/apply-ticket.mjs <N> --dry-run`
|
- `node scripts/apply-ticket.mjs <N> --dry-run`
|
||||||
- `node scripts/apply-ticket.mjs <N>`
|
- `node scripts/apply-ticket.mjs <N>`
|
||||||
|
|
||||||
|
|||||||
250
docs/TOPO_LOGIQUE_METIER.md
Normal file
250
docs/TOPO_LOGIQUE_METIER.md
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
# Topo – Logique métier de l’outil éditorial (Web Edition ↔ Gitea ↔ apply-ticket ↔ CI)
|
||||||
|
|
||||||
|
Ce document explique **ce que fait réellement l’outil**, pourquoi il existe, et comment le maintenir **sans casser** la promesse centrale : **citabilité durable + édition traçable + intégration sûre**.
|
||||||
|
|
||||||
|
> Public visé : “commun des mortels” (nouveau contributeur, mainteneur occasionnel, moi dans 6 mois).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0) La promesse (le “pourquoi”)
|
||||||
|
|
||||||
|
On veut un site (Astro) où chaque paragraphe est :
|
||||||
|
- **citable** (URL + ancre stable),
|
||||||
|
- **corrigeable** (ouvrir un ticket pré-rempli depuis le paragraphe),
|
||||||
|
- **réparable de manière sûre** (appliquer la proposition dans les sources sans magie),
|
||||||
|
- **vérifié automatiquement** (tests + build + garde-fous en CI).
|
||||||
|
|
||||||
|
En bref :
|
||||||
|
**Lecture → proposition → ticket → application → tests → build → publication**
|
||||||
|
…sans perdre la traçabilité, ni casser les citations historiques.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Glossaire (pas de jargon sans définition)
|
||||||
|
|
||||||
|
- **Ancre** : partie `#...` d’une URL (ex : `#p-8-0e65838d`). Le navigateur saute à l’élément HTML portant cet `id`.
|
||||||
|
- **Paragraphe citable** : un `<p id="p-...">...</p>` dans l’article.
|
||||||
|
- **Ticket / Issue** : demande de correction sur Gitea (ex : #14).
|
||||||
|
- **PR (Pull Request)** : “demande d’ajout / fusion” : une proposition de merge d’une branche vers `master`.
|
||||||
|
- **CI** : tests automatiques exécutés sur le serveur à chaque push/PR.
|
||||||
|
- **Runner** : machine/conteneur qui exécute la CI (chez nous : DS220+ / ds220-runner).
|
||||||
|
- **DEV** : `npm run dev` (serveur Astro à la volée, pas de `dist/`).
|
||||||
|
- **BUILD / PROD-like** : `npm run build` puis servir `dist/` (ce qui ressemble à la prod).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Vue d’ensemble : le pipeline (la “machine éditoriale”)
|
||||||
|
|
||||||
|
### 2.1 Le flux humain (ce que fait l’éditeur)
|
||||||
|
1) Lire une page sur “Archicratie – Web Edition”.
|
||||||
|
2) Sur un paragraphe : cliquer **Proposer**.
|
||||||
|
3) Choisir **Type** (Correction / Fact-check) + **Category** (lexique, style, etc.).
|
||||||
|
4) Gitea s’ouvre en **nouvel onglet** avec un ticket pré-rempli.
|
||||||
|
5) L’éditeur écrit la proposition, justifie, et valide le ticket.
|
||||||
|
|
||||||
|
### 2.2 Le flux technique (ce que fait la machine)
|
||||||
|
6) `apply-ticket` récupère le ticket via l’API Gitea.
|
||||||
|
7) Il retrouve le bon fichier source (MDX) + le bon paragraphe (matching sûr).
|
||||||
|
8) Il applique la modification (ou refuse si trop incertain).
|
||||||
|
9) On lance `npm test` → build + test ancres + check JS inline.
|
||||||
|
10) On commit / push.
|
||||||
|
11) La CI revalide automatiquement sur le serveur.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Composant A — Le site (Astro) et les outils de paragraphe
|
||||||
|
|
||||||
|
Dans le rendu final, chaque paragraphe important reçoit un `id` de forme :
|
||||||
|
|
||||||
|
- `p-<index>-<hash8>`
|
||||||
|
ex : `p-8-0e65838d`
|
||||||
|
|
||||||
|
Ensuite, un script ajoute des **outils de paragraphe** (“para-tools”) :
|
||||||
|
- **¶** : lien direct sur le paragraphe (ancre)
|
||||||
|
- **Citer** : copie une citation (titre + version + URL sans query-string)
|
||||||
|
- **Proposer** : ouvre la modale (2 étapes), puis ouvre le ticket Gitea
|
||||||
|
|
||||||
|
### Point crucial : progressive enhancement
|
||||||
|
- Si JS marche : UX optimale.
|
||||||
|
- Si JS casse : le site reste lisible, et le lien de proposition reste utilisable au minimum via `href`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) Composant B — Contrat des tickets (machine-readable)
|
||||||
|
|
||||||
|
Pour que `apply-ticket` puisse travailler, le ticket doit rester **structuré**.
|
||||||
|
|
||||||
|
Le ticket contient des lignes “clé: valeur” (une par ligne), par ex :
|
||||||
|
- `Chemin: /archicratie/prologue/`
|
||||||
|
- `URL locale: http://localhost:4321/archicratie/prologue/#p-...`
|
||||||
|
- `Ancre: #p-8-...`
|
||||||
|
- `Version: ...`
|
||||||
|
- `Type: type/correction` (ou type/fact-check)
|
||||||
|
- `State: state/recevable` (ou state/a-sourcer)
|
||||||
|
- `Category: cat/lexique` (optionnel)
|
||||||
|
|
||||||
|
**Règle d’or :** ces clés doivent rester simples, stables, sur une ligne chacune.
|
||||||
|
Sinon, scripts + CI + auto-labeling deviennent fragiles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Composant C — apply-ticket (appliquer sans magie)
|
||||||
|
|
||||||
|
`apply-ticket` fait 4 choses :
|
||||||
|
|
||||||
|
1) **Fetch** : récupère le ticket via API (FORGE_TOKEN requis).
|
||||||
|
2) **Parse** : extrait Chemin/Ancre/Proposition/(Texte actuel si présent).
|
||||||
|
3) **Match** : retrouve le paragraphe dans le fichier source (MDX) :
|
||||||
|
- idéal : “Texte actuel (copie exacte…)” → match fort
|
||||||
|
- sinon : match par score → si score trop faible, **refus** (sécurité)
|
||||||
|
4) **Apply** : modifie le fichier source, sans casser le reste.
|
||||||
|
|
||||||
|
### Mode sûr
|
||||||
|
- `--dry-run` : montre BEFORE/AFTER, **n’écrit rien**
|
||||||
|
- mode normal : écrit + propose ensuite `git diff`, `git add`, `git commit`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Composant D — Citabilité P0 : ancres, churn test, aliases
|
||||||
|
|
||||||
|
### 6.1 Le problème
|
||||||
|
Si l’ID dépend du contenu, modifier un paragraphe peut changer l’ID.
|
||||||
|
Donc une citation historique `...#ancien` casse.
|
||||||
|
|
||||||
|
### 6.2 La solution robuste (“web native”)
|
||||||
|
On ne “résout” pas l’ancre : **on la fait exister**.
|
||||||
|
|
||||||
|
On maintient un fichier :
|
||||||
|
- `src/anchors/anchor-aliases.json`
|
||||||
|
|
||||||
|
Exemple (par page/chemin) :
|
||||||
|
en json
|
||||||
|
{
|
||||||
|
"/archicratie/prologue/": {
|
||||||
|
"p-8-e7075fe3": "p-8-0e65838d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Au build, un script injecte dans dist/.../index.html :
|
||||||
|
<span class="para-alias" id="p-8-e7075fe3"></span>
|
||||||
|
<p id="p-8-0e65838d">...</p>
|
||||||
|
|
||||||
|
Résultat :
|
||||||
|
|
||||||
|
le navigateur résout #p-8-e7075fe3 sans JS.
|
||||||
|
|
||||||
|
### 6.3 Pourquoi un fallback JS existe encore ?
|
||||||
|
|
||||||
|
En mode npm run dev, on ne passe pas par dist/, donc pas d’injection build-time.
|
||||||
|
Le fallback JS est un filet :
|
||||||
|
|
||||||
|
utile en DEV,
|
||||||
|
|
||||||
|
utile si un alias manque,
|
||||||
|
|
||||||
|
mais pas la solution principale.
|
||||||
|
|
||||||
|
## 7) Tests & garde-fous (qualité automatique)
|
||||||
|
### 7.1 npm test doit rester “simple et vrai”
|
||||||
|
|
||||||
|
Il exécute :
|
||||||
|
|
||||||
|
npm run build (génère dist)
|
||||||
|
|
||||||
|
npm run test:anchors (stabilité des ancres)
|
||||||
|
|
||||||
|
node scripts/check-inline-js.mjs (évite les scripts inline invalides)
|
||||||
|
|
||||||
|
### 7.2 test:anchors (baseline + churn)
|
||||||
|
|
||||||
|
tests/anchors-baseline.json = snapshot de référence.
|
||||||
|
|
||||||
|
check-anchors compare dist à la baseline.
|
||||||
|
|
||||||
|
Si trop de churn → échec (donc on voit la casse).
|
||||||
|
|
||||||
|
On met à jour la baseline uniquement quand on accepte consciemment la nouvelle réalité :
|
||||||
|
npm run test:anchors:update
|
||||||
|
|
||||||
|
## 8) DEV vs BUILD : comprendre sans se tromper
|
||||||
|
|
||||||
|
npm run dev → serveur Astro “live”, pas de dist/
|
||||||
|
⇒ les alias build-time n’existent pas ⇒ fallback JS peut s’activer.
|
||||||
|
|
||||||
|
npm run build && npx serve dist → rendu final “prod-like”
|
||||||
|
⇒ alias injectés ⇒ citations historiques fonctionnent sans JS.
|
||||||
|
|
||||||
|
Repère immédiat :
|
||||||
|
|
||||||
|
port 4321 = DEV
|
||||||
|
|
||||||
|
port 3000 (serve dist) = PROD-like
|
||||||
|
|
||||||
|
## 9) Rituels opérationnels (les 3 recettes)
|
||||||
|
### Recette 1 — Créer un ticket propre
|
||||||
|
|
||||||
|
Cliquer “Proposer”
|
||||||
|
|
||||||
|
Choisir Type + Category
|
||||||
|
|
||||||
|
Dans Gitea : compléter “Proposition (remplacer par)” + “Justification”
|
||||||
|
|
||||||
|
Ne pas détruire les lignes Chemin / Ancre / Type / State / Category
|
||||||
|
|
||||||
|
### Recette 2 — Appliquer un ticket
|
||||||
|
en bash :
|
||||||
|
node scripts/apply-ticket.mjs <ID> --dry-run
|
||||||
|
node scripts/apply-ticket.mjs <ID>
|
||||||
|
git diff
|
||||||
|
npm test
|
||||||
|
git add ...
|
||||||
|
git commit -m "edit: apply ticket #<ID> (<chemin>#<ancre>)"
|
||||||
|
git push
|
||||||
|
|
||||||
|
### Recette 3 — Quand un ID change (citabilité)
|
||||||
|
|
||||||
|
trouver ancien id + nouveau id
|
||||||
|
|
||||||
|
ajouter alias dans src/anchors/anchor-aliases.json
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
vérifier ...#ancien → scroll sur le bon paragraphe
|
||||||
|
|
||||||
|
npm test
|
||||||
|
|
||||||
|
## 10) Règles d’or (invariants non négociables)
|
||||||
|
|
||||||
|
Ne jamais éditer dist/ à la main (artefact).
|
||||||
|
|
||||||
|
Ne jamais sacrifier l’ancre : Chemin + Ancre doivent exister.
|
||||||
|
|
||||||
|
Tickets toujours “parsables” (clés stables ligne-par-ligne).
|
||||||
|
|
||||||
|
apply-ticket doit pouvoir refuser quand c’est ambigu.
|
||||||
|
|
||||||
|
npm test doit rester “le bouton rouge” fiable.
|
||||||
|
|
||||||
|
## 11) Dépannage rapide
|
||||||
|
|
||||||
|
401 invalid token : FORGE_TOKEN absent ou mal exporté.
|
||||||
|
|
||||||
|
Header invalid value : token collé avec des caractères parasites (espaces, retours, texte autour).
|
||||||
|
|
||||||
|
Proposer ouvre 2 onglets / remplace la page : bug d’interception click → vérifier preventDefault + stopPropagation et l’“openInNewTab”.
|
||||||
|
|
||||||
|
Ancien #id ne marche plus :
|
||||||
|
|
||||||
|
en PROD-like : vérifier alias injecté + JSON
|
||||||
|
|
||||||
|
en DEV : normal que fallback JS soit requis
|
||||||
|
|
||||||
|
## 12) Où lire quoi (docs)
|
||||||
|
|
||||||
|
docs/QUICKSTART.md : démarrer vite
|
||||||
|
|
||||||
|
docs/MANUEL_REFERENCE.md : usage complet + procédures
|
||||||
|
|
||||||
|
docs/CONTRAT_TICKETS.md : format strict des issues
|
||||||
|
|
||||||
|
(ce doc) docs/TOPO_LOGIQUE_METIER.md : la logique qui relie tout
|
||||||
@@ -9,18 +9,25 @@ function usage(exitCode = 0) {
|
|||||||
apply-ticket — applique une proposition de correction depuis un ticket Gitea (robuste)
|
apply-ticket — applique une proposition de correction depuis un ticket Gitea (robuste)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
node scripts/apply-ticket.mjs <issue_number> [--dry-run] [--no-build]
|
node scripts/apply-ticket.mjs <issue_number> [--dry-run] [--no-build] [--alias] [--commit]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
--dry-run : ne modifie rien, affiche BEFORE/AFTER
|
||||||
|
--no-build : n'exécute pas "npm run build" (INCOMPATIBLE avec --alias)
|
||||||
|
--alias : après application, ajoute l'alias d'ancre (old -> new) dans src/anchors/anchor-aliases.json
|
||||||
|
--commit : git add + git commit automatiquement (inclut alias si --alias)
|
||||||
|
|
||||||
Env (recommandé):
|
Env (recommandé):
|
||||||
FORGE_API = base API (LAN) ex: http://192.168.1.20:3000 (évite DNS)
|
FORGE_API = base API (LAN) ex: http://192.168.1.20:3000
|
||||||
FORGE_BASE = base web ex: https://gitea.xxx.tld
|
FORGE_BASE = base web ex: https://gitea.xxx.tld
|
||||||
FORGE_TOKEN = PAT (avec accès au repo + issues)
|
FORGE_TOKEN = PAT (accès repo + issues)
|
||||||
GITEA_OWNER = owner (optionnel si auto-détecté depuis git remote)
|
GITEA_OWNER = owner (optionnel si auto-détecté depuis git remote)
|
||||||
GITEA_REPO = repo (optionnel si auto-détecté depuis git remote)
|
GITEA_REPO = repo (optionnel si auto-détecté depuis git remote)
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- Si dist/<chemin>/index.html est absent, le script lance "npm run build" sauf si --no-build.
|
- Si dist/<chemin>/index.html est absent, le script lance "npm run build" sauf si --no-build.
|
||||||
- Sauvegarde automatique: <fichier>.bak.issue-<N> (uniquement si on écrit)
|
- Sauvegarde automatique: <fichier>.bak.issue-<N> (uniquement si on écrit)
|
||||||
|
- Avec --alias : le script build pour obtenir le NOUVEL id, puis écrit l'alias old->new.
|
||||||
`);
|
`);
|
||||||
process.exit(exitCode);
|
process.exit(exitCode);
|
||||||
}
|
}
|
||||||
@@ -36,10 +43,28 @@ if (!Number.isFinite(issueNum) || issueNum <= 0) {
|
|||||||
|
|
||||||
const DRY_RUN = argv.includes("--dry-run");
|
const DRY_RUN = argv.includes("--dry-run");
|
||||||
const NO_BUILD = argv.includes("--no-build");
|
const NO_BUILD = argv.includes("--no-build");
|
||||||
|
const DO_ALIAS = argv.includes("--alias");
|
||||||
|
const DO_COMMIT = argv.includes("--commit");
|
||||||
|
|
||||||
|
if (DO_ALIAS && NO_BUILD) {
|
||||||
|
console.error("❌ --alias est incompatible avec --no-build (risque d'alias faux).");
|
||||||
|
console.error("➡️ Relance sans --no-build.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DRY_RUN && (DO_ALIAS || DO_COMMIT)) {
|
||||||
|
console.warn("ℹ️ --dry-run : --alias/--commit sont ignorés (aucune écriture).");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof fetch !== "function") {
|
||||||
|
console.error("❌ fetch() indisponible dans ce Node. Utilise Node 18+ (ou plus).");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
const CWD = process.cwd();
|
const CWD = process.cwd();
|
||||||
const CONTENT_ROOT = path.join(CWD, "src", "content");
|
const CONTENT_ROOT = path.join(CWD, "src", "content");
|
||||||
const DIST_ROOT = path.join(CWD, "dist");
|
const DIST_ROOT = path.join(CWD, "dist");
|
||||||
|
const ALIASES_FILE = path.join(CWD, "src", "anchors", "anchor-aliases.json");
|
||||||
|
|
||||||
function normalizeText(s) {
|
function normalizeText(s) {
|
||||||
return String(s ?? "")
|
return String(s ?? "")
|
||||||
@@ -76,9 +101,20 @@ function tokenize(s) {
|
|||||||
|
|
||||||
function run(cmd, args, opts = {}) {
|
function run(cmd, args, opts = {}) {
|
||||||
const r = spawnSync(cmd, args, { stdio: "inherit", ...opts });
|
const r = spawnSync(cmd, args, { stdio: "inherit", ...opts });
|
||||||
|
if (r.error) throw r.error;
|
||||||
if (r.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
|
if (r.status !== 0) throw new Error(`Command failed: ${cmd} ${args.join(" ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function runQuiet(cmd, args, opts = {}) {
|
||||||
|
const r = spawnSync(cmd, args, { encoding: "utf8", stdio: "pipe", ...opts });
|
||||||
|
if (r.error) throw r.error;
|
||||||
|
if (r.status !== 0) {
|
||||||
|
const out = (r.stdout || "") + (r.stderr || "");
|
||||||
|
throw new Error(`Command failed: ${cmd} ${args.join(" ")}\n${out}`);
|
||||||
|
}
|
||||||
|
return r.stdout || "";
|
||||||
|
}
|
||||||
|
|
||||||
async function fileExists(p) {
|
async function fileExists(p) {
|
||||||
try { await fs.access(p); return true; } catch { return false; }
|
try { await fs.access(p); return true; } catch { return false; }
|
||||||
}
|
}
|
||||||
@@ -154,13 +190,27 @@ async function readHtmlParagraphText(htmlPath, anchorId) {
|
|||||||
const re = new RegExp(`<p[^>]*\\bid=["']${escapeRegExp(anchorId)}["'][^>]*>([\\s\\S]*?)<\\/p>`, "i");
|
const re = new RegExp(`<p[^>]*\\bid=["']${escapeRegExp(anchorId)}["'][^>]*>([\\s\\S]*?)<\\/p>`, "i");
|
||||||
const m = html.match(re);
|
const m = html.match(re);
|
||||||
if (!m) return "";
|
if (!m) return "";
|
||||||
let inner = m[1];
|
return cleanHtmlInner(m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
inner = inner.replace(/<span[^>]*class=["'][^"']*para-tools[^"']*["'][^>]*>[\s\S]*?<\/span>/gi, " ");
|
function cleanHtmlInner(inner) {
|
||||||
inner = inner.replace(/<[^>]+>/g, " ");
|
let s = String(inner ?? "");
|
||||||
inner = inner.replace(/\s+/g, " ").trim();
|
s = s.replace(/<span[^>]*class=["'][^"']*para-tools[^"']*["'][^>]*>[\s\S]*?<\/span>/gi, " ");
|
||||||
inner = inner.replace(/\b(¶|Citer|Proposer|Copié)\b/gi, "").replace(/\s+/g, " ").trim();
|
s = s.replace(/<[^>]+>/g, " ");
|
||||||
return inner;
|
s = s.replace(/\s+/g, " ").trim();
|
||||||
|
s = s.replace(/\b(¶|Citer|Proposer|Copié)\b/gi, "").replace(/\s+/g, " ").trim();
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readAllHtmlParagraphs(htmlPath) {
|
||||||
|
const html = await fs.readFile(htmlPath, "utf-8");
|
||||||
|
const out = [];
|
||||||
|
const re = /<p\b[^>]*\sid=["'](p-\d+-[0-9a-f]{8})["'][^>]*>([\s\S]*?)<\/p>/gi;
|
||||||
|
let m;
|
||||||
|
while ((m = re.exec(html))) {
|
||||||
|
out.push({ id: m[1], text: cleanHtmlInner(m[2]) });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitParagraphBlocks(mdxText) {
|
function splitParagraphBlocks(mdxText) {
|
||||||
@@ -173,13 +223,13 @@ function isLikelyExcerpt(s) {
|
|||||||
if (!t) return true;
|
if (!t) return true;
|
||||||
if (t.length < 120) return true;
|
if (t.length < 120) return true;
|
||||||
if (/[.…]$/.test(t)) return true;
|
if (/[.…]$/.test(t)) return true;
|
||||||
if (t.includes("tronqu")) return true; // tronqué/tronquee etc (sans diacritiques)
|
if (t.includes("tronqu")) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function scoreBlock(block, targetText) {
|
function scoreText(candidate, targetText) {
|
||||||
const tgt = tokenize(targetText);
|
const tgt = tokenize(targetText);
|
||||||
const blk = tokenize(block);
|
const blk = tokenize(candidate);
|
||||||
if (!tgt.length || !blk.length) return 0;
|
if (!tgt.length || !blk.length) return 0;
|
||||||
|
|
||||||
const tgtSet = new Set(tgt);
|
const tgtSet = new Set(tgt);
|
||||||
@@ -188,13 +238,11 @@ function scoreBlock(block, targetText) {
|
|||||||
let hit = 0;
|
let hit = 0;
|
||||||
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
for (const w of tgtSet) if (blkSet.has(w)) hit++;
|
||||||
|
|
||||||
// Bonus si un long préfixe ressemble (moins strict qu'un includes brut)
|
|
||||||
const tgtNorm = normalizeText(stripMd(targetText));
|
const tgtNorm = normalizeText(stripMd(targetText));
|
||||||
const blkNorm = normalizeText(stripMd(block));
|
const blkNorm = normalizeText(stripMd(candidate));
|
||||||
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
const prefix = tgtNorm.slice(0, Math.min(180, tgtNorm.length));
|
||||||
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
const prefixBonus = prefix && blkNorm.includes(prefix) ? 1000 : 0;
|
||||||
|
|
||||||
// Ratio bonus (0..100)
|
|
||||||
const ratio = hit / Math.max(1, tgtSet.size);
|
const ratio = hit / Math.max(1, tgtSet.size);
|
||||||
const ratioBonus = Math.round(ratio * 100);
|
const ratioBonus = Math.round(ratio * 100);
|
||||||
|
|
||||||
@@ -204,15 +252,21 @@ function scoreBlock(block, targetText) {
|
|||||||
function bestBlockMatchIndex(blocks, targetText) {
|
function bestBlockMatchIndex(blocks, targetText) {
|
||||||
let best = { i: -1, score: -1 };
|
let best = { i: -1, score: -1 };
|
||||||
for (let i = 0; i < blocks.length; i++) {
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
const b = blocks[i];
|
const sc = scoreText(blocks[i], targetText);
|
||||||
const sc = scoreBlock(b, targetText);
|
|
||||||
if (sc > best.score) best = { i, score: sc };
|
if (sc > best.score) best = { i, score: sc };
|
||||||
}
|
}
|
||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeChemin(chemin) {
|
||||||
|
let c = String(chemin || "").trim();
|
||||||
|
if (!c.startsWith("/")) c = "/" + c;
|
||||||
|
if (!c.endsWith("/")) c = c + "/";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
async function findContentFileFromChemin(chemin) {
|
async function findContentFileFromChemin(chemin) {
|
||||||
const clean = chemin.replace(/^\/+|\/+$/g, "");
|
const clean = normalizeChemin(chemin).replace(/^\/+|\/+$/g, "");
|
||||||
const parts = clean.split("/").filter(Boolean);
|
const parts = clean.split("/").filter(Boolean);
|
||||||
if (parts.length < 2) return null;
|
if (parts.length < 2) return null;
|
||||||
const collection = parts[0];
|
const collection = parts[0];
|
||||||
@@ -260,7 +314,7 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
|
|||||||
headers: {
|
headers: {
|
||||||
"Authorization": `token ${token}`,
|
"Authorization": `token ${token}`,
|
||||||
"Accept": "application/json",
|
"Accept": "application/json",
|
||||||
"User-Agent": "archicratie-apply-ticket/1.1",
|
"User-Agent": "archicratie-apply-ticket/1.2",
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -270,6 +324,79 @@ async function fetchIssue({ forgeApiBase, owner, repo, token, issueNum }) {
|
|||||||
return await res.json();
|
return await res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAliases() {
|
||||||
|
try {
|
||||||
|
const s = await fs.readFile(ALIASES_FILE, "utf8");
|
||||||
|
const obj = JSON.parse(s);
|
||||||
|
return obj && typeof obj === "object" ? obj : {};
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortObjectKeys(obj) {
|
||||||
|
return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveAliases(obj) {
|
||||||
|
let out = obj || {};
|
||||||
|
// tri stable
|
||||||
|
for (const k of Object.keys(out)) {
|
||||||
|
if (out[k] && typeof out[k] === "object") out[k] = sortObjectKeys(out[k]);
|
||||||
|
}
|
||||||
|
out = sortObjectKeys(out);
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(ALIASES_FILE), { recursive: true });
|
||||||
|
await fs.writeFile(ALIASES_FILE, JSON.stringify(out, null, 2) + "\n", "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upsertAlias({ chemin, oldId, newId }) {
|
||||||
|
const route = normalizeChemin(chemin);
|
||||||
|
if (!oldId || !newId) throw new Error("Alias: oldId/newId requis");
|
||||||
|
if (oldId === newId) return { changed: false, reason: "same" };
|
||||||
|
|
||||||
|
const data = await loadAliases();
|
||||||
|
if (!data[route]) data[route] = {};
|
||||||
|
|
||||||
|
const prev = data[route][oldId];
|
||||||
|
if (prev && prev !== newId) {
|
||||||
|
throw new Error(`Alias conflict: ${route}${oldId} already mapped to ${prev} (new=${newId})`);
|
||||||
|
}
|
||||||
|
if (prev === newId) return { changed: false, reason: "already" };
|
||||||
|
|
||||||
|
data[route][oldId] = newId;
|
||||||
|
await saveAliases(data);
|
||||||
|
return { changed: true, reason: "written" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitHasStagedChanges() {
|
||||||
|
const r = spawnSync("git", ["diff", "--cached", "--quiet"]);
|
||||||
|
return r.status === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function computeNewIdFromDistByContent(distHtmlPath, afterBlock) {
|
||||||
|
const paras = await readAllHtmlParagraphs(distHtmlPath);
|
||||||
|
if (!paras.length) throw new Error(`Aucun <p id="p-..."> trouvé dans ${distHtmlPath}`);
|
||||||
|
|
||||||
|
let best = { id: null, score: -1, text: "" };
|
||||||
|
const target = stripMd(afterBlock).slice(0, 1200);
|
||||||
|
|
||||||
|
for (const p of paras) {
|
||||||
|
const sc = scoreText(p.text, target);
|
||||||
|
if (sc > best.score) best = { id: p.id, score: sc, text: p.text };
|
||||||
|
}
|
||||||
|
|
||||||
|
// seuil de sécurité (évite alias faux)
|
||||||
|
if (!best.id || best.score < 60) {
|
||||||
|
throw new Error(
|
||||||
|
`Impossible d'identifier le nouvel id dans dist (score trop faible: ${best.score}).\n` +
|
||||||
|
`➡️ Vérifie que la proposition correspond bien à UN paragraphe.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return best.id;
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const token = getEnv("FORGE_TOKEN");
|
const token = getEnv("FORGE_TOKEN");
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -298,6 +425,7 @@ async function main() {
|
|||||||
|
|
||||||
let chemin = pickLine(body, "Chemin") || pickHeadingValue(body, "Chemin");
|
let chemin = pickLine(body, "Chemin") || pickHeadingValue(body, "Chemin");
|
||||||
let ancre = pickLine(body, "Ancre") || pickHeadingValue(body, "Ancre paragraphe") || pickHeadingValue(body, "Ancre");
|
let ancre = pickLine(body, "Ancre") || pickHeadingValue(body, "Ancre paragraphe") || pickHeadingValue(body, "Ancre");
|
||||||
|
chemin = normalizeChemin(chemin);
|
||||||
ancre = (ancre || "").trim();
|
ancre = (ancre || "").trim();
|
||||||
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
if (ancre.startsWith("#")) ancre = ancre.slice(1);
|
||||||
|
|
||||||
@@ -344,14 +472,12 @@ async function main() {
|
|||||||
|
|
||||||
const best = bestBlockMatchIndex(blocks, targetText);
|
const best = bestBlockMatchIndex(blocks, targetText);
|
||||||
|
|
||||||
// seuil de sécurité : on veut au moins un overlap raisonnable.
|
// seuil de sécurité
|
||||||
// Avec le bonus prefix+ratio, un match correct dépasse très vite ~60–80.
|
|
||||||
if (best.i < 0 || best.score < 40) {
|
if (best.i < 0 || best.score < 40) {
|
||||||
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
console.error("❌ Match trop faible: je refuse de remplacer automatiquement.");
|
||||||
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
console.error(`➡️ Score=${best.score}. Recommandation: ticket avec 'Texte actuel (copie exacte du paragraphe)'.`);
|
||||||
// debug: top 5
|
|
||||||
const ranked = blocks
|
const ranked = blocks
|
||||||
.map((b, i) => ({ i, score: scoreBlock(b, targetText), excerpt: stripMd(b).slice(0, 140) }))
|
.map((b, i) => ({ i, score: scoreText(b, targetText), excerpt: stripMd(b).slice(0, 140) }))
|
||||||
.sort((a, b) => b.score - a.score)
|
.sort((a, b) => b.score - a.score)
|
||||||
.slice(0, 5);
|
.slice(0, 5);
|
||||||
|
|
||||||
@@ -388,9 +514,58 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(contentFile, updated, "utf-8");
|
await fs.writeFile(contentFile, updated, "utf-8");
|
||||||
console.log("✅ Applied. Next:");
|
console.log("✅ Applied.");
|
||||||
|
|
||||||
|
let aliasChanged = false;
|
||||||
|
let newId = null;
|
||||||
|
|
||||||
|
if (DO_ALIAS) {
|
||||||
|
console.log("🔁 Rebuild to compute new anchor ids (npm run build) …");
|
||||||
|
run("npm", ["run", "build"], { cwd: CWD });
|
||||||
|
|
||||||
|
if (!(await fileExists(distHtmlPath))) {
|
||||||
|
throw new Error(`dist introuvable après build: ${distHtmlPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
newId = await computeNewIdFromDistByContent(distHtmlPath, afterBlock);
|
||||||
|
|
||||||
|
const res = await upsertAlias({ chemin, oldId: ancre, newId });
|
||||||
|
aliasChanged = res.changed;
|
||||||
|
|
||||||
|
if (aliasChanged) {
|
||||||
|
console.log(`✅ Alias ajouté: ${chemin} ${ancre} -> ${newId}`);
|
||||||
|
// met à jour dist immédiatement (sans rebuild complet)
|
||||||
|
run("node", ["scripts/inject-anchor-aliases.mjs"], { cwd: CWD });
|
||||||
|
} else {
|
||||||
|
console.log(`ℹ️ Alias déjà présent ou inutile (${ancre} -> ${newId}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// garde-fous rapides
|
||||||
|
run("npm", ["run", "test:anchors"], { cwd: CWD });
|
||||||
|
run("node", ["scripts/check-inline-js.mjs"], { cwd: CWD });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DO_COMMIT) {
|
||||||
|
const files = [path.relative(CWD, contentFile)];
|
||||||
|
if (DO_ALIAS && aliasChanged) files.push(path.relative(CWD, ALIASES_FILE));
|
||||||
|
|
||||||
|
run("git", ["add", ...files], { cwd: CWD });
|
||||||
|
|
||||||
|
if (!gitHasStagedChanges()) {
|
||||||
|
console.log("ℹ️ Nothing to commit (aucun changement staged).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msg = `edit: apply ticket #${issueNum} (${chemin}#${ancre})`;
|
||||||
|
run("git", ["commit", "-m", msg], { cwd: CWD });
|
||||||
|
console.log(`✅ Committed: ${msg}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mode manuel (historique)
|
||||||
|
console.log("Next (manuel) :");
|
||||||
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
console.log(` git diff -- ${path.relative(CWD, contentFile)}`);
|
||||||
console.log(` git add ${path.relative(CWD, contentFile)}`);
|
console.log(` git add ${path.relative(CWD, contentFile)}${DO_ALIAS ? " src/anchors/anchor-aliases.json" : ""}`);
|
||||||
console.log(` git commit -m "edit: apply ticket #${issueNum} (${chemin}#${ancre})"`);
|
console.log(` git commit -m "edit: apply ticket #${issueNum} (${chemin}#${ancre})"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
{
|
{
|
||||||
"/archicratie/prologue/": {
|
"/archicratie/prologue/": {
|
||||||
"p-8-e7075fe3": "p-8-0e65838d"
|
"p-8-e7075fe3": "p-8-0e65838d",
|
||||||
|
"p-3-76df8102": "p-3-539ac0fd",
|
||||||
|
"p-5-85126fa5": "p-5-285d27a7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,12 +17,11 @@ C'est cette perte de prise sur le réel que ce livre souhaite prendre au sérieu
|
|||||||
|
|
||||||
Cette tenue du monde n'équivaut ni à la paix civile, ni à la stabilité des institutions, ni à l'ordre établi. C'est une difficulté conceptuelle que d'envisager *la possibilité pour un ordre de durer sans s'effondrer*, alors même qu'il est traversé en permanence par des forces et des légitimités qui le travaillent, l'éprouvent, le modifient, l'usent, le contestent, le prolongent ou le sapent. Cette possibilité de tenir le monde commun, nous la nommons *co-viabilité*.
|
Cette tenue du monde n'équivaut ni à la paix civile, ni à la stabilité des institutions, ni à l'ordre établi. C'est une difficulté conceptuelle que d'envisager *la possibilité pour un ordre de durer sans s'effondrer*, alors même qu'il est traversé en permanence par des forces et des légitimités qui le travaillent, l'éprouvent, le modifient, l'usent, le contestent, le prolongent ou le sapent. Cette possibilité de tenir le monde commun, nous la nommons *co-viabilité*.
|
||||||
|
|
||||||
Le terme n'est pas trivial. Il ne s'agit pas simplement d'une viabilité partagée, ni d'une coexistence pacifique, ni même d'une durabilité écologique élargie. Il s'agit d'un état dynamique, instable, fragile, dans lequel un ensemble — une société, d'un système biologique, d'une formation historique, d'un milieu technique ou d'un monde institué — parvient à maintenir une *existence viable*, *malgré et grâce à ses tensions constitutives*.
|
Le terme n’est pas trivial. Il ne s’agit pas simplement d’une viabilité partagée, ni d’une coexistence pacifique, ni même d’une durabilité écologique élargie. Il s’agit d’un état dynamique, instable, fragile, dans lequel un ensemble — une société, un système biologique, une formation historique, un milieu technique ou un monde institué — parvient à maintenir une existence viable, malgré et grâce à ses tensions constitutives.
|
||||||
|
|
||||||
La *co-viabilité* ne désigne ni un état d'équilibre, ni une finalité normative. Elle nomme un état dynamique et instable, dans lequel un monde — société, milieu technique, formation historique — tient non pas par homogénéité ou harmonie, mais parce qu'il parvient à réguler ce qui le menace sans se détruire lui-même. Il compose entre des éléments hétérogènes — forces d'inertie et d'innovation, attachements profonds et ruptures nécessaires — sans chercher à les unifier. C'est cette disposition active, faite de compromis fragiles et d'ajustements toujours révisables, que nous tenons pour première, et non dérivée.
|
La *co-viabilité* ne désigne ni un état d'équilibre, ni une finalité normative. Elle nomme un état dynamique et instable, dans lequel un monde — société, milieu technique, formation historique — tient non pas par homogénéité ou harmonie, mais parce qu'il parvient à réguler ce qui le menace sans se détruire lui-même. Il compose entre des éléments hétérogènes — forces d'inertie et d'innovation, attachements profonds et ruptures nécessaires — sans chercher à les unifier. C'est cette disposition active, faite de compromis fragiles et d'ajustements toujours révisables, que nous tenons pour première, et non dérivée.
|
||||||
|
|
||||||
Ce qui revient à dire que la question politique — au sens fort — n'a peut-être jamais été qui commande ? Mais bien plus : *Comment un ordre tient-il malgré ce qui le défait ?* *Quels sont les dispositifs qui permettent à une société de ne pas se désagréger sous l'effet de ses propres contradictions ?* *Comment sont régulées les tensions qui traversent le tissu du monde commun sans le déchirer ?*\
|
Ce qui revient à dire que la question politique — au sens fort — n’a peut-être jamais été qui commande ? Mais bien plus : Comment un ordre tient-il malgré ce qui le défait ? Quels sont les dispositifs qui permettent à une société de ne pas se désagréger sous l’effet de ses propres contradictions ? Comment sont régulées les tensions qui traversent le tissu du monde commun sans le déchirer ? Cette bascule de perspective prolonge des intuitions anciennes. Max Weber (Économie et société, 1922) rappelait que ce qui fait tenir un ordre, ce n’est pas seulement la force ou la loi, mais les « chances de validité » socialement reconnues. Norbert Elias (La dynamique de l’Occident, 1939/1974) montrait, quant à lui, que les sociétés se maintiennent par des équilibres toujours précaires entre interdépendances, rivalités et pacifications. Notre démarche s’inscrit dans ce sillage : travailler cette interrogation sur les conditions de viabilité d’un monde commun.
|
||||||
Cette bascule de perspective prolonge des intuitions anciennes. Max Weber (*Économie et société*, 1922) rappelait que ce qui fait tenir un ordre, ce n'est pas seulement la force ou la loi, mais les « chances de validité » socialement reconnues. Norbert Elias (*La dynamique de l'Occident*, 1939/1975) montrait, quant à lui, que les sociétés se maintiennent par des équilibres toujours précaires entre interdépendances, rivalités et pacifications. Notre démarche s'inscrit dans ce sillage : travailler cette interrogation sur les *conditions de viabilité d'un monde commun*.
|
|
||||||
|
|
||||||
Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient l'ordre en surplomb. Comme le rappelle Michel Foucault, il n'y a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. C'est précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque.
|
Ce changement de perspective implique une rupture profonde dans la manière même de poser la question politique. Pendant des siècles, les sociétés ont pensé le politique à partir de principes transcendants — Dieu, Nature, Volonté générale, Pacte social. Ces principes, supposés extérieurs aux conflits du présent, garantissaient l'ordre en surplomb. Comme le rappelle Michel Foucault, il n'y a pas de principe extérieur au jeu des forces : seulement des rapports de pouvoir situés, modulés, réversibles. C'est précisément cette exigence — trouver dans les relations elles-mêmes les ressources nécessaires pour maintenir des mondes vivables — qui définit notre époque.
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,8 @@ const GITEA_REPO = import.meta.env.PUBLIC_GITEA_REPO ?? "";
|
|||||||
const replacement = document.querySelector(`[id^="${prefix}"]`);
|
const replacement = document.querySelector(`[id^="${prefix}"]`);
|
||||||
if (!replacement) return;
|
if (!replacement) return;
|
||||||
|
|
||||||
history.replaceState(null, "", `${window.location.pathname}#${replacement.id}`);
|
console.warn("[anchors] legacy hash fallback used:", `#${oldId}`, "→", `#${replacement.id}`);
|
||||||
|
// On ne réécrit PAS le hash : on garde la citabilité historique visible.
|
||||||
replacement.scrollIntoView({ block: "start" });
|
replacement.scrollIntoView({ block: "start" });
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
"p-0-d7974f88",
|
"p-0-d7974f88",
|
||||||
"p-1-2ef25f29",
|
"p-1-2ef25f29",
|
||||||
"p-2-edb49e0a",
|
"p-2-edb49e0a",
|
||||||
"p-3-76df8102",
|
"p-3-539ac0fd",
|
||||||
"p-4-8ed4f807",
|
"p-4-8ed4f807",
|
||||||
"p-5-85126fa5",
|
"p-5-285d27a7",
|
||||||
"p-6-3515039d",
|
"p-6-3515039d",
|
||||||
"p-7-64a0ca9c",
|
"p-7-64a0ca9c",
|
||||||
"p-8-0e65838d",
|
"p-8-0e65838d",
|
||||||
@@ -142,6 +142,8 @@
|
|||||||
"p-135-c19330ce",
|
"p-135-c19330ce",
|
||||||
"p-136-17f1cf51",
|
"p-136-17f1cf51",
|
||||||
"p-137-d8f1539e",
|
"p-137-d8f1539e",
|
||||||
|
"p-3-76df8102",
|
||||||
|
"p-5-85126fa5",
|
||||||
"p-8-e7075fe3"
|
"p-8-e7075fe3"
|
||||||
],
|
],
|
||||||
"atlas/00-demarrage/index.html": [
|
"atlas/00-demarrage/index.html": [
|
||||||
|
|||||||
Reference in New Issue
Block a user