PageSourceSearch

https://jkms.kr/js/forthcomingAdd.js?v=20260724

js jkms.kr collected 2026-10-08 21:10:41 UTC 24,681 bytes, 607 lines download raw bytes

1async function initForthcoming(articleList) {
2    const container = document.querySelector('.forthcoming .content-row');
3    if (!container || !articleList) return;
4	
5	// 1. Skeleton UI 미리 생성 (리스트 개수만큼)
6    articleList.forEach((item, index) => {
7        if (item.articleType) {
8            const subHeadingHtml = `<h6 class="AOPSubheading">${item.articleType}</h6>`;
9            container.insertAdjacentHTML('beforeend', subHeadingHtml);
10        } else {
11	        const skeletonHtml = `
12	            <div class="articleBox card border-0" id="article-slot-${index}">
13	                <div class="row gx-3 gy-4">
14	                    <div class="col-md-12">
15	                        <div class="skeleton skeleton-img"></div>
16	                    </div>
17	                    <div class="col-md-12">
18	                        <div class="content">
19	                            <div class="skeleton skeleton-text" style="width:30%"></div>
20	                            <div class="skeleton skeleton-title"></div>
21	                            <div class="skeleton skeleton-text"></div>
22	                            <div class="skeleton skeleton-text" style="width:50%"></div>
23	                        </div>
24	                    </div>
25	                </div>
26	            </div>
27	        `;
28	        container.insertAdjacentHTML('beforeend', skeletonHtml);
29	    }
30	});
31
32    // 정렬용 빈 박스 미리 삽입 (2개)
33    const preEmptyHtml = `
34        <div class="articleBox card border-0 empty-box" style="visibility:hidden; height:0; margin:0!important; padding:0;"></div>
35        <div class="articleBox card border-0 empty-box" style="visibility:hidden; height:0; margin:0!important; padding:0;"></div>`;
36    container.insertAdjacentHTML('beforeend', preEmptyHtml);
37
38    // 2. 실행 전략 분할
39	const PRIORITY_COUNT = 3; // 먼저 처리할 개수
40	const priorityItems = articleList.slice(0, PRIORITY_COUNT);
41	const backgroundItems = articleList.slice(PRIORITY_COUNT);
42
43	/**
44	 * A. 상위 3편: 병렬 처리 (즉시 렌더링)
45	 */
46	const priorityPromises = priorityItems.map(async (item, index) => {
47	    if (!item.doi) return null;
48	    try {
49	        const data = await fetchForthcomingArticleData(item.doi, item.im, item.sub);
50	        updateArticleSlot(index, data); // 완료되는 대로 즉시 화면 업데이트
51
52	        if (typeof checkAdditionalFiles === 'function') {
53	            const articleBoxes = document.querySelectorAll('.articleBox');
54	            const targetSlot = articleBoxes[index]; // 여기서 실제 HTMLElement를 추출
55
56	            if (targetSlot) {
57	                // 첫 번째 인자로 인덱스(숫자)가 아닌 요소(Object)를 전달합니다.
58	                checkAdditionalFiles(targetSlot, data); 
59	            }
60	        }
61	    } catch (err) {
62	        updateArticleSlot(index, { error: true, doi: item.doi });
63	    }
64	});
65
66
67	// 우선 순위 작업이 모두 시작됨 (완료를 기다리지 않고 백그라운드 루프 예약 가능)
68	// 하지만 초기 화면의 안정성을 위해 우선 순위 완료를 기다린 후 나머지를 돌리는 것이 좋습니다.
69	await Promise.all(priorityPromises);
70
71	/**
72	 * B. 나머지: 순차 처리 (하나씩 차례대로)
73	 * for...of 문을 사용하면 await가 걸릴 때마다 다음 루프로 넘어가기 전에 대기합니다.
74	 */
75	for (let i = 0; i < backgroundItems.length; i++) {
76	    const item = backgroundItems[i];
77	    const originalIndex = i + PRIORITY_COUNT; // 전체 리스트에서의 인덱스 계산
78
79	    if (!item.doi) continue;
80
81	    try {
82	        // 이전 요청이 끝나야 다음 요청이 시작됨 (서버 부하 방지 및 순서 보장)
83	        const data = await fetchForthcomingArticleData(item.doi, item.im, item.sub);
84	        updateArticleSlot(originalIndex, data);
85
86	        if (typeof checkAdditionalFiles === 'function') {
87	            const articleBoxes = document.querySelectorAll('.articleBox');
88	            const targetSlot = articleBoxes[originalIndex]; // 여기서 실제 HTMLElement를 추출
89
90	            if (targetSlot) {
91	                // 첫 번째 인자로 인덱스(숫자)가 아닌 요소(Object)를 전달합니다.
92	                checkAdditionalFiles(targetSlot, data); 
93	            }
94	        }
95	    } catch (err) {
96	        updateArticleSlot(originalIndex, { error: true, doi: item.doi });
97	    }
98	}
99
100}
101
102// 데이터만 가져오는 순수 파싱 함수 (UI 조작 제외)
103async function fetchForthcomingArticleData(doi, imInput, subsubject) {
104	const journalID = '0063JKMS';
105	const journalAcron = 'jkms'
106
107	const urlsToTry = [
108        `https://jkms.org/DOIx.php?id=${doi}`,
109        `https://www.jkms.org/DOIx.php?id=${doi}`
110    ];
111
112    let response;
113    let finalUrl = "";
114
115    // 2. 성공할 때까지 순차적으로 시도합니다.
116    for (const url of urlsToTry) {
117        try {
118            const res = await fetch(url);
119            if (res.ok) {
120                response = res;
121                finalUrl = url;
122                break; // 성공하면 루프 종료
123            }
124        } catch (err) {
125            console.warn(`${url} 접속 실패, 다음 주소 시도 중...`);
126        }
127    }
128
129    if (!response) {
130        console.error("모든 URL 접속에 실패했습니다.");
131        return;
132    }
133
134	try {
135	    const htmlText = await response.text();
136	    const parser = new DOMParser();
137	    const doc = parser.parseFromString(htmlText, "text/html");
138
139	    // 기본 데이터 할당
140	    let frontMeta = doc.querySelector("#article-front-meta-left")?.innerText || "";
141	    let vol = "", page = "", epub = "";
142	    
143	    if (frontMeta.includes('.')) {
144	        vol = frontMeta.split('.')[1].trim().split(';')[1].split(':')[0];
145	        page = frontMeta.split('.')[1].trim().split(':')[1].split('-')[0];
146	    	epub = frontMeta.split('.')[4].trim()
147	    }
148
149	    let im = imInput;
150	    let imgDisplay = "";
151	    let sliderclassName = "col-md-6 col-xs-6";
152
153	    // thumbnail 강제로 안보이게 하는 경우 추가
154		if (im == 'xxxx') {
155	    	imgDisplay = " d-none";
156	    	sliderclassName = "col-md-12 col-xs-12";
157	    } else if (!im) {
158	    	// 1. Graphical Abstract 체크
159			const headers = Array.from(doc.querySelectorAll('.article-section-header h2'));
160			const hasGraphicalAbstract = headers.some(h => h.innerText.toLowerCase().includes('graphical abstract'));
161
162			
163			if (hasGraphicalAbstract) {
164			    im = "abf001";
165			} else {
166			    // 2. Figures 체크
167			    const portletHeaders = Array.from(doc.querySelectorAll('.porlet h5'));
168			    const hasFigures = portletHeaders.some(h => h.innerText.toLowerCase().includes('figures'));
169			    const hasTables = portletHeaders.some(h => h.innerText.toLowerCase().includes('tables'));
170
171		        if (hasFigures) {
172		            im = "g001-l";
173		        } else if (hasTables) {
174		            im = "i001-l";
175		        } else {
176		            // 둘 다 없을 경우
177		            im = "xxxx";
178		            imgDisplay = " d-none"; // 부트스트랩 클래스 또는 CSS 클래스
179		            sliderclassName = "col-md-12 col-xs-12";
180		        }
181			}
182
183			
184
185	    }
186
187		const citeButton = doc.querySelector('.CiteDialog');
188		let author = "";
189		const metaJournal = doc.querySelector('meta[name="citation_journal_title"]')?.getAttribute('content')?.trim();
190
191
192	    // 1. data 속성에서 암호화된 텍스트 가져오기
193	    const encodedCitation = citeButton.getAttribute('data-article-citation');
194	    
195	    if (encodedCitation) {
196	        try {
197	            // 2. Base64 디코딩 (atob 함수 사용)
198	            // 인코딩된 문자열 내부에 HTML 태그나 특수문자가 섞여 있을 수 있으므로 처리
199	            const decodedText = decodeURIComponent(escape(window.atob(encodedCitation)));
200	            
201	            // 3. 디코딩된 텍스트에서 첫 번째 마침표(.)까지 자르기
202	            author = decodedText.split('.')[0].trim() + ".";
203
204		        // 4. 추출된 텍스트 안에 학술지명이 포함되어 있다면 이탤릭체로 치환
205		        if (author.includes(metaJournal)) {
206		            // 해당 학술지명 부분만 <i>태그로 감싸기
207		            author = author.replace(metaJournal, `<i>${metaJournal}</i>`);
208		        }
209
210		        if (author === '[No authors listed].') {
211		        	author = "";
212		        }
213
214	        } catch (e) {
215	            console.error("Base64 디코딩 실패:", e);
216	            // 디코딩 실패 시 대안으로 메타데이터 사용
217	            const metaAuthor = doc.querySelector('meta[name="citation_author"]')?.content;
218	            author = metaAuthor ? metaAuthor + "." : "Author Unknown.";
219	        }
220	    }
221
222		let subject = doc.querySelector('.article-type')?.innerText;
223
224		// 1. 페이지 내 모든 <script> 태그를 검사
225		const scripts = Array.from(doc.querySelectorAll('script'));
226		const typeScript = scripts.find(s => s.textContent.includes('document.getElementsByClassName("article-type")[0].innerHTML'));
227
228		if (typeScript) {
229		    // 정규표현식으로 "Research Article" 부분만 추출
230		    // .innerHTML = "..." 또는 .innerHTML = '...' 형태를 모두 찾음
231		    const match = typeScript.textContent.match(/\.innerHTML\s*=\s*["'](.+?)["']/);
232		    if (match && match[1]) {
233		        subject = match[1];
234		    }
235		}
236
237		// 본문 형태에 따라 abs만 연결한 논문인지 확인
238		const bodyElement = doc.querySelector('#article-level-0-body');
239		let isAbsOnly = false;
240
241		if (!bodyElement) {
242		  isAbsOnly = true;
243		} else {
244		  const children = bodyElement.children;
245		  const hasOnlySupplementary = (children.length === 1 && children[0].id === 'supplementary-material');
246		  
247		  const textContent = bodyElement.innerText.trim();
248		  if (hasOnlySupplementary || textContent.length < 50) { 
249		      isAbsOnly = true;
250		  }
251		}
252
253		let displayLink = `https://doi.org/${doi}`;
254		let displayText = displayLink;
255
256		if (isAbsOnly) {
257		 epub = epub.replace('Published ', 'Posted ');
258		 displayLink = finalUrl;
259		 displayText = 'Abstract';
260		}
261
262
263	    // 예시용 데이터 객체 반환
264	    return {
265			vol: vol,
266    		page: page,
267	    	imgDisplay: imgDisplay,
268	        imgSrc: `../ArticleImage/${journalID}/${journalAcron}-${vol}-${page}-${im}.jpg` || '',
269	        sliderclassName: sliderclassName,
270	        subject: subject,
271	        subsubject: subsubject,
272	        title: doc.querySelector('.article-title .content-title')?.innerHTML || '',
273	        author: author,
274	        history: doc.querySelector('.article-received-accepted-date')?.innerText || '',
275	        epub: epub,
276	        link: finalUrl,
277	        doi: doi,
278	        page: page,
279	        displayLink: displayLink,
280	        displayText: displayText
281		};
282    } catch (e) {
283        return { title: "Error loading data", doi: doi };
284    }
285}
286
287function updateArticleSlot(index, data) {
288    const slot = document.getElementById(`article-slot-${index}`);
289    if (!slot) return;
290
291    let articleHtml = "";
292
293    if (data.error) {
294        // [로드 실패 시 UI]
295        articleHtml = `
296            <div class="row gx-3 gy-3 error-box">
297                <div class="col-12 text-center py-4">
298                    <div class="error-icon" style="font-size: 24px;">⚠️</div>
299                    <div class="error-text" style="color: #888; font-size: 13px; margin-top: 10px;">
300                        Failed to load data.<br>
301                        <small><a href="https://doi.org/${data.doi}" target="_blank">${data.doi}</a></small>
302                    </div>
303                </div>
304            </div>
305        `;
306    } else {
307        // [정상 로드 시 UI]
308    	articleHtml = `
309	        <div class="row gx-3 gy-4">
310	            <div class="col-md-12${data.imgDisplay}">
311	                <div class="thumbnail"><img class="img-fluid myImg" src="${data.imgSrc}"/></div>
312	            </div>
313	            <div class="col-md-12">
314	                <div class="content">
315    					<div>
316		                    <div class="subject d-flex justify-content-between">${data.subject}<span class="Forthcoming">Forthcoming</span></div>
317		                    <div class="sub_articleType">${data.subsubject}</div>
318	                    </div>
319	                    <div class="card-text title"><a href="${data.link}">${data.title}</a></div>
320	                    <div class="card-text author">${data.author}</div>
321	                    <div class="card-text disc">
322	                    	<div>${data.history}</div>
323	                    	<div>${data.epub}</div>
324	                    	<div><a href="https://doi.org/${data.doi}">https://doi.org/${data.doi}</a></div>
325    					</div>
326    					<div class="card-text doi"></div>
327    					<div class="card-text doi"></div>
328	                </div>
329	            </div>
330	        </div>
331    	`;
332    }
333    
334    slot.innerHTML = articleHtml;
335    slot.classList.add('loaded');
336}
337
338
339/**
340 * 추가 파일(Author Summary, AVF) 존재 여부 체크 및 버튼 삽입
341 * @param {HTMLElement} slot - 해당 논문의 articleBox 요소
342 * @param {Object} data - 논문 데이터 (vol, page/id 등 포함)
343 */
344async function checkAdditionalFiles(slot, data) {
345    const vol = data.vol; // fetch 시 vol 정보를 저장해둬야 함
346    const id = data.page; // 혹은 id로 사용되는 값
347    
348    if (!vol || !id) return;
349
350    const fileTypes = [
351        {
352            name: 'Author Summary in Korean',
353            url: `https://jkms.org/src/jkms-summary/jkms-${vol}-${id}.html`,
354            containerIdx: 0 // .doi 클래스의 첫 번째 위치 혹은 특정 위치
355        },
356        {
357            name: 'Audio-Visual File',
358            url: `https://jkms.org/src/jkms-summary-AVF/jkms-${vol}-${id}-AVF.html`,
359            containerIdx: 1
360        }
361    ];
362
363    for (const file of fileTypes) {
364        try {
365            // HEAD 요청으로 파일 존재 여부만 가볍게 체크
366            const response = await fetch(file.url, { method: 'HEAD' });
367
368            if (response.ok) {
369                const doiContainers = slot.querySelectorAll('.doi');
370                if (doiContainers[file.containerIdx]) {
371                    const btnHtml = `
372                        <span class="summary-btn">
373                            <a href="${file.url}" 
374                               onclick="window.open(this.href, '', 'width=760,height=450, top=630, left=590, scrollbars=yes'); return false;" 
375                               target="_new1">
376                               ${file.name}
377                            </a>
378                        </span>`;
379                    
380                    // 기존 DOI 내용 뒤에 추가하거나 내용을 교체
381                    doiContainers[file.containerIdx].insertAdjacentHTML('beforeend', btnHtml);
382                }
383            } else {
384                console.log(`[${id}] ${file.name} Not Found (404)`);
385            }
386        } catch (err) {
387            console.error(`Error checking file: ${file.name}`, err);
388        }
389    }
390}
391
392
393
394
395// main 페이지 forthcoming 논문 업데이트 관련
396/**
397 * 메인 페이지용: 다중 Forthcoming 페이지를 순회하며 모든 명단 객체를 통합 추출
398 */
399async function syncArticleListFromForthcoming() {
400    const sliderContainer = document.querySelector('#carousel-highlight');
401    if (!sliderContainer) return;
402
403    try {
404        // console.log("📡 [Slider] 다중 페이지 명단 수집 시작...");
405
406        // --- [STEP 1] 첫 번째 페이지 호출 및 분석 ---
407        const firstPageUrl = 'index.php?body=forthcoming';
408        const response = await fetch(firstPageUrl);
409        const htmlText = await response.text();
410        
411        // 가상의 DOM 객체를 만들어 내비게이션 링크를 파싱합니다.
412        const parser = new DOMParser();
413        const doc = parser.parseFromString(htmlText, 'text/html');
414
415        // .buttonNavi a 태그들에서 href 추출 (중복 및 현재페이지 제외)
416        const naviLinks = Array.from(doc.querySelectorAll('.buttonNavi a'))
417            .map(a => a.getAttribute('href'))
418            .filter(href => href && href.includes('main=forthcoming') && !href.includes(firstPageUrl));
419
420        // 고유한 추가 페이지 주소 리스트 (첫 페이지 제외)
421        const additionalPages = [...new Set(naviLinks)];
422        console.log(`📑 [Slider] 추가 페이지 발견: ${additionalPages.length}개`);
423
424        // --- [STEP 2] 명단 추출 헬퍼 함수 ---
425        const extractList = (text) => {
426            const listRegex = /const\s+articleList\s*=\s*(\[[\s\S]*?\]);/;
427            const match = text.match(listRegex);
428            if (match && match[1]) {
429                try {
430                    const cleanStr = match[1].replace(/\/\/.*$/gm, '');
431                    return new Function(`return ${cleanStr}`)();
432                } catch (e) { return []; }
433            }
434            return [];
435        };
436
437        // 첫 페이지 데이터 먼저 확보
438        let totalRawList = extractList(htmlText);
439
440        // --- [STEP 3] 나머지 페이지들 '순차적으로' 읽어오기 ---
441        if (additionalPages.length > 0) {
442            
443            for (const url of additionalPages) {
444                try {
445                    const res = await fetch(url);
446                    const txt = await res.text();
447                    const pageList = extractList(txt);
448                    
449                    // 기존 리스트 뒤에 순서대로 병합
450                    totalRawList = totalRawList.concat(pageList);
451                    console.log(`✅ [Slider] 페이지 로드 완료: ${url}`);
452                } catch (err) {
453                    console.error(`❌ [Slider] ${url} 로드 실패`, err);
454                }
455            }
456        }
457
458        // --- [STEP 4] 최종 필터링 및 전달 ---
459        if (Array.isArray(totalRawList) && totalRawList.length > 0) {
460            const seenDoi = new Set();
461            const finalArticleList = totalRawList.filter(item => {
462                if (item && item.doi && !seenDoi.has(item.doi)) {
463                    seenDoi.add(item.doi);
464                    return true;
465                }
466                return false;
467            });
468
469            console.log(`✅ [Slider] 통합 완료 (총 ${finalArticleList.length}개 논문)`);
470            
471            // 이제 이 리스트는 페이지1 -> 페이지2 -> 페이지3 순서가 완벽히 보장됩니다.
472            initMainSlider(finalArticleList);
473        }
474
475    } catch (err) {
476        console.error("❌ [Slider] 동기화 중 오류:", err);
477    }
478}
479
480/**
481 * 메인 페이지용: 명단을 받아 실제 데이터를 파싱하고 슬라이더를 초기화함
482 */
483async function initMainSlider(articleList) {
484    if (!articleList || articleList.length === 0) return;
485
486    // 1. DOI가 있는 실제 논문만 필터링
487    const doiItems = articleList.filter(item => item.doi);
488    const firstBatchSize = 2; // 먼저 띄울 개수
489    
490    // 1. 상위 2개 데이터 먼저 파싱 (순서 보장을 위해 Promise.all 사용)
491    console.log("🚀 [Slider] 하이브리드 로딩 시작 (상위 2개 병렬 + 나머지 직렬)");
492    const firstBatch = doiItems.slice(0, firstBatchSize);
493	if (firstBatch.length > 0) {
494	    const firstResults = await Promise.all(
495	        firstBatch.map(item => fetchForthcomingArticleData(item.doi, item.im, item.sub))
496	    );
497
498	    // 상위 2개 즉시 화면 출력
499	    firstResults.forEach((data, idx) => {
500	        if (data && !data.error) {
501	            appendMainSlider(data); 
502	        }
503	    });
504	}
505
506    // 2. 나머지 논문들 로딩 (백그라운드에서 순차적으로 진행)
507    if (doiItems.length > firstBatchSize) {
508        console.log("📡 [Slider] 2단계: 나머지 논문 백그라운드 로딩 시작...");
509        const remainingBatch = doiItems.slice(firstBatchSize);
510
511        // 나머지 항목들은 하나씩 순서대로 처리하여 순서 꼬임 방지
512        for (const item of remainingBatch) {
513            try {
514                const data = await fetchForthcomingArticleData(item.doi, item.im, item.sub);
515                if (data && !data.error) {
516                    appendMainSlider(data);
517                    // console.log(`✅ [Slider] 순차 추가 완료: ${item.doi}`);
518                }
519            } catch (err) {
520                console.error(`❌ [Slider] ${item.doi} 추가 실패`, err);
521            }
522        }
523    }
524}
525
526
527
528function appendMainSlider(data) {
529    const sliderContainer = document.querySelector('#carousel-highlight .carousel-inner.test');
530    const highlightWrapper = document.querySelector('.highlight-slider');
531    if (!sliderContainer) return;
532
533    // 현재 슬라이더에 아이템이 하나도 없는지 확인 (최초 로드 판별)
534    const isFirstItem = sliderContainer.children.length === 0;
535    const activeClass = isFirstItem ? " active" : "";
536    
537    // 슬라이더 아이템에도 ID를 부여하여 파일 체크 함수가 타겟팅할 수 있게 합니다.
538    const sliderItemId = `slider-item-${data.doi.replace(/[^a-zA-Z0-9]/g, '')}`;
539
540    const sliderHtml = `
541        <div class="carousel-item${activeClass}" id="${sliderItemId}">
542            <div class="articleBox card border-0">
543                <div class="row gx-5 gy-3 justify-content-evenly">
544                    <div class="col-md-5 col-xs-6${data.imgDisplay}">
545                        <div class="thumbnail">
546                        	<img class="img-fluid myImg" src="${data.imgSrc}" alt="Article Image">
547                        </div>
548                    </div>
549                    <div class="${data.sliderclassName}">
550                        <div class="content">
551                            <div>
552                            	<div class="d-flex justify-content-between">${data.subject}<span class="Forthcoming">Forthcoming</span></div>
553    							<div class="sub_articleType">${data.subsubject}</div>
554                            </div>
555                            <div class="card-text title"><a href="${data.link}">${data.title}</a></div>
556                            <div class="card-text author">${data.author}</div>
557		                    <div class="card-text disc">
558		                    	<div>${data.history}</div>
559		                    	<div>${data.epub}</div>
560		                    	<div><a href="https://doi.org/${data.doi}">https://doi.org/${data.doi}</a></div>
561	    					</div>
562	    					<div class="card-text doi"></div>
563	    					<div class="card-text doi"></div>
564                        </div>
565                    </div>
566                </div>
567            </div>
568        </div>
569    `;
570
571    // 순서대로 맨 뒤에 삽입
572    sliderContainer.insertAdjacentHTML('beforeend', sliderHtml);
573
574    // 파일 체크 실행
575    const currentSliderItem = document.getElementById(sliderItemId);
576    if (currentSliderItem && typeof checkAdditionalFiles === 'function') {
577        // 기존 리스트 페이지와 동일하게 요소와 데이터를 넘겨줍니다.
578        checkAdditionalFiles(currentSliderItem, data);
579    }
580
581    // 첫 아이템 로드 시 슬라이더 전체 영역 노출
582    if (isFirstItem && highlightWrapper) {
583        highlightWrapper.classList.add('loaded');
584    }
585
586    // Bootstrap Carousel 상태 업데이트
587    const carouselEl = document.querySelector('#carousel-highlight');
588    if (carouselEl && typeof bootstrap !== 'undefined') {
589        let carousel = bootstrap.Carousel.getInstance(carouselEl);
590        if (!carousel) {
591            // 인스턴스가 없으면 새로 생성 (자동 재생 시작)
592            carousel = new bootstrap.Carousel(carouselEl, { 
593                interval: 5000, 
594                ride: 'carousel',
595            });
596            carousel.cycle();
597        } else {
598            // ⚠️ 이미 실행 중인 경우, 새로운 아이템을 인식시키기 위해 인벤토리 업데이트
599            // Bootstrap 5는 DOM 변화를 감지하지만, 때때로 명시적인 cycle() 호출이 필요합니다.
600            if (isFirstItem) {
601                carousel.to(0); 
602                carousel.cycle();
603            }
604        }
605    }
606}
607

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.