From c37b938354d9879e066db48c3f9db8c57a471850 Mon Sep 17 00:00:00 2001 From: bits-bytes-nn Date: Thu, 20 Aug 2026 01:07:13 +0900 Subject: [PATCH] feat(search): strip math delimiters from the index, keep the TeX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kramdown's mathjax engine leaves `\(...\)` and `\[...\]` in the HTML as literal text, so a flattened post read `메커니즘의 \(O(L^2)\) 계산 복잡도` — and now that the excerpt is cut around the match rather than taken from the opening, a window landing on a formula showed those delimiters to the reader. 30 of 35 posts carried them. Only the delimiters go. The TeX inside stays, because it is what the sentence is about and because a symbol inside a formula should remain findable: before 메커니즘의 \(O(L^2)\) 계산 복잡도는 after 메커니즘의 O(L^2) 계산 복잡도는 Inline math is replaced with no padding, display math with spaces. That asymmetry is load-bearing: Korean attaches a particle straight onto the symbol, so `\(\theta\)로` has to come out as `\theta로` and not `\theta 로`. Display math is already its own block. An unclosed `\(` is left alone rather than swallowing the rest of the post, and a bare `f(x)` in prose is untouched — both pinned by tests. --- _plugins/search_index.rb | 15 +++++++++++++- docs/tech-doc.md | 17 +++++++++++----- test/test_search_index.rb | 41 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/_plugins/search_index.rb b/_plugins/search_index.rb index f5e2260..731f525 100644 --- a/_plugins/search_index.rb +++ b/_plugins/search_index.rb @@ -22,13 +22,26 @@ module SearchIndex |tbody|tr|td|th|section|article|header|footer|figure|figcaption |br|hr)\b[^>]*>}xi + # kramdown's mathjax engine leaves the delimiters in the HTML as literal text, + # so a flattened post reads "메커니즘의 \(O(L^2)\) 계산 복잡도" and an excerpt landing + # there showed the delimiters to the reader. Only the delimiters go; the TeX + # inside stays, both because it is what the sentence is about and so that a + # symbol inside a formula is still findable. + INLINE_MATH = /\\\((.+?)\\\)/m + DISPLAY_MATH = /\\\[(.+?)\\\]/m + def self.plain_text(html) text = html.to_s.gsub(NON_PROSE, " ").gsub(COMMENT, " ") # Strip before decoding, never after. A post that quotes markup contains # `<script>` as text; decoding first would make it a real tag and the # strip would then delete the words the author actually wrote. text = text.gsub(BOUNDARY, " ").gsub(/<[^>]*>/, "") - CGI.unescapeHTML(text).gsub(/\s+/, " ").strip + text = CGI.unescapeHTML(text) + # Display math is its own block, so it may take spaces. Inline math must not: + # Korean attaches particles directly, and `\(\theta\)로` has to stay one word. + text = text.gsub(DISPLAY_MATH) { " #{Regexp.last_match(1)} " } + text = text.gsub(INLINE_MATH) { Regexp.last_match(1) } + text.gsub(/\s+/, " ").strip end end diff --git a/docs/tech-doc.md b/docs/tech-doc.md index d5bd35a..3b36954 100644 --- a/docs/tech-doc.md +++ b/docs/tech-doc.md @@ -242,8 +242,14 @@ MathJax 설정은 `head.html`에 있고 `{% if page.use_math %}`로 감싸 **프 순서가 중요하다 — **태그를 먼저 지우고 그다음에 엔티티를 디코딩한다.** 반대로 하면 마크업을 인용한 글의 `<script>`가 진짜 태그가 되고, 이어지는 태그 제거가 저자가 쓴 글자를 - 지운다. 인라인 태그(``, ``)는 공백 없이 벗겨 `강조된`이 한 단어로 - 남는다. `test/test_search_index.rb`가 이 경계들을 고정한다. + 지운다. 인라인 태그(``, ``)는 공백 없이 벗겨 `강조된`이 한 단어로 남는다. + + 같은 필터가 **수식 구분자도 벗긴다.** kramdown의 mathjax 엔진은 `\(...\)` · `\[...\]`를 + HTML에 글자 그대로 남기므로, 평문화한 본문이 `메커니즘의 \(O(L^2)\) 계산 복잡도`가 되고 + 발췌가 그 구간에 걸리면 구분자가 독자에게 보였다. 구분자만 지우고 **안의 TeX는 남긴다** — + 문장이 말하는 대상이고, 수식 속 기호도 계속 검색돼야 한다. 인라인 수식에는 공백을 붙이지 + 않는다(한국어는 조사가 기호에 바로 붙어 `\(\theta\)로`가 한 단어여야 한다). 디스플레이 + 수식은 원래 블록이라 붙여도 된다. `test/test_search_index.rb`가 이 경계들을 고정한다. - **왜 발췌가 아니라 전체 본문을 색인하나** — `simple-jekyll-search`는 형태소 분석 없이 단순 부분문자열 매칭을 한다. **색인에 없는 글자는 못 찾는다.** 발췌만 색인하면 본문 중·후반에만 나오는 단어("어텐션", "트랜스포머" 같은)는 검색 결과가 0건이 된다. `content`로 @@ -399,6 +405,7 @@ Bourbon → base/ → Neat → _layout → _post → _tags → _syntax(Rouge 코 이미 잘려 나간 뒤에 정렬하기 때문이다. 제대로 하려면 매칭·정렬·자르기를 직접 들고 있어야 하고, 그 시점에는 라이브러리를 걷어내는 게 맞다(현재 쓰는 기능은 페치·부분문자열 매칭·템플릿 치환뿐이다). -- **수식이 발췌에 날것으로 보인다** — 색인은 렌더된 HTML을 평문화한 것이라 MathJax 구분자가 - `\(O(L^2)\)` 형태로 남는다. 발췌가 그 구간에 걸리면 그대로 찍힌다. 색인에서 수식 구간을 - 지우면 깔끔해지지만 수식 안의 문자는 검색되지 않게 된다. +- **발췌에 TeX 명령이 남는다** — 수식 구분자는 벗기지만 안의 TeX는 남기므로(§5), 발췌가 + 수식에 걸리면 `\theta`처럼 명령어가 그대로 보인다. `O(L^2)`처럼 읽히는 경우가 대부분이라 + 그대로 두었다. 유니코드로 바꾸려면 TeX→기호 매핑이 필요하고, 그건 색인이 감당할 범위를 + 넘는다. diff --git a/test/test_search_index.rb b/test/test_search_index.rb index 5270381..70e1e85 100644 --- a/test/test_search_index.rb +++ b/test/test_search_index.rb @@ -75,3 +75,44 @@ def test_is_idempotent_on_already_plain_text assert_equal plain, SearchIndex.plain_text(plain) end end + +class TestMathDelimiters < Minitest::Test + # kramdown leaves these in the HTML as text, so an excerpt landing on a formula + # used to show the reader `\(O(L^2)\)`. + def test_strips_inline_delimiters_and_keeps_the_tex + assert_equal "메커니즘의 O(L^2) 계산 복잡도", + SearchIndex.plain_text("

