@aicayzer/inkkit 0.0.2 → 0.0.3

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 CHANGED
@@ -33,10 +33,10 @@ editor.loadDocument({
33
33
  const snapshot = editor.snapshot(1)
34
34
  ```
35
35
 
36
- Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
36
+ Markdown includes CommonMark, task lists, strikethrough, autolinks, GFM tables, editable footnotes and reference-style links, bounded callouts, highlights and author comments. Unsupported constructs remain literal Markdown. TXT mode treats all text literally. Images require a host adapter. RTF files remain outside the package scope.
37
37
 
38
- See [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
38
+ See [supported syntax](docs/supported-syntax.md), [the host API](docs/api.md), [preservation and clipboard behavior](docs/preservation.md), [development](docs/development.md), and [releases](docs/releasing.md).
39
39
 
40
- Planned work and release scope are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones). Releases 0.0.2 and 0.0.3 are authorised for sequential delivery; targets from 0.0.4 remain provisional.
40
+ Planned work and release scope are tracked in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [milestones](https://github.com/aicayzer/inkkit/milestones). Release 0.0.2 is published and verified; 0.0.3 delivery is authorised. Targets from 0.0.4 remain provisional.
41
41
 
42
42
  MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
@@ -0,0 +1,24 @@
1
+ import type { Parent, RootContent } from 'mdast';
2
+ interface Callout extends Parent {
3
+ type: 'inkkitCallout';
4
+ kind: string;
5
+ marker: string;
6
+ fold: string;
7
+ title: string;
8
+ titleSource: string;
9
+ listPlaceholder: boolean;
10
+ children: RootContent[];
11
+ }
12
+ declare module 'mdast' {
13
+ interface BlockContentMap {
14
+ inkkitCallout: Callout;
15
+ }
16
+ interface RootContentMap {
17
+ inkkitCallout: Callout;
18
+ }
19
+ }
20
+ export declare const remarkCalloutsPlugin: import("@milkdown/kit/utils").$Remark<"inkkitCallouts", unknown>;
21
+ export declare const calloutSchema: import("@milkdown/kit/utils").$Node;
22
+ export declare const calloutView: import("@milkdown/kit/utils").$Prose;
23
+ export declare const callouts: (import("@milkdown/kit/utils").$Node | import("@milkdown/kit/utils").$Prose)[];
24
+ export {};
@@ -0,0 +1,296 @@
1
+ import { $node, $prose, $remark } from '@milkdown/kit/utils';
2
+ import { remarkCtx } from '@milkdown/kit/core';
3
+ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
4
+ import { defaultHandlers } from 'mdast-util-to-markdown';
5
+ const kinds = new Set(['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION']);
6
+ function plainHeader(processor, source) {
7
+ const node = processor.parse(source).children[0];
8
+ return node?.type === 'paragraph' &&
9
+ node.children.length === 1 &&
10
+ node.children[0]?.type === 'text'
11
+ ? node.children[0].value
12
+ : undefined;
13
+ }
14
+ const calloutHandler = (untyped, parent, state, info) => {
15
+ const node = untyped;
16
+ const body = defaultHandlers.blockquote({ type: 'blockquote', children: node.children }, parent, state, info);
17
+ const title = node.titleSource ?? node.title;
18
+ return `> ${node.marker}${node.fold}${title ? ` ${title}` : ''}\n${body}`;
19
+ };
20
+ function remarkCallouts() {
21
+ const processor = this;
22
+ const data = this.data();
23
+ (data.toMarkdownExtensions ??= []).push({
24
+ handlers: { inkkitCallout: calloutHandler },
25
+ });
26
+ return (tree, file) => {
27
+ const source = String(file.value).replace(/^\uFEFF/, '');
28
+ const visit = (node, parent, index) => {
29
+ if (node.type === 'blockquote') {
30
+ const start = node.position?.start.offset, end = node.position?.end.offset;
31
+ const raw = start == null || end == null ? '' : source.slice(start, end);
32
+ const header = /^>[ \t]*([\s\S]*?)(?:\r?\n|$)/.exec(raw)?.[1] ?? '';
33
+ if (header.startsWith('[!')) {
34
+ const match = /^(\[!([A-Za-z]+)\])([+-]?)(?:[ \t]+([^\r\n]*))?$/.exec(header);
35
+ const first = node.children[0];
36
+ const decodedHeader = plainHeader(processor, header);
37
+ // A bounded header occupies one unformatted line; other variants stay source.
38
+ if (!match ||
39
+ decodedHeader == null ||
40
+ !kinds.has(match[2].toUpperCase()) ||
41
+ /^[+-](?:[ \t]|[+-])/.test(match[4] ?? '') ||
42
+ first?.type !== 'paragraph' ||
43
+ first.children[0]?.type !== 'text')
44
+ return {
45
+ type: 'inkkitLiteral',
46
+ value: raw,
47
+ position: node.position,
48
+ };
49
+ const text = first.children[0];
50
+ // Entities can decode to newlines without ending the authored header line.
51
+ const ending = /^\r?\n/.exec(text.value.slice(decodedHeader.length));
52
+ const lineEnd = ending ? decodedHeader.length : -1;
53
+ if (lineEnd < 0 && first.children.length > 1)
54
+ return {
55
+ type: 'inkkitLiteral',
56
+ value: raw,
57
+ position: node.position,
58
+ };
59
+ const children = [...node.children];
60
+ if (lineEnd >= 0) {
61
+ const remaining = text.value.slice(lineEnd + ending[0].length);
62
+ const firstStart = text.position?.start.offset;
63
+ const token = firstStart == null
64
+ ? ''
65
+ : source.slice(firstStart, text.position?.end.offset);
66
+ const boundary = /\r?\n[ \t]*>[ \t]?/.exec(token);
67
+ const bodyStart = firstStart == null || !boundary
68
+ ? undefined
69
+ : firstStart + boundary.index + boundary[0].length;
70
+ const point = bodyStart == null
71
+ ? undefined
72
+ : {
73
+ line: source.slice(0, bodyStart).split('\n').length,
74
+ column: bodyStart - source.lastIndexOf('\n', bodyStart - 1),
75
+ offset: bodyStart,
76
+ };
77
+ children[0] = {
78
+ ...first,
79
+ children: [
80
+ ...(remaining
81
+ ? [
82
+ {
83
+ ...text,
84
+ value: remaining,
85
+ position: point && text.position
86
+ ? { start: point, end: text.position.end }
87
+ : undefined,
88
+ },
89
+ ]
90
+ : []),
91
+ ...first.children.slice(1),
92
+ ],
93
+ };
94
+ }
95
+ else
96
+ children.shift();
97
+ if (!children.length)
98
+ children.push({ type: 'paragraph', children: [] });
99
+ return {
100
+ type: 'inkkitCallout',
101
+ kind: match[2].toUpperCase(),
102
+ marker: match[1],
103
+ fold: match[3],
104
+ title: decodedHeader
105
+ .slice(header.length - (match[4]?.length ?? 0))
106
+ .trimEnd(),
107
+ titleSource: match[4]?.trimEnd() ?? '',
108
+ listPlaceholder: parent?.type === 'listItem' && index === 0,
109
+ children: children.map((child) => visit(child)),
110
+ position: node.position,
111
+ };
112
+ }
113
+ }
114
+ if ('children' in node)
115
+ node.children = node.children.map((child, index) => visit(child, node, index));
116
+ return node;
117
+ };
118
+ tree.children = tree.children.map((node) => visit(node));
119
+ };
120
+ }
121
+ export const remarkCalloutsPlugin = $remark('inkkitCallouts', () => remarkCallouts);
122
+ export const calloutSchema = $node('inkkit_callout', (ctx) => ({
123
+ group: 'block',
124
+ content: 'block+',
125
+ defining: true,
126
+ attrs: {
127
+ kind: { default: 'NOTE' },
128
+ marker: { default: '[!NOTE]' },
129
+ fold: { default: '' },
130
+ title: { default: '' },
131
+ titleSource: { default: null },
132
+ listPlaceholder: { default: false },
133
+ },
134
+ parseDOM: [
135
+ {
136
+ tag: 'blockquote[data-inkkit-callout]',
137
+ priority: 100,
138
+ contentElement: '.inkkit-callout-body',
139
+ getAttrs: (dom) => {
140
+ const kind = (dom.getAttribute('data-inkkit-callout') ?? '').toUpperCase();
141
+ const marker = dom.getAttribute('data-inkkit-marker') ?? `[!${kind}]`;
142
+ const fold = dom.getAttribute('data-inkkit-fold') ?? '';
143
+ const titleSource = dom.getAttribute('data-inkkit-title');
144
+ const placeholder = dom.getAttribute('data-inkkit-list-placeholder');
145
+ const previous = dom.previousElementSibling;
146
+ const titleHeader = titleSource == null
147
+ ? undefined
148
+ : plainHeader(ctx.get(remarkCtx), `${marker} ${titleSource}`);
149
+ if (!kinds.has(kind) ||
150
+ !/^\[![A-Za-z]+\]$/.test(marker) ||
151
+ marker.slice(2, -1).toUpperCase() !== kind ||
152
+ !['', '+', '-'].includes(fold) ||
153
+ (titleSource != null &&
154
+ (/[\r\n]/.test(titleSource) || titleHeader == null)))
155
+ return false;
156
+ return {
157
+ kind,
158
+ marker,
159
+ fold,
160
+ title: titleHeader == null
161
+ ? (dom.querySelector('.inkkit-callout-title')?.textContent ?? '')
162
+ : titleHeader.slice(marker.length + 1).trimEnd(),
163
+ titleSource,
164
+ listPlaceholder: placeholder == null
165
+ ? dom.parentElement?.tagName === 'LI' &&
166
+ (!previous ||
167
+ (previous.tagName === 'P' &&
168
+ !previous.textContent &&
169
+ !previous.childElementCount))
170
+ : placeholder === 'true',
171
+ };
172
+ },
173
+ },
174
+ ],
175
+ toDOM: (node) => [
176
+ 'blockquote',
177
+ {
178
+ class: 'inkkit-callout',
179
+ 'data-inkkit-callout': node.attrs.kind,
180
+ 'data-inkkit-marker': node.attrs.marker,
181
+ 'data-inkkit-fold': node.attrs.fold,
182
+ 'data-inkkit-title': node.attrs.titleSource ?? node.attrs.title,
183
+ 'data-inkkit-list-placeholder': String(node.attrs.listPlaceholder),
184
+ },
185
+ [
186
+ 'strong',
187
+ { class: 'inkkit-callout-title' },
188
+ node.attrs.title ||
189
+ String(node.attrs.kind)
190
+ .toLowerCase()
191
+ .replace(/^./, (letter) => letter.toUpperCase()),
192
+ ],
193
+ ['div', { class: 'inkkit-callout-body' }, 0],
194
+ ],
195
+ parseMarkdown: {
196
+ match: (node) => node.type === 'inkkitCallout',
197
+ runner(state, node, type) {
198
+ state.openNode(type, {
199
+ kind: node.kind,
200
+ marker: node.marker,
201
+ fold: node.fold,
202
+ title: node.title,
203
+ titleSource: node.titleSource,
204
+ listPlaceholder: node.listPlaceholder,
205
+ });
206
+ state.next(node.children);
207
+ state.closeNode();
208
+ },
209
+ },
210
+ toMarkdown: {
211
+ match: (node) => node.type.name === 'inkkit_callout',
212
+ runner(state, node) {
213
+ const parent = state.top();
214
+ const first = parent?.children?.[0];
215
+ // Lists require a leading paragraph even when their authored first block is a callout.
216
+ if (node.attrs.listPlaceholder &&
217
+ parent?.type === 'listItem' &&
218
+ parent.children?.length === 1 &&
219
+ first?.type === 'paragraph' &&
220
+ first.children?.length === 1 &&
221
+ first.children[0]?.type === 'html' &&
222
+ first.children[0].value === '<br />')
223
+ parent.children = [];
224
+ state.openNode('inkkitCallout', undefined, { ...node.attrs });
225
+ state.next(node.content);
226
+ state.closeNode();
227
+ },
228
+ },
229
+ }));
230
+ export const calloutView = $prose(() => new Plugin({
231
+ key: new PluginKey('inkkitCalloutView'),
232
+ props: {
233
+ nodeViews: {
234
+ inkkit_callout(node) {
235
+ const dom = document.createElement('blockquote');
236
+ dom.className = 'inkkit-callout';
237
+ dom.setAttribute('data-inkkit-callout', String(node.attrs.kind));
238
+ const header = document.createElement('div');
239
+ header.className = 'inkkit-callout-header';
240
+ header.contentEditable = 'false';
241
+ const title = document.createElement('strong');
242
+ title.className = 'inkkit-callout-title';
243
+ const contentDOM = document.createElement('div');
244
+ contentDOM.className = 'inkkit-callout-body';
245
+ let folded = node.attrs.fold === '-';
246
+ const button = document.createElement('button');
247
+ button.type = 'button';
248
+ button.setAttribute('data-inkkit-callout-toggle', '');
249
+ const render = () => {
250
+ title.textContent =
251
+ node.attrs.title ||
252
+ String(node.attrs.kind)
253
+ .toLowerCase()
254
+ .replace(/^./, (letter) => letter.toUpperCase());
255
+ dom.setAttribute('data-inkkit-folded', String(folded));
256
+ button.hidden = !node.attrs.fold;
257
+ button.setAttribute('aria-expanded', String(!folded));
258
+ button.setAttribute('aria-label', `${folded ? 'Expand' : 'Collapse'} ${title.textContent}`);
259
+ button.textContent = folded ? 'Expand' : 'Collapse';
260
+ };
261
+ const toggle = () => {
262
+ folded = !folded;
263
+ render();
264
+ };
265
+ button.addEventListener('click', toggle);
266
+ button.addEventListener('keydown', (event) => {
267
+ if (event.key === 'Enter' || event.key === ' ') {
268
+ event.preventDefault();
269
+ event.stopPropagation();
270
+ toggle();
271
+ }
272
+ });
273
+ header.append(title, button);
274
+ dom.append(header, contentDOM);
275
+ render();
276
+ return {
277
+ dom,
278
+ contentDOM,
279
+ update(next) {
280
+ if (next.type !== node.type)
281
+ return false;
282
+ if (next.attrs.fold !== node.attrs.fold)
283
+ folded = next.attrs.fold === '-';
284
+ node = next;
285
+ render();
286
+ return true;
287
+ },
288
+ stopEvent: (event) => header.contains(event.target),
289
+ ignoreMutation: (mutation) => mutation.type !== 'selection' &&
290
+ !contentDOM.contains(mutation.target),
291
+ };
292
+ },
293
+ },
294
+ },
295
+ }));
296
+ export const callouts = [calloutSchema, calloutView].flat();
package/dist/clipboard.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { DOMSerializer, } from '@milkdown/kit/prose/model';
2
2
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
3
  import { $prose } from '@milkdown/kit/utils';
4
+ import { shareableFragment } from './comments.js';
4
5
  // Clipboard text is the visible document, not Markdown serialization: escaping
5
6
  // punctuation or trailing spaces here leaks source syntax into other apps.
6
7
  export function clipboardText(content, missingImages = new Set()) {
8
+ content = shareableFragment(content);
7
9
  const blocks = [];
8
10
  content.forEach((node) => {
9
11
  if (node.type.name !== 'reference_definition')
@@ -13,6 +15,11 @@ export function clipboardText(content, missingImages = new Set()) {
13
15
  return blocks.join(inline ? '' : '\n\n');
14
16
  }
15
17
  function nodeText(node, missingImages) {
18
+ if (node.type.name === 'inkkit_callout')
19
+ return `${node.attrs.title ||
20
+ String(node.attrs.kind)
21
+ .toLowerCase()
22
+ .replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages)}`;
16
23
  if (node.type.name === 'reference_definition')
17
24
  return '';
18
25
  if (node.type.name === 'footnote_reference')
@@ -67,6 +74,7 @@ function nodeText(node, missingImages) {
67
74
  return clipboardText(node.content, missingImages);
68
75
  }
69
76
  export function clipboardContent(content, schema) {
77
+ content = shareableFragment(content);
70
78
  const container = document.createElement('div');
71
79
  const serializer = DOMSerializer.fromSchema(schema);
72
80
  // Construct image elements without a source so detached clipboard DOM never
@@ -103,6 +111,7 @@ export const visibleClipboard = $prose(() => new Plugin({
103
111
  }));
104
112
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
105
113
  export async function portableClipboard(content, schema, markdown, adapter, context) {
114
+ content = shareableFragment(content);
106
115
  const base = clipboardContent(content, schema);
107
116
  const container = document.createElement('div');
108
117
  container.innerHTML = base.html;
@@ -0,0 +1,55 @@
1
+ import { Fragment, type Node as ProseNode } from '@milkdown/kit/prose/model';
2
+ import type { EditorView } from '@milkdown/kit/prose/view';
3
+ import type { Literal } from 'mdast';
4
+ export type CommentSyntax = 'html' | 'obsidian';
5
+ interface Comment extends Literal {
6
+ type: 'inkkitCommentInline' | 'inkkitCommentBlock';
7
+ syntax: CommentSyntax;
8
+ block?: boolean;
9
+ }
10
+ declare module 'mdast' {
11
+ interface PhrasingContentMap {
12
+ inkkitCommentInline: Comment & {
13
+ type: 'inkkitCommentInline';
14
+ };
15
+ }
16
+ interface BlockContentMap {
17
+ inkkitCommentBlock: Comment & {
18
+ type: 'inkkitCommentBlock';
19
+ };
20
+ }
21
+ interface RootContentMap {
22
+ inkkitCommentInline: Comment & {
23
+ type: 'inkkitCommentInline';
24
+ };
25
+ inkkitCommentBlock: Comment & {
26
+ type: 'inkkitCommentBlock';
27
+ };
28
+ }
29
+ }
30
+ declare module 'micromark-util-types' {
31
+ interface TokenTypeMap {
32
+ inkkitCommentInline: 'inkkitCommentInline';
33
+ inkkitCommentBlock: 'inkkitCommentBlock';
34
+ inkkitCommentData: 'inkkitCommentData';
35
+ }
36
+ }
37
+ export declare function commentSource(node: ProseNode): string;
38
+ export interface CommentSpan {
39
+ start: number;
40
+ end: number;
41
+ syntax: CommentSyntax;
42
+ value: string;
43
+ block?: boolean;
44
+ }
45
+ export declare function literalCommentSpans(value: string): CommentSpan[];
46
+ export declare const remarkCommentsPlugin: import("@milkdown/kit/utils").$Remark<"remarkComments", unknown>;
47
+ export declare const commentInline: import("@milkdown/kit/utils").$Node;
48
+ export declare const commentBlock: import("@milkdown/kit/utils").$Node;
49
+ export declare const commentVisibility: import("@milkdown/kit/utils").$Prose;
50
+ export declare function setCommentVisibility(view: EditorView, visible: boolean): void;
51
+ export declare const comments: (import("@milkdown/kit/utils").$Node | import("@milkdown/kit/utils").$Prose)[];
52
+ export declare function isComment(node: ProseNode): boolean;
53
+ export declare function shareableFragment(content: Fragment): Fragment;
54
+ export declare function commentSelectionContent(doc: ProseNode, from: number, to: number, content: Fragment): Fragment;
55
+ export {};