mdsquare 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +218 -2
- package/dist/callout.d.ts +17 -0
- package/dist/callout.js +541 -0
- package/dist/calloutHeader.d.ts +47 -0
- package/dist/calloutHeader.js +75 -0
- package/dist/codeBlock.d.ts +78 -0
- package/dist/codeBlock.js +686 -0
- package/dist/codeTheme.d.ts +47 -0
- package/dist/codeTheme.js +394 -0
- package/dist/delimiter.d.ts +20 -0
- package/dist/delimiter.js +73 -0
- package/dist/editor.d.ts +132 -0
- package/dist/editor.js +225 -0
- package/dist/emphasis.d.ts +6 -0
- package/dist/emphasis.js +101 -0
- package/dist/escape.d.ts +4 -0
- package/dist/escape.js +39 -0
- package/dist/extensions.d.ts +74 -0
- package/dist/extensions.js +81 -0
- package/dist/footnote.d.ts +24 -0
- package/dist/footnote.js +456 -0
- package/dist/footnoteSyntax.d.ts +23 -0
- package/dist/footnoteSyntax.js +107 -0
- package/dist/hashtag.d.ts +4 -0
- package/dist/hashtag.js +64 -0
- package/dist/hashtagSyntax.d.ts +36 -0
- package/dist/hashtagSyntax.js +99 -0
- package/dist/heading.d.ts +7 -0
- package/dist/heading.js +159 -0
- package/dist/highlight.d.ts +5 -0
- package/dist/highlight.js +6 -0
- package/dist/horizontalRule.d.ts +21 -0
- package/dist/horizontalRule.js +142 -0
- package/dist/image.d.ts +58 -0
- package/dist/image.js +344 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +16 -0
- package/dist/inlineCode.d.ts +4 -0
- package/dist/inlineCode.js +68 -0
- package/dist/link.d.ts +47 -0
- package/dist/link.js +217 -0
- package/dist/list.d.ts +14 -0
- package/dist/list.js +136 -0
- package/dist/listEnter.d.ts +20 -0
- package/dist/listEnter.js +72 -0
- package/dist/listIndent.d.ts +23 -0
- package/dist/listIndent.js +335 -0
- package/dist/markVision.d.ts +17 -0
- package/dist/markVision.js +56 -0
- package/dist/math.d.ts +45 -0
- package/dist/math.js +233 -0
- package/dist/mathSyntax.d.ts +24 -0
- package/dist/mathSyntax.js +179 -0
- package/dist/mention.d.ts +4 -0
- package/dist/mention.js +58 -0
- package/dist/mentionSyntax.d.ts +14 -0
- package/dist/mentionSyntax.js +71 -0
- package/dist/quote.d.ts +9 -0
- package/dist/quote.js +122 -0
- package/dist/setup.d.ts +13 -0
- package/dist/setup.js +40 -0
- package/dist/style.css +6 -0
- package/dist/style.css.d.ts +2 -0
- package/dist/table.d.ts +38 -0
- package/dist/table.js +960 -0
- package/dist/tableEdit.d.ts +30 -0
- package/dist/tableEdit.js +305 -0
- package/dist/tableMenu.d.ts +65 -0
- package/dist/tableMenu.js +1125 -0
- package/dist/tableModel.d.ts +168 -0
- package/dist/tableModel.js +433 -0
- package/dist/tableState.d.ts +36 -0
- package/dist/tableState.js +96 -0
- package/dist/task.d.ts +13 -0
- package/dist/task.js +179 -0
- package/dist/textMate.d.ts +49 -0
- package/dist/textMate.js +284 -0
- package/dist/tikz.d.ts +56 -0
- package/dist/tikz.js +343 -0
- package/dist/underline.d.ts +5 -0
- package/dist/underline.js +6 -0
- package/dist/utils/blockMargin.d.ts +6 -0
- package/dist/utils/blockMargin.js +6 -0
- package/dist/utils/codePoint.d.ts +13 -0
- package/dist/utils/codePoint.js +43 -0
- package/dist/utils/container.d.ts +10 -0
- package/dist/utils/container.js +10 -0
- package/dist/utils/reveal.d.ts +99 -0
- package/dist/utils/reveal.js +161 -0
- package/dist/utils/selection.d.ts +33 -0
- package/dist/utils/selection.js +45 -0
- package/dist/utils/syntaxTree.d.ts +18 -0
- package/dist/utils/syntaxTree.js +23 -0
- package/package.json +79 -4
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ChangeSpec, EditorState, Text, TransactionSpec } from "@codemirror/state";
|
|
2
|
+
import type { SyntaxNode } from "@lezer/common";
|
|
3
|
+
import type { MarkdownConfig } from "@lezer/markdown";
|
|
4
|
+
/**
|
|
5
|
+
* GFM은 표의 첫 줄(TableHeader)을 제목(tags.heading)으로 강조해 밑줄/굵게가 생깁니다.
|
|
6
|
+
* 제목 행 여부는 표 설정(제목 행 켜고 끄기)으로 그리므로 이 강조를 없앱니다. (칸 안의 굵게 등은 그대로)
|
|
7
|
+
*/
|
|
8
|
+
export declare const TableSyntax: MarkdownConfig;
|
|
9
|
+
export type TableAlign = "left" | "center" | "right" | null;
|
|
10
|
+
export interface TableCellInfo {
|
|
11
|
+
/** 구분자 사이 구간 (양옆 공백 포함, 구분자 제외) */
|
|
12
|
+
readonly from: number;
|
|
13
|
+
readonly to: number;
|
|
14
|
+
/** 앞뒤 공백을 뺀 내용. 빈 칸이면 커서를 둘 자리(공백이 있으면 첫 공백 뒤) */
|
|
15
|
+
readonly contentFrom: number;
|
|
16
|
+
readonly contentTo: number;
|
|
17
|
+
/**
|
|
18
|
+
* 화면에 보이는 범위: "|" 바로 옆 공백 한 칸씩(`| a |`의 표준 여백)만 숨기고 나머지 공백은 보입니다.
|
|
19
|
+
* 커서가 숨긴 범위 "안"에 있으면 입력한 글자가 엉뚱한 자리에 들어가므로, 숨긴 범위는 한 칸이라 안쪽 자리가 없습니다.
|
|
20
|
+
* 빈 칸은 숨기지 않으며 contentFrom과 같습니다.
|
|
21
|
+
*/
|
|
22
|
+
readonly textFrom: number;
|
|
23
|
+
readonly textTo: number;
|
|
24
|
+
/**
|
|
25
|
+
* 칸 색 표식: 칸 내용 맨 앞의 `<!--fg:red bg:blue-->` (다른 렌더러에서는 보이지 않는 HTML 주석).
|
|
26
|
+
* 칸 내용과 함께 움직이므로 직접 편집해도 색이 다른 칸으로 가지 않습니다.
|
|
27
|
+
* 표식이 있으면 contentFrom/textFrom은 표식 뒤(공백 건너뜀)부터입니다.
|
|
28
|
+
*/
|
|
29
|
+
readonly marker: CellMarker | null;
|
|
30
|
+
}
|
|
31
|
+
export interface CellMarker {
|
|
32
|
+
readonly from: number;
|
|
33
|
+
readonly to: number;
|
|
34
|
+
readonly fg: TableColorName | null;
|
|
35
|
+
readonly bg: TableColorName | null;
|
|
36
|
+
}
|
|
37
|
+
/** 색 표식 원문. 색이 없으면 null */
|
|
38
|
+
export declare function cellMarkerText(fg: TableColorName | null, bg: TableColorName | null): string | null;
|
|
39
|
+
export interface TableRowInfo {
|
|
40
|
+
/** 0: 첫 줄(머리글 줄), 1부터: 본문 줄 */
|
|
41
|
+
readonly index: number;
|
|
42
|
+
readonly lineFrom: number;
|
|
43
|
+
readonly lineTo: number;
|
|
44
|
+
/** 컨테이너 접두어(`> `, 목록 들여쓰기)를 뺀 행 시작 */
|
|
45
|
+
readonly from: number;
|
|
46
|
+
readonly cells: readonly TableCellInfo[];
|
|
47
|
+
/** 칸을 나누는 "|" (행 앞/뒤의 "|" 포함) */
|
|
48
|
+
readonly pipes: readonly {
|
|
49
|
+
readonly from: number;
|
|
50
|
+
readonly to: number;
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
53
|
+
export interface TableOptions {
|
|
54
|
+
/** 첫 줄을 제목 행으로 보여줄지 (기본 true: GFM의 머리글 줄) */
|
|
55
|
+
readonly headerRow: boolean;
|
|
56
|
+
/** 첫 칸을 제목 열로 보여줄지 (기본 false) */
|
|
57
|
+
readonly headerColumn: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare const DEFAULT_TABLE_OPTIONS: TableOptions;
|
|
60
|
+
/** 행/열 색 (Notion과 같은 9가지) */
|
|
61
|
+
export declare const TABLE_COLORS: readonly ["gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"];
|
|
62
|
+
export type TableColorName = (typeof TABLE_COLORS)[number];
|
|
63
|
+
/** 글자 색(`red`) 또는 배경 색(`red-bg`) */
|
|
64
|
+
export type TableColor = TableColorName | `${TableColorName}-bg`;
|
|
65
|
+
export type TableAxis = "row" | "column";
|
|
66
|
+
/** 글자 색과 배경 색 (따로 정함) */
|
|
67
|
+
export interface TableColorPair {
|
|
68
|
+
readonly fg: TableColorName | null;
|
|
69
|
+
readonly bg: TableColorName | null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 행/열 번호(0부터) → 색. 설정 주석의 `row-2:red row-2:yellow-bg col-1:blue` (1부터 셈: row-1이 첫 행).
|
|
73
|
+
* 글자 색(`red`)과 배경 색(`red-bg`)은 따로 있어, 한 행/열에 둘 다 지정할 수 있습니다. (칸 색과 같음)
|
|
74
|
+
*/
|
|
75
|
+
export interface TableColors {
|
|
76
|
+
readonly rows: ReadonlyMap<number, TableColorPair>;
|
|
77
|
+
readonly columns: ReadonlyMap<number, TableColorPair>;
|
|
78
|
+
}
|
|
79
|
+
/** 표 바로 윗줄의 설정 주석 `<!-- table: header-column -->` */
|
|
80
|
+
export interface TableComment {
|
|
81
|
+
readonly lineFrom: number;
|
|
82
|
+
readonly lineTo: number;
|
|
83
|
+
/** 알 수 없는 단어도 보존해 다시 쓸 때 잃지 않습니다. */
|
|
84
|
+
readonly words: readonly string[];
|
|
85
|
+
}
|
|
86
|
+
/** 문서 전체에서 표마다 기억하는 정보 (행 내용은 필요할 때 readTableRows로 읽음) */
|
|
87
|
+
export interface TableBlock {
|
|
88
|
+
/** Table 노드 범위 */
|
|
89
|
+
readonly from: number;
|
|
90
|
+
readonly to: number;
|
|
91
|
+
/** 표의 첫 줄 시작 ~ 마지막 줄 끝 (컨테이너 접두어 포함) */
|
|
92
|
+
readonly lineFrom: number;
|
|
93
|
+
readonly lineTo: number;
|
|
94
|
+
/** 구분 줄(`|---|`) 전체 범위 */
|
|
95
|
+
readonly delimiterLine: {
|
|
96
|
+
readonly from: number;
|
|
97
|
+
readonly to: number;
|
|
98
|
+
};
|
|
99
|
+
/** 열 수 (머리글 줄의 칸 수) */
|
|
100
|
+
readonly columns: number;
|
|
101
|
+
readonly aligns: readonly TableAlign[];
|
|
102
|
+
readonly comment: TableComment | null;
|
|
103
|
+
readonly options: TableOptions;
|
|
104
|
+
readonly colors: TableColors;
|
|
105
|
+
}
|
|
106
|
+
/** TableHeader/TableRow 노드 → 칸 목록 */
|
|
107
|
+
export declare function readTableRow(doc: Text, node: SyntaxNode, index: number): TableRowInfo;
|
|
108
|
+
/** 구분 줄(`|---|:-:|`)을 행처럼 칸으로 나눕니다. (index -1) */
|
|
109
|
+
export declare function readDelimiterRow(doc: Text, table: TableBlock): TableRowInfo;
|
|
110
|
+
/** Table 노드 → 표 정보 (행 내용은 읽지 않음: 머리글 줄과 구분 줄만) */
|
|
111
|
+
export declare function readTableBlock(state: EditorState, node: SyntaxNode): TableBlock | null;
|
|
112
|
+
/** 표가 들어갈 수 있는 컨테이너 (그 외 블록은 내려가지 않음) */
|
|
113
|
+
export declare const TABLE_CONTAINERS: ReadonlySet<string>;
|
|
114
|
+
/** 문서 전체의 표 (컨테이너만 훑음) */
|
|
115
|
+
export declare function scanTables(state: EditorState): TableBlock[];
|
|
116
|
+
/** 표의 행(머리글 줄 + 본문 줄). 구분 줄은 빠집니다. */
|
|
117
|
+
export declare function readTableRows(state: EditorState, tableNode: SyntaxNode): TableRowInfo[];
|
|
118
|
+
/** pos가 들어 있는 Table 노드 */
|
|
119
|
+
export declare function tableNodeAt(state: EditorState, pos: number): SyntaxNode | null;
|
|
120
|
+
/**
|
|
121
|
+
* 표 줄의 컨테이너 접두어로 새 줄의 접두어를 만듭니다. `>`와 공백은 그대로,
|
|
122
|
+
* 목록 기호 등 나머지는 같은 폭의 공백으로 (`- | a |`의 다음 줄은 ` `)
|
|
123
|
+
*/
|
|
124
|
+
export declare function continuationPrefix(prefix: string): string;
|
|
125
|
+
/** 빈 행 원문 `| | |` */
|
|
126
|
+
export declare function emptyRow(columns: number): string;
|
|
127
|
+
/** 설정 주석 원문. 기본값과 같으면 null (주석 없음) */
|
|
128
|
+
export declare function commentText(words: readonly string[]): string | null;
|
|
129
|
+
/** 표 설정 하나를 바꾸는 트랜잭션 (설정 주석을 추가/수정/삭제) */
|
|
130
|
+
export declare function setTableOption(state: EditorState, table: TableBlock, option: keyof TableOptions, value: boolean): TransactionSpec | null;
|
|
131
|
+
/** 설정 주석을 words로 바꾸는 변경 (주석 추가/수정/삭제). 바뀌는 것이 없으면 null */
|
|
132
|
+
export declare function commentChange(state: EditorState, table: TableBlock, words: readonly string[]): ChangeSpec | null;
|
|
133
|
+
/** 행/열 하나의 글자 색(fg) 또는 배경 색(bg)을 바꾸는 트랜잭션 (null: 그 색을 없앰, 다른 쪽 색은 그대로) */
|
|
134
|
+
export declare function setTableColor(state: EditorState, table: TableBlock, axis: TableAxis, index: number, kind: "fg" | "bg", color: TableColorName | null): TransactionSpec | null;
|
|
135
|
+
/**
|
|
136
|
+
* 행/열이 추가·삭제·이동될 때 색 번호를 옮긴 설정 주석 단어들. 바뀌는 것이 없으면 null.
|
|
137
|
+
* map(옛 번호) → 새 번호들 (빈 배열: 지워짐, 둘: 복제됨)
|
|
138
|
+
*/
|
|
139
|
+
export declare function remapColorWords(table: TableBlock, axis: TableAxis, map: (index: number) => readonly number[]): string[] | null;
|
|
140
|
+
/** 구분 줄의 칸별 내용 범위 (`:---:` 부분, 앞뒤 공백 제외) */
|
|
141
|
+
export declare function delimiterCells(state: EditorState, table: TableBlock): {
|
|
142
|
+
from: number;
|
|
143
|
+
to: number;
|
|
144
|
+
}[];
|
|
145
|
+
/** 열 정렬을 바꾸는 트랜잭션 (구분 줄의 그 칸만 바꿈, 길이 유지). 왼쪽은 기본값(`---`)으로 씁니다. */
|
|
146
|
+
export declare function setColumnAlign(state: EditorState, table: TableBlock, column: number, align: TableAlign): TransactionSpec | null;
|
|
147
|
+
/** pos가 있는 줄의 표 행 (머리글 줄/본문 줄). 구분 줄이나 표 밖이면 null. 행 하나만 읽습니다. */
|
|
148
|
+
export declare function tableRowAt(state: EditorState, pos: number): {
|
|
149
|
+
row: TableRowInfo;
|
|
150
|
+
table: SyntaxNode;
|
|
151
|
+
} | null;
|
|
152
|
+
/** 머리글보다 많은 칸(GFM에서는 무시)의 시작: 그 앞 "|" 위치. 없으면 줄 끝 */
|
|
153
|
+
export declare function extraCellsFrom(row: TableRowInfo, columns: number): number;
|
|
154
|
+
/**
|
|
155
|
+
* 행에서 빈 커서가 놓일 수 있는 범위와 행의 처음/끝 커서 자리.
|
|
156
|
+
* - from ~ to: 첫 칸 시작(앞 "|" 뒤) ~ 마지막 칸 끝(뒤 "|" 앞, 넘치는 칸이 있으면 줄 끝).
|
|
157
|
+
* "|" 사이 어디에 입력해도 표 구조는 그대로지만, 앞 "|" 앞(접두어 포함)이나 뒤 "|" 뒤에 입력하면 칸이 새로 생겨
|
|
158
|
+
* 열이 밀립니다. (화면에서는 그 자리가 보이지 않아 사용자가 알아챌 수 없음)
|
|
159
|
+
* - start / end: 행의 처음/끝으로 갈 때의 자리 (첫 칸 내용 처음, 마지막 칸 내용 끝 또는 넘치는 칸의 줄 끝)
|
|
160
|
+
*/
|
|
161
|
+
export declare function rowCursorRange(row: TableRowInfo, columns: number): {
|
|
162
|
+
from: number;
|
|
163
|
+
to: number;
|
|
164
|
+
start: number;
|
|
165
|
+
end: number;
|
|
166
|
+
} | null;
|
|
167
|
+
/** pos를 행의 커서 범위 안으로 옮깁니다. 앞으로 벗어나면 행의 처음, 뒤로 벗어나면 행의 끝 */
|
|
168
|
+
export declare function clampToRow(row: TableRowInfo, columns: number, pos: number): number;
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { syntaxTree } from "@codemirror/language";
|
|
2
|
+
import { styleTags } from "@lezer/highlight";
|
|
3
|
+
/**
|
|
4
|
+
* GFM은 표의 첫 줄(TableHeader)을 제목(tags.heading)으로 강조해 밑줄/굵게가 생깁니다.
|
|
5
|
+
* 제목 행 여부는 표 설정(제목 행 켜고 끄기)으로 그리므로 이 강조를 없앱니다. (칸 안의 굵게 등은 그대로)
|
|
6
|
+
*/
|
|
7
|
+
export const TableSyntax = {
|
|
8
|
+
props: [styleTags({ "TableHeader/...": [] })],
|
|
9
|
+
};
|
|
10
|
+
const MARKER_RE = /^<!--\s*((?:(?:fg|bg):[a-z]+\s*)+)-->/;
|
|
11
|
+
/** 칸 내용 앞의 색 표식 해석. 모르는 색이 있으면 표식이 아닙니다. */
|
|
12
|
+
function readMarker(text) {
|
|
13
|
+
const m = MARKER_RE.exec(text);
|
|
14
|
+
if (!m)
|
|
15
|
+
return null;
|
|
16
|
+
let fg = null;
|
|
17
|
+
let bg = null;
|
|
18
|
+
for (const token of m[1].trim().split(/\s+/)) {
|
|
19
|
+
const [kind, color] = token.split(":");
|
|
20
|
+
if (!TABLE_COLORS.includes(color))
|
|
21
|
+
return null;
|
|
22
|
+
if (kind === "fg")
|
|
23
|
+
fg = color;
|
|
24
|
+
else
|
|
25
|
+
bg = color;
|
|
26
|
+
}
|
|
27
|
+
return { length: m[0].length, fg, bg };
|
|
28
|
+
}
|
|
29
|
+
/** 색 표식 원문. 색이 없으면 null */
|
|
30
|
+
export function cellMarkerText(fg, bg) {
|
|
31
|
+
const tokens = [fg && `fg:${fg}`, bg && `bg:${bg}`].filter(Boolean);
|
|
32
|
+
return tokens.length > 0 ? `<!--${tokens.join(" ")}-->` : null;
|
|
33
|
+
}
|
|
34
|
+
export const DEFAULT_TABLE_OPTIONS = { headerRow: true, headerColumn: false };
|
|
35
|
+
/** 행/열 색 (Notion과 같은 9가지) */
|
|
36
|
+
export const TABLE_COLORS = ["gray", "brown", "orange", "yellow", "green", "blue", "purple", "pink", "red"];
|
|
37
|
+
const COLOR_WORD = /^(row|col)-(\d+):([a-z]+)(-bg)?$/;
|
|
38
|
+
const EMPTY_COLORS = { rows: new Map(), columns: new Map() };
|
|
39
|
+
function parseColorWord(word) {
|
|
40
|
+
const m = COLOR_WORD.exec(word);
|
|
41
|
+
if (!m || !TABLE_COLORS.includes(m[3]) || Number(m[2]) < 1)
|
|
42
|
+
return null;
|
|
43
|
+
return { axis: m[1] === "row" ? "row" : "column", index: Number(m[2]) - 1, color: `${m[3]}${m[4] ?? ""}` };
|
|
44
|
+
}
|
|
45
|
+
const colorWord = (axis, index, color) => `${axis === "row" ? "row" : "col"}-${index + 1}:${color}`;
|
|
46
|
+
/** `red-bg` → 배경 색인지와 색 이름 */
|
|
47
|
+
const splitColor = (color) => color.endsWith("-bg") ? { bg: true, name: color.slice(0, -3) } : { bg: false, name: color };
|
|
48
|
+
function colorsOf(comment) {
|
|
49
|
+
if (!comment)
|
|
50
|
+
return EMPTY_COLORS;
|
|
51
|
+
const rows = new Map();
|
|
52
|
+
const columns = new Map();
|
|
53
|
+
for (const word of comment.words) {
|
|
54
|
+
const parsed = parseColorWord(word);
|
|
55
|
+
if (!parsed)
|
|
56
|
+
continue;
|
|
57
|
+
const map = parsed.axis === "row" ? rows : columns;
|
|
58
|
+
const { bg, name } = splitColor(parsed.color);
|
|
59
|
+
const pair = map.get(parsed.index) ?? { fg: null, bg: null };
|
|
60
|
+
map.set(parsed.index, bg ? { ...pair, bg: name } : { ...pair, fg: name });
|
|
61
|
+
}
|
|
62
|
+
return rows.size || columns.size ? { rows, columns } : EMPTY_COLORS;
|
|
63
|
+
}
|
|
64
|
+
const OPTION_WORDS = { headerColumn: "header-column", noHeaderRow: "no-header-row" };
|
|
65
|
+
const COMMENT_RE = /^[\s>]*<!--\s*table:([^]*?)-->\s*$/;
|
|
66
|
+
const isSpace = (code) => code === 32 || code === 9;
|
|
67
|
+
function trimRange(doc, from, to) {
|
|
68
|
+
const text = doc.sliceString(from, to);
|
|
69
|
+
let start = 0;
|
|
70
|
+
let end = text.length;
|
|
71
|
+
while (start < end && isSpace(text.charCodeAt(start)))
|
|
72
|
+
start++;
|
|
73
|
+
while (end > start && isSpace(text.charCodeAt(end - 1)))
|
|
74
|
+
end--;
|
|
75
|
+
return [from + start, from + end];
|
|
76
|
+
}
|
|
77
|
+
/** TableHeader/TableRow 노드 → 칸 목록 */
|
|
78
|
+
export function readTableRow(doc, node, index) {
|
|
79
|
+
const pipes = [];
|
|
80
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
81
|
+
if (child.name === "TableDelimiter")
|
|
82
|
+
pipes.push({ from: child.from, to: child.to });
|
|
83
|
+
}
|
|
84
|
+
// 행 끝은 줄 끝 (머리글 줄은 노드에 뒤 공백이 들어가고 본문 줄은 빠지므로 줄 기준으로 통일)
|
|
85
|
+
return splitRow(doc, node.from, pipes, index);
|
|
86
|
+
}
|
|
87
|
+
/** 행 시작(접두어 뒤)과 "|" 위치들로 칸을 나눕니다. */
|
|
88
|
+
function splitRow(doc, start, pipes, index) {
|
|
89
|
+
const line = doc.lineAt(start);
|
|
90
|
+
const end = line.to;
|
|
91
|
+
const bounds = [];
|
|
92
|
+
let prev = start;
|
|
93
|
+
for (const pipe of pipes) {
|
|
94
|
+
bounds.push([prev, pipe.from]);
|
|
95
|
+
prev = pipe.to;
|
|
96
|
+
}
|
|
97
|
+
bounds.push([prev, end]);
|
|
98
|
+
const blank = ([from, to]) => doc.sliceString(from, to).trim() === "";
|
|
99
|
+
// 앞/뒤 "|" 바깥의 빈 구간은 칸이 아닙니다.
|
|
100
|
+
if (pipes.length > 0 && blank(bounds[0]))
|
|
101
|
+
bounds.shift();
|
|
102
|
+
if (pipes.length > 0 && bounds.length > 0 && blank(bounds[bounds.length - 1]) && bounds[bounds.length - 1][0] === pipes[pipes.length - 1].to) {
|
|
103
|
+
bounds.pop();
|
|
104
|
+
}
|
|
105
|
+
const cells = bounds.map(([from, to]) => {
|
|
106
|
+
const [trimFrom, contentTo] = trimRange(doc, from, to);
|
|
107
|
+
const found = trimFrom < contentTo ? readMarker(doc.sliceString(trimFrom, contentTo)) : null;
|
|
108
|
+
const marker = found && { from: trimFrom, to: trimFrom + found.length, fg: found.fg, bg: found.bg };
|
|
109
|
+
if (marker) {
|
|
110
|
+
// 표식 뒤 공백을 건너뛴 곳부터 내용입니다.
|
|
111
|
+
const [contentFrom] = trimRange(doc, marker.to, contentTo);
|
|
112
|
+
if (contentFrom < contentTo) {
|
|
113
|
+
const textTo = contentTo < to ? to - 1 : to;
|
|
114
|
+
return { from, to, contentFrom, contentTo, textFrom: contentFrom, textTo, marker };
|
|
115
|
+
}
|
|
116
|
+
// 표식만 있는 빈 칸: 표식 바로 뒤가 커서 자리 (입력해도 표식이 칸 맨 앞에 남음)
|
|
117
|
+
return { from, to, contentFrom: marker.to, contentTo: marker.to, textFrom: marker.to, textTo: marker.to, marker };
|
|
118
|
+
}
|
|
119
|
+
if (trimFrom < contentTo) {
|
|
120
|
+
const textFrom = from < trimFrom ? from + 1 : from;
|
|
121
|
+
const textTo = contentTo < to ? to - 1 : to;
|
|
122
|
+
return { from, to, contentFrom: trimFrom, contentTo, textFrom, textTo, marker: null };
|
|
123
|
+
}
|
|
124
|
+
// 빈 칸: 공백이 있으면 첫 공백 뒤 ("| |"에 입력하면 "| x |")
|
|
125
|
+
const caret = Math.min(to, from + 1);
|
|
126
|
+
return { from, to, contentFrom: caret, contentTo: caret, textFrom: caret, textTo: caret, marker: null };
|
|
127
|
+
});
|
|
128
|
+
return { index, lineFrom: line.from, lineTo: line.to, from: start, cells, pipes };
|
|
129
|
+
}
|
|
130
|
+
/** 구분 줄(`|---|:-:|`)을 행처럼 칸으로 나눕니다. (index -1) */
|
|
131
|
+
export function readDelimiterRow(doc, table) {
|
|
132
|
+
const { from: lineFrom, to: lineTo } = table.delimiterLine;
|
|
133
|
+
const text = doc.sliceString(lineFrom, lineTo);
|
|
134
|
+
const start = lineFrom + /^[\s>]*/.exec(text)[0].length;
|
|
135
|
+
const pipes = [];
|
|
136
|
+
for (let i = start - lineFrom; i < text.length; i++)
|
|
137
|
+
if (text[i] === "|")
|
|
138
|
+
pipes.push({ from: lineFrom + i, to: lineFrom + i + 1 });
|
|
139
|
+
return splitRow(doc, start, pipes, -1);
|
|
140
|
+
}
|
|
141
|
+
function readAligns(text) {
|
|
142
|
+
let cells = text.trim();
|
|
143
|
+
if (cells.startsWith("|"))
|
|
144
|
+
cells = cells.slice(1);
|
|
145
|
+
if (cells.endsWith("|"))
|
|
146
|
+
cells = cells.slice(0, -1);
|
|
147
|
+
return cells.split("|").map((cell) => {
|
|
148
|
+
const c = cell.trim();
|
|
149
|
+
const left = c.startsWith(":");
|
|
150
|
+
const right = c.endsWith(":") && c.length > 1;
|
|
151
|
+
return left && right ? "center" : right ? "right" : left ? "left" : null;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function readComment(state, tableLine) {
|
|
155
|
+
if (tableLine <= 1)
|
|
156
|
+
return null;
|
|
157
|
+
const line = state.doc.line(tableLine - 1);
|
|
158
|
+
const match = COMMENT_RE.exec(line.text);
|
|
159
|
+
if (!match)
|
|
160
|
+
return null;
|
|
161
|
+
// 주석이 실제 HTML 주석 블록인지 확인합니다. (코드 블록 안의 글자 등 제외)
|
|
162
|
+
// resolveInner는 lang-markdown이 HTML 블록 안에 다시 파싱한 HTML 트리로 들어가므로 resolve를 씁니다.
|
|
163
|
+
const start = line.from + line.text.indexOf("<!--");
|
|
164
|
+
if (syntaxTree(state).resolve(start, 1).name !== "CommentBlock")
|
|
165
|
+
return null;
|
|
166
|
+
const words = match[1].split(/[\s,]+/).filter(Boolean);
|
|
167
|
+
return { lineFrom: line.from, lineTo: line.to, words };
|
|
168
|
+
}
|
|
169
|
+
function optionsOf(comment) {
|
|
170
|
+
if (!comment)
|
|
171
|
+
return DEFAULT_TABLE_OPTIONS;
|
|
172
|
+
return {
|
|
173
|
+
headerRow: !comment.words.includes(OPTION_WORDS.noHeaderRow),
|
|
174
|
+
headerColumn: comment.words.includes(OPTION_WORDS.headerColumn),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** Table 노드 → 표 정보 (행 내용은 읽지 않음: 머리글 줄과 구분 줄만) */
|
|
178
|
+
export function readTableBlock(state, node) {
|
|
179
|
+
const { doc } = state;
|
|
180
|
+
const header = node.getChild("TableHeader");
|
|
181
|
+
let delimiter = null;
|
|
182
|
+
for (let child = node.firstChild; child; child = child.nextSibling) {
|
|
183
|
+
if (child.name === "TableDelimiter") {
|
|
184
|
+
delimiter = child;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!header || !delimiter)
|
|
189
|
+
return null;
|
|
190
|
+
const first = doc.lineAt(node.from);
|
|
191
|
+
const delimiterLine = doc.lineAt(delimiter.from);
|
|
192
|
+
const comment = readComment(state, first.number);
|
|
193
|
+
return {
|
|
194
|
+
from: node.from,
|
|
195
|
+
to: node.to,
|
|
196
|
+
lineFrom: first.from,
|
|
197
|
+
lineTo: doc.lineAt(node.to).to,
|
|
198
|
+
delimiterLine: { from: delimiterLine.from, to: delimiterLine.to },
|
|
199
|
+
columns: readTableRow(doc, header, 0).cells.length,
|
|
200
|
+
aligns: readAligns(doc.sliceString(delimiter.from, delimiter.to)),
|
|
201
|
+
comment,
|
|
202
|
+
options: optionsOf(comment),
|
|
203
|
+
colors: colorsOf(comment),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** 표가 들어갈 수 있는 컨테이너 (그 외 블록은 내려가지 않음) */
|
|
207
|
+
export const TABLE_CONTAINERS = new Set([
|
|
208
|
+
"Document",
|
|
209
|
+
"Blockquote",
|
|
210
|
+
"BulletList",
|
|
211
|
+
"OrderedList",
|
|
212
|
+
"ListItem",
|
|
213
|
+
]);
|
|
214
|
+
/** 문서 전체의 표 (컨테이너만 훑음) */
|
|
215
|
+
export function scanTables(state) {
|
|
216
|
+
const tables = [];
|
|
217
|
+
syntaxTree(state).iterate({
|
|
218
|
+
enter(node) {
|
|
219
|
+
if (node.name === "Table") {
|
|
220
|
+
const table = readTableBlock(state, node.node);
|
|
221
|
+
if (table)
|
|
222
|
+
tables.push(table);
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (!TABLE_CONTAINERS.has(node.name))
|
|
226
|
+
return false;
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
return tables;
|
|
230
|
+
}
|
|
231
|
+
/** 표의 행(머리글 줄 + 본문 줄). 구분 줄은 빠집니다. */
|
|
232
|
+
export function readTableRows(state, tableNode) {
|
|
233
|
+
const rows = [];
|
|
234
|
+
for (let child = tableNode.firstChild; child; child = child.nextSibling) {
|
|
235
|
+
if (child.name === "TableHeader" || child.name === "TableRow")
|
|
236
|
+
rows.push(readTableRow(state.doc, child, rows.length));
|
|
237
|
+
}
|
|
238
|
+
return rows;
|
|
239
|
+
}
|
|
240
|
+
/** pos가 들어 있는 Table 노드 */
|
|
241
|
+
export function tableNodeAt(state, pos) {
|
|
242
|
+
for (const side of [1, -1]) {
|
|
243
|
+
for (let node = syntaxTree(state).resolveInner(pos, side); node; node = node.parent) {
|
|
244
|
+
if (node.name === "Table")
|
|
245
|
+
return node;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// 원문 만들기
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
/**
|
|
254
|
+
* 표 줄의 컨테이너 접두어로 새 줄의 접두어를 만듭니다. `>`와 공백은 그대로,
|
|
255
|
+
* 목록 기호 등 나머지는 같은 폭의 공백으로 (`- | a |`의 다음 줄은 ` `)
|
|
256
|
+
*/
|
|
257
|
+
export function continuationPrefix(prefix) {
|
|
258
|
+
return prefix.replace(/[^\s>]/g, " ");
|
|
259
|
+
}
|
|
260
|
+
/** 빈 행 원문 `| | |` */
|
|
261
|
+
export function emptyRow(columns) {
|
|
262
|
+
return "|" + " |".repeat(Math.max(1, columns));
|
|
263
|
+
}
|
|
264
|
+
/** 설정 주석 원문. 기본값과 같으면 null (주석 없음) */
|
|
265
|
+
export function commentText(words) {
|
|
266
|
+
return words.length > 0 ? `<!-- table: ${words.join(" ")} -->` : null;
|
|
267
|
+
}
|
|
268
|
+
/** 표 설정 하나를 바꾸는 트랜잭션 (설정 주석을 추가/수정/삭제) */
|
|
269
|
+
export function setTableOption(state, table, option, value) {
|
|
270
|
+
if (table.options[option] === value)
|
|
271
|
+
return null;
|
|
272
|
+
const word = option === "headerColumn" ? OPTION_WORDS.headerColumn : OPTION_WORDS.noHeaderRow;
|
|
273
|
+
const add = option === "headerColumn" ? value : !value;
|
|
274
|
+
const old = table.comment?.words ?? [];
|
|
275
|
+
const change = commentChange(state, table, add ? [...old, word] : old.filter((w) => w !== word));
|
|
276
|
+
return change ? { changes: change } : null;
|
|
277
|
+
}
|
|
278
|
+
/** 설정 주석을 words로 바꾸는 변경 (주석 추가/수정/삭제). 바뀌는 것이 없으면 null */
|
|
279
|
+
export function commentChange(state, table, words) {
|
|
280
|
+
const old = table.comment?.words ?? [];
|
|
281
|
+
if (old.length === words.length && old.every((w, i) => w === words[i]))
|
|
282
|
+
return null;
|
|
283
|
+
const text = commentText(words);
|
|
284
|
+
const { doc } = state;
|
|
285
|
+
if (table.comment) {
|
|
286
|
+
const { lineFrom, lineTo } = table.comment;
|
|
287
|
+
// 비면 주석 줄을 줄바꿈까지 지웁니다.
|
|
288
|
+
if (text === null)
|
|
289
|
+
return { from: lineFrom, to: Math.min(doc.length, lineTo + 1) };
|
|
290
|
+
const start = lineFrom + doc.sliceString(lineFrom, lineTo).indexOf("<!--");
|
|
291
|
+
return { from: start, to: lineTo, insert: text };
|
|
292
|
+
}
|
|
293
|
+
if (text === null)
|
|
294
|
+
return null;
|
|
295
|
+
// 표 첫 줄 앞에, 같은 컨테이너 접두어를 붙여 넣습니다.
|
|
296
|
+
const prefix = continuationPrefix(doc.sliceString(table.lineFrom, table.from));
|
|
297
|
+
return { from: table.lineFrom, insert: `${prefix}${text}\n` };
|
|
298
|
+
}
|
|
299
|
+
/** 행/열 하나의 글자 색(fg) 또는 배경 색(bg)을 바꾸는 트랜잭션 (null: 그 색을 없앰, 다른 쪽 색은 그대로) */
|
|
300
|
+
export function setTableColor(state, table, axis, index, kind, color) {
|
|
301
|
+
const current = (axis === "row" ? table.colors.rows : table.colors.columns).get(index)?.[kind] ?? null;
|
|
302
|
+
if (current === color)
|
|
303
|
+
return null;
|
|
304
|
+
const words = (table.comment?.words ?? []).filter((w) => {
|
|
305
|
+
const parsed = parseColorWord(w);
|
|
306
|
+
return !parsed || parsed.axis !== axis || parsed.index !== index || splitColor(parsed.color).bg !== (kind === "bg");
|
|
307
|
+
});
|
|
308
|
+
if (color)
|
|
309
|
+
words.push(colorWord(axis, index, kind === "bg" ? `${color}-bg` : color));
|
|
310
|
+
const change = commentChange(state, table, words);
|
|
311
|
+
return change ? { changes: change } : null;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 행/열이 추가·삭제·이동될 때 색 번호를 옮긴 설정 주석 단어들. 바뀌는 것이 없으면 null.
|
|
315
|
+
* map(옛 번호) → 새 번호들 (빈 배열: 지워짐, 둘: 복제됨)
|
|
316
|
+
*/
|
|
317
|
+
export function remapColorWords(table, axis, map) {
|
|
318
|
+
const words = table.comment?.words;
|
|
319
|
+
if (!words)
|
|
320
|
+
return null;
|
|
321
|
+
let changed = false;
|
|
322
|
+
const out = [];
|
|
323
|
+
for (const word of words) {
|
|
324
|
+
const parsed = parseColorWord(word);
|
|
325
|
+
if (!parsed || parsed.axis !== axis) {
|
|
326
|
+
out.push(word);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const targets = map(parsed.index);
|
|
330
|
+
if (targets.length !== 1 || targets[0] !== parsed.index)
|
|
331
|
+
changed = true;
|
|
332
|
+
for (const index of targets)
|
|
333
|
+
out.push(colorWord(axis, index, parsed.color));
|
|
334
|
+
}
|
|
335
|
+
return changed ? out : null;
|
|
336
|
+
}
|
|
337
|
+
/** 구분 줄의 칸별 내용 범위 (`:---:` 부분, 앞뒤 공백 제외) */
|
|
338
|
+
export function delimiterCells(state, table) {
|
|
339
|
+
const { from: lineFrom, to: lineTo } = table.delimiterLine;
|
|
340
|
+
const text = state.doc.sliceString(lineFrom, lineTo);
|
|
341
|
+
const start = /^[\s>]*/.exec(text)[0].length; // 컨테이너 접두어(`> `, 들여쓰기)
|
|
342
|
+
const cells = [];
|
|
343
|
+
let segment = start;
|
|
344
|
+
for (let i = start; i <= text.length; i++) {
|
|
345
|
+
if (i < text.length && text[i] !== "|")
|
|
346
|
+
continue;
|
|
347
|
+
const raw = text.slice(segment, i);
|
|
348
|
+
const lead = raw.length - raw.trimStart().length;
|
|
349
|
+
const content = raw.trim();
|
|
350
|
+
cells.push({ from: lineFrom + segment + lead, to: lineFrom + segment + lead + content.length });
|
|
351
|
+
segment = i + 1;
|
|
352
|
+
}
|
|
353
|
+
// 앞/뒤 "|" 바깥의 빈 조각은 칸이 아닙니다.
|
|
354
|
+
if (cells.length > 0 && cells[0].from === cells[0].to)
|
|
355
|
+
cells.shift();
|
|
356
|
+
if (cells.length > 0 && cells[cells.length - 1].from === cells[cells.length - 1].to)
|
|
357
|
+
cells.pop();
|
|
358
|
+
return cells;
|
|
359
|
+
}
|
|
360
|
+
/** 열 정렬을 바꾸는 트랜잭션 (구분 줄의 그 칸만 바꿈, 길이 유지). 왼쪽은 기본값(`---`)으로 씁니다. */
|
|
361
|
+
export function setColumnAlign(state, table, column, align) {
|
|
362
|
+
const target = align === "left" ? null : align;
|
|
363
|
+
const current = table.aligns[column] ?? null;
|
|
364
|
+
if ((current === "left" ? null : current) === target)
|
|
365
|
+
return null;
|
|
366
|
+
const cell = delimiterCells(state, table)[column];
|
|
367
|
+
if (!cell)
|
|
368
|
+
return null;
|
|
369
|
+
// 원래 칸 길이를 유지해 원문의 열 맞춤을 흐트러뜨리지 않습니다. (최소 3: `:-:`)
|
|
370
|
+
const width = Math.max(3, cell.to - cell.from);
|
|
371
|
+
const insert = target === "center" ? `:${"-".repeat(width - 2)}:` : target === "right" ? `${"-".repeat(width - 1)}:` : "-".repeat(width);
|
|
372
|
+
return { changes: { from: cell.from, to: cell.to, insert } };
|
|
373
|
+
}
|
|
374
|
+
/** pos가 있는 줄의 표 행 (머리글 줄/본문 줄). 구분 줄이나 표 밖이면 null. 행 하나만 읽습니다. */
|
|
375
|
+
export function tableRowAt(state, pos) {
|
|
376
|
+
const { doc } = state;
|
|
377
|
+
const line = doc.lineAt(pos);
|
|
378
|
+
const isRow = (node) => node.name === "TableHeader" || node.name === "TableRow";
|
|
379
|
+
// 줄 끝에서 위로 올라가면 그 줄의 행이나(행 노드가 줄 끝 공백을 포함하지 않으면) Table을 만납니다.
|
|
380
|
+
for (let node = syntaxTree(state).resolveInner(line.to, -1); node; node = node.parent) {
|
|
381
|
+
let row = null;
|
|
382
|
+
if (isRow(node))
|
|
383
|
+
row = node;
|
|
384
|
+
else if (node.name === "Table") {
|
|
385
|
+
const before = node.childBefore(line.to);
|
|
386
|
+
if (before && isRow(before))
|
|
387
|
+
row = before;
|
|
388
|
+
}
|
|
389
|
+
if (row) {
|
|
390
|
+
if (doc.lineAt(row.from).number !== line.number)
|
|
391
|
+
return null;
|
|
392
|
+
return { row: readTableRow(doc, row, row.name === "TableHeader" ? 0 : 1), table: row.parent };
|
|
393
|
+
}
|
|
394
|
+
if (node.name === "Table")
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
/** 머리글보다 많은 칸(GFM에서는 무시)의 시작: 그 앞 "|" 위치. 없으면 줄 끝 */
|
|
400
|
+
export function extraCellsFrom(row, columns) {
|
|
401
|
+
const first = row.cells[columns];
|
|
402
|
+
if (!first)
|
|
403
|
+
return row.lineTo;
|
|
404
|
+
const pipe = row.pipes.filter((p) => p.to <= first.from).pop();
|
|
405
|
+
return pipe ? pipe.from : first.from;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* 행에서 빈 커서가 놓일 수 있는 범위와 행의 처음/끝 커서 자리.
|
|
409
|
+
* - from ~ to: 첫 칸 시작(앞 "|" 뒤) ~ 마지막 칸 끝(뒤 "|" 앞, 넘치는 칸이 있으면 줄 끝).
|
|
410
|
+
* "|" 사이 어디에 입력해도 표 구조는 그대로지만, 앞 "|" 앞(접두어 포함)이나 뒤 "|" 뒤에 입력하면 칸이 새로 생겨
|
|
411
|
+
* 열이 밀립니다. (화면에서는 그 자리가 보이지 않아 사용자가 알아챌 수 없음)
|
|
412
|
+
* - start / end: 행의 처음/끝으로 갈 때의 자리 (첫 칸 내용 처음, 마지막 칸 내용 끝 또는 넘치는 칸의 줄 끝)
|
|
413
|
+
*/
|
|
414
|
+
export function rowCursorRange(row, columns) {
|
|
415
|
+
const first = row.cells[0];
|
|
416
|
+
if (!first)
|
|
417
|
+
return null;
|
|
418
|
+
const last = row.cells[Math.min(row.cells.length, Math.max(1, columns)) - 1];
|
|
419
|
+
const extra = row.cells.length > columns;
|
|
420
|
+
return {
|
|
421
|
+
from: first.from,
|
|
422
|
+
to: extra ? row.lineTo : last.to,
|
|
423
|
+
start: first.textFrom,
|
|
424
|
+
end: extra ? row.lineTo : last.textTo,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/** pos를 행의 커서 범위 안으로 옮깁니다. 앞으로 벗어나면 행의 처음, 뒤로 벗어나면 행의 끝 */
|
|
428
|
+
export function clampToRow(row, columns, pos) {
|
|
429
|
+
const range = rowCursorRange(row, columns);
|
|
430
|
+
if (!range)
|
|
431
|
+
return pos;
|
|
432
|
+
return pos < range.from ? range.start : pos > range.to ? range.end : pos;
|
|
433
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { StateField, type EditorState } from "@codemirror/state";
|
|
2
|
+
import { type TableBlock } from "./tableModel.js";
|
|
3
|
+
/** 문서의 모든 표 (문서 순서). 문서/구문 트리/설정이 바뀔 때만 다시 찾습니다. */
|
|
4
|
+
export declare const tablesField: StateField<readonly TableBlock[]>;
|
|
5
|
+
/** pos에서 시작하는(Table 노드 시작이 pos인) 표 */
|
|
6
|
+
export declare function tableBlockAt(state: EditorState, from: number): TableBlock | null;
|
|
7
|
+
/** 칸 범위 (행/열 번호, 양 끝 포함) */
|
|
8
|
+
export interface TableCellRange {
|
|
9
|
+
readonly fromRow: number;
|
|
10
|
+
readonly toRow: number;
|
|
11
|
+
readonly fromColumn: number;
|
|
12
|
+
readonly toColumn: number;
|
|
13
|
+
}
|
|
14
|
+
export interface TableSelection {
|
|
15
|
+
/** 표(Table 노드) 시작 */
|
|
16
|
+
readonly table: number;
|
|
17
|
+
/** row/column: 손잡이 메뉴, cells: 칸 메뉴 (cells 범위) */
|
|
18
|
+
readonly kind: "row" | "column" | "cells";
|
|
19
|
+
readonly index: number;
|
|
20
|
+
readonly cells?: TableCellRange;
|
|
21
|
+
}
|
|
22
|
+
export declare const setTableSelection: import("@codemirror/state").StateEffectType<TableSelection | null>;
|
|
23
|
+
export declare const tableSelectionField: StateField<TableSelection | null>;
|
|
24
|
+
/** pos가 있는 칸: [행 번호(0: 머리글 줄), 열 번호]. 표 행이 아니면 null */
|
|
25
|
+
export declare function cellIndexAt(state: EditorState, pos: number): {
|
|
26
|
+
table: number;
|
|
27
|
+
row: number;
|
|
28
|
+
column: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* 선택이 같은 표의 두 칸 이상에 걸치면 그 사각형 칸 범위 (Notion처럼 칸 단위로 고름). 아니면 null.
|
|
32
|
+
* 한 칸 안의 선택은 글자 선택입니다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function selectedCells(state: EditorState): (TableCellRange & {
|
|
35
|
+
table: number;
|
|
36
|
+
}) | null;
|