1/* ============================================================ 2 * CREA FILES â joindre un dossier : lecture récursive et consentie 3 * 4 * crea-files.js â JOINDRE UN DOSSIER (lecture récursive, consentie). 5 * 6 * Transformé depuis "Lire des fichiers" (fichiers individuels) vers 7 * "Joindre un dossier" : lit tout un dossier récursivement. 8 * Le menu (+) gère déjà les fichiers individuels â ce bouton comble 9 * le manque de lecture de projets/dossiers entiers. 10 * 11 * ⢠window.creaReadFolder() â showDirectoryPicker (Chrome/Edge) si dispo, 12 * sinon repli <input webkitdirectory> (Firefox/Safari). 13 * ⢠Bouton ð près de l'envoi. JAMAIS d'écriture/suppression. 14 * 100 % autonome. Expose window.CREA_FILES. 15 * ============================================================ */ 16;(function () { 17 'use strict' 18 if (window.CREA_FILES) return 19 20 /* ââ CE QUI EST DU TEXTE SE LIT DANS LES OCTETS, PAS DANS LE NOM âââââââ 21 * 22 * « Je dois pouvoir charger tout type de dossier, fichier et dossier lourd. 23 * J'ai beau charger, n'importe comment⦠» 24 * 25 * MESURà (tools/mesures/un-dossier-lourd-entre-entier.mjs) sur le dépôt CRÃA 26 * lui-même, chargé par « Dossier de l'ordinateur » : 27 * 28 * 1 612 fichiers sur 3 129 entraient VIDES, étiquetés « binaire » 29 * · dont 1 018 `.mjs` â toutes les gardes du dépôt 30 * · plus .cjs, .jsonl, .svg, .gitignore, .gitattributes, .ipynb⦠31 * 28 de plus entraient vides, « trop volumineux » 32 * · dont src/app.js (4 831 Ko) et serve.js (1 865 Ko) â le tronc 33 * 34 * La liste d'extensions était la faute de forme : elle ne peut que dériver, 35 * et appeler `.mjs` « binaire » est faux au sens propre. On ne devine plus. 36 * On refuse d'emblée ce qui est VRAIMENT binaire (on ne va pas lire dix Mo 37 * de wasm pour le découvrir), et pour tout le reste on regarde les premiers 38 * octets : un octet nul, c'est du binaire â c'est le test que `git` fait, et 39 * il ne se périme pas. 40 * 41 * ET « TROP GROS » NE VEUT PLUS DIRE « RIEN ». Un fichier au-delà du plafond 42 * est lu par son DÃBUT, et la note dit où ça coupe. Un début vaut mieux qu'un 43 * vide : sur `src/app.js`, c'est la différence entre un fichier que Créa peut 44 * commencer à lire et un fichier dont elle ignore l'existence même. */ 45 var BINAIRE_RE = /\.(wasm|png|jpe?g|gif|webp|avif|bmp|ico|icns|tiff?|mp[34]|m4[av]|wav|ogg|opus|flac|aac|mov|avi|mkv|webm|zip|gz|tgz|bz2|xz|7z|rar|tar|pdf|woff2?|ttf|otf|eot|exe|dll|so|dylib|bin|dat|db|sqlite3?|pyc|class|jar|o|a|node|pack|idx)$/i 46 var MAX_FILE_BYTES = 1000000 // 1 Mo lu en entier 47 var TETE_SI_TROP_GROS = 200000 // au-delà , on lit ce début-là plutôt que rien 48 var OCTETS_RENIFLES = 4096 // de quoi trancher texte/binaire sans tout lire 49 var MAX_TOTAL_CHARS = 80000 // plafond global injecté dans le textarea 50 51 /* Un octet nul dans les premiers kilo-octets : c'est binaire. Aucun encodage 52 de texte usuel n'en produit â UTF-16 mis à part, que les navigateurs 53 décodent de toute façon par leur nomenclature. */ 54 function renifleBinaire(morceau) { 55 return new Promise(function (resolve) { 56 var fr = new FileReader() 57 fr.onload = function () { 58 var vue = new Uint8Array(fr.result || new ArrayBuffer(0)) 59 for (var i = 0; i < vue.length; i++) if (vue[i] === 0) { resolve(true); return } 60 resolve(false) 61 } 62 fr.onerror = function () { resolve(true) /* illisible : on ne prétend pas que c'est du texte */ } 63 fr.readAsArrayBuffer(morceau) 64 }) 65 } 66 67 function lireTexte(morceau) { 68 return new Promise(function (resolve) { 69 var fr = new FileReader() 70 fr.onload = function () { resolve(String(fr.result || '')) } 71 fr.onerror = function () { resolve(null) } 72 fr.readAsText(morceau) 73 }) 74 } 75 76 async function readBlob(file, path) { 77 var base = { name: file.name, path: path, size: file.size } 78 if (BINAIRE_RE.test(file.name)) return Object.assign(base, { text: '', skipped: 'binaire' }) 79 80 var trop = file.size > MAX_FILE_BYTES 81 var aLire = trop ? file.slice(0, TETE_SI_TROP_GROS) : file 82 /* On renifle sur ce qu'on va lire, pas sur le fichier entier : un `.bin` 83 de dix Mo ne doit pas être chargé pour être refusé. */ 84 var estBinaire = await renifleBinaire(file.slice(0, OCTETS_RENIFLES)) 85 if (estBinaire) return Object.assign(base, { text: '', skipped: 'binaire' }) 86 87 var texte = await lireTexte(aLire) 88 if (texte === null) return Object.assign(base, { text: '', skipped: 'illisible' }) 89 return Object.assign(base, { 90 text: texte, 91 skipped: trop 92 ? 'lu en partie â les ' + Math.round(TETE_SI_TROP_GROS / 1024) + ' premiers Ko sur ' + Math.round(file.size / 1024) 93 : undefined, 94 }) 95 } 96 97 // Lit récursivement un FileSystemDirectoryHandle (File System Access API) 98 async function readDirHandle(handle, prefix) { 99 var out = [] 100 for await (var entry of handle.values()) { 101 if (entry.kind === 'file') { 102 var f = await entry.getFile() 103 out.push(await readBlob(f, prefix + entry.name)) 104 } else if (entry.kind === 'directory') { 105 var sub = await readDirHandle(entry, prefix + entry.name + '/') 106 out = out.concat(sub) 107 } 108 } 109 return out 110 } 111 112 // Lecture dossier â renvoie Promise<[{name,path,size,text,skipped?}]> 113 window.creaReadFolder = async function () { 114 // 1) File System Access API (Chrome/Edge) 115 if (window.showDirectoryPicker) {
116 try { 117 var dirHandle = await window.showDirectoryPicker({ mode: 'read' }) 118 return await readDirHandle(dirHandle, dirHandle.name + '/') 119 } catch (e) { if (e && e.name === 'AbortError') return [] } 120 } 121 // 2) Repli webkitdirectory (Firefox, Safari, Electron) 122 return await new Promise(function (resolve) { 123 var inp = document.createElement('input') 124 inp.type = 'file'; inp.webkitdirectory = true; inp.multiple = true; inp.style.display = 'none' 125 inp.onchange = async function () { 126 var files = [].slice.call(inp.files || []) 127 var out = [] 128 for (var i = 0; i < files.length; i++) { 129 var f = files[i] 130 out.push(await readBlob(f, f.webkitRelativePath || f.name)) 131 } 132 try { inp.remove() } catch (e) {} 133 resolve(out) 134 } 135 document.body.appendChild(inp); inp.click() 136 }) 137 } 138 139 function activeInput() { 140 var a = document.activeElement 141 if (a && a.tagName === 'TEXTAREA') return a 142 return document.getElementById('chat-input') || document.querySelector('textarea') 143 } 144 145 async function pickFolderAndInsert() { 146 var files = await window.creaReadFolder() 147 if (!files || !files.length) return 148 var lines = [] 149 var total = 0 150 for (var i = 0; i < files.length; i++) { 151 var f = files[i] 152 if (f.text && total + f.text.length <= MAX_TOTAL_CHARS) { 153 lines.push('```' + f.path + '\n' + f.text + '\n```') 154 total += f.text.length 155 } else if (f.text) { 156 lines.push('[' + f.path + ' â tronqué (limite atteinte)]') 157 } else { 158 lines.push('[' + f.path + ' â ' + (f.skipped || 'ignoré') + ']') 159 } 160 } 161 var summary = '[Dossier joint â ' + files.length + ' fichier(s), ' + lines.filter(function(l){return l.startsWith('```')}).length + ' lus]\n\n' + lines.join('\n\n') 162 var inp = activeInput(); if (!inp) return 163 inp.value = (inp.value ? inp.value + '\n\n' : '') + summary 164 try { inp.dispatchEvent(new Event('input', { bubbles: true })) } catch (e) {} 165 inp.focus() 166 } 167 168 // Pas de bouton standalone â accessible via le menu + â "Joindre un dossier" 169 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function(){}) 170 /* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 171 * PDF.JS SE CHARGE QUAND UN PDF ARRIVE â PAS AVANT 172 * ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 173 * 174 * MESURà en ouvrant le Studio Code dans Chromium : la personne n'a rien 175 * demandé, et l'application affiche 176 * 177 * « Erreur : Failed to fetch dynamically imported module: 178 * https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs » 179 * 180 * Trois choses dans une, et elles s'aggravent. 181 * 182 * ââ 1. IL PARTAIT à CHAQUE OUVERTURE DE PAGE ââââââââââââââââââââââââââ 183 * Une balise `<script type="module">` en tête d'index.html et de 184 * client.html, pour « l'extraction texte des PDFs joints » â c'est-à -dire 185 * pour un geste que la plupart des sessions ne font jamais. Toutes les 186 * autres bibliothèques lourdes de cette page (Monaco, Tone, Meyda) sont 187 * servies depuis `/vendor/â¦` avec le CDN en filet ; PDF.js, seul, n'avait 188 * ni copie locale ni filet, et partait toujours sur le réseau. 189 * 190 * ââ 2. L'ÃCHEC ÃTAIT RELANCÃ, PUIS JETà à LA FIGURE âââââââââââââââââââ 191 * Un `setTimeout(â¦, 2000)` réimportait le module si `window.pdfjsLib` 192 * manquait â donc PRÃCISÃMENT quand le premier chargement venait 193 * d'échouer. Un second échec, dont la promesse n'était rattrapée par 194 * personne : la porte globale l'affichait à quelqu'un qui venait d'ouvrir 195 * un éditeur de code et n'avait jamais parlé de PDF. 196 * 197 * ââ 3. ET UN PDF ILLISIBLE PASSAIT POUR UN PDF VIDE âââââââââââââââââââ 198 * Les deux lecteurs faisaient `if (!pdfjsLib) return ''`. La personne 199 * joignait un PDF, rien n'en sortait, et rien ne le disait. C'est le 200 * silence que ce dépôt compte cliquet après cliquet. 201 * 202 * ââ CE QU'ON FAIT âââââââââââââââââââââââââââââââââââââââââââââââââââââ 203 * On charge à la DEMANDE, une seule fois pour tout le monde (la promesse 204 * est partagée), en essayant d'abord la copie locale â `/vendor/pdfjs`, la
205 * même porte que Monaco et Tone, une ligne à ajouter dans la table VENDORS 206 * de serve.js le jour où le paquet est installé â puis le CDN. 207 * Et quand ça ne marche pas, on le DIT à qui vient de joindre un PDF. */ 208 var LOCAL_PDFJS = '/vendor/pdfjs/pdf.min.mjs' 209 var CDN_PDFJS = 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.min.mjs' 210 var CDN_OUVRIER = 'https://cdn.jsdelivr.net/npm/[email protected]/build/pdf.worker.min.mjs' 211 var _pdf = null // la promesse, partagée : dix PDF joints d'un coup = UN chargement 212 var _reprises = 0 213 214 /* UNE ADRESSE NEUVE à CHAQUE REPRISE, ET C'EST LA MESURE QUI L'A EXIGÃ. 215 Rendre la main ne suffisait pas : le navigateur garde un module DONT LE 216 CHARGEMENT A ÃCHOUà dans sa carte des modules, et réimporter la MÃME 217 adresse rend le même échec â même quand le réseau est revenu. Le premier 218 essai garde donc l'adresse propre (le cache HTTP sert), et seules les 219 reprises en portent une neuve. */ 220 function avecReprise(adresse) { 221 return _reprises ? adresse + (adresse.indexOf('?') < 0 ? '?' : '&') + 'reprise=' + _reprises : adresse 222 } 223 224 function chargerPdfJs() { 225 if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib) 226 if (_pdf) return _pdf 227 _pdf = import(/* webpackIgnore: true */ avecReprise(LOCAL_PDFJS)) 228 .catch(function () { 229 /* Pas de copie locale (le paquet n'est pas installé) : on prend le 230 CDN. On ne DIT rien ici â c'est le cas normal aujourd'hui, et une 231 alerte à chaque PDF pour un repli qui marche ne serait que du bruit. */ 232 return import(/* webpackIgnore: true */ avecReprise(CDN_PDFJS)) 233 }) 234 .then(function (m) { 235 var lib = m && (m.getDocument ? m : m.default) || m 236 try { 237 if (lib && lib.GlobalWorkerOptions && !lib.GlobalWorkerOptions.workerSrc) { 238 lib.GlobalWorkerOptions.workerSrc = CDN_OUVRIER 239 } 240 } catch (e) { 241 /* Certaines constructions figent GlobalWorkerOptions. L'ouvrier 242 gardera son adresse par défaut ; le pire est une lecture plus 243 lente, pas une lecture fausse. */ 244 } 245 window.pdfjsLib = lib 246 return lib 247 }) 248 .catch(function (erreur) { 249 /* ON REND LA MAIN POUR UN PROCHAIN ESSAI. Garder la promesse rejetée 250 condamnerait toute la session : un réseau revenu ne servirait à 251 rien. L'appelant, lui, reçoit `null` et DIT ce qui manque. */ 252 _pdf = null 253 _reprises++ 254 try { console.warn('[pdf] chargement impossible', erreur) } catch (e) { /* console absente : on ne fait pas tomber la lecture pour une trace */ } 255 return null 256 }) 257 return _pdf 258 } 259 260 window.CREA_FILES = { readFolder: window.creaReadFolder, pick: pickFolderAndInsert, chargerPdfJs: chargerPdfJs } 261})()
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.