@@ -328,6 +328,7 @@ const copy = {
328328 search : "搜索章节" ,
329329 top : "顶部" ,
330330 readerKicker : "Engineering Field Guide" ,
331+ chapterKicker : "章节片段" ,
331332 loading : "Loading guide..." ,
332333 loadError : "Could not load guide" ,
333334 pdf : "下载 PDF" ,
@@ -398,6 +399,7 @@ const copy = {
398399 search : "Search chapters" ,
399400 top : "Top" ,
400401 readerKicker : "Engineering Field Guide" ,
402+ chapterKicker : "Chapter Focus" ,
401403 loading : "Loading guide..." ,
402404 loadError : "Could not load guide" ,
403405 pdf : "Download PDF" ,
@@ -510,6 +512,8 @@ const state = {
510512 day : 1 ,
511513 headings : [ ] ,
512514 chapterTargets : [ ] ,
515+ chapterSections : [ ] ,
516+ focusedChapterIndex : null ,
513517 observer : null ,
514518 guidePromise : null ,
515519} ;
@@ -585,6 +589,7 @@ const els = {
585589 readerKicker : document . querySelector ( "#reader-kicker" ) ,
586590 readerTitle : document . querySelector ( "#reader-title" ) ,
587591 readerSummary : document . querySelector ( "#reader-summary" ) ,
592+ readerMetrics : document . querySelector ( ".reader-metrics" ) ,
588593 metricChapters : document . querySelector ( "#metric-chapters" ) ,
589594 metricLanguages : document . querySelector ( "#metric-languages" ) ,
590595 metricPdf : document . querySelector ( "#metric-pdf" ) ,
@@ -620,6 +625,12 @@ function preferredDay() {
620625 return Number . isInteger ( stored ) && stored >= 1 && stored <= PLAN_DAY_COUNT ? stored : 1 ;
621626}
622627
628+ function preferredChapter ( ) {
629+ const params = new URLSearchParams ( window . location . search ) ;
630+ const requested = Number ( params . get ( "chapter" ) ) ;
631+ return Number . isInteger ( requested ) && requested >= 1 && requested <= CHAPTER_COUNT ? requested : null ;
632+ }
633+
623634function getStoredText ( key ) {
624635 try {
625636 return window . localStorage . getItem ( key ) || "" ;
@@ -824,6 +835,7 @@ function setLanguageChrome(lang) {
824835 els . metricChapters . innerHTML = `<strong>${ CHAPTER_COUNT } </strong> ${ doc . metrics . chapters } ` ;
825836 els . metricLanguages . innerHTML = `<strong>2</strong> ${ doc . metrics . languages } ` ;
826837 els . metricPdf . innerHTML = `<strong>PDF</strong> ${ doc . metrics . pdf } ` ;
838+ renderReaderHeader ( ) ;
827839
828840 els . langButtons . forEach ( ( button ) => {
829841 const isActive = button . dataset . lang === lang ;
@@ -858,10 +870,106 @@ function updateUrl() {
858870 params . set ( "lang" , state . lang ) ;
859871 params . set ( "day" , String ( state . day ) ) ;
860872 params . set ( "view" , state . mode ) ;
861- const hash = state . mode === "reader" ? window . location . hash : "" ;
873+ if ( state . mode === "reader" && state . focusedChapterIndex ) {
874+ params . set ( "chapter" , String ( state . focusedChapterIndex ) ) ;
875+ } else {
876+ params . delete ( "chapter" ) ;
877+ }
878+ const hash = state . mode === "reader" && state . focusedChapterIndex ? window . location . hash : "" ;
862879 window . history . replaceState ( null , "" , `${ window . location . pathname } ?${ params . toString ( ) } ${ hash } ` ) ;
863880}
864881
882+ function focusedChapterSection ( ) {
883+ if ( ! state . focusedChapterIndex ) return null ;
884+ return state . chapterSections [ state . focusedChapterIndex - 1 ] || null ;
885+ }
886+
887+ function readerVisibleHeadings ( ) {
888+ const headings = state . headings . filter ( ( heading ) => heading . depth === 2 || heading . depth === 3 ) ;
889+ const section = focusedChapterSection ( ) ;
890+ if ( ! section ) return headings ;
891+
892+ const nodes = new Set ( section . nodes ) ;
893+ return headings . filter ( ( heading ) => nodes . has ( heading . element ) ) ;
894+ }
895+
896+ function renderReaderHeader ( ) {
897+ const doc = docs [ state . lang ] ;
898+ const text = copy [ state . lang ] ;
899+ const section = focusedChapterSection ( ) ;
900+
901+ if ( section ) {
902+ const chapter = chapters [ state . lang ] [ section . chapterIndex - 1 ] ;
903+ els . readerKicker . textContent = text . chapterKicker ;
904+ els . readerTitle . textContent = section . text ;
905+ els . readerSummary . textContent = chapter ?. focus || doc . summary ;
906+ els . readerMetrics . hidden = true ;
907+ return ;
908+ }
909+
910+ els . readerKicker . textContent = text . readerKicker ;
911+ els . readerTitle . textContent = doc . title ;
912+ els . readerSummary . textContent = doc . summary ;
913+ els . readerMetrics . hidden = false ;
914+ }
915+
916+ function updateReaderStatus ( ) {
917+ const section = focusedChapterSection ( ) ;
918+ els . status . textContent = section
919+ ? `${ docs [ state . lang ] . status } · ${ section . text } `
920+ : `${ docs [ state . lang ] . status } · ${ state . chapterTargets . length } ${ docs [ state . lang ] . sections } ` ;
921+ }
922+
923+ function applyReaderFocus ( ) {
924+ let section = focusedChapterSection ( ) ;
925+ if ( state . focusedChapterIndex && ! section ) {
926+ state . focusedChapterIndex = null ;
927+ section = null ;
928+ }
929+
930+ const visibleNodes = new Set ( section ?. nodes || [ ] ) ;
931+ els . article . classList . toggle ( "is-chapter-focused" , Boolean ( section ) ) ;
932+ els . article . querySelectorAll ( ".is-focused-chapter-start" ) . forEach ( ( heading ) => {
933+ heading . classList . remove ( "is-focused-chapter-start" ) ;
934+ } ) ;
935+
936+ [ ...els . article . children ] . forEach ( ( node ) => {
937+ node . hidden = Boolean ( section ) && ! visibleNodes . has ( node ) ;
938+ } ) ;
939+
940+ if ( section ) {
941+ section . element . classList . add ( "is-focused-chapter-start" ) ;
942+ }
943+
944+ renderReaderHeader ( ) ;
945+ updateReaderStatus ( ) ;
946+ }
947+
948+ function setReaderFocus ( chapterIndex ) {
949+ const next = Number ( chapterIndex ) ;
950+ state . focusedChapterIndex = Number . isInteger ( next ) && next >= 1 && next <= CHAPTER_COUNT ? next : null ;
951+ applyReaderFocus ( ) ;
952+ renderToc ( ) ;
953+ observeHeadings ( ) ;
954+ updateReadingProgress ( ) ;
955+ }
956+
957+ function currentHashTarget ( ) {
958+ try {
959+ return decodeURIComponent ( window . location . hash . slice ( 1 ) ) ;
960+ } catch {
961+ return window . location . hash . slice ( 1 ) ;
962+ }
963+ }
964+
965+ function syncReaderHashToFocus ( ) {
966+ const section = focusedChapterSection ( ) ;
967+ if ( state . mode !== "reader" || ! section ) return ;
968+ if ( currentHashTarget ( ) !== section . id ) {
969+ window . location . hash = section . id ;
970+ }
971+ }
972+
865973function getDayTaskDefs ( day , lang = state . lang ) {
866974 const text = copy [ lang ] ;
867975 const schedule = getDaySchedule ( day , lang ) ;
@@ -1381,12 +1489,14 @@ async function loadGuide(lang, shouldUpdateUrl = true) {
13811489 els . article . classList . remove ( "loading" ) ;
13821490
13831491 postProcessArticle ( ) ;
1492+ applyReaderFocus ( ) ;
1493+ syncReaderHashToFocus ( ) ;
13841494 renderToc ( ) ;
13851495 observeHeadings ( ) ;
13861496 updateReadingProgress ( ) ;
13871497 window . lucide ?. createIcons ( ) ;
13881498
1389- els . status . textContent = ` ${ docs [ lang ] . status } · ${ state . chapterTargets . length } ${ docs [ lang ] . sections } ` ;
1499+ updateReaderStatus ( ) ;
13901500
13911501 if ( shouldUpdateUrl ) updateUrl ( ) ;
13921502
@@ -1457,14 +1567,29 @@ function postProcessArticle() {
14571567 } ) ;
14581568 mermaid . run ( { querySelector : ".mermaid" } ) ;
14591569 }
1570+
1571+ const articleChildren = [ ...els . article . children ] ;
1572+ state . chapterSections = state . chapterTargets
1573+ . map ( ( target , index ) => {
1574+ const start = articleChildren . indexOf ( target . element ) ;
1575+ const nextTarget = state . chapterTargets [ index + 1 ] ;
1576+ const end = nextTarget ? articleChildren . indexOf ( nextTarget . element ) : articleChildren . length ;
1577+ if ( start < 0 ) return null ;
1578+
1579+ return {
1580+ ...target ,
1581+ chapterIndex : index + 1 ,
1582+ nodes : articleChildren . slice ( start , end > start ? end : articleChildren . length ) ,
1583+ } ;
1584+ } )
1585+ . filter ( Boolean ) ;
14601586}
14611587
14621588function renderToc ( ) {
14631589 const query = els . search . value . trim ( ) . toLowerCase ( ) ;
14641590 els . toc . innerHTML = "" ;
14651591
1466- state . headings
1467- . filter ( ( heading ) => heading . depth === 2 || heading . depth === 3 )
1592+ readerVisibleHeadings ( )
14681593 . forEach ( ( heading ) => {
14691594 const item = document . createElement ( "li" ) ;
14701595 const link = document . createElement ( "a" ) ;
@@ -1502,7 +1627,7 @@ function observeHeadings() {
15021627 } ,
15031628 ) ;
15041629
1505- state . headings . forEach ( ( heading ) => state . observer . observe ( heading . element ) ) ;
1630+ readerVisibleHeadings ( ) . forEach ( ( heading ) => state . observer . observe ( heading . element ) ) ;
15061631}
15071632
15081633function updateReadingProgress ( ) {
@@ -1517,11 +1642,11 @@ async function openChapter(chapterIndex) {
15171642 await state . guidePromise . catch ( ( ) => { } ) ;
15181643 }
15191644
1520- setMode ( "reader" ) ;
1521-
15221645 const target = state . chapterTargets [ chapterIndex - 1 ] ;
15231646 if ( ! target ) return ;
15241647
1648+ setReaderFocus ( chapterIndex ) ;
1649+ setMode ( "reader" ) ;
15251650 window . location . hash = target . id ;
15261651 window . setTimeout ( ( ) => {
15271652 target . element . scrollIntoView ( { behavior : "smooth" , block : "start" } ) ;
@@ -1548,6 +1673,9 @@ document.addEventListener("click", (event) => {
15481673
15491674 const modeButton = event . target . closest ( "button[data-mode]" ) ;
15501675 if ( modeButton ) {
1676+ if ( modeButton . dataset . mode === "reader" ) {
1677+ setReaderFocus ( null ) ;
1678+ }
15511679 setMode ( modeButton . dataset . mode ) ;
15521680 return ;
15531681 }
@@ -1624,6 +1752,7 @@ window.addEventListener("resize", updateReadingProgress);
16241752state . lang = preferredLanguage ( ) ;
16251753state . mode = preferredMode ( ) ;
16261754state . day = preferredDay ( ) ;
1755+ state . focusedChapterIndex = state . mode === "reader" ? preferredChapter ( ) : null ;
16271756setLanguageChrome ( state . lang ) ;
16281757renderPlanner ( ) ;
16291758renderSchedulePlan ( ) ;
0 commit comments