LLM 작업실
수식을 복사하면 TeX 원문이 나오게
KaTeX가 그리는 두 벌의 글자 중 원문 annotation만 text/plain으로 넣고, 제목 옆 링크 아이콘과 안내 버튼의 글자를 ::before로 옮겨 복사에서 뺀 일
관련 파일: assets/, assets/, _sass/, _sass/, 커밋
사용자는 블로그 글을 선택해서 다른 곳에 붙여 넣다가 수식 부분이 이상하게 나오는 것을 봤다.
지금 우리 그 뭐지, 블로그 웹에서 복사 붙여넣기를 하면, 수식 부분이 예상대로 복사되지 않거든? 수정 가능해?
원인은 KaTeX가 수식 하나를 두 벌로 그린다는 데 있다. 화면에 보이는 .katex-html과, 스크린리더용으로 화면 밖에 숨겨 둔 .katex-mathml이다. 뒤쪽에는 MathML 트리와 함께 입력한 TeX 원문을 담은 <annotation>이 들어 있다. 브라우저의 기본 복사는 선택 범위의 글자를 DOM 순서대로 모으기 때문에, 두 벌의 글자가 한 수식 자리에서 뒤섞여 나온다. 보이는 쪽은 x, 2, + 같은 조각이고 숨은 쪽은 MathML 글자다.
수식을 원문으로 바꾸는 복사 핸들러
Copy_tex.js는 copy 이벤트에서 선택을 가로챈다. 선택 안에 .katex-이 하나도 없으면 아무것도 하지 않고 기본 동작에 맡긴다. 있으면 선택을 복제한 조각 안의 수식을 하나씩 원문으로 바꾼다.
frag.querySelectorAll('.katex').forEach(function (k) {
var ann = k.querySelector('.katex-mathml annotation');
if (!ann) return;
var tex = ann.textContent.trim();
var display = k.closest('.katex-display');
if (display) {
var block = document.createElement('div');
block.style.whiteSpace = 'pre-wrap';
block.textContent = '$$' + tex + '$$';
display.replaceWith(block);
} else {
k.replaceWith(document.createTextNode('$' + tex + '$'));
}
});
인라인은 $…$, 디스플레이는 $$…$$로 감싼다. 디스플레이 쪽에만 pre-wrap이 붙는 것은 aligned 같은 여러 줄 원문의 줄바꿈을 살리기 위해서다. 선택의 시작이나 끝이 수식 한가운데에 걸려 있으면 setStartBefore/setEndAfter로 범위를 수식 전체(디스플레이면 .katex-display까지)로 넓힌다. 반쪽짜리 수식 원문은 어디에도 쓸 수 없기 때문이다.
문단 사이의 줄바꿈이 남아야 한다는 점이 구현에서 가장 성가셨다. 바꾼 조각의 textContent를 읽으면 블록 사이의 줄바꿈이 사라진다. 그래서 조각을 선택의 공통 조상 안, 화면 밖(left:-99999px)에 잠깐 붙이고 innerText로 읽은 뒤 바로 지운다. 공통 조상 안에 붙이는 이유는, 그 자리에 걸려 있는 CSS(숨김 처리 등)가 원래 선택에서와 똑같이 적용되어야 innerText가 브라우저 복사와 같은 결과를 내기 때문이다.
결과는 text/plain으로만 넣는다. text/html을 같이 넣지 않은 이유는 파일 머리 주석에 적혀 있다. 붙여 넣는 쪽은 HTML을 먼저 쓰는데, KaTeX의 HTML은 KaTeX CSS 없이는 다시 뒤섞인 글자가 된다. 원문으로 바꾼 HTML을 넣어도 Obsidian 같은 HTML→마크다운 변환기가 \와 _를 이스케이프해 버린다. 한계도 하나 있다. 원문은 글에 쓴 그대로이므로 katex-macros.js의 커스텀 매크로는 펼쳐지지 않고, 매크로 이름이 그대로 복사된다.
제목 옆 링크 아이콘과 안내 버튼
수식 얘기를 하다가 내가 이것도 걸릴 것 같다고 말했고, 사용자는 곧바로 그것까지 정리하라고 했다.
응 그래, 근데 permalink 같은 경우는 무슨 말이야? 그 예를 들어서 섹션 제목 옆에 붙는 permalink가 그대로 복사된단 말? 그렇다면 그것도 정리해줬으면 좋겠는데.
그랬다. _main.js는 제목마다 앵커를 붙이면서 안에 스크린리더용 글자와 아이콘 글자를 함께 넣고 있었다.
// before
anchor.innerHTML = '<span class="sr-only">Permalink</span><i class="material-icons" style="vertical-align:-.2em">link</i>';
// after
anchor.setAttribute('aria-label', 'Permalink');
제목을 선택해 복사하면 제목 뒤에 Permalink와 link가 따라 나왔다. 글자 노드를 아예 없애고 접근성 이름은 aria-label로 옮겼다. 아이콘은 _page.scss의 .header-가 Material Symbols의 \e157(link) 코드포인트로 그린다. ::before의 content는 선택과 복사의 대상이 아니라서 따로 걸러낼 코드가 필요 없다.
같은 문제가 i가 적힌 안내 버튼에도 있었다. 선수지식 영역과 댓글창의 .comment-은 버튼 글자로 i를 갖고 있었는데, 이것도 템플릿 두 곳(semantic-, comments-)에서 지우고 _comments.scss의 .comment-로 옮겼다. 버튼 이름은 원래 aria-label이 맡고 있었으므로 접근성은 달라지지 않는다.
복사 핸들러로 이것들을 걸러내는 방법도 있었지만, 장식 글자는 DOM에 글자로 존재하지 않게 하는 쪽이 원인에 가깝다. 핸들러는 수식이 없는 선택에서는 아예 동작하지 않기 때문에, 핸들러에 맡겼다면 수식이 없는 제목만 복사할 때는 여전히 Permalink가 따라 나왔을 것이다.
공유하기
Twitter Facebook LinkedIn댓글
- 공개 저장소입니다. 댓글을 지워도 Git 히스토리에는 남습니다.
- 암호와 이메일은 암호화해 Cloudflare KV에만 보관합니다.
아직 댓글이 없습니다.