@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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  InkKit is a reusable Markdown editor for simple writing tools and richer document apps. It provides formatted editing, source preservation, portable clipboard content and optional host-managed images, while your app controls its appearance, files and interface.
4
4
 
5
- Write in formatted or source mode with shared undo/redo. Edit tables, task lists, footnotes, reference links, highlights and callouts. Preview bounded Mermaid diagrams offline. Copy readable text and semantic HTML, explicitly copy Markdown source, or capture a complete printable document. Unsupported syntax remains editable literal Markdown.
5
+ Write in formatted or source mode with shared undo/redo. Edit tables, task lists, footnotes, reference links, highlights and callouts. Preview bounded Mermaid diagrams offline. Copy readable text and semantic HTML, explicitly copy Markdown source, or capture a complete printable document. Native hosts can use scoped readable-text ranges and demand-driven geometry with their own search controls and viewport insets. Unsupported syntax remains editable literal Markdown.
6
6
 
7
- **Version status:** 0.0.7 is published. This branch documents the **unpublished 0.0.8 candidate**, adding read-only and text-input policies, command availability, scoped appearance, labels and shortcuts. Installing from npm currently gets the published release; the new controls require the candidate.
7
+ Host controls, read-only and text-input policies, command availability, scoped appearance, labels and shortcuts are available from 0.0.8. Check the [releases](https://github.com/aicayzer/inkkit/releases) for published versions and verification evidence.
8
8
 
9
9
  ```sh
10
10
  npm install @aicayzer/inkkit
@@ -43,6 +43,8 @@ InkKit runs on the web and in offline web views. Hosts use its public TypeScript
43
43
 
44
44
  Start with the [feature guide](docs/supported-syntax.md) and [simple and rich integration examples](docs/integration.md). The [documentation index](docs/README.md) links to precise API, preservation, playground, verification and publication guidance.
45
45
 
46
- Scope and release status live in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [version milestones](https://github.com/aicayzer/inkkit/milestones). Planned search, viewport and media extensions are tracked there; they are not capabilities of this candidate.
46
+ Scope and release status live in the [InkKit Project](https://github.com/users/aicayzer/projects/3) and [version milestones](https://github.com/aicayzer/inkkit/milestones). Optional media extensions are tracked there. Only documented, released features form the supported package contract.
47
47
 
48
48
  MIT licensed. InkKit builds on the public PadPad and Memos editors; see [NOTICE](NOTICE).
49
+
50
+ Optional wiki links and a host-owned asynchronous file adapter support named/path images, audio, video, PDFs and file cards. Authored source and sizing are preserved; portable copy/print uses validated images or documented descriptive fallbacks. See [linked syntax](docs/supported-syntax.md#linked-syntax) and [adapter contracts](docs/api.md#optional-wiki-links-and-files).
package/dist/callouts.js CHANGED
@@ -243,7 +243,7 @@ export const calloutView = $prose((ctx) => new Plugin({
243
243
  key: new PluginKey('inkkitCalloutView'),
244
244
  props: {
245
245
  nodeViews: {
246
- inkkit_callout(node) {
246
+ inkkit_callout(node, view) {
247
247
  const dom = document.createElement('blockquote');
248
248
  dom.className = 'inkkit-callout';
249
249
  dom.setAttribute('data-inkkit-callout', String(node.attrs.kind));
@@ -265,6 +265,7 @@ export const calloutView = $prose((ctx) => new Plugin({
265
265
  .toLowerCase()
266
266
  .replace(/^./, (letter) => letter.toUpperCase());
267
267
  dom.setAttribute('data-inkkit-folded', String(folded));
268
+ view.dom.dispatchEvent(new Event('inkkit-media-policy'));
268
269
  button.hidden = !node.attrs.fold;
269
270
  button.setAttribute('aria-expanded', String(!folded));
270
271
  button.setAttribute('aria-label', `${folded ? editorLabels(ctx).expandCallout : editorLabels(ctx).collapseCallout} ${title.textContent}`);
@@ -1,11 +1,11 @@
1
1
  import { type Fragment, type Schema } from '@milkdown/kit/prose/model';
2
- import type { ClipboardOutput, DocumentContext, ImageAdapter } from './types.js';
2
+ import type { ClipboardOutput, DocumentContext, ImageAdapter, FileAdapter } from './types.js';
3
3
  export interface ClipboardContent {
4
4
  text: string;
5
5
  html: string;
6
6
  }
7
- export declare function clipboardText(content: Fragment, missingImages?: ReadonlySet<string>): string;
7
+ export declare function clipboardText(content: Fragment, missingImages?: ReadonlySet<string>, fileDescriptions?: ReadonlyMap<object, string>): string;
8
8
  export declare function clipboardContent(content: Fragment, schema: Schema): ClipboardContent;
9
9
  export declare const visibleClipboard: import("@milkdown/kit/utils").$Prose;
10
10
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
11
- export declare function portableClipboard(content: Fragment, schema: Schema, markdown: string, adapter: ImageAdapter | undefined, context: DocumentContext, completeDiagrams?: ReadonlySet<string>): Promise<ClipboardOutput>;
11
+ export declare function portableClipboard(content: Fragment, schema: Schema, markdown: string, adapter: ImageAdapter | undefined, context: DocumentContext, completeDiagrams?: ReadonlySet<string>, files?: FileAdapter, signal?: AbortSignal): Promise<ClipboardOutput>;
package/dist/clipboard.js CHANGED
@@ -3,30 +3,35 @@ import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
3
3
  import { $prose } from '@milkdown/kit/utils';
4
4
  import { shareableFragment } from './comments.js';
5
5
  import { diagramImage } from './mermaid.js';
6
+ import { fileReference } from './linked-syntax.js';
7
+ import { portableFile, portableFileImage } from './portable-files.js';
8
+ import { withFileSignal } from './files.js';
6
9
  // Clipboard text is the visible document, not Markdown serialization: escaping
7
10
  // punctuation or trailing spaces here leaks source syntax into other apps.
8
- export function clipboardText(content, missingImages = new Set()) {
11
+ export function clipboardText(content, missingImages = new Set(), fileDescriptions = new Map()) {
9
12
  content = shareableFragment(content);
10
13
  const blocks = [];
11
14
  content.forEach((node) => {
12
15
  if (node.type.name !== 'reference_definition')
13
- blocks.push(nodeText(node, missingImages));
16
+ blocks.push(nodeText(node, missingImages, fileDescriptions));
14
17
  });
15
18
  const inline = content.firstChild?.isInline ?? false;
16
19
  return blocks.join(inline ? '' : '\n\n');
17
20
  }
18
- function nodeText(node, missingImages) {
21
+ function nodeText(node, missingImages, fileDescriptions) {
22
+ if (node.type.name === 'inkkit_wiki_link')
23
+ return String(node.attrs.label || node.attrs.target || 'Link');
19
24
  if (node.type.name === 'inkkit_callout')
20
25
  return `${node.attrs.title ||
21
26
  String(node.attrs.kind)
22
27
  .toLowerCase()
23
- .replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages)}`;
28
+ .replace(/^./, (letter) => letter.toUpperCase())}\n${clipboardText(node.content, missingImages, fileDescriptions)}`;
24
29
  if (node.type.name === 'reference_definition')
25
30
  return '';
26
31
  if (node.type.name === 'footnote_reference')
27
32
  return `[${node.attrs.label}]`;
28
33
  if (node.type.name === 'footnote_definition')
29
- return `[${node.attrs.label}] ${clipboardText(node.content, missingImages)}`;
34
+ return `[${node.attrs.label}] ${clipboardText(node.content, missingImages, fileDescriptions)}`;
30
35
  if (node.isText)
31
36
  return node.text ?? '';
32
37
  if (node.type.name === 'hardbreak' || node.type.name === 'hard_break')
@@ -42,7 +47,7 @@ function nodeText(node, missingImages) {
42
47
  const indent = ' '.repeat(marker.length);
43
48
  const blocks = [];
44
49
  item.forEach((child, _childOffset, childIndex) => {
45
- const text = nodeText(child, missingImages);
50
+ const text = nodeText(child, missingImages, fileDescriptions);
46
51
  const nestedList = child.type.name === 'bullet_list' ||
47
52
  child.type.name === 'ordered_list';
48
53
  const lines = text.split('\n');
@@ -58,7 +63,10 @@ function nodeText(node, missingImages) {
58
63
  return items.join('\n');
59
64
  }
60
65
  if (node.type.name === 'image') {
61
- const alt = String(node.attrs.alt ?? '').replace(/\|\d{1,5}$/, '') || 'Image';
66
+ const description = fileDescriptions.get(node.attrs);
67
+ if (description != null)
68
+ return description;
69
+ const alt = fileReference(node).label || 'Image';
62
70
  return missingImages.has(String(node.attrs.src ?? ''))
63
71
  ? `[${alt}: image unavailable]`
64
72
  : alt;
@@ -67,12 +75,12 @@ function nodeText(node, missingImages) {
67
75
  const rows = [];
68
76
  node.forEach((row) => {
69
77
  const cells = [];
70
- row.forEach((cell) => cells.push(clipboardText(cell.content, missingImages)));
78
+ row.forEach((cell) => cells.push(clipboardText(cell.content, missingImages, fileDescriptions)));
71
79
  rows.push(cells.join('\t'));
72
80
  });
73
81
  return rows.join('\n');
74
82
  }
75
- return clipboardText(node.content, missingImages);
83
+ return clipboardText(node.content, missingImages, fileDescriptions);
76
84
  }
77
85
  export function clipboardContent(content, schema) {
78
86
  content = shareableFragment(content);
@@ -82,6 +90,10 @@ export function clipboardContent(content, schema) {
82
90
  // fetches authored references or temporary presentation URLs.
83
91
  const safe = new DOMSerializer({
84
92
  ...serializer.nodes,
93
+ inkkit_wiki_link: (node) => [
94
+ 'span',
95
+ String(node.attrs.label || node.attrs.target || 'Link'),
96
+ ],
85
97
  image: (node) => [
86
98
  'img',
87
99
  {
@@ -111,31 +123,53 @@ export const visibleClipboard = $prose(() => new Plugin({
111
123
  },
112
124
  }));
113
125
  /** Export a frozen document fragment; presentation URLs never enter the clipboard. */
114
- export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams) {
126
+ export async function portableClipboard(content, schema, markdown, adapter, context, completeDiagrams, files, signal = new AbortController().signal) {
127
+ signal.throwIfAborted();
115
128
  content = shareableFragment(content);
116
129
  const base = clipboardContent(content, schema);
117
130
  const container = document.createElement('div');
118
131
  container.innerHTML = base.html;
119
132
  const images = [];
120
133
  const references = [];
134
+ const fileDescriptions = new Map();
121
135
  content.descendants((node) => {
122
136
  if (node.type.name === 'image')
123
- references.push(String(node.attrs.src ?? ''));
137
+ references.push(node);
124
138
  });
125
139
  const captured = [...container.querySelectorAll('img')].map((element, index) => {
126
- const reference = references[index] ?? '';
127
- const alt = element.getAttribute('alt')?.replace(/\|\d{1,5}$/, '') || 'Image';
140
+ const node = references[index];
141
+ const reference = String(node?.attrs.src ?? '');
142
+ const { label: authoredAlt, width } = fileReference(node);
143
+ const alt = authoredAlt || 'Image';
144
+ if (width != null && width > 0)
145
+ element.setAttribute('width', String(Math.min(4096, width)));
128
146
  element.removeAttribute('src');
129
147
  element.removeAttribute('srcset');
130
- return { element, reference, alt };
148
+ return { element, node, reference, alt };
131
149
  });
132
- await Promise.all(captured.map(async ({ element, reference, alt }, index) => {
150
+ await Promise.all(captured.map(async ({ element, node, reference, alt }, index) => {
151
+ let label = alt;
133
152
  element.setAttribute('data-inkkit-image-slot', String(index));
134
153
  element.setAttribute('data-inkkit-authored-image-slot', String(index));
135
154
  try {
136
- if (!adapter)
155
+ const file = files
156
+ ? await portableFile(node, files, context, signal)
157
+ : undefined;
158
+ if (file && file.presentation.kind !== 'image') {
159
+ const fallback = document.createElement('span');
160
+ fallback.textContent = file.description;
161
+ element.replaceWith(fallback);
162
+ fileDescriptions.set(node.attrs, file.description);
163
+ return;
164
+ }
165
+ label = file?.label || alt;
166
+ if (!files && !adapter)
137
167
  throw new Error('The image is unavailable.');
138
- const image = await adapter.exportImage(reference, { ...context });
168
+ const input = file
169
+ ? await portableFileImage(file, files, adapter, context, signal)
170
+ : await withFileSignal(adapter.exportImage(reference, { ...context }), signal);
171
+ signal.throwIfAborted();
172
+ const image = { ...input, bytes: new Uint8Array(input.bytes) };
139
173
  if (!/^image\/(png|jpeg|gif|webp|avif|bmp|tiff)$/i.test(image.mimeType))
140
174
  throw new Error('The image format cannot be exported.');
141
175
  let binary = '';
@@ -143,15 +177,18 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
143
177
  binary += String.fromCharCode(byte);
144
178
  const portable = `data:${image.mimeType};base64,${btoa(binary)}`;
145
179
  element.setAttribute('src', portable);
146
- element.setAttribute('alt', alt);
147
- images[index] = { reference, alt, image };
180
+ element.setAttribute('alt', label);
181
+ fileDescriptions.set(node.attrs, label);
182
+ images[index] = { reference, alt: label, image };
148
183
  }
149
184
  catch (error) {
185
+ signal.throwIfAborted();
150
186
  const message = error instanceof Error ? error.message : 'The image is unavailable.';
151
187
  const warning = document.createElement('span');
152
- warning.textContent = `[${alt}: image unavailable]`;
188
+ warning.textContent = `[${label}: image unavailable]`;
153
189
  element.replaceWith(warning);
154
- images[index] = { reference, alt, error: message };
190
+ fileDescriptions.set(node.attrs, warning.textContent);
191
+ images[index] = { reference, alt: label, error: message };
155
192
  }
156
193
  }));
157
194
  const diagrams = [];
@@ -162,7 +199,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
162
199
  const result = { source };
163
200
  diagrams.push(result);
164
201
  try {
165
- const image = await diagramImage(source);
202
+ const image = await withFileSignal(diagramImage(source), signal);
166
203
  let binary = '';
167
204
  for (const byte of image.bytes)
168
205
  binary += String.fromCharCode(byte);
@@ -179,6 +216,7 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
179
216
  result.image = image;
180
217
  }
181
218
  catch (error) {
219
+ signal.throwIfAborted();
182
220
  result.error =
183
221
  error instanceof Error
184
222
  ? error.message
@@ -211,12 +249,13 @@ export async function portableClipboard(content, schema, markdown, adapter, cont
211
249
  /^javascript:/i.test(attribute.value)))
212
250
  element.removeAttribute(attribute.name);
213
251
  }
214
- const missing = new Set(images.filter((image) => image.error).map((image) => image.reference));
252
+ const missing = new Set(images.filter((image) => image?.error).map((image) => image.reference));
253
+ signal.throwIfAborted();
215
254
  return {
216
- text: clipboardText(content, missing),
255
+ text: clipboardText(content, missing, fileDescriptions),
217
256
  html: container.innerHTML,
218
257
  markdown,
219
- images: [...orderedImages, ...images.filter((image) => image.error)],
258
+ images: [...orderedImages, ...images.filter((image) => image?.error)],
220
259
  diagrams,
221
260
  };
222
261
  }
package/dist/dialect.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { Node as ProseNode } from '@milkdown/kit/prose/model';
3
3
  import { type Options as StringifyOptions } from 'mdast-util-to-markdown';
4
4
  export declare const remarkDialectPlugin: import("@milkdown/kit/utils").$Remark<"remarkDialect", unknown>;
5
5
  export declare const tables: MilkdownPlugin[];
6
- export declare function createDialect(images: boolean): MilkdownPlugin[];
6
+ export declare function createDialect(images: boolean, wikiLinks?: boolean, files?: boolean): MilkdownPlugin[];
7
7
  export declare const dialect: MilkdownPlugin[];
8
8
  export declare const stringifyOptions: StringifyOptions;
9
9
  export declare function serialize(ctx: Ctx, doc?: ProseNode): string;
package/dist/dialect.js CHANGED
@@ -4,6 +4,7 @@ import { extendListItemSchemaForTask, strikethroughAttr, strikethroughInputRule,
4
4
  import { $node, $remark } from '@milkdown/kit/utils';
5
5
  import { autolinkInputRule } from './autolink.js';
6
6
  import { sourceAttribute } from './source.js';
7
+ import { linkedSyntax, wikiLinkSchema, namedFileMarkdown, fileReference, } from './linked-syntax.js';
7
8
  import { normalizeTableAlignment, normalizeTableColumnAlignment, } from './table-alignment.js';
8
9
  import { preservingCodeBlocks, remarkCodeFences, fencedCodeHandler, } from './code-fences.js';
9
10
  import { literalBlock, createLiteralPreservation } from './literals.js';
@@ -178,6 +179,14 @@ const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
178
179
  const schema = base(ctx);
179
180
  return {
180
181
  ...schema,
182
+ attrs: {
183
+ ...schema.attrs,
184
+ inkkitFileRaw: { default: null },
185
+ inkkitFileTarget: { default: null },
186
+ inkkitFileFragment: { default: null },
187
+ inkkitFileWidth: { default: null },
188
+ inkkitFileInTable: { default: false },
189
+ },
181
190
  parseDOM: [
182
191
  {
183
192
  tag: 'img[src]',
@@ -197,6 +206,29 @@ const normalizedImages = imageSchema.extendSchema((base) => (ctx) => {
197
206
  src: String(node.url ?? ''),
198
207
  alt: String(node.alt ?? ''),
199
208
  title: String(node.title ?? ''),
209
+ inkkitFileRaw: node.inkkitFileRaw ?? null,
210
+ inkkitFileTarget: node.inkkitFileTarget ?? null,
211
+ inkkitFileFragment: node.inkkitFileFragment ?? null,
212
+ inkkitFileWidth: node.inkkitFileWidth ?? null,
213
+ inkkitFileInTable: node.inkkitFileInTable ?? false,
214
+ });
215
+ },
216
+ },
217
+ toMarkdown: {
218
+ ...schema.toMarkdown,
219
+ runner(state, node) {
220
+ const raw = namedFileMarkdown(node, Boolean(node.attrs.inkkitFileInTable));
221
+ if (raw == null)
222
+ return schema.toMarkdown.runner(state, node);
223
+ state.addNode('image', undefined, undefined, {
224
+ url: node.attrs.src,
225
+ alt: node.attrs.alt,
226
+ title: node.attrs.title || null,
227
+ inkkitFileRaw: raw,
228
+ inkkitFileTarget: node.attrs.inkkitFileTarget,
229
+ inkkitFileFragment: node.attrs.inkkitFileFragment,
230
+ inkkitFileWidth: fileReference(node).width ?? null,
231
+ inkkitFileInTable: node.attrs.inkkitFileInTable,
200
232
  });
201
233
  },
202
234
  },
@@ -266,14 +298,15 @@ export const tables = [
266
298
  setAlignCommand,
267
299
  exitTable,
268
300
  ].flat();
269
- export function createDialect(images) {
301
+ export function createDialect(images, wikiLinks = false, files = false) {
270
302
  return [
271
303
  remarkCodeFences,
272
304
  remarkReferencesPlugin,
273
305
  remarkCalloutsPlugin,
274
306
  remarkCommentsPlugin,
307
+ wikiLinks || files ? linkedSyntax(wikiLinks, files) : [],
275
308
  // Protect unsupported inline HTML before the empty-line plugin consumes break nodes.
276
- createLiteralPreservation(images),
309
+ createLiteralPreservation(images || files),
277
310
  commonmarkWithLiteralReferences,
278
311
  $node('doc', () => ({
279
312
  ...docSchema.schema,
@@ -283,6 +316,7 @@ export function createDialect(images) {
283
316
  },
284
317
  })),
285
318
  references,
319
+ wikiLinks ? wikiLinkSchema : [],
286
320
  callouts,
287
321
  comments,
288
322
  inlineHighlight,
package/dist/editor.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type EditorLabels } from './labels.js';
2
2
  import { type TableCommand, type TableOptions } from './tables.js';
3
- import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument } from './types.js';
3
+ import { type DocumentInput, type DocumentSnapshot, type ImageAdapter, type FileAdapter, type WikiLinkAdapter, type ClipboardInput, type ClipboardOutput, type PrintableDocument, type TextRange, type ReadableTextSnapshot, type TextRect, type ViewportOptions, type ViewportSnapshot } from './types.js';
4
4
  import { type Heading } from './outline.js';
5
5
  export type Mark = 'bold' | 'italic' | 'strikethrough' | 'highlight' | 'code' | 'link';
6
6
  export type Block = {
@@ -34,6 +34,8 @@ export interface TextInputPreferences {
34
34
  }
35
35
  export interface EditorOptions {
36
36
  images?: ImageAdapter;
37
+ files?: FileAdapter;
38
+ wikiLinks?: WikiLinkAdapter;
37
39
  editable?: boolean;
38
40
  textInput?: TextInputPreferences;
39
41
  labels?: Partial<EditorLabels>;
@@ -89,6 +91,7 @@ export declare class InkKitEditor {
89
91
  private revision;
90
92
  private documentEpoch;
91
93
  private operationSequence;
94
+ private exportControllers;
92
95
  private plainComposing;
93
96
  private plainSearch;
94
97
  private literalMatch?;
@@ -110,6 +113,10 @@ export declare class InkKitEditor {
110
113
  private generation;
111
114
  private reportingSuppressed;
112
115
  private loading;
116
+ private readonly textIdentity;
117
+ private textScope?;
118
+ private viewportContainer?;
119
+ private viewportInsets;
113
120
  private writable;
114
121
  private inputPreferences;
115
122
  private policyEpoch;
@@ -120,6 +127,9 @@ export declare class InkKitEditor {
120
127
  private keys;
121
128
  private constructor();
122
129
  private context;
130
+ private cancelExports;
131
+ private exportContent;
132
+ private mediaPolicy;
123
133
  private assertAlive;
124
134
  private assertCurrent;
125
135
  private assertMutation;
@@ -155,6 +165,22 @@ export declare class InkKitEditor {
155
165
  static mount(root: HTMLElement, events: EditorEvents, options?: EditorOptions): Promise<InkKitEditor>;
156
166
  loadDocument(input: DocumentInput): void;
157
167
  reloadDocument(input: DocumentInput): void;
168
+ setViewport(options: ViewportOptions): void;
169
+ viewport(): ViewportSnapshot;
170
+ private readableScope;
171
+ textSnapshot(expectedGeneration?: number): ReadableTextSnapshot;
172
+ private validateTextRange;
173
+ selectTextRange(range: TextRange, options?: {
174
+ focus?: boolean;
175
+ reveal?: boolean;
176
+ }): void;
177
+ replaceTextRange(range: TextRange, text: string): boolean;
178
+ textRangeRects(range: TextRange): readonly TextRect[];
179
+ visibleTextRanges(snapshotId: string): readonly TextRange[];
180
+ revealTextRange(range: TextRange): void;
181
+ private revealRects;
182
+ private revealCurrentSelection;
183
+ private revealLiteralSelection;
158
184
  private load;
159
185
  find(text: string, expectedGeneration?: number): void;
160
186
  replace(search: string, replacement: string, expectedGeneration?: number): boolean;