메커니즘의 \\(O(L^2)\\) 계산 복잡도

") + end + + def test_strips_display_delimiters + assert_equal "앞 E = mc^2 뒤", SearchIndex.plain_text("앞 \\[E = mc^2\\] 뒤") + end + + # Inline math gets no padding: Korean attaches a particle straight onto the + # symbol, and a space here would break the word in two. + def test_inline_math_does_not_gain_a_space_before_a_korean_particle + assert_equal "비율 \\theta로 나눕니다", + SearchIndex.plain_text("비율 \\(\\theta\\)로 나눕니다") + end + + def test_handles_several_formulas_in_one_paragraph + assert_equal "a x b y c", + SearchIndex.plain_text("

a \\(x\\) b \\(y\\) c

") + end + + # The symbol stays searchable — that is why the TeX is kept rather than dropped. + def test_the_contents_remain_findable + assert_includes SearchIndex.plain_text("

복잡도는 \\(O(L^2)\\)입니다

"), "O(L^2)" + end + + # An opening delimiter with no partner is left alone rather than eating the rest + # of the post. + def test_an_unclosed_delimiter_is_left_as_is + assert_equal "열린 \\( 그리고 나머지 본문", + SearchIndex.plain_text("

열린 \\( 그리고 나머지 본문

") + end + + def test_a_lone_backslash_paren_in_prose_is_untouched + assert_equal "함수 f(x) 는 그대로", SearchIndex.plain_text("

함수 f(x) 는 그대로

") + end +end