@aicayzer/inkkit 0.0.9 → 0.0.11

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
@@ -39,10 +39,14 @@ editor.loadDocument({
39
39
  const snapshot = editor.snapshot(1)
40
40
  ```
41
41
 
42
- InkKit runs on the web and in offline web views. Hosts use its public TypeScript facade and own toolbars, menus, persistence, clipboard bridges, Print/PDF actions and image storage. Images require an adapter; private references stay opaque. TXT stays literal. RTF files and richer media are outside current support.
42
+ InkKit runs on the web and in offline web views. Hosts use its public TypeScript facade and own toolbars, menus, persistence, clipboard bridges, Print/PDF actions and image storage. Images require an adapter; private references stay opaque. TXT stays literal. RTF document editing is outside current support.
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). Optional media extensions are tracked there. Only documented, released features form the supported package contract.
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). 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).
51
+
52
+ From 0.0.11, minimal hosts can mount with `rendering: { codeHighlighting: false, diagramPreview: false }` to skip optional presentation work. Source, shared history and explicit portable exports retain their contracts. Mermaid loads on first actual render. Single-file offline bundles still contain the rendering code; see [performance measurements](docs/performance.md).
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, type TextRange, type ReadableTextSnapshot, type TextRect, type ViewportOptions, type ViewportSnapshot } 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,10 +34,16 @@ 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>;
40
42
  keymap?: Keymap;
43
+ rendering?: {
44
+ codeHighlighting?: boolean;
45
+ diagramPreview?: boolean;
46
+ };
41
47
  }
42
48
  export interface CommandState {
43
49
  documentId: string;
@@ -89,12 +95,14 @@ export declare class InkKitEditor {
89
95
  private revision;
90
96
  private documentEpoch;
91
97
  private operationSequence;
98
+ private exportControllers;
92
99
  private plainComposing;
93
100
  private plainSearch;
94
101
  private literalMatch?;
95
102
  private beforePlainInput?;
96
103
  private mode;
97
104
  private sourcePreservations;
105
+ private sourceTexts;
98
106
  private outlineEpoch;
99
107
  private ready;
100
108
  private destroyed;
@@ -124,6 +132,9 @@ export declare class InkKitEditor {
124
132
  private keys;
125
133
  private constructor();
126
134
  private context;
135
+ private cancelExports;
136
+ private exportContent;
137
+ private mediaPolicy;
127
138
  private assertAlive;
128
139
  private assertCurrent;
129
140
  private assertMutation;