Skip to content

Commit ed1a188

Browse files
committed
feat: refine code block editing and paste output
1 parent 28d5f94 commit ed1a188

17 files changed

Lines changed: 1082 additions & 95 deletions

src/lib/dc/export-document.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { normalizeTutorialStepNumber } from "$lib/editor/tutorial-block";
1717
import {
1818
defaultProseFontFamily,
1919
safeDcCodeFontFamily as safeCodeFontFamily,
20+
safeDcInlineCodeFontFamily as safeInlineCodeFontFamily,
2021
safeDcProseFontFamily as safeProseFontFamily,
2122
} from "./font-stacks";
2223
import { normalizeCodeFilename } from "$lib/highlighter/code-block-metadata";
@@ -673,7 +674,7 @@ function applyMarks(
673674
const style = joinStyle({
674675
"background-color": context.inlineCodeBackground ?? palette.inlineCodeBackground,
675676
color: context.inlineCodeText ?? palette.inlineCodeText,
676-
"font-family": safeCodeFontFamily(),
677+
"font-family": safeInlineCodeFontFamily(),
677678
"font-size": "0.92em",
678679
padding: "1px 4px",
679680
"border-radius": "4px",

src/lib/dc/font-stacks.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ export const codeFallbackFonts = [
9292
"monospace",
9393
] as const;
9494

95+
export const inlineCodeFallbackFonts = [
96+
"Pretendard",
97+
"Cascadia Mono",
98+
"D2Coding",
99+
"나눔고딕코딩",
100+
"Noto Sans Mono CJK",
101+
"JetBrains Mono",
102+
"Fira Code",
103+
"Hack",
104+
"Source Code Pro",
105+
"IBM Plex Mono",
106+
"Roboto Mono",
107+
"Consolas",
108+
"Menlo",
109+
"Monaco",
110+
"monospace",
111+
] as const;
112+
95113
function normalizeFontName(value: string): string {
96114
return value
97115
.trim()
@@ -186,6 +204,10 @@ export function safeCodeFontFamily(value = ""): string {
186204
return buildFontStack(parseFontFamily(value), "code");
187205
}
188206

207+
export function safeInlineCodeFontFamily(): string {
208+
return appendFallbackFonts([], inlineCodeFallbackFonts);
209+
}
210+
189211
export function safeDcProseFontFamily(value = ""): string {
190212
const primaryFonts = parseFontFamily(value);
191213
const selectedFonts = primaryFonts.length > 0 ? primaryFonts : ["Malgun Gothic"];
@@ -197,5 +219,9 @@ export function safeDcCodeFontFamily(): string {
197219
return appendFallbackFonts([], ["Cascadia Mono", "Pretendard", "D2Coding", "monospace"]);
198220
}
199221

222+
export function safeDcInlineCodeFontFamily(): string {
223+
return appendFallbackFonts([], ["Pretendard", "Cascadia Mono", "D2Coding", "monospace"]);
224+
}
225+
200226
export const defaultProseFontFamily = safeProseFontFamily();
201227
export const defaultCodeFontFamily = safeCodeFontFamily();

src/lib/editor/code-block-highlight.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Plugin, PluginKey, type EditorState } from "@tiptap/pm/state";
22
import { Decoration, DecorationSet } from "@tiptap/pm/view";
33
import { isSupportedLanguage, type DcLanguageId } from "$lib/highlighter/catalog";
4+
import { highlightedLineIndexes } from "$lib/highlighter/highlight-lines";
45

56
export type EditorCodeTokenKind = "comment" | "string" | "keyword" | "number" | "function";
67

@@ -10,6 +11,21 @@ export type EditorCodeToken = {
1011
kind: EditorCodeTokenKind;
1112
};
1213

14+
export type EditorCodeLineDecorationKind = "highlight" | "addition" | "deletion";
15+
16+
export type EditorCodeLineDecoration = {
17+
from: number;
18+
to: number;
19+
kind: EditorCodeLineDecorationKind;
20+
empty?: boolean;
21+
};
22+
23+
type CodeLineDecorationAttrs = {
24+
highlightLines?: unknown;
25+
additionLines?: unknown;
26+
deletionLines?: unknown;
27+
};
28+
1329
const keywordSets: Partial<Record<DcLanguageId, readonly string[]>> = {
1430
c: [
1531
"auto",
@@ -355,6 +371,35 @@ const keywordSets: Partial<Record<DcLanguageId, readonly string[]>> = {
355371
json: ["false", "null", "true"],
356372
yaml: ["false", "null", "off", "on", "true", "yes", "no"],
357373
toml: ["false", "true"],
374+
mermaid: [
375+
"activate",
376+
"alt",
377+
"and",
378+
"class",
379+
"classDef",
380+
"click",
381+
"deactivate",
382+
"else",
383+
"end",
384+
"erDiagram",
385+
"flowchart",
386+
"gantt",
387+
"gitGraph",
388+
"graph",
389+
"journey",
390+
"loop",
391+
"mindmap",
392+
"note",
393+
"opt",
394+
"participant",
395+
"pie",
396+
"quadrantChart",
397+
"sequenceDiagram",
398+
"stateDiagram",
399+
"stateDiagram-v2",
400+
"subgraph",
401+
"timeline",
402+
],
358403
sql: [
359404
"add",
360405
"alter",
@@ -541,6 +586,10 @@ function lineCommentPrefixes(language: DcLanguageId): readonly string[] {
541586
return ["--"];
542587
}
543588

589+
if (language === "mermaid") {
590+
return ["%%"];
591+
}
592+
544593
if (cFamilyLanguages.has(language)) {
545594
return ["//"];
546595
}
@@ -680,6 +729,38 @@ function scanRegexTokens(
680729
}
681730
}
682731

732+
function scanMarkdownTokens(
733+
line: string,
734+
lineOffset: number,
735+
protectedRanges: readonly ProtectedRange[],
736+
tokens: EditorCodeToken[],
737+
): void {
738+
scanRegexTokens(
739+
line,
740+
lineOffset,
741+
protectedRanges,
742+
/^\s{0,3}#{1,6}(?=\s|$)|^\s{0,3}(?:[-*+]|\d+\.)\s|^\s{0,3}>+\s?/g,
743+
"keyword",
744+
tokens,
745+
);
746+
scanRegexTokens(
747+
line,
748+
lineOffset,
749+
protectedRanges,
750+
/```+[^`]*|~~~+[^~]*/g,
751+
"keyword",
752+
tokens,
753+
);
754+
scanRegexTokens(
755+
line,
756+
lineOffset,
757+
protectedRanges,
758+
/\[[^\]]+]\([^)]+\)/g,
759+
"string",
760+
tokens,
761+
);
762+
}
763+
683764
function scanFunctionTokens(
684765
line: string,
685766
lineOffset: number,
@@ -722,6 +803,10 @@ export function highlightCodeTokens(code: string, language: unknown): EditorCode
722803
const protectedRanges: ProtectedRange[] = [];
723804
scanProtectedTokens(line, offset, normalizedLanguage, tokens, protectedRanges);
724805

806+
if (normalizedLanguage === "markdown") {
807+
scanMarkdownTokens(line, offset, protectedRanges, tokens);
808+
}
809+
725810
const keywordRegex = keywordRegexFor(normalizedLanguage);
726811
if (keywordRegex) {
727812
scanRegexTokens(line, offset, protectedRanges, keywordRegex, "keyword", tokens);
@@ -745,6 +830,53 @@ export function highlightCodeTokens(code: string, language: unknown): EditorCode
745830
return cloneTokens(sortedTokens);
746831
}
747832

833+
export function codeLineDecorations(
834+
code: string,
835+
attrs: CodeLineDecorationAttrs,
836+
): EditorCodeLineDecoration[] {
837+
const lines = code.split("\n");
838+
const highlightIndexes = highlightedLineIndexes(attrs.highlightLines, lines.length);
839+
const additionIndexes = highlightedLineIndexes(attrs.additionLines, lines.length);
840+
const deletionIndexes = highlightedLineIndexes(attrs.deletionLines, lines.length);
841+
842+
if (highlightIndexes.size === 0 && additionIndexes.size === 0 && deletionIndexes.size === 0) {
843+
return [];
844+
}
845+
846+
const decorations: EditorCodeLineDecoration[] = [];
847+
let offset = 0;
848+
849+
lines.forEach((line, index) => {
850+
const kind: EditorCodeLineDecorationKind | undefined = deletionIndexes.has(index)
851+
? "deletion"
852+
: additionIndexes.has(index)
853+
? "addition"
854+
: highlightIndexes.has(index)
855+
? "highlight"
856+
: undefined;
857+
858+
if (kind) {
859+
const lineEnd = offset + line.length;
860+
decorations.push(
861+
lineEnd > offset
862+
? { from: offset, to: lineEnd, kind }
863+
: { from: offset, to: offset, kind, empty: true },
864+
);
865+
}
866+
867+
offset += line.length + 1;
868+
});
869+
870+
return decorations;
871+
}
872+
873+
function codeLineDecorationElement(kind: EditorCodeLineDecorationKind): HTMLElement {
874+
const marker = document.createElement("span");
875+
marker.className = `dc-code-line dc-code-line-${kind}`;
876+
marker.setAttribute("aria-hidden", "true");
877+
return marker;
878+
}
879+
748880
function codeBlockDecorations(state: EditorState): DecorationSet {
749881
const decorations: Decoration[] = [];
750882

@@ -753,6 +885,16 @@ function codeBlockDecorations(state: EditorState): DecorationSet {
753885
return;
754886
}
755887

888+
for (const lineDecoration of codeLineDecorations(node.textContent, node.attrs)) {
889+
decorations.push(
890+
Decoration.widget(
891+
pos + 1 + lineDecoration.from,
892+
() => codeLineDecorationElement(lineDecoration.kind),
893+
{ side: -1 },
894+
),
895+
);
896+
}
897+
756898
for (const token of highlightCodeTokens(node.textContent, node.attrs.language)) {
757899
decorations.push(
758900
Decoration.inline(pos + 1 + token.from, pos + 1 + token.to, {

src/lib/editor/code-line-range.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
export type CodeLineRange = {
2+
fromLine: number;
3+
toLine: number;
4+
};
5+
6+
function codeLineCount(text: string) {
7+
return Math.max(1, text.split("\n").length);
8+
}
9+
10+
function clampOffset(text: string, offset: number) {
11+
if (!Number.isFinite(offset)) {
12+
return 0;
13+
}
14+
15+
return Math.max(0, Math.min(text.length, Math.trunc(offset)));
16+
}
17+
18+
export function codeLineFromOffset(text: string, offset: number) {
19+
const clampedOffset = clampOffset(text, offset);
20+
let line = 1;
21+
22+
for (let index = 0; index < clampedOffset; index += 1) {
23+
if (text[index] === "\n") {
24+
line += 1;
25+
}
26+
}
27+
28+
return Math.max(1, Math.min(codeLineCount(text), line));
29+
}
30+
31+
export function selectedCodeLineRangeFromOffsets(
32+
text: string,
33+
fromOffset: number,
34+
toOffset: number,
35+
): CodeLineRange | undefined {
36+
const startOffset = clampOffset(text, Math.min(fromOffset, toOffset));
37+
const endOffset = clampOffset(text, Math.max(fromOffset, toOffset));
38+
39+
if (endOffset <= startOffset) {
40+
return undefined;
41+
}
42+
43+
const inclusiveEndOffset = Math.max(startOffset, endOffset - 1);
44+
const fromLine = codeLineFromOffset(text, startOffset);
45+
const toLine = codeLineFromOffset(text, inclusiveEndOffset);
46+
47+
return {
48+
fromLine: Math.min(fromLine, toLine),
49+
toLine: Math.max(fromLine, toLine),
50+
};
51+
}
52+
53+
export function codeLineRangeContains(range: CodeLineRange, line: number) {
54+
return line >= range.fromLine && line <= range.toLine;
55+
}
56+
57+
export function codeLineRangeLabel(range: CodeLineRange) {
58+
return range.fromLine === range.toLine
59+
? `${range.fromLine}번 줄`
60+
: `${range.fromLine}-${range.toLine}번 줄`;
61+
}

src/lib/editor/markdown-import.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ function normalizeLanguage(value: string | undefined, fallback: DcLanguageId): D
8282
mjs: "javascript",
8383
ts: "typescript",
8484
mts: "typescript",
85+
md: "markdown",
86+
markdown: "markdown",
87+
mdx: "markdown",
88+
mmd: "mermaid",
89+
mermaid: "mermaid",
8590
jsonc: "json",
8691
yml: "yaml",
8792
mysql: "sql",

0 commit comments

Comments
 (0)