feat: 파이썬 삼중따옴표 안의 HTML을 HTML로 하이라이팅
sprinkler_backend.py의 `DASHBOARD_HTML = """<!DOCTYPE html>…"""` 174줄이
통째로 단색으로 보였다. Monaco의 파이썬 문법(Monarch)이 삼중따옴표 내부를
`string` 토큰 하나로 처리하기 때문이고, VS Code의 TextMate 문법과 달리
Monarch에는 언어 주입이 기본으로 없다.
다만 Monarch에도 nextEmbedded가 있어서 구간을 다른 언어 토크나이저에
넘길 수 있다(Monaco 자체 HTML 문법이 <script>/<style>을 이렇게 처리한다).
그걸 파이썬에 얹었다. HTML로 넘기면 그 안의 <style>/<script>까지 연쇄로
살아난다.
판별: 삼중따옴표 뒤 첫 비어있지 않은 내용이 `<`로 시작하면 HTML로 본다.
Monarch는 줄 단위라 여는 줄에서 바로 판단할 수 없어 대기 상태를 하나 뒀다.
SQL 독스트링(conn.execute("""CREATE TABLE…))과 산문 독스트링은 `<`로
시작하지 않으므로 종전대로 문자열 색을 유지한다. 빗나가도 색만 이상해질 뿐
편집·저장에는 영향이 없다.
구현 중 밟은 것 둘:
- 빈 줄에서 `/^\s*$/`는 폭 0으로 매칭돼 같은 상태에 머물며 무한 루프가
된다(Monarch가 "진행 없음"으로 예외를 던진다). `/\s+$/`로 최소 한 글자를
요구하면 빈 줄은 아예 매칭되지 않고 다음 줄로 넘어가며 상태만 유지된다.
- HTML 언어를 미리 로드해야 한다. Monaco는 언어를 지연 로딩해서, 파이썬
파일만 열면 HTML 토크나이저가 없어 임베딩이 조용히 실패한다.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,68 @@ function setCodeAiTech(tech) {
|
||||
localStorage.setItem('codeAiTech', tech);
|
||||
}
|
||||
|
||||
// 파이썬 삼중따옴표 안의 HTML을 HTML로 칠한다.
|
||||
//
|
||||
// Monaco의 파이썬 문법(Monarch)은 삼중따옴표 내부를 통째로 `string` 토큰 하나로 처리해서,
|
||||
// `DASHBOARD_HTML = """<!DOCTYPE html>…"""` 같은 블록이 174줄 내내 단색으로 보였다
|
||||
// (2026-08-18 리포트). VS Code의 TextMate 문법과 달리 Monarch에는 언어 주입이 기본으로 없다.
|
||||
//
|
||||
// 다만 Monarch에도 `nextEmbedded`가 있어서 다른 언어 토크나이저에 구간을 넘길 수 있다
|
||||
// (Monaco 자체 HTML 문법이 <script>/<style>을 이렇게 처리한다). 그걸 파이썬에 얹는다.
|
||||
// HTML을 넘기면 그 안의 <style>/<script>까지 연쇄로 살아난다.
|
||||
//
|
||||
// 판별: 삼중따옴표 다음의 **첫 비어있지 않은 내용이 `<`로 시작**하면 HTML로 본다.
|
||||
// Monarch는 줄 단위라 여는 줄에서 바로 판단할 수 없어서, 대기 상태를 하나 둔다.
|
||||
// 빗나가도(예: `<`로 시작하는 산문 독스트링) 색만 이상해질 뿐 편집·저장에는 영향이 없다.
|
||||
function enablePythonEmbeddedHtml() {
|
||||
try {
|
||||
const py = monaco.languages.getLanguages().find(l => l.id === 'python');
|
||||
if (!py || typeof py.loader !== 'function') return;
|
||||
// HTML 토크나이저가 먼저 등록돼 있어야 nextEmbedded가 붙는다. Monaco는 언어를
|
||||
// 지연 로딩하므로, 파이썬 파일만 열면 HTML은 아직 없어 임베딩이 조용히 실패한다.
|
||||
const html = monaco.languages.getLanguages().find(l => l.id === 'html');
|
||||
Promise.all([py.loader(), html && typeof html.loader === 'function' ? html.loader() : null])
|
||||
.then(([pyMod]) => {
|
||||
const lang = pyMod && pyMod.language;
|
||||
if (!lang || !lang.tokenizer || !lang.tokenizer.whitespace) return;
|
||||
const t = lang.tokenizer;
|
||||
|
||||
const openHtml = (quote, waitState) => [
|
||||
[new RegExp(quote), { token: 'string', next: '@' + waitState }],
|
||||
];
|
||||
// 여는 따옴표 뒤 첫 실질 내용을 보고 갈라지는 대기 상태
|
||||
const waitRules = (embedState, docState) => [
|
||||
// 공백만 있는 줄은 소비하고 계속 대기한다. `/^\s*$/`로 쓰면 완전히 빈 줄에서
|
||||
// **폭 0으로 매칭돼 같은 상태에 머물며 무한 루프**가 된다(Monarch가 진행 없음으로
|
||||
// 예외를 던진다). `\s+`로 최소 한 글자를 요구하면 그 경우 아예 매칭되지 않고,
|
||||
// 빈 줄은 규칙 없이 다음 줄로 넘어가면서 상태만 유지된다 — 원하는 동작이다.
|
||||
[/\s+$/, 'string'],
|
||||
[/(?=\s*<)/, { token: '@rematch', next: '@' + embedState, nextEmbedded: 'text/html' }],
|
||||
[/(?=[\s\S])/, { token: '@rematch', next: '@' + docState }], // HTML 아님 → 평범한 독스트링
|
||||
];
|
||||
const embedRules = (quote) => [
|
||||
[new RegExp(quote), { token: '@rematch', next: '@pop', nextEmbedded: '@pop' }],
|
||||
[/[\s\S]/, ''],
|
||||
];
|
||||
|
||||
// 기존 삼중따옴표 규칙보다 **앞에** 끼워야 먼저 걸린다.
|
||||
t.whitespace = [
|
||||
...openHtml("'''", 'pyHtmlWaitSq'),
|
||||
...openHtml('"""', 'pyHtmlWaitDq'),
|
||||
...t.whitespace.filter(r => !(Array.isArray(r) && String(r[0]) === String(/'''/)) &&
|
||||
!(Array.isArray(r) && String(r[0]) === String(/"""/))),
|
||||
];
|
||||
t.pyHtmlWaitSq = waitRules('pyHtmlSq', 'endDocString');
|
||||
t.pyHtmlWaitDq = waitRules('pyHtmlDq', 'endDblDocString');
|
||||
t.pyHtmlSq = embedRules("'''");
|
||||
t.pyHtmlDq = embedRules('"""');
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('python', lang);
|
||||
})
|
||||
.catch(e => console.warn('[enablePythonEmbeddedHtml]', e));
|
||||
} catch (e) { console.warn('[enablePythonEmbeddedHtml]', e); }
|
||||
}
|
||||
|
||||
function initCodeView() {
|
||||
updateCodeAiLabel();
|
||||
setCodeAiTech(codeAiTech);
|
||||
@@ -163,6 +225,7 @@ function initCodeView() {
|
||||
// create() 전에 정의해야 한다. 뒤에 두면 첫 화면은 내장 기본값으로 그려지고,
|
||||
// 사용자가 테마를 한 번 바꾸기 전까지 주석 색 재정의가 반영되지 않는다.
|
||||
defineCodeThemes();
|
||||
enablePythonEmbeddedHtml();
|
||||
const isDark = document.documentElement.getAttribute('data-theme') !== 'light';
|
||||
const savedTheme = (() => { try { return localStorage.getItem('codeEditorTheme'); } catch { return null; } })();
|
||||
monacoEditor = monaco.editor.create(document.getElementById('code-editor-container'), {
|
||||
|
||||
Reference in New Issue
Block a user