@@ -111,61 +111,62 @@ const generatedDate = new Date(generatedAt).toLocaleString('ja-JP', {
111111 includeMatches: true,
112112 });
113113
114- // HTML特殊文字をエスケープ
115114 function escapeHtml(text: string): string {
116- const div = document.createElement('div');
117- div.textContent = text;
118- return div.innerHTML;
115+ return text
116+ .replace(/&/g, '&')
117+ .replace(/</g, '<')
118+ .replace(/>/g, '> ')
119+ .replace(/"/g, '" ')
120+ .replace(/'/g, '' ');
119121 }
120122
121- // バッククォートをcodeタグに変換(HTMLエスケープ後)
122- function formatCode(text: string): string {
123- const escaped = escapeHtml(text);
124- return escaped.replace(/`([^`]+)`/g, '<code>$1</code>');
123+ // バッククォート内を <code > 表示(エスケープ後)
124+ function renderWithCode(rawText: string): string {
125+ return escapeHtml (rawText ).replace (/ `([^ `] + )`/ g , ' <code>$1</code>' );
125126 }
126127
127- // ハイライト適用後にformatCodeを適用(code表示を維持)
128- function applyCodeFormatting(html: string): string {
129- // ハイライト用のspanタグを一時的に保護
130- const placeholders: string[] = [];
131- const protectedHtml = html.replace(/<span class="highlight">([^<]*)<\/span>/g, (match, content) => {
132- const idx = placeholders.length;
133- placeholders.push(match);
134- return `\x00HIGHLIGHT${idx}\x00`;
135- });
136-
137- // バッククォートをcodeタグに変換
138- let result = protectedHtml.replace(/`([^`]+)`/g, '<code>$1</code>');
139-
140- // ハイライトを復元
141- result = result.replace(/\x00HIGHLIGHT(\d+)\x00/g, (_, idx) => placeholders[parseInt(idx)]);
128+ const HIGHLIGHT_START = '\uE000';
129+ const HIGHLIGHT_END = '\uE001';
142130
143- return result;
144- }
131+ function renderHighlightedHtml(rawText: string, indices: ReadonlyArray<[ number , number ]>): string {
132+ if ( ! indices || indices . length === 0 ) return renderWithCode ( rawText );
145133
146- // 検索結果をハイライト(code表示を維持)
147- function highlightText(element: HTMLElement, matches: Fuse.FuseResultMatch[] | undefined, key: string) {
148- if (!matches) return;
134+ const sorted = [... indices ].sort ((a , b ) => a [0 ] - b [0 ]);
135+ const merged: Array <[number , number ]> = [];
149136
150- const match = matches.find((m) => m.key === key);
151- if (!match || !match.indices || match.indices.length === 0) return;
137+ for (const [start, end] of sorted ) {
138+ const last = merged [merged .length - 1 ];
139+ if (! last || start > last [1 ] + 1 ) {
140+ merged .push ([start , end ]);
141+ } else {
142+ last [1 ] = Math .max (last [1 ], end );
143+ }
144+ }
152145
153- const text = element.textContent || '';
154- let result = '';
146+ let marked = ' ' ;
155147 let lastIndex = 0 ;
156-
157- // インデックスをソートして処理
158- const sortedIndices = [...match.indices].sort((a, b) => a[0] - b[0]);
159-
160- for (const [start, end] of sortedIndices) {
161- result += escapeHtml(text.slice(lastIndex, start));
162- result += `<span class="highlight">${escapeHtml(text.slice(start, end + 1))}</span>`;
148+ for (const [start, end] of merged ) {
149+ marked += rawText .slice (lastIndex , start );
150+ marked += HIGHLIGHT_START + rawText .slice (start , end + 1 ) + HIGHLIGHT_END ;
163151 lastIndex = end + 1 ;
164152 }
165- result += escapeHtml(text .slice(lastIndex) );
153+ marked += rawText .slice (lastIndex );
166154
167- // ハイライト後にformatCodeを適用してcode表示を維持
168- element.innerHTML = applyCodeFormatting(result);
155+ let html = escapeHtml (marked ).replace (/ `([^ `] + )`/ g , ' <code>$1</code>' );
156+ html = html .split (HIGHLIGHT_START ).join (' <span class="highlight">' );
157+ html = html .split (HIGHLIGHT_END ).join (' </span>' );
158+ return html ;
159+ }
160+
161+ function applyHighlight(
162+ element: HTMLElement,
163+ rawText: string,
164+ matches: Fuse.FuseResultMatch[] | undefined,
165+ key: 'ja' | 'en'
166+ ) {
167+ const match = matches ?.find ((m ) => m .key === key );
168+ const indices = match ?.indices as ReadonlyArray <[number , number ]> | undefined ;
169+ element .innerHTML = indices ? renderHighlightedHtml (rawText , indices ) : renderWithCode (rawText );
169170 }
170171
171172 // 検索を実行
@@ -186,8 +187,8 @@ const generatedDate = new Date(generatedAt).toLocaleString('ja-JP', {
186187 // ハイライトをリセット
187188 const jaCell = row .querySelector (' .entry-ja' ) as HTMLElement ;
188189 const enCell = row .querySelector (' .entry-en' ) as HTMLElement ;
189- if (jaCell) jaCell.innerHTML = formatCode ((row as HTMLElement).dataset.ja || '');
190- if (enCell) enCell.innerHTML = formatCode ((row as HTMLElement).dataset.en || '');
190+ if (jaCell ) jaCell .innerHTML = renderWithCode ((row as HTMLElement ).dataset .ja || ' ' );
191+ if (enCell ) enCell .innerHTML = renderWithCode ((row as HTMLElement ).dataset .en || ' ' );
191192 });
192193 });
193194 searchStats .hidden = true ;
@@ -254,14 +255,12 @@ const generatedDate = new Date(generatedAt).toLocaleString('ja-JP', {
254255 const jaCell = row .querySelector (' .entry-ja' ) as HTMLElement ;
255256 const enCell = row .querySelector (' .entry-en' ) as HTMLElement ;
256257
257- // まずテキストをリセット
258- if (jaCell) jaCell.innerHTML = formatCode((row as HTMLElement).dataset.ja || '');
259- if (enCell) enCell.innerHTML = formatCode((row as HTMLElement).dataset.en || '');
260-
261258 // ハイライトを適用
262259 if (matchResult ?.matches ) {
263- if (jaCell) highlightText(jaCell, matchResult.matches, 'ja');
264- if (enCell) highlightText(enCell, matchResult.matches, 'en');
260+ const rawJa = (row as HTMLElement ).dataset .ja || ' ' ;
261+ const rawEn = (row as HTMLElement ).dataset .en || ' ' ;
262+ if (jaCell ) applyHighlight (jaCell , rawJa , matchResult .matches , ' ja' );
263+ if (enCell ) applyHighlight (enCell , rawEn , matchResult .matches , ' en' );
265264 }
266265 } else {
267266 row .classList .add (' hidden' );
0 commit comments