Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion _plugins/search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 12 additions & 5 deletions docs/tech-doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,8 +242,14 @@ MathJax 설정은 `head.html`에 있고 `{% if page.use_math %}`로 감싸 **프

순서가 중요하다 — **태그를 먼저 지우고 그다음에 엔티티를 디코딩한다.** 반대로 하면 마크업을
인용한 글의 `&lt;script&gt;`가 진짜 태그가 되고, 이어지는 태그 제거가 저자가 쓴 글자를
지운다. 인라인 태그(`<em>`, `<code>`)는 공백 없이 벗겨 `<em>강조</em>된`이 한 단어로
남는다. `test/test_search_index.rb`가 이 경계들을 고정한다.
지운다. 인라인 태그(`<em>`, `<code>`)는 공백 없이 벗겨 `<em>강조</em>된`이 한 단어로 남는다.

같은 필터가 **수식 구분자도 벗긴다.** kramdown의 mathjax 엔진은 `\(...\)` · `\[...\]`를
HTML에 글자 그대로 남기므로, 평문화한 본문이 `메커니즘의 \(O(L^2)\) 계산 복잡도`가 되고
발췌가 그 구간에 걸리면 구분자가 독자에게 보였다. 구분자만 지우고 **안의 TeX는 남긴다** —
문장이 말하는 대상이고, 수식 속 기호도 계속 검색돼야 한다. 인라인 수식에는 공백을 붙이지
않는다(한국어는 조사가 기호에 바로 붙어 `\(\theta\)로`가 한 단어여야 한다). 디스플레이
수식은 원래 블록이라 붙여도 된다. `test/test_search_index.rb`가 이 경계들을 고정한다.
- **왜 발췌가 아니라 전체 본문을 색인하나** — `simple-jekyll-search`는 형태소 분석 없이
단순 부분문자열 매칭을 한다. **색인에 없는 글자는 못 찾는다.** 발췌만 색인하면 본문
중·후반에만 나오는 단어("어텐션", "트랜스포머" 같은)는 검색 결과가 0건이 된다. `content`로
Expand Down Expand Up @@ -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→기호 매핑이 필요하고, 그건 색인이 감당할 범위를
넘는다.
41 changes: 41 additions & 0 deletions test/test_search_index.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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("<p>메커니즘의 \\(O(L^2)\\) 계산 복잡도</p>")
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("<p>a \\(x\\) b \\(y\\) c</p>")
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("<p>복잡도는 \\(O(L^2)\\)입니다</p>"), "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("<p>열린 \\( 그리고 나머지 본문</p>")
end

def test_a_lone_backslash_paren_in_prose_is_untouched
assert_equal "함수 f(x) 는 그대로", SearchIndex.plain_text("<p>함수 f(x) 는 그대로</p>")
end
end
Loading