fix(cafe24): 소스 들여쓰기 정상화 + 줄 번호·가로 스크롤 편집기
1) 들여쓰기가 안 되던 원인 인라인 요소가 여러 줄에 걸쳐 있으면 그 구간을 하나의 텍스트 덩어리로 다뤄 첫 줄에만 들여쓰기를 붙이고 있었다. 그래서 <img> 가 한 줄에 하나씩 적힌 상세페이지에서 두 번째 <img> 부터 1열에 붙어 나왔다. 원문 줄바꿈을 살리면서 각 줄을 현재 깊이로 들여쓰도록 고쳤다. 줄 앞 공백은 렌더링에 영향이 없으므로 안전하다. 상세페이지는 <img> 를 한 줄에 하나씩 적어두는 경우가 많고 그 모양이 저자의 의도라, 한 줄로 합치는 것보다 살리는 쪽이 읽기 좋다. 주석도 줄을 강제로 나누지 않게 바꿨다. `<!-- 대파_타임랩스 --><img ...>` 처럼 바로 뒤 요소를 설명하는 주석이 많아서, 나누면 라벨과 대상이 떨어져 오히려 읽기 나빠진다. 빈 줄은 구획 표시로 한 줄까지 유지한다. 이 과정에서 멱등성이 다시 깨지는 것을 테스트가 잡았다 — 텍스트 끝에 남은 "\n " 조각이 매번 빈 줄로 바뀌고 있었고, 양 끝 공백을 함께 제거하도록 고쳤다. 2) 줄 번호와 가로 스크롤 전문 편집기처럼 줄 번호 칸을 넣었다. 줄바꿈을 허용하면 한 논리 줄이 여러 행이 되어 번호가 어긋나므로, 줄바꿈을 끄고(white-space:pre + wrap=off) 가로로 스크롤한다. 번호 칸은 position:sticky 라 가로로 스크롤해도 왼쪽에 남는다. 크기 계산도 단순해졌다. <pre> 가 흐름에 남아 상자 크기를 정하고 textarea 가 inset:0 으로 그 위를 덮는다 — JS 로 높이를 맞추지 않으니 어긋날 여지가 없다. 색상 계열은 그대로 뒀다(태그 초록·속성 갈색·값 남색·주석 회색). 검증: 유닛테스트 45개 통과(신규 4개 — 모든 줄 들여쓰기, 주석과 요소 붙임, 빈 줄 1개 유지, 실제 상세페이지 모양 멱등). 브라우저 실측: 줄 번호 개수가 줄 수와 일치(18/18, 19/19), 번호와 코드의 세로 위치 일치, 긴 줄에서 편집기 안에서만 가로 스크롤(1920 > 716)되고 페이지는 가로 스크롤 없음, 400px 스크롤 후에도 번호 칸 고정, textarea 내부 스크롤 0(두 층 정렬 유지). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,12 +65,18 @@
|
||||
</div>
|
||||
|
||||
{# 색칠된 <pre> 위에 투명한 <textarea> 를 겹쳐 문법 강조를 만든다.
|
||||
두 요소의 글자 위치가 어긋나면 안 되므로 폰트·여백은 CSS 에서 함께 관리한다. #}
|
||||
<pre> 가 크기를 정하고 <textarea> 는 inset:0 으로 그 위를 정확히 덮는다.
|
||||
줄바꿈을 하지 않고(wrap=off) 가로로 스크롤하므로 줄 번호가 항상 맞는다.
|
||||
두 요소의 폰트·여백이 다르면 글자가 어긋난다 — CSS 에서 함께 관리한다. #}
|
||||
<div class="cf24-code" id="cf24-code-pc">
|
||||
<pre class="cf24-code-hl" aria-hidden="true"><code id="cf24-hl-pc"></code></pre>
|
||||
<textarea id="cf24-html-pc" class="cf24-code-input" name="html"
|
||||
spellcheck="false" autocapitalize="off" autocorrect="off"
|
||||
wrap="soft">{{ html_pc }}</textarea>
|
||||
<div class="cf24-code-rows">
|
||||
<div class="cf24-gutter" id="cf24-gutter-pc" aria-hidden="true"></div>
|
||||
<div class="cf24-code-body">
|
||||
<pre class="cf24-code-hl" aria-hidden="true"><code id="cf24-hl-pc"></code></pre>
|
||||
<textarea id="cf24-html-pc" class="cf24-code-input" name="html" wrap="off"
|
||||
spellcheck="false" autocapitalize="off" autocorrect="off">{{ html_pc }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814f" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -148,27 +148,35 @@
|
||||
function setupCodeEditor() {
|
||||
var ta = document.getElementById("cf24-html-pc");
|
||||
var hl = document.getElementById("cf24-hl-pc");
|
||||
var gutter = document.getElementById("cf24-gutter-pc");
|
||||
if (!ta || !hl) return;
|
||||
|
||||
var timer = null;
|
||||
|
||||
function renderGutter(count) {
|
||||
if (!gutter || gutter.dataset.lines === String(count)) return;
|
||||
var out = "";
|
||||
for (var i = 1; i <= count; i++) out += i + "\n";
|
||||
gutter.textContent = out;
|
||||
gutter.dataset.lines = String(count);
|
||||
}
|
||||
|
||||
function repaint() {
|
||||
// 마지막 줄이 잘리지 않게 개행을 하나 덧붙인다(<pre> 특성).
|
||||
if (ta.value.length > HL_LIMIT) hl.textContent = ta.value + "\n";
|
||||
else hl.innerHTML = paintHtml(ta.value) + "\n";
|
||||
}
|
||||
function resize() {
|
||||
// 색칠된 <pre> 는 같은 내용·같은 스타일이라 이것이 진짜 콘텐츠 높이다.
|
||||
// textarea 의 scrollHeight 를 쓰면 브라우저마다 한두 줄 더 잡혀 두 층의
|
||||
// 글자 위치가 어긋난다(실측 830 vs 792).
|
||||
var target = hl.parentElement.scrollHeight;
|
||||
if (target > 0) ta.style.height = target + "px";
|
||||
// 높이가 잠깐 부족했던 사이 내부 스크롤이 생겼으면 되돌린다(두 층 정렬 유지).
|
||||
renderGutter(ta.value.split("\n").length);
|
||||
// 크기는 <pre> 가 정하고 textarea 가 그 위를 덮는다. 다시 칠하는 사이
|
||||
// 내부 스크롤이 생겼으면 되돌려 두 층의 정렬을 유지한다.
|
||||
ta.scrollTop = 0;
|
||||
ta.scrollLeft = 0;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
// 높이는 색칠 결과에서 나오므로 다시 칠한 뒤에 맞춘다.
|
||||
// 짧은 소스는 바로 칠해야 줄 번호·상자 크기가 즉시 따라온다.
|
||||
if (ta.value.length < 50000) { repaint(); return; }
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(function () { repaint(); resize(); }, 60);
|
||||
timer = setTimeout(repaint, 80);
|
||||
}
|
||||
|
||||
ta.addEventListener("input", function () { dirty = true; refresh(); });
|
||||
@@ -184,7 +192,6 @@
|
||||
});
|
||||
|
||||
repaint();
|
||||
resize();
|
||||
}
|
||||
|
||||
// ── 편집기 조각을 새로 끼워 넣은 뒤 다시 연결 ──
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814f" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "erp_base.html" %}
|
||||
|
||||
{% block head_extra %}
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814f" />
|
||||
<link rel="stylesheet" href="/static/cafe24.css?v=20260814g" />
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
Reference in New Issue
Block a user