@aicayzer/inkkit 0.0.8 → 0.0.10
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/README.md +5 -3
- package/dist/callouts.js +2 -1
- package/dist/clipboard.d.ts +3 -3
- package/dist/clipboard.js +64 -25
- package/dist/dialect.d.ts +1 -1
- package/dist/dialect.js +36 -2
- package/dist/editor.d.ts +27 -1
- package/dist/editor.js +478 -35
- package/dist/files.d.ts +40 -0
- package/dist/files.js +133 -0
- package/dist/images.d.ts +20 -4
- package/dist/images.js +242 -25
- package/dist/index.d.ts +1 -1
- package/dist/labels.d.ts +8 -0
- package/dist/labels.js +8 -0
- package/dist/linked-syntax.d.ts +48 -0
- package/dist/linked-syntax.js +308 -0
- package/dist/literals.js +40 -1
- package/dist/portable-files.d.ts +10 -0
- package/dist/portable-files.js +36 -0
- package/dist/preserve.js +7 -0
- package/dist/print.d.ts +2 -2
- package/dist/print.js +49 -17
- package/dist/search.d.ts +2 -1
- package/dist/search.js +53 -22
- package/dist/source.d.ts +0 -1
- package/dist/source.js +0 -60
- package/dist/style.css +39 -0
- package/dist/text-ranges.d.ts +26 -0
- package/dist/text-ranges.js +233 -0
- package/dist/types.d.ts +84 -2
- package/dist/wiki-view.d.ts +21 -0
- package/dist/wiki-view.js +78 -0
- package/package.json +1 -1
package/dist/search.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment, Slice } from '@milkdown/kit/prose/model';
|
|
2
2
|
import { closeHistory } from '@milkdown/kit/prose/history';
|
|
3
|
-
import { Selection, TextSelection } from '@milkdown/kit/prose/state';
|
|
3
|
+
import { Selection, TextSelection, } from '@milkdown/kit/prose/state';
|
|
4
4
|
import { SearchQuery, setSearchState, getSearchState } from 'prosemirror-search';
|
|
5
5
|
import { InkKitError } from './types.js';
|
|
6
6
|
function escapedPattern(text) {
|
|
@@ -82,21 +82,44 @@ export function findFormatted(view, text) {
|
|
|
82
82
|
const query = formattedQuery(text);
|
|
83
83
|
const previous = getSearchState(view.state)?.query.search;
|
|
84
84
|
let tr = setSearchState(view.state.tr, query);
|
|
85
|
+
let found = false;
|
|
85
86
|
if (text) {
|
|
86
87
|
const start = previous === query.search
|
|
87
88
|
? view.state.selection.to
|
|
88
89
|
: view.state.selection.from;
|
|
89
90
|
const match = query.findNext(view.state, start) ?? query.findNext(view.state, 0);
|
|
90
|
-
if (match)
|
|
91
|
+
if (match) {
|
|
92
|
+
found = true;
|
|
91
93
|
tr = tr
|
|
92
94
|
.setSelection(TextSelection.create(tr.doc, match.from, match.to))
|
|
93
95
|
.scrollIntoView();
|
|
96
|
+
}
|
|
94
97
|
else
|
|
95
98
|
tr = tr.setSelection(Selection.near(view.state.selection.$to));
|
|
96
99
|
}
|
|
97
100
|
else if (previous)
|
|
98
101
|
tr = tr.setSelection(Selection.near(view.state.selection.$to));
|
|
99
102
|
view.dispatch(tr);
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
function replacementSlice(state, from, to, value) {
|
|
106
|
+
const start = state.doc.resolve(from), end = state.doc.resolve(to);
|
|
107
|
+
const marks = start.marksAcross(end) ?? start.marks();
|
|
108
|
+
const nodes = [];
|
|
109
|
+
if (start.parent.type.spec.code) {
|
|
110
|
+
if (value)
|
|
111
|
+
nodes.push(state.schema.text(value, marks));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const lines = value.split('\n');
|
|
115
|
+
lines.forEach((line, index) => {
|
|
116
|
+
if (index)
|
|
117
|
+
nodes.push(state.schema.nodes.hardbreak.create({ isHTML: index === lines.length - 1 && line === '' }, null, marks));
|
|
118
|
+
if (line)
|
|
119
|
+
nodes.push(state.schema.text(line, marks));
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return new Slice(Fragment.fromArray(nodes), 0, 0);
|
|
100
123
|
}
|
|
101
124
|
export function replaceFormatted(view, search, replacement, preflight, all = false) {
|
|
102
125
|
if (!search)
|
|
@@ -132,26 +155,7 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
|
|
|
132
155
|
return 0;
|
|
133
156
|
let tr = setSearchState(state.tr, query);
|
|
134
157
|
for (const match of [...changed].reverse()) {
|
|
135
|
-
|
|
136
|
-
const to = state.doc.resolve(match.to);
|
|
137
|
-
const marks = from.marksAcross(to) ?? from.marks();
|
|
138
|
-
let content = Fragment.empty;
|
|
139
|
-
if (from.parent.type.spec.code) {
|
|
140
|
-
if (value)
|
|
141
|
-
content = Fragment.from(state.schema.text(value, marks));
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
const nodes = [];
|
|
145
|
-
const lines = value.split('\n');
|
|
146
|
-
lines.forEach((line, index) => {
|
|
147
|
-
if (index)
|
|
148
|
-
nodes.push(state.schema.nodes.hardbreak.create({ isHTML: index === lines.length - 1 && line === '' }, null, marks));
|
|
149
|
-
if (line)
|
|
150
|
-
nodes.push(state.schema.text(line, marks));
|
|
151
|
-
});
|
|
152
|
-
content = Fragment.fromArray(nodes);
|
|
153
|
-
}
|
|
154
|
-
tr.replace(match.from, match.to, new Slice(content, 0, 0));
|
|
158
|
+
tr.replace(match.from, match.to, replacementSlice(state, match.from, match.to, value));
|
|
155
159
|
}
|
|
156
160
|
if (!all) {
|
|
157
161
|
const match = changed[0];
|
|
@@ -169,3 +173,30 @@ export function replaceFormatted(view, search, replacement, preflight, all = fal
|
|
|
169
173
|
view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
|
|
170
174
|
return changed.length;
|
|
171
175
|
}
|
|
176
|
+
export function replaceFormattedRange(view, from, to, replacement, preflight) {
|
|
177
|
+
const start = view.state.doc.resolve(from);
|
|
178
|
+
const end = view.state.doc.resolve(to);
|
|
179
|
+
let textOnly = start.parent === end.parent && start.parent.inlineContent;
|
|
180
|
+
view.state.doc.nodesBetween(from, to, (node) => {
|
|
181
|
+
if (!node.isText && !node.isTextblock)
|
|
182
|
+
textOnly = false;
|
|
183
|
+
});
|
|
184
|
+
if (!textOnly)
|
|
185
|
+
throw new InkKitError('invalid-range', 'Replacement requires literal text within one block');
|
|
186
|
+
const value = replacement.replace(/\r\n?/g, '\n');
|
|
187
|
+
if (view.state.doc.textBetween(from, to) === value)
|
|
188
|
+
return false;
|
|
189
|
+
const tr = closeHistory(view.state.tr.replace(from, to, replacementSlice(view.state, from, to, value)));
|
|
190
|
+
tr.setSelection(TextSelection.create(tr.doc, from, tr.mapping.map(to, 1)));
|
|
191
|
+
try {
|
|
192
|
+
preflight(tr.doc);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (error instanceof InkKitError)
|
|
196
|
+
throw error;
|
|
197
|
+
throw new InkKitError('preservation', error instanceof Error ? error.message : 'Cannot preserve replacement');
|
|
198
|
+
}
|
|
199
|
+
view.dispatch(tr);
|
|
200
|
+
view.dispatch(closeHistory(view.state.tr).setMeta('addToHistory', false));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
package/dist/source.d.ts
CHANGED
package/dist/source.js
CHANGED
|
@@ -55,63 +55,3 @@ export function sourceSelection(source, start, end) {
|
|
|
55
55
|
shareable += source.slice(cursor, to);
|
|
56
56
|
return { markdown: source.slice(from, to), shareable };
|
|
57
57
|
}
|
|
58
|
-
export function revealSourceCaret(plain) {
|
|
59
|
-
if (!plain.clientWidth || !plain.clientHeight)
|
|
60
|
-
return;
|
|
61
|
-
const document = plain.ownerDocument;
|
|
62
|
-
const style = document.defaultView.getComputedStyle(plain);
|
|
63
|
-
const mirror = document.createElement('div');
|
|
64
|
-
for (const property of [
|
|
65
|
-
'font',
|
|
66
|
-
'font-family',
|
|
67
|
-
'font-size',
|
|
68
|
-
'font-weight',
|
|
69
|
-
'font-style',
|
|
70
|
-
'font-stretch',
|
|
71
|
-
'font-variant',
|
|
72
|
-
'line-height',
|
|
73
|
-
'letter-spacing',
|
|
74
|
-
'word-spacing',
|
|
75
|
-
'tab-size',
|
|
76
|
-
'padding',
|
|
77
|
-
'text-indent',
|
|
78
|
-
'text-transform',
|
|
79
|
-
'direction',
|
|
80
|
-
'word-break',
|
|
81
|
-
])
|
|
82
|
-
mirror.style.setProperty(property, style.getPropertyValue(property));
|
|
83
|
-
Object.assign(mirror.style, {
|
|
84
|
-
position: 'fixed',
|
|
85
|
-
visibility: 'hidden',
|
|
86
|
-
left: '-10000px',
|
|
87
|
-
top: '0',
|
|
88
|
-
width: `${plain.clientWidth}px`,
|
|
89
|
-
boxSizing: 'border-box',
|
|
90
|
-
whiteSpace: plain.wrap === 'off' ? 'pre' : 'pre-wrap',
|
|
91
|
-
overflowWrap: plain.wrap === 'off' ? 'normal' : 'break-word',
|
|
92
|
-
});
|
|
93
|
-
mirror.setAttribute('aria-hidden', 'true');
|
|
94
|
-
const caret = document.createElement('span');
|
|
95
|
-
caret.setAttribute('data-inkkit-source-caret', '');
|
|
96
|
-
caret.textContent = plain.value.slice(plain.selectionStart) || '\u200b';
|
|
97
|
-
mirror.append(document.createTextNode(plain.value.slice(0, plain.selectionStart)), caret);
|
|
98
|
-
document.body.append(mirror);
|
|
99
|
-
try {
|
|
100
|
-
// Textareas expose selection offsets but no caret geometry; matching their layout also accounts for wrapped lines.
|
|
101
|
-
const rect = caret.getClientRects()[0] ?? caret.getBoundingClientRect();
|
|
102
|
-
const top = rect.top - mirror.getBoundingClientRect().top;
|
|
103
|
-
const lineHeight = Math.max(rect.height, Number.parseFloat(style.lineHeight) || rect.height);
|
|
104
|
-
const insetTop = Number.parseFloat(style.paddingTop) || 0;
|
|
105
|
-
const insetBottom = Number.parseFloat(style.paddingBottom) || 0;
|
|
106
|
-
if (plain.selectionStart === 0)
|
|
107
|
-
plain.scrollTop = 0;
|
|
108
|
-
else if (top < plain.scrollTop + insetTop)
|
|
109
|
-
plain.scrollTop = Math.max(0, top - insetTop);
|
|
110
|
-
else if (top + lineHeight >
|
|
111
|
-
plain.scrollTop + plain.clientHeight - insetBottom)
|
|
112
|
-
plain.scrollTop = Math.max(0, top + lineHeight - plain.clientHeight + insetBottom);
|
|
113
|
-
}
|
|
114
|
-
finally {
|
|
115
|
-
mirror.remove();
|
|
116
|
-
}
|
|
117
|
-
}
|
package/dist/style.css
CHANGED
|
@@ -235,6 +235,45 @@
|
|
|
235
235
|
background: var(--_inkkit-accent);
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
.inkkit-root .ProseMirror .inkkit-file {
|
|
239
|
+
vertical-align: middle;
|
|
240
|
+
border: 1px solid var(--_inkkit-rule);
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
padding: 4px;
|
|
243
|
+
white-space: normal;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.inkkit-root .ProseMirror .inkkit-file audio,
|
|
247
|
+
.inkkit-root .ProseMirror .inkkit-file video,
|
|
248
|
+
.inkkit-root .ProseMirror .inkkit-file iframe {
|
|
249
|
+
display: block;
|
|
250
|
+
max-width: 100%;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.inkkit-root .ProseMirror .inkkit-file video {
|
|
254
|
+
max-height: 480px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.inkkit-root .ProseMirror .inkkit-file iframe {
|
|
258
|
+
width: min(600px, 100%);
|
|
259
|
+
height: 360px;
|
|
260
|
+
border: 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.inkkit-root .ProseMirror .inkkit-file-label {
|
|
264
|
+
overflow-wrap: anywhere;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.inkkit-root .ProseMirror .inkkit-file-control {
|
|
268
|
+
margin-inline-start: 0.5em;
|
|
269
|
+
font: inherit;
|
|
270
|
+
color: var(--_inkkit-text);
|
|
271
|
+
background: var(--_inkkit-code-bg);
|
|
272
|
+
border: 1px solid var(--_inkkit-rule);
|
|
273
|
+
border-radius: 3px;
|
|
274
|
+
cursor: pointer;
|
|
275
|
+
}
|
|
276
|
+
|
|
238
277
|
.inkkit-root .ProseMirror {
|
|
239
278
|
/* The viewport, not the wrapper Milkdown adds, so a click below the text still places the caret. */
|
|
240
279
|
min-height: var(--inkkit-min-height, 100%);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
2
|
+
import type { EditorView } from '@milkdown/kit/prose/view';
|
|
3
|
+
import { type TextRect } from './types.js';
|
|
4
|
+
export interface TextSpan {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
kind: 'text' | 'break' | 'embed' | 'separator';
|
|
10
|
+
}
|
|
11
|
+
export interface TextProjection {
|
|
12
|
+
text: string;
|
|
13
|
+
spans: TextSpan[];
|
|
14
|
+
}
|
|
15
|
+
export declare function readableProjection(doc: ProseNode): TextProjection;
|
|
16
|
+
export declare function projectionPosition(projection: TextProjection, offset: number, side: -1 | 1): number;
|
|
17
|
+
export declare function projectionOffset(projection: TextProjection, position: number): number;
|
|
18
|
+
export declare function validateOffsets(text: string, from: number, to: number): void;
|
|
19
|
+
export declare function textRect(rect: Pick<TextRect, 'left' | 'top' | 'right' | 'bottom'>): TextRect;
|
|
20
|
+
export declare function formattedRects(view: EditorView, projection: TextProjection, from: number, to: number): TextRect[];
|
|
21
|
+
export declare function literalRects(plain: HTMLTextAreaElement, from: number, to: number): TextRect[];
|
|
22
|
+
export declare function intersectsViewport(rect: Pick<TextRect, 'top' | 'right' | 'bottom' | 'left'>, viewport: TextRect): boolean;
|
|
23
|
+
export declare function literalVisibleRanges(plain: HTMLTextAreaElement, viewport: TextRect): {
|
|
24
|
+
from: number;
|
|
25
|
+
to: number;
|
|
26
|
+
}[];
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { InkKitError } from './types.js';
|
|
2
|
+
import { fileReference } from './linked-syntax.js';
|
|
3
|
+
export function readableProjection(doc) {
|
|
4
|
+
const result = { text: '', spans: [] };
|
|
5
|
+
let blocks = 0;
|
|
6
|
+
const append = (text, start, end, kind) => {
|
|
7
|
+
const from = result.text.length;
|
|
8
|
+
result.text += text;
|
|
9
|
+
result.spans.push({ from, to: result.text.length, start, end, kind });
|
|
10
|
+
};
|
|
11
|
+
const block = (start) => {
|
|
12
|
+
if (blocks++)
|
|
13
|
+
append('\n', result.spans.at(-1)?.end ?? start, start, 'separator');
|
|
14
|
+
};
|
|
15
|
+
doc.descendants((node, position) => {
|
|
16
|
+
const name = node.type.name;
|
|
17
|
+
if (name === 'comment_inline' ||
|
|
18
|
+
name === 'comment_block' ||
|
|
19
|
+
name === 'reference_definition')
|
|
20
|
+
return false;
|
|
21
|
+
if (name === 'inkkit_callout') {
|
|
22
|
+
block(position);
|
|
23
|
+
append(node.attrs.title ||
|
|
24
|
+
String(node.attrs.kind).replace(/^./, (value) => value.toUpperCase()), position, position + 1, 'embed');
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
if (node.isTextblock) {
|
|
28
|
+
block(position + 1);
|
|
29
|
+
if (!node.content.size)
|
|
30
|
+
append('', position + 1, position + 1, 'text');
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (node.isText) {
|
|
34
|
+
append(node.text ?? '', position, position + node.nodeSize, 'text');
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (name === 'hardbreak' || name === 'hard_break') {
|
|
38
|
+
append('\n', position, position + node.nodeSize, 'break');
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (name === 'image' ||
|
|
42
|
+
name === 'footnote_reference' ||
|
|
43
|
+
name === 'inkkit_wiki_link') {
|
|
44
|
+
append(name === 'image'
|
|
45
|
+
? fileReference(node).label || 'Image'
|
|
46
|
+
: name === 'inkkit_wiki_link'
|
|
47
|
+
? String(node.attrs.label || node.attrs.target || 'Link')
|
|
48
|
+
: `[${node.attrs.label}]`, position, position + node.nodeSize, 'embed');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
});
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
export function projectionPosition(projection, offset, side) {
|
|
56
|
+
const spans = projection.spans;
|
|
57
|
+
const span = spans.find((span) => (offset > span.from && offset < span.to) ||
|
|
58
|
+
(side > 0 ? offset === span.from : offset === span.to)) ?? (side > 0 ? spans.at(-1) : spans[0]);
|
|
59
|
+
if (!span)
|
|
60
|
+
return 0;
|
|
61
|
+
if (span.kind === 'text')
|
|
62
|
+
return (span.start +
|
|
63
|
+
Math.max(0, Math.min(offset - span.from, span.to - span.from)));
|
|
64
|
+
return offset <= span.from ? span.start : span.end;
|
|
65
|
+
}
|
|
66
|
+
export function projectionOffset(projection, position) {
|
|
67
|
+
for (const span of projection.spans) {
|
|
68
|
+
if (position < span.start)
|
|
69
|
+
return span.from;
|
|
70
|
+
if (position <= span.end)
|
|
71
|
+
return span.kind === 'text'
|
|
72
|
+
? span.from + Math.max(0, position - span.start)
|
|
73
|
+
: position === span.start
|
|
74
|
+
? span.from
|
|
75
|
+
: span.to;
|
|
76
|
+
}
|
|
77
|
+
return projection.text.length;
|
|
78
|
+
}
|
|
79
|
+
export function validateOffsets(text, from, to) {
|
|
80
|
+
const split = (offset) => offset > 0 &&
|
|
81
|
+
offset < text.length &&
|
|
82
|
+
/[\uD800-\uDBFF]/.test(text[offset - 1]) &&
|
|
83
|
+
/[\uDC00-\uDFFF]/.test(text[offset]);
|
|
84
|
+
if (!Number.isInteger(from) ||
|
|
85
|
+
!Number.isInteger(to) ||
|
|
86
|
+
from < 0 ||
|
|
87
|
+
to < from ||
|
|
88
|
+
to > text.length ||
|
|
89
|
+
split(from) ||
|
|
90
|
+
split(to))
|
|
91
|
+
throw new InkKitError('invalid-range', 'Range must use valid UTF-16 boundaries in the text snapshot');
|
|
92
|
+
}
|
|
93
|
+
export function textRect(rect) {
|
|
94
|
+
return {
|
|
95
|
+
left: rect.left,
|
|
96
|
+
top: rect.top,
|
|
97
|
+
right: rect.right,
|
|
98
|
+
bottom: rect.bottom,
|
|
99
|
+
width: Math.max(0, rect.right - rect.left),
|
|
100
|
+
height: Math.max(0, rect.bottom - rect.top),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
export function formattedRects(view, projection, from, to) {
|
|
104
|
+
if (from === to) {
|
|
105
|
+
const position = projectionPosition(projection, from, 1);
|
|
106
|
+
const dom = view.domAtPos(position);
|
|
107
|
+
const element = dom.node.nodeType === 1 ? dom.node : dom.node.parentElement;
|
|
108
|
+
if (!element?.getClientRects().length)
|
|
109
|
+
return [];
|
|
110
|
+
return [textRect(view.coordsAtPos(position))];
|
|
111
|
+
}
|
|
112
|
+
const rects = [];
|
|
113
|
+
for (const span of projection.spans) {
|
|
114
|
+
if (span.to <= from || span.from >= to || span.kind === 'separator')
|
|
115
|
+
continue;
|
|
116
|
+
if (span.kind === 'embed' || span.kind === 'break') {
|
|
117
|
+
const node = view.nodeDOM(span.start);
|
|
118
|
+
if (node instanceof Element) {
|
|
119
|
+
const label = node.matches('.inkkit-callout')
|
|
120
|
+
? (node.querySelector(':scope > .inkkit-callout-header > .inkkit-callout-title') ?? node)
|
|
121
|
+
: node;
|
|
122
|
+
rects.push(...Array.from(label.getClientRects(), textRect));
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const start = view.domAtPos(span.start + Math.max(0, from - span.from), 1);
|
|
127
|
+
const end = view.domAtPos(span.start + Math.min(span.to - span.from, to - span.from), -1);
|
|
128
|
+
const range = view.dom.ownerDocument.createRange();
|
|
129
|
+
range.setStart(start.node, start.offset);
|
|
130
|
+
range.setEnd(end.node, end.offset);
|
|
131
|
+
rects.push(...Array.from(range.getClientRects(), textRect));
|
|
132
|
+
}
|
|
133
|
+
return rects.filter((rect) => rect.height > 0 && rect.width >= 0);
|
|
134
|
+
}
|
|
135
|
+
function withLiteralLayout(plain, measure) {
|
|
136
|
+
if (!plain.clientWidth || !plain.clientHeight)
|
|
137
|
+
return undefined;
|
|
138
|
+
const document = plain.ownerDocument;
|
|
139
|
+
const style = document.defaultView.getComputedStyle(plain);
|
|
140
|
+
const mirror = document.createElement('div');
|
|
141
|
+
for (const property of [
|
|
142
|
+
'font',
|
|
143
|
+
'font-family',
|
|
144
|
+
'font-size',
|
|
145
|
+
'font-weight',
|
|
146
|
+
'font-style',
|
|
147
|
+
'font-stretch',
|
|
148
|
+
'font-variant',
|
|
149
|
+
'line-height',
|
|
150
|
+
'letter-spacing',
|
|
151
|
+
'word-spacing',
|
|
152
|
+
'tab-size',
|
|
153
|
+
'padding',
|
|
154
|
+
'text-indent',
|
|
155
|
+
'text-transform',
|
|
156
|
+
'direction',
|
|
157
|
+
'word-break',
|
|
158
|
+
])
|
|
159
|
+
mirror.style.setProperty(property, style.getPropertyValue(property));
|
|
160
|
+
const origin = plain.getBoundingClientRect();
|
|
161
|
+
Object.assign(mirror.style, {
|
|
162
|
+
position: 'fixed',
|
|
163
|
+
visibility: 'hidden',
|
|
164
|
+
pointerEvents: 'none',
|
|
165
|
+
left: `${origin.left + plain.clientLeft - plain.scrollLeft}px`,
|
|
166
|
+
top: `${origin.top + plain.clientTop - plain.scrollTop}px`,
|
|
167
|
+
width: `${plain.clientWidth}px`,
|
|
168
|
+
boxSizing: 'border-box',
|
|
169
|
+
whiteSpace: plain.wrap === 'off' ? 'pre' : 'pre-wrap',
|
|
170
|
+
overflowWrap: plain.wrap === 'off' ? 'normal' : 'break-word',
|
|
171
|
+
});
|
|
172
|
+
mirror.setAttribute('aria-hidden', 'true');
|
|
173
|
+
const text = document.createTextNode(plain.value + '\u200b');
|
|
174
|
+
mirror.append(text);
|
|
175
|
+
document.body.append(mirror);
|
|
176
|
+
try {
|
|
177
|
+
return measure(text);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
mirror.remove();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function literalRects(plain, from, to) {
|
|
184
|
+
return (withLiteralLayout(plain, (text) => {
|
|
185
|
+
const range = plain.ownerDocument.createRange();
|
|
186
|
+
range.setStart(text, from);
|
|
187
|
+
range.setEnd(text, from === to ? Math.min(to + 1, text.length) : to);
|
|
188
|
+
return Array.from(range.getClientRects(), (rect) => textRect({
|
|
189
|
+
left: rect.left,
|
|
190
|
+
right: from === to ? rect.left : rect.right,
|
|
191
|
+
top: rect.top,
|
|
192
|
+
bottom: rect.bottom,
|
|
193
|
+
}));
|
|
194
|
+
}) ?? []);
|
|
195
|
+
}
|
|
196
|
+
export function intersectsViewport(rect, viewport) {
|
|
197
|
+
return (viewport.width > 0 &&
|
|
198
|
+
viewport.height > 0 &&
|
|
199
|
+
rect.bottom > viewport.top &&
|
|
200
|
+
rect.top < viewport.bottom &&
|
|
201
|
+
rect.right >= viewport.left &&
|
|
202
|
+
rect.left < viewport.right);
|
|
203
|
+
}
|
|
204
|
+
export function literalVisibleRanges(plain, viewport) {
|
|
205
|
+
return (withLiteralLayout(plain, (text) => {
|
|
206
|
+
const range = plain.ownerDocument.createRange();
|
|
207
|
+
const ranges = [];
|
|
208
|
+
const visible = (from, to) => {
|
|
209
|
+
range.setStart(text, from);
|
|
210
|
+
range.setEnd(text, to);
|
|
211
|
+
return Array.from(range.getClientRects()).some((rect) => intersectsViewport(rect, viewport));
|
|
212
|
+
};
|
|
213
|
+
for (let start = 0; start < plain.value.length;) {
|
|
214
|
+
const newline = plain.value.indexOf('\n', start);
|
|
215
|
+
const end = newline < 0 ? plain.value.length : newline + 1;
|
|
216
|
+
if (visible(start, end)) {
|
|
217
|
+
for (let from = start; from < end;) {
|
|
218
|
+
const to = from + (plain.value.codePointAt(from) > 0xffff ? 2 : 1);
|
|
219
|
+
if (visible(from, to)) {
|
|
220
|
+
const previous = ranges.at(-1);
|
|
221
|
+
if (previous?.to === from)
|
|
222
|
+
previous.to = to;
|
|
223
|
+
else
|
|
224
|
+
ranges.push({ from, to });
|
|
225
|
+
}
|
|
226
|
+
from = to;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
start = end;
|
|
230
|
+
}
|
|
231
|
+
return ranges;
|
|
232
|
+
}) ?? []);
|
|
233
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface DocumentSnapshot extends DocumentInput {
|
|
|
14
14
|
revision: number;
|
|
15
15
|
dirty: boolean;
|
|
16
16
|
}
|
|
17
|
-
export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'read-only' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable';
|
|
17
|
+
export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'read-only' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable' | 'invalid-range';
|
|
18
18
|
export declare class InkKitError extends Error {
|
|
19
19
|
readonly code: EditorErrorCode;
|
|
20
20
|
constructor(code: EditorErrorCode, message: string);
|
|
@@ -39,6 +39,49 @@ export interface ImageAdapter {
|
|
|
39
39
|
}>;
|
|
40
40
|
exportImage(reference: string, context: DocumentContext): Promise<PortableImage>;
|
|
41
41
|
}
|
|
42
|
+
export interface WikiLinkReference {
|
|
43
|
+
target: string;
|
|
44
|
+
fragment?: string;
|
|
45
|
+
label: string;
|
|
46
|
+
}
|
|
47
|
+
export interface WikiLinkAdapter {
|
|
48
|
+
resolve?(reference: WikiLinkReference, context: DocumentContext): {
|
|
49
|
+
missing?: boolean;
|
|
50
|
+
} | undefined;
|
|
51
|
+
open(reference: WikiLinkReference, context: DocumentContext): void;
|
|
52
|
+
}
|
|
53
|
+
export interface FileReference {
|
|
54
|
+
reference: string;
|
|
55
|
+
kind: 'path' | 'wiki';
|
|
56
|
+
fragment?: string;
|
|
57
|
+
label?: string;
|
|
58
|
+
width?: number;
|
|
59
|
+
}
|
|
60
|
+
export interface FileContext extends DocumentContext {
|
|
61
|
+
signal: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
export type FilePresentation = {
|
|
64
|
+
kind: 'image' | 'audio' | 'video' | 'pdf';
|
|
65
|
+
url: string;
|
|
66
|
+
label?: string;
|
|
67
|
+
mimeType?: string;
|
|
68
|
+
} | {
|
|
69
|
+
kind: 'file' | 'missing';
|
|
70
|
+
label?: string;
|
|
71
|
+
} | {
|
|
72
|
+
kind: 'error';
|
|
73
|
+
label?: string;
|
|
74
|
+
message: string;
|
|
75
|
+
};
|
|
76
|
+
export interface FileAdapter {
|
|
77
|
+
resolve(reference: FileReference, context: FileContext): Promise<FilePresentation>;
|
|
78
|
+
exportImage?(reference: FileReference, context: FileContext): Promise<PortableImage>;
|
|
79
|
+
open?(reference: FileReference, context: DocumentContext): void;
|
|
80
|
+
contextMenu?(reference: FileReference, point: {
|
|
81
|
+
clientX: number;
|
|
82
|
+
clientY: number;
|
|
83
|
+
}, context: DocumentContext): void;
|
|
84
|
+
}
|
|
42
85
|
export interface ClipboardInput {
|
|
43
86
|
text: string;
|
|
44
87
|
markdown?: string;
|
|
@@ -65,7 +108,7 @@ export interface ClipboardDiagram {
|
|
|
65
108
|
error?: string;
|
|
66
109
|
}
|
|
67
110
|
export interface PrintableWarning {
|
|
68
|
-
code: 'diagram-unavailable';
|
|
111
|
+
code: 'diagram-unavailable' | 'attachment-fallback' | 'attachment-unavailable';
|
|
69
112
|
message: string;
|
|
70
113
|
}
|
|
71
114
|
export interface PrintableDocument {
|
|
@@ -78,3 +121,42 @@ export interface PrintableDocument {
|
|
|
78
121
|
assets: readonly PortableImage[];
|
|
79
122
|
warnings: readonly PrintableWarning[];
|
|
80
123
|
}
|
|
124
|
+
export interface TextRange {
|
|
125
|
+
snapshotId: string;
|
|
126
|
+
from: number;
|
|
127
|
+
to: number;
|
|
128
|
+
}
|
|
129
|
+
export interface ReadableTextSnapshot {
|
|
130
|
+
snapshotId: string;
|
|
131
|
+
documentId: string;
|
|
132
|
+
generation: number;
|
|
133
|
+
revision: number;
|
|
134
|
+
format: DocumentFormat;
|
|
135
|
+
mode: 'source' | 'formatted';
|
|
136
|
+
text: string;
|
|
137
|
+
selection: TextRange;
|
|
138
|
+
}
|
|
139
|
+
export interface TextRect {
|
|
140
|
+
left: number;
|
|
141
|
+
top: number;
|
|
142
|
+
right: number;
|
|
143
|
+
bottom: number;
|
|
144
|
+
width: number;
|
|
145
|
+
height: number;
|
|
146
|
+
}
|
|
147
|
+
export interface ViewportInsets {
|
|
148
|
+
top: number;
|
|
149
|
+
right: number;
|
|
150
|
+
bottom: number;
|
|
151
|
+
left: number;
|
|
152
|
+
}
|
|
153
|
+
export interface ViewportOptions {
|
|
154
|
+
scrollContainer?: HTMLElement;
|
|
155
|
+
insets?: Partial<ViewportInsets>;
|
|
156
|
+
}
|
|
157
|
+
export interface ViewportSnapshot {
|
|
158
|
+
rect: TextRect;
|
|
159
|
+
insets: ViewportInsets;
|
|
160
|
+
scrollTop: number;
|
|
161
|
+
scrollLeft: number;
|
|
162
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Node as ProseNode } from '@milkdown/kit/prose/model';
|
|
2
|
+
import type { EditorView } from '@milkdown/kit/prose/view';
|
|
3
|
+
import type { DocumentContext, WikiLinkAdapter } from './types.js';
|
|
4
|
+
import type { EditorLabels } from './labels.js';
|
|
5
|
+
interface WikiRuntime {
|
|
6
|
+
capture(): {
|
|
7
|
+
context: DocumentContext;
|
|
8
|
+
isCurrent(): boolean;
|
|
9
|
+
};
|
|
10
|
+
active(): boolean;
|
|
11
|
+
labels: Readonly<EditorLabels>;
|
|
12
|
+
error?(error: Error): void;
|
|
13
|
+
}
|
|
14
|
+
export declare function wikiLinkView(adapter: WikiLinkAdapter, runtime: WikiRuntime): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView) => {
|
|
15
|
+
dom: HTMLSpanElement;
|
|
16
|
+
update(next: ProseNode): boolean;
|
|
17
|
+
stopEvent(event: Event): boolean;
|
|
18
|
+
ignoreMutation: () => true;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}>;
|
|
21
|
+
export {};
|