@aicayzer/inkkit 0.0.0-stage → 0.0.1

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.
@@ -0,0 +1 @@
1
+ export declare const highlightPlugin: import("@milkdown/kit/utils").$Prose;
@@ -0,0 +1,49 @@
1
+ import { common, createLowlight } from 'lowlight';
2
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
+ import { Decoration, DecorationSet } from '@milkdown/kit/prose/view';
4
+ import { $prose } from '@milkdown/kit/utils';
5
+ const lowlight = createLowlight(common);
6
+ // Colors are decorations over the plain text, so the markdown is untouched;
7
+ // a block without a known language stays plain.
8
+ function decorations(doc) {
9
+ const found = [];
10
+ doc.descendants((node, pos) => {
11
+ if (node.type.name !== 'code_block')
12
+ return true;
13
+ const language = String(node.attrs.language ?? '').toLowerCase();
14
+ if (!language || !lowlight.registered(language))
15
+ return false;
16
+ let offset = pos + 1;
17
+ const walk = (children, classes) => {
18
+ for (const child of children) {
19
+ if (child.type === 'text') {
20
+ const end = offset + child.value.length;
21
+ if (classes.length > 0)
22
+ found.push(Decoration.inline(offset, end, { class: classes.join(' ') }));
23
+ offset = end;
24
+ }
25
+ else if (child.type === 'element') {
26
+ const own = child.properties?.className ?? [];
27
+ walk(child.children, [...classes, ...own]);
28
+ }
29
+ }
30
+ };
31
+ walk(lowlight.highlight(language, node.textContent).children, []);
32
+ return false;
33
+ });
34
+ return DecorationSet.create(doc, found);
35
+ }
36
+ export const highlightPlugin = $prose(() => new Plugin({
37
+ key: new PluginKey('highlight'),
38
+ state: {
39
+ init: (_, state) => decorations(state.doc),
40
+ apply: (tr, previous) => tr.docChanged
41
+ ? decorations(tr.doc)
42
+ : previous.map(tr.mapping, tr.doc),
43
+ },
44
+ props: {
45
+ decorations(state) {
46
+ return this.getState(state);
47
+ },
48
+ },
49
+ }));
@@ -0,0 +1,32 @@
1
+ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ import type { EditorView, NodeView } from '@milkdown/kit/prose/view';
3
+ import type { ImageAdapter } from './types.js';
4
+ /** A width rides in the alt text, as `![a picture|400](images/….png)`. Other Markdown readers
5
+ * show the picture and keep the number without requiring a private extension. */
6
+ export declare function splitAlt(alt: string): {
7
+ alt: string;
8
+ width: number | null;
9
+ };
10
+ export declare function joinAlt(alt: string, width: number | null): string;
11
+ /** Draws the image and the handle that sizes it. The width is kept in the alt text, so the schema is the
12
+ * one commonmark gives us; a picture is never wider than its editor, whatever number the alt text holds. */
13
+ declare class ImageView implements NodeView {
14
+ private node;
15
+ private readonly view;
16
+ private readonly getPos;
17
+ private readonly adapter;
18
+ dom: HTMLElement;
19
+ private image?;
20
+ private width;
21
+ private stopResize?;
22
+ constructor(node: ProseNode, view: EditorView, getPos: () => number | undefined, adapter: ImageAdapter | undefined);
23
+ update(node: ProseNode): boolean;
24
+ stopEvent(event: Event): boolean;
25
+ ignoreMutation(): boolean;
26
+ destroy(): void;
27
+ private render;
28
+ private startResize;
29
+ private commit;
30
+ }
31
+ export declare function imageView(adapter?: ImageAdapter): import("@milkdown/kit/utils").$View<import("@milkdown/kit/utils").$Node, (node: ProseNode, view: EditorView, getPos: () => number | undefined) => ImageView>;
32
+ export {};
package/dist/images.js ADDED
@@ -0,0 +1,129 @@
1
+ import { imageSchema } from '@milkdown/kit/preset/commonmark';
2
+ import { $view } from '@milkdown/kit/utils';
3
+ /** A width rides in the alt text, as `![a picture|400](images/….png)`. Other Markdown readers
4
+ * show the picture and keep the number without requiring a private extension. */
5
+ export function splitAlt(alt) {
6
+ const match = /^([\s\S]*)\|(\d{1,5})$/.exec(alt);
7
+ if (!match)
8
+ return { alt, width: null };
9
+ return { alt: match[1] ?? '', width: Number(match[2]) };
10
+ }
11
+ export function joinAlt(alt, width) {
12
+ return width == null ? alt : `${alt}|${width}`;
13
+ }
14
+ const minimumWidth = 48;
15
+ /** Draws the image and the handle that sizes it. The width is kept in the alt text, so the schema is the
16
+ * one commonmark gives us; a picture is never wider than its editor, whatever number the alt text holds. */
17
+ class ImageView {
18
+ node;
19
+ view;
20
+ getPos;
21
+ adapter;
22
+ dom;
23
+ image;
24
+ width = null;
25
+ stopResize;
26
+ constructor(node, view, getPos, adapter) {
27
+ this.node = node;
28
+ this.view = view;
29
+ this.getPos = getPos;
30
+ this.adapter = adapter;
31
+ this.dom = document.createElement('span');
32
+ this.dom.className = 'image';
33
+ this.render();
34
+ }
35
+ update(node) {
36
+ if (node.type !== this.node.type)
37
+ return false;
38
+ this.node = node;
39
+ this.render();
40
+ return true;
41
+ }
42
+ /// The handle's own events are the view's business, not the document's.
43
+ stopEvent(event) {
44
+ return (event.target instanceof HTMLElement &&
45
+ event.target.classList.contains('image-handle'));
46
+ }
47
+ ignoreMutation() {
48
+ return true;
49
+ }
50
+ destroy() {
51
+ this.stopResize?.();
52
+ this.stopResize = undefined;
53
+ }
54
+ render() {
55
+ this.stopResize?.();
56
+ this.stopResize = undefined;
57
+ const src = String(this.node.attrs.src ?? '');
58
+ const { alt, width } = splitAlt(String(this.node.attrs.alt ?? ''));
59
+ this.width = width;
60
+ this.dom.textContent = '';
61
+ this.image = undefined;
62
+ // Only the host adapter decides which authored references can be presented.
63
+ const presentation = this.adapter?.presentation(src);
64
+ if (!presentation) {
65
+ this.dom.classList.add('image-absent');
66
+ this.dom.textContent = alt || src;
67
+ return;
68
+ }
69
+ this.dom.classList.remove('image-absent');
70
+ const image = document.createElement('img');
71
+ image.src = presentation.url;
72
+ image.alt = alt;
73
+ if (this.node.attrs.title)
74
+ image.title = String(this.node.attrs.title);
75
+ if (width != null)
76
+ image.style.width = `${width}px`;
77
+ this.image = image;
78
+ const handle = document.createElement('span');
79
+ handle.className = 'image-handle';
80
+ handle.addEventListener('pointerdown', this.startResize);
81
+ this.dom.append(image, handle);
82
+ }
83
+ startResize = (event) => {
84
+ const image = this.image;
85
+ if (!image || !this.view.editable)
86
+ return;
87
+ event.preventDefault();
88
+ const handle = event.currentTarget;
89
+ const startX = event.clientX;
90
+ const startWidth = image.getBoundingClientRect().width;
91
+ const limit = this.view.dom.clientWidth;
92
+ handle.setPointerCapture?.(event.pointerId);
93
+ const move = (moved) => {
94
+ const next = Math.round(Math.min(limit, Math.max(minimumWidth, startWidth + (moved.clientX - startX))));
95
+ this.width = next;
96
+ image.style.width = `${next}px`;
97
+ };
98
+ const cleanup = () => {
99
+ handle.removeEventListener('pointermove', move);
100
+ handle.removeEventListener('pointerup', done);
101
+ handle.removeEventListener('pointercancel', done);
102
+ };
103
+ const done = () => {
104
+ cleanup();
105
+ this.stopResize = undefined;
106
+ this.commit();
107
+ };
108
+ this.stopResize = cleanup;
109
+ handle.addEventListener('pointermove', move);
110
+ handle.addEventListener('pointerup', done);
111
+ handle.addEventListener('pointercancel', done);
112
+ };
113
+ commit() {
114
+ if (!this.view.editable)
115
+ return;
116
+ const pos = this.getPos();
117
+ if (pos == null)
118
+ return;
119
+ const { alt } = splitAlt(String(this.node.attrs.alt ?? ''));
120
+ const next = joinAlt(alt, this.width);
121
+ if (next === this.node.attrs.alt)
122
+ return;
123
+ const { state } = this.view;
124
+ this.view.dispatch(state.tr.setNodeMarkup(pos, undefined, { ...this.node.attrs, alt: next }));
125
+ }
126
+ }
127
+ export function imageView(adapter) {
128
+ return $view(imageSchema.node, () => (node, view, getPos) => new ImageView(node, view, getPos, adapter));
129
+ }
@@ -0,0 +1,5 @@
1
+ export { InkKitEditor } from './editor.js';
2
+ export type { EditorEvents, CaretState, Mark, Block, FormatCommand, Keymap, } from './editor.js';
3
+ export type { TableCommand } from './tables.js';
4
+ export { InkKitError } from './types.js';
5
+ export type { DocumentFormat, DocumentContext, DocumentInput, DocumentSnapshot, EditorErrorCode, ImageAdapter, CapturedImage, PortableImage, ClipboardInput, ClipboardOutput, ClipboardImage, } from './types.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { InkKitEditor } from './editor.js';
2
+ export { InkKitError } from './types.js';
@@ -0,0 +1,13 @@
1
+ import type { Literal } from 'mdast';
2
+ interface InkKitLiteral extends Literal {
3
+ type: 'inkkitLiteral';
4
+ }
5
+ declare module 'mdast' {
6
+ interface RootContentMap {
7
+ inkkitLiteral: InkKitLiteral;
8
+ }
9
+ }
10
+ export declare function createLiteralPreservation(images: boolean): import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
11
+ export declare const preserveLiterals: import("@milkdown/kit/utils").$Remark<"preserveLiterals", unknown>;
12
+ export declare const literalBlock: import("@milkdown/kit/utils").$Node;
13
+ export {};
@@ -0,0 +1,127 @@
1
+ import { $node, $remark } from '@milkdown/kit/utils';
2
+ const unsupported = new Set([
3
+ 'html',
4
+ 'image',
5
+ 'imageReference',
6
+ 'definition',
7
+ 'linkReference',
8
+ ]);
9
+ function restoreInlineBreaks(node) {
10
+ if ((node.type === 'paragraph' || node.type === 'heading') &&
11
+ node.children.length > 1) {
12
+ node.children = node.children.map((child) => child.type === 'html' &&
13
+ ['<br />', '<br>', '<br >', '<br/>'].includes(child.value.trim())
14
+ ? {
15
+ type: 'break',
16
+ data: { inkkitHTMLBreak: true },
17
+ position: child.position,
18
+ }
19
+ : child);
20
+ }
21
+ if ('children' in node)
22
+ node.children.forEach(restoreInlineBreaks);
23
+ }
24
+ function containsUnsupported(node, images, inline = false) {
25
+ // Only a standalone break represents a spacer; inline HTML must remain literal.
26
+ if (!inline &&
27
+ node.type === 'html' &&
28
+ ['<br />', '<br>', '<br >', '<br/>'].includes(node.value.trim()))
29
+ return false;
30
+ if (node.type === 'paragraph' && node.children.length === 1) {
31
+ const child = node.children[0];
32
+ if (child?.type === 'html' &&
33
+ ['<br />', '<br>', '<br >', '<br/>'].includes(child.value.trim()))
34
+ return false;
35
+ }
36
+ return ((unsupported.has(node.type) && !(images && node.type === 'image')) ||
37
+ ('children' in node &&
38
+ node.children.some((child) => containsUnsupported(child, images, inline || node.type === 'paragraph' || node.type === 'heading'))));
39
+ }
40
+ // Keep syntax we cannot faithfully edit as literal text, without making the rest
41
+ // of the document a source editor or allowing HTML and image network requests.
42
+ export function createLiteralPreservation(images) {
43
+ return $remark('preserveLiterals', () => () => (tree, file) => {
44
+ restoreInlineBreaks(tree);
45
+ // Micromark's positions exclude an initial byte-order mark.
46
+ const source = String(file.value).replace(/^\uFEFF/, '');
47
+ const frontmatter = /^(---|\+\+\+)\r?\n[\s\S]*?\r?\n\1(?:\r?\n|$)/.exec(source);
48
+ const frontmatterEnd = frontmatter?.[0].length ?? 0;
49
+ const blocks = [];
50
+ if (frontmatter)
51
+ blocks.push({
52
+ type: 'inkkitLiteral',
53
+ value: frontmatter[0].trimEnd(),
54
+ position: {
55
+ start: { line: 1, column: 1, offset: 0 },
56
+ end: {
57
+ line: 1,
58
+ column: 1,
59
+ offset: frontmatter[0].trimEnd().length,
60
+ },
61
+ },
62
+ });
63
+ for (const node of tree.children) {
64
+ const start = node.position?.start.offset;
65
+ const end = node.position?.end.offset;
66
+ if (start == null || end == null) {
67
+ blocks.push(node);
68
+ continue;
69
+ }
70
+ if (start < frontmatterEnd) {
71
+ // TOML delimiters are ordinary paragraph text to CommonMark, so a
72
+ // paragraph may also contain body text immediately after the header.
73
+ if (end > frontmatterEnd)
74
+ blocks.push({
75
+ type: 'inkkitLiteral',
76
+ value: source.slice(frontmatterEnd, end),
77
+ position: {
78
+ start: { line: 1, column: 1, offset: frontmatterEnd },
79
+ end: node.position.end,
80
+ },
81
+ });
82
+ continue;
83
+ }
84
+ const raw = source.slice(start, end);
85
+ const footnote = node.type !== 'code' && /\[\^[^\]]+\]/.test(raw);
86
+ if (containsUnsupported(node, images) || footnote)
87
+ blocks.push({
88
+ type: 'inkkitLiteral',
89
+ value: raw,
90
+ position: node.position,
91
+ });
92
+ else
93
+ blocks.push(node);
94
+ }
95
+ tree.children = blocks;
96
+ });
97
+ }
98
+ export const preserveLiterals = createLiteralPreservation(false);
99
+ export const literalBlock = $node('literal_markdown', () => ({
100
+ group: 'block',
101
+ content: 'text*',
102
+ marks: '',
103
+ code: true,
104
+ defining: true,
105
+ parseDOM: [
106
+ { tag: 'pre.literal-markdown', preserveWhitespace: 'full', priority: 100 },
107
+ ],
108
+ toDOM: () => [
109
+ 'pre',
110
+ { class: 'literal-markdown', 'aria-label': 'Preserved Markdown' },
111
+ 0,
112
+ ],
113
+ parseMarkdown: {
114
+ match: (node) => node.type === 'inkkitLiteral',
115
+ runner: (state, node, type) => {
116
+ state.openNode(type);
117
+ state.addText(String(node.value ?? ''));
118
+ state.closeNode();
119
+ },
120
+ },
121
+ toMarkdown: {
122
+ match: (node) => node.type.name === 'literal_markdown',
123
+ runner: (state, node) => {
124
+ state.addNode('html', undefined, node.textContent);
125
+ },
126
+ },
127
+ }));
@@ -0,0 +1,23 @@
1
+ import type { Ctx } from '@milkdown/kit/ctx';
2
+ import { type ClipboardInput, type DocumentContext, type ImageAdapter } from './types.js';
3
+ export interface PasteControllerOptions {
4
+ ctx: () => Ctx;
5
+ context: () => DocumentContext;
6
+ adapter?: ImageAdapter;
7
+ onError?: (error: unknown) => void;
8
+ editable?: () => boolean;
9
+ literalText?: () => boolean;
10
+ }
11
+ export declare class PasteController {
12
+ private readonly options;
13
+ private active?;
14
+ private dead;
15
+ get pending(): boolean;
16
+ readonly plugin: import("@milkdown/kit/utils").$Prose;
17
+ constructor(options: PasteControllerOptions);
18
+ paste(input: ClipboardInput): Promise<void>;
19
+ cancelPending(): void;
20
+ destroy(): void;
21
+ private assertContext;
22
+ private run;
23
+ }