@@ -228,8 +228,66 @@ export async function scanFonts(dir, root = dir) {
228228/**
229229 * If it's the root dir, dirPrefix should be an empty string.
230230 */
231+ // Persistent snapshot across scans
232+ let lastFileSnapshot = new Map ( ) ;
233+
234+ /**
235+ * Scans all markdown files recursively and detects added/removed/modified files.
236+ * Returns { added, removed, modified } with full paths like "md/subdir/file.md".
237+ */
231238export async function scanFiles ( prefix , dir , root = dir ) {
232- // Reset global data structures before scanning
239+ const previousSnapshot = new Map ( lastFileSnapshot ) ;
240+ const newSnapshot = new Map ( ) ;
241+
242+ // Recursively collect file mtimes
243+ function snapshotDir ( d ) {
244+ const files = fs . readdirSync ( d ) ;
245+ for ( const f of files ) {
246+ const filePath = path . join ( d , f ) ;
247+ const stat = fs . statSync ( filePath ) ;
248+ if ( stat . isDirectory ( ) ) {
249+ // Skip excluded folders
250+ const nmods = process . env . NEXT_PUBLIC_IS_APP_FOLDER ? "/app/node_modules" : "node_modules" ;
251+ const slides = process . env . NEXT_PUBLIC_IS_APP_FOLDER ? "/app/slides" : "slides" ;
252+ if (
253+ filePath . startsWith ( "." ) ||
254+ filePath . startsWith ( nmods ) ||
255+ filePath . startsWith ( slides )
256+ )
257+ continue ;
258+ snapshotDir ( filePath ) ;
259+ } else if ( path . extname ( f ) === ".md" ) {
260+ const rel = prefix + path . relative ( root , filePath ) . replace ( / \\ / g, "/" ) ;
261+ newSnapshot . set ( rel , stat . mtimeMs ) ;
262+ }
263+ }
264+ }
265+
266+ snapshotDir ( dir ) ;
267+
268+ // Diff detection
269+ const added = [ ] ;
270+ const removed = [ ] ;
271+ const modified = [ ] ;
272+
273+ for ( const [ file , mtime ] of newSnapshot ) {
274+ if ( ! previousSnapshot . has ( file ) ) {
275+ added . push ( file ) ;
276+ } else if ( previousSnapshot . get ( file ) !== mtime ) {
277+ modified . push ( file ) ;
278+ }
279+ }
280+
281+ for ( const [ file ] of previousSnapshot ) {
282+ if ( ! newSnapshot . has ( file ) ) {
283+ removed . push ( file ) ;
284+ }
285+ }
286+
287+ // Save new snapshot
288+ lastFileSnapshot = newSnapshot ;
289+
290+ // Reset old global data (compatibility)
233291 dirPrefix = prefix ;
234292 Object . keys ( mdFilesMap ) . forEach ( key => delete mdFilesMap [ key ] ) ;
235293 Object . keys ( filesMap ) . forEach ( key => delete filesMap [ key ] ) ;
@@ -241,8 +299,11 @@ export async function scanFiles(prefix, dir, root = dir) {
241299 navFontsArray . length = 0 ;
242300 contentMap = { } ;
243301 mdFilesDirStructure = { } ;
244-
302+
303+ // Rebuild maps
245304 scanFilesInternal ( dir , root ) ;
305+
306+ // Build file metadata
246307 let mdFiles = await Promise . all (
247308 Object . keys ( mdFilesDir ) . map ( async ( file ) => {
248309 const pwe = mdFilesDir [ file ] ;
@@ -251,63 +312,55 @@ export async function scanFiles(prefix, dir, root = dir) {
251312 if ( folderArray . length === 1 && folderArray [ 0 ] === "" ) {
252313 folderArray . pop ( ) ;
253314 }
315+ const absPath = path . join ( dir , file ) ;
316+ const relFullPath = prefix + file ;
317+ const mtime = fs . existsSync ( absPath ) ? fs . statSync ( absPath ) . mtimeMs : 0 ;
254318 return {
255319 [ file ] : {
256320 path : file ,
321+ fullPath : relFullPath , // <-- new: full md/... path
257322 pathWithoutExt : pwe ,
258- folders : folders ,
259- folderArray : folderArray ,
323+ folders,
324+ folderArray,
260325 depth : folders === "" ? 0 : folders . split ( "/" ) . length ,
261326 fileName : file . split ( "/" ) . pop ( ) ,
262327 fileNameWithoutExtension : pwe . split ( "/" ) . pop ( ) . split ( "." ) [ 0 ] ,
263328 lastFolder : pwe . split ( "/" ) . slice ( - 2 , - 1 ) [ 0 ] || "" ,
264329 cssName : makeSafeForCSS ( folders ) ,
265330 permissions : await getPermissionsFor ( dirPrefix + file ) ,
331+ mtime,
266332 } ,
267333 } ;
268334 } )
269335 ) ;
270336
337+ // Flatten
271338 mdFiles = mdFiles . reduce ( ( acc , file ) => {
272339 const key = Object . keys ( file ) [ 0 ] ;
273340 acc [ key ] = file [ key ] ;
274341 return acc ;
275342 } , { } ) ;
276343
277- // Convert to array, sort, and convert back to object
344+ // Sort like before
278345 mdFiles = Object . entries ( mdFiles )
279346 . sort ( ( [ keyA , valueA ] , [ keyB , valueB ] ) => {
280- // Iterate over the folderArray
281- for (
282- let i = 0 ;
283- i < Math . min ( valueA . folderArray . length , valueB . folderArray . length ) ;
284- i ++
285- ) {
286- const comp = valueA . folderArray [ i ] . localeCompare (
287- valueB . folderArray [ i ] ,
288- undefined ,
289- { sensitivity : "base" }
290- ) ;
291- if ( comp !== 0 ) {
292- // If a difference is found, return the comparison result
293- return comp ;
294- }
347+ for ( let i = 0 ; i < Math . min ( valueA . folderArray . length , valueB . folderArray . length ) ; i ++ ) {
348+ const comp = valueA . folderArray [ i ] . localeCompare ( valueB . folderArray [ i ] , undefined , { sensitivity : "base" } ) ;
349+ if ( comp !== 0 ) return comp ;
295350 }
296- // If all elements are equal, compare the lengths of the arrays
297351 if ( valueA . folderArray . length !== valueB . folderArray . length ) {
298352 return valueB . folderArray . length - valueA . folderArray . length ;
299353 }
300- // If the folderArrays are equal, compare the name
301- return valueA . fileName . localeCompare ( valueB . fileName , undefined , {
302- sensitivity : "base" ,
303- } ) ;
354+ return valueA . fileName . localeCompare ( valueB . fileName , undefined , { sensitivity : "base" } ) ;
304355 } )
305356 . reduce ( ( acc , [ key , value ] ) => {
306357 acc [ key ] = value ;
307358 return acc ;
308359 } , { } ) ;
309360
310361 mdFilesDirStructure = mdFiles ;
362+
363+ return { added, removed, modified } ;
311364}
312365
313366function scanFilesInternal ( dir , root = dir ) {
@@ -601,7 +654,7 @@ function preReplaceObsidianFileLinks(html, req) {
601654 }
602655 f = f . split ( 0 , - 3 ) ;
603656 }
604- f = encodeURIComponent ( f ) ;
657+ // f = encodeURIComponent(f);
605658 const serverUrl = `${ req . protocol } ://${ req . get ( "host" ) } ` ;
606659 return `[${ alt ? alt : fileName } ](${ serverUrl } /${ dirPrefix + f } )` ;
607660 } else {
@@ -1357,19 +1410,81 @@ function getMermaidScriptEntry() {
13571410
13581411function getAutoReloadScript ( ) {
13591412 return `<script>
1360- (function() {
1361- const es = new EventSource('/hot-reload');
1413+ (function connectSSE() {
1414+ function normalizeToMdPath(raw) {
1415+ try {
1416+ // strip domain if present
1417+ raw = raw.replace(/^https?:\\/\\/[^/]+/, "");
1418+ // strip query + hash
1419+ raw = raw.split("?")[0].split("#")[0];
1420+ // strip leading slashes
1421+ raw = raw.replace(/^\\/+/, "");
1422+ // If already md/... keep it
1423+ if (raw.startsWith("md/")) return raw;
1424+ // If it's /md/... after stripping, also ok
1425+ if (raw.startsWith("/md/")) return raw.slice(1);
1426+ // If it's a .md at root or elsewhere, coerce to md/<filename>.md
1427+ if (raw.endsWith(".md")) {
1428+ const parts = raw.split("/");
1429+ const fname = parts[parts.length - 1];
1430+ return "md/" + fname;
1431+ }
1432+ return raw; // fallback (non-md pages)
1433+ } catch {
1434+ return "md/unknown.md";
1435+ }
1436+ }
13621437
1363- es.addEventListener('reload', function() {
1364- if (window.Reveal && Reveal.getIndices) {
1365- const slideIndices = Reveal.getIndices();
1366- sessionStorage.setItem("revealSlide", JSON.stringify(slideIndices));
1367- } else {
1368- sessionStorage.setItem("scrollY", window.scrollY);
1438+ // Prefer Reveal's configured URL (if present), otherwise use pathname
1439+ const revealUrl = (window.Reveal && typeof Reveal.getConfig === "function" && Reveal.getConfig()?.url) || null;
1440+ const currentFile = normalizeToMdPath(revealUrl || location.pathname);
1441+
1442+ const context = {
1443+ type: window.Reveal ? 'reveal' : 'page',
1444+ currentFile: decodeURIComponent(location.pathname.replace(/^\\/+/, ""))
1445+ };
1446+
1447+ const es = new EventSource('/hot-reload?context=' +
1448+ encodeURIComponent(JSON.stringify(context))
1449+ );
1450+
1451+
1452+ es.addEventListener('reload', function(event) {
1453+ try {
1454+ const payload = JSON.parse(event.data || '{}');
1455+ console.log('[SSE] Reload event:', payload);
1456+
1457+ // Save position before reload
1458+ if (window.Reveal && Reveal.getIndices) {
1459+ const slideIndices = Reveal.getIndices();
1460+ sessionStorage.setItem("revealSlide", JSON.stringify(slideIndices));
1461+ } else {
1462+ sessionStorage.setItem("scrollY", window.scrollY);
1463+ }
1464+
1465+ if (payload.type === 'nav' && !window.Reveal) {
1466+ location.reload();
1467+ } else if (payload.type === 'page') {
1468+ const current = location.pathname.replace(/^\\/+/, "");
1469+ if (payload.files && payload.files.some(f => current.endsWith(f))) {
1470+ location.reload();
1471+ } else {
1472+ console.log('[SSE] Skipping reload: not affected', { current, files: payload.files });
1473+ }
1474+ } else {
1475+ console.log('[SSE] Skipping reload: not affected', { current, files: payload.files });
1476+ }
1477+ } catch (err) {
1478+ console.error('[SSE] Error parsing reload payload:', err);
13691479 }
1370- location.reload();
13711480 });
13721481
1482+ es.onerror = function(err) {
1483+ console.warn('[SSE] Connection lost. Reconnecting in 3s...', err);
1484+ es.close();
1485+ setTimeout(connectSSE, 3000);
1486+ };
1487+
13731488 window.addEventListener("DOMContentLoaded", function() {
13741489 document.body.style.display = "none";
13751490
@@ -1385,18 +1500,15 @@ function getAutoReloadScript() {
13851500
13861501 const restore = () => {
13871502 Reveal.slide(h, v, f);
1388- Reveal.layout(); // neu berechnen
1503+ Reveal.layout();
13891504 document.body.style.display = "";
13901505 sessionStorage.removeItem("revealSlide");
13911506 };
13921507
1393- // Wenn Reveal schon ready ist, sofort
1394- if (Reveal.isReady()) {
1508+ if (Reveal.isReady && Reveal.isReady()) {
13951509 restore();
13961510 } else {
1397- // Sonst auf ready warten
1398- Reveal.on('ready', restore, { once: true });
1399- // Fallback, falls ready nicht feuert
1511+ Reveal.on && Reveal.on('ready', restore, { once: true });
14001512 setTimeout(() => {
14011513 document.body.style.display = "";
14021514 }, 1000);
0 commit comments