@aicayzer/inkkit 0.0.16 → 0.0.18

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/dist/print.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { clipboardContent } from './clipboard.js';
2
2
  import { shareableFragment } from './comments.js';
3
+ import { isSafeLinkTarget } from './link-safety.js';
3
4
  import { fileReference } from './linked-syntax.js';
4
5
  import { sourceFrontmatter } from './literals.js';
5
6
  import { diagramImage, renderDiagram } from './mermaid.js';
@@ -220,15 +221,11 @@ const printClasses = new Set([
220
221
  'inkkit-print-diagram',
221
222
  ]);
222
223
  function safeLink(value) {
223
- if (/[\u0000-\u001f\u007f\\]/.test(value))
224
+ if (value.includes('\\') || !isSafeLinkTarget(value))
224
225
  return undefined;
225
226
  const href = value.trim();
226
227
  if (!href)
227
228
  return undefined;
228
- // A colon in the first path component denotes a scheme, including an
229
- // obfuscated one. Relative paths can still contain colons after a slash.
230
- if (/^[^/?#]*:/.test(href) && !/^(https?:|mailto:)/i.test(href))
231
- return undefined;
232
229
  return href.replaceAll(' ', '%20');
233
230
  }
234
231
  function cleanSemantics(container, localIDs) {
@@ -6,6 +6,8 @@ export interface LocatedDefinition {
6
6
  }
7
7
  export declare const referenceDefinitions: (doc: ProseNode) => Map<string, LocatedDefinition>;
8
8
  export declare const footnoteDefinitions: (doc: ProseNode) => Map<string, LocatedDefinition>;
9
+ /** Replaces each line break, with the whitespace around it, by one space. */
10
+ export declare function singleLine(value: string): string;
9
11
  export declare const remarkReferencesPlugin: import("@milkdown/kit/utils").$Remark<"inkkitReferences", unknown>;
10
12
  export declare const referenceLinks: import("@milkdown/kit/utils").$MarkSchema<"link">;
11
13
  export declare const referenceDefinition: import("@milkdown/kit/utils").$Node;
@@ -1,4 +1,5 @@
1
1
  import { editorLabels } from './labels.js';
2
+ import { isSafeLinkTarget } from './link-safety.js';
2
3
  import { $node, $remark, $prose } from '@milkdown/kit/utils';
3
4
  import { Plugin, PluginKey } from '@milkdown/kit/prose/state';
4
5
  import { linkSchema } from '@milkdown/kit/preset/commonmark';
@@ -36,6 +37,38 @@ function contentShape(value) {
36
37
  };
37
38
  return JSON.stringify(clean(value));
38
39
  }
40
+ /** Replaces each line break, with the whitespace around it, by one space. */
41
+ export function singleLine(value) {
42
+ return value.replace(/[ \t]*[\r\n][ \t\r\n]*/g, ' ');
43
+ }
44
+ function nullableSingleLine(value) {
45
+ return value == null ? value : singleLine(value);
46
+ }
47
+ /**
48
+ * A label is written as its authored text, so one that did not come from
49
+ * Markdown could close its brackets early or span lines. Line breaks become
50
+ * one space and an unescaped bracket is escaped. An authored label has no
51
+ * unescaped bracket, and source preservation keeps an unedited one as written.
52
+ */
53
+ function writableLabel(label) {
54
+ const flat = singleLine(String(label));
55
+ let written = '';
56
+ for (let index = 0; index < flat.length; index++) {
57
+ const character = flat[index];
58
+ if (character === '\\') {
59
+ written += character;
60
+ if (index + 1 < flat.length)
61
+ written += flat[++index];
62
+ else
63
+ written += '\\';
64
+ }
65
+ else if (character === '[' || character === ']')
66
+ written += `\\${character}`;
67
+ else
68
+ written += character;
69
+ }
70
+ return written;
71
+ }
39
72
  const labelPlaceholder = 'inkkit-authored-label';
40
73
  const authoredLinkReference = (untyped, parent, state, info) => {
41
74
  const node = untyped;
@@ -50,19 +83,19 @@ const authoredLinkReference = (untyped, parent, state, info) => {
50
83
  node.referenceContent === contentShape(node.children);
51
84
  if (node.referenceType !== 'full' &&
52
85
  (unchanged || normaliseLabel(display) === normaliseLabel(node.identifier)))
53
- return `[${unchanged ? node.label : display}]${node.referenceType === 'collapsed' ? '[]' : ''}`;
86
+ return `[${unchanged ? writableLabel(node.label) : display}]${node.referenceType === 'collapsed' ? '[]' : ''}`;
54
87
  // Raw entity and escape spelling participates in reference matching.
55
88
  return (full.slice(0, -labelPlaceholder.length - 2) +
56
- `[${node.label ?? node.identifier}]`);
89
+ `[${writableLabel(node.label ?? node.identifier)}]`);
57
90
  };
58
91
  authoredLinkReference.peek = () => '[';
59
92
  const authoredDefinition = (node, parent, state, info) => defaultHandlers
60
93
  .definition({ ...node, label: labelPlaceholder, identifier: labelPlaceholder }, parent, state, info)
61
- .replace(`[${labelPlaceholder}]:`, `[${String(node.label ?? node.identifier)}]:`);
94
+ .replace(`[${labelPlaceholder}]:`, `[${writableLabel(node.label ?? node.identifier)}]:`);
62
95
  const footnoteHandlers = gfmFootnoteToMarkdown().handlers;
63
- const authoredFootnoteReference = (node) => `[^${String(node.label ?? node.identifier)}]`;
96
+ const authoredFootnoteReference = (node) => `[^${writableLabel(node.label ?? node.identifier)}]`;
64
97
  authoredFootnoteReference.peek = () => '[';
65
- const authoredFootnoteDefinition = (node, parent, state, info) => footnoteHandlers.footnoteDefinition({ ...node, label: labelPlaceholder, identifier: labelPlaceholder }, parent, state, info).replace(`[^${labelPlaceholder}]:`, `[^${String(node.label ?? node.identifier)}]:`);
98
+ const authoredFootnoteDefinition = (node, parent, state, info) => footnoteHandlers.footnoteDefinition({ ...node, label: labelPlaceholder, identifier: labelPlaceholder }, parent, state, info).replace(`[^${labelPlaceholder}]:`, `[^${writableLabel(node.label ?? node.identifier)}]:`);
66
99
  function remarkReferences() {
67
100
  const data = this.data();
68
101
  (data.micromarkExtensions ??= []).push(gfmFootnote());
@@ -137,8 +170,13 @@ export const referenceLinks = linkSchema.extendSchema((base) => (ctx) => {
137
170
  tag: 'a[href]',
138
171
  getAttrs(dom) {
139
172
  const element = dom;
173
+ const href = element.getAttribute('href') ?? '';
174
+ // Imported HTML keeps the text of a link it cannot trust. Markdown
175
+ // source does not pass through here, so authored addresses stay.
176
+ if (!isSafeLinkTarget(href))
177
+ return false;
140
178
  return {
141
- href: element.getAttribute('href') ?? '',
179
+ href,
142
180
  title: element.getAttribute('title'),
143
181
  label: element.getAttribute('data-inkkit-label'),
144
182
  identifier: element.getAttribute('data-inkkit-reference'),
@@ -211,12 +249,18 @@ export const referenceDefinition = $node('reference_definition', () => ({
211
249
  parseDOM: [
212
250
  {
213
251
  tag: '[data-inkkit-reference-definition]',
214
- getAttrs: (dom) => ({
215
- label: dom.getAttribute('data-inkkit-label') ?? '',
216
- identifier: dom.getAttribute('data-inkkit-reference-definition') ?? '',
217
- url: dom.getAttribute('data-inkkit-url') ?? '',
218
- title: dom.getAttribute('data-inkkit-title'),
219
- }),
252
+ getAttrs(dom) {
253
+ const url = dom.getAttribute('data-inkkit-url') ?? '';
254
+ if (!isSafeLinkTarget(url))
255
+ return false;
256
+ return {
257
+ label: dom.getAttribute('data-inkkit-label') ?? '',
258
+ identifier: dom.getAttribute('data-inkkit-reference-definition') ?? '',
259
+ url,
260
+ // Saved on one line, as the writer would save it.
261
+ title: nullableSingleLine(dom.getAttribute('data-inkkit-title')),
262
+ };
263
+ },
220
264
  },
221
265
  ],
222
266
  toDOM: (node) => [
@@ -244,7 +288,13 @@ export const referenceDefinition = $node('reference_definition', () => ({
244
288
  toMarkdown: {
245
289
  match: (node) => node.type.name === 'reference_definition',
246
290
  runner: (state, node) => {
247
- state.addNode('definition', undefined, undefined, { ...node.attrs });
291
+ // A line break in a definition title is written as it is, and a blank
292
+ // line would end the definition.
293
+ const title = node.attrs.title;
294
+ state.addNode('definition', undefined, undefined, {
295
+ ...node.attrs,
296
+ title: typeof title === 'string' ? singleLine(title) : title,
297
+ });
248
298
  },
249
299
  },
250
300
  }));
@@ -0,0 +1,15 @@
1
+ /** Rounds a distance away from zero, to a whole number of pixels. */
2
+ export declare const scrollOutward: (distance: number) => number;
3
+ /**
4
+ * Scrolls an element by a distance and returns what it did not scroll. A
5
+ * scroll position is a whole number of pixels in some engines, so a fraction
6
+ * is rounded away from zero: rounded to nothing, a reveal would leave the last
7
+ * fraction of the target outside the element.
8
+ */
9
+ export declare function scrollWholePixels(element: HTMLElement, vertical: number, horizontal: number, padding?: {
10
+ top: number;
11
+ bottom: number;
12
+ }): {
13
+ vertical: number;
14
+ horizontal: number;
15
+ };
package/dist/scroll.js ADDED
@@ -0,0 +1,27 @@
1
+ /** Rounds a distance away from zero, to a whole number of pixels. */
2
+ export const scrollOutward = (distance) => distance > 0 ? Math.ceil(distance) : Math.floor(distance);
3
+ /**
4
+ * Scrolls an element by a distance and returns what it did not scroll. A
5
+ * scroll position is a whole number of pixels in some engines, so a fraction
6
+ * is rounded away from zero: rounded to nothing, a reveal would leave the last
7
+ * fraction of the target outside the element.
8
+ */
9
+ export function scrollWholePixels(element, vertical, horizontal, padding = { top: 0, bottom: 0 }) {
10
+ const top = element.scrollTop, left = element.scrollLeft;
11
+ const target = top + scrollOutward(vertical);
12
+ const last = element.scrollHeight - element.clientHeight;
13
+ // Only padding is left beyond the target, so show it: a reveal that stops
14
+ // at the first or last line would otherwise hide that padding.
15
+ element.scrollTop =
16
+ vertical < 0 && target <= padding.top
17
+ ? 0
18
+ : vertical > 0 && last > 0 && target >= last - padding.bottom
19
+ ? last
20
+ : target;
21
+ element.scrollLeft += scrollOutward(horizontal);
22
+ const remaining = (distance, moved) => distance > 0 ? Math.max(0, distance - moved) : Math.min(0, distance - moved);
23
+ return {
24
+ vertical: remaining(vertical, element.scrollTop - top),
25
+ horizontal: remaining(horizontal, element.scrollLeft - left),
26
+ };
27
+ }
@@ -1,3 +1,8 @@
1
1
  import type { Ctx } from '@milkdown/kit/ctx';
2
2
  import { type ClipboardInput } from './types.js';
3
- export declare function pasteSpreadsheet(ctx: Ctx, input: ClipboardInput): boolean;
3
+ /** Pastes a spreadsheet at the selection, or at `at` for a drop, which
4
+ * always lands outside a table. */
5
+ export declare function pasteSpreadsheet(ctx: Ctx, input: ClipboardInput, at?: {
6
+ from: number;
7
+ to: number;
8
+ }): boolean;
@@ -1,15 +1,20 @@
1
1
  import { editorViewCtx } from '@milkdown/kit/core';
2
- import { DOMParser as ProseParser, Fragment, } from '@milkdown/kit/prose/model';
2
+ import { DOMParser as ProseParser, Fragment, Slice, } from '@milkdown/kit/prose/model';
3
3
  import { isInTable, selectedRect } from '@milkdown/kit/prose/tables';
4
4
  import { InkKitError } from './types.js';
5
5
  import { literalCommentSpans } from './comments.js';
6
+ import { restoreConvertedSpaces } from './paste-html.js';
7
+ import { importableSlice } from './html-import.js';
8
+ import { caretAfter } from './drop-placement.js';
6
9
  import { normalizeTableAlignment } from './table-alignment.js';
7
10
  import { dispatchTableOperation, editableTable, selectTableRectangle, } from './tables.js';
8
- export function pasteSpreadsheet(ctx, input) {
11
+ /** Pastes a spreadsheet at the selection, or at `at` for a drop, which
12
+ * always lands outside a table. */
13
+ export function pasteSpreadsheet(ctx, input, at) {
9
14
  const view = ctx.get(editorViewCtx);
10
15
  if (input.plainText || input.markdown != null)
11
16
  return false;
12
- const inTable = isInTable(view.state);
17
+ const inTable = !at && isInTable(view.state);
13
18
  if (input.html &&
14
19
  /data-inkkit-(?:markdown|reference|footnote|comment)/i.test(input.html)) {
15
20
  if (inTable && /<table[\s>]/i.test(input.html))
@@ -56,13 +61,18 @@ export function pasteSpreadsheet(ctx, input) {
56
61
  }
57
62
  if (cursor < input.html.length)
58
63
  children.push(schema.text(input.html.slice(cursor)));
59
- const literal = schema.nodes.literal_markdown.create(null, children);
64
+ // The source is kept only where it reads back as one literal block.
65
+ const content = importableSlice(ctx, new Slice(Fragment.from(schema.nodes.literal_markdown.create(null, children)), 0, 0)).content;
60
66
  const selection = view.state.selection;
61
- const tr = selection.$from.parent.type.name === 'paragraph' &&
62
- selection.empty &&
63
- selection.$from.parent.content.size === 0
64
- ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), literal)
65
- : view.state.tr.replaceSelectionWith(literal);
67
+ const tr = at
68
+ ? view.state.tr.replaceWith(at.from, at.to, content)
69
+ : selection.$from.parent.type.name === 'paragraph' &&
70
+ selection.empty &&
71
+ selection.$from.parent.content.size === 0
72
+ ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), content)
73
+ : content.childCount === 1
74
+ ? view.state.tr.replaceSelectionWith(content.firstChild)
75
+ : view.state.tr.replaceSelection(new Slice(content, 0, 0));
66
76
  dispatchTableOperation(view, tr);
67
77
  return true;
68
78
  }
@@ -73,11 +83,13 @@ export function pasteSpreadsheet(ctx, input) {
73
83
  const rows = grid.cells.map((row, index) => schema.nodes[index ? 'table_row' : 'table_header_row'].create(null, row.map((content, column) => schema.nodes[index ? 'table_cell' : 'table_header'].create({ alignment: grid.alignment?.[column] ?? 'left' }, schema.nodes.paragraph.create(null, content)))));
74
84
  const table = schema.nodes.table.create(null, rows);
75
85
  const selection = view.state.selection;
76
- const tr = selection.$from.parent.type.name === 'paragraph' &&
77
- selection.empty &&
78
- selection.$from.parent.content.size === 0
79
- ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), table)
80
- : view.state.tr.replaceSelectionWith(table);
86
+ const tr = at
87
+ ? view.state.tr.replaceWith(at.from, at.to, table)
88
+ : selection.$from.parent.type.name === 'paragraph' &&
89
+ selection.empty &&
90
+ selection.$from.parent.content.size === 0
91
+ ? view.state.tr.replaceWith(selection.$from.before(), selection.$from.after(), table)
92
+ : view.state.tr.replaceSelectionWith(table);
81
93
  let tableStart = -1;
82
94
  tr.doc.descendants((node, pos) => {
83
95
  if (node === table)
@@ -85,7 +97,8 @@ export function pasteSpreadsheet(ctx, input) {
85
97
  });
86
98
  if (tableStart < 0)
87
99
  reject();
88
- selectTableRectangle(tr, tableStart, table, 0, 0, grid.cells.length, grid.width);
100
+ // Selecting the new table would let the next keystroke replace it.
101
+ tr.setSelection(caretAfter(tr.doc, tableStart - 1 + table.nodeSize));
89
102
  dispatchTableOperation(view, tr);
90
103
  return true;
91
104
  }
@@ -261,6 +274,7 @@ function htmlGrid(schema, html) {
261
274
  for (const link of cell.querySelectorAll('a'))
262
275
  if (!link.hasAttribute('href'))
263
276
  reject();
277
+ restoreConvertedSpaces(cell);
264
278
  const parsed = parser.parse(cell, { preserveWhitespace: true });
265
279
  if (parsed.childCount === 0)
266
280
  return Fragment.empty;
package/dist/style.css CHANGED
@@ -684,24 +684,16 @@
684
684
  border-radius: 4px;
685
685
  }
686
686
 
687
- /* Syntax colors: One Dark and One Light, without the blues. */
687
+ /* Syntax colors: One Light and One Dark hues without the blues, darkened or
688
+ lightened to WCAG AA contrast on the code background. light-dark() follows
689
+ the editor's color scheme, which a host can force. */
688
690
  .inkkit-root {
689
691
  --_inkkit-code-comment: var(--_inkkit-muted);
690
- --_inkkit-code-keyword: #a626a4;
691
- --_inkkit-code-string: #50a14f;
692
- --_inkkit-code-number: #986801;
693
- --_inkkit-code-title: #c18401;
694
- --_inkkit-code-attr: #e45649;
695
- }
696
-
697
- @media (prefers-color-scheme: dark) {
698
- .inkkit-root {
699
- --_inkkit-code-keyword: #c678dd;
700
- --_inkkit-code-string: #98c379;
701
- --_inkkit-code-number: #d19a66;
702
- --_inkkit-code-title: #e5c07b;
703
- --_inkkit-code-attr: #e06c75;
704
- }
692
+ --_inkkit-code-keyword: light-dark(#a626a4, #c77bde);
693
+ --_inkkit-code-string: light-dark(#3b763a, #98c379);
694
+ --_inkkit-code-number: light-dark(#8d6001, #d19a66);
695
+ --_inkkit-code-title: light-dark(#8d6101, #e5c07b);
696
+ --_inkkit-code-attr: light-dark(#c82c1e, #e37981);
705
697
  }
706
698
 
707
699
  .inkkit-root .hljs-comment,
@@ -898,8 +890,8 @@
898
890
  .inkkit-root .inkkit-mermaid-preview {
899
891
  padding: 16px;
900
892
  overflow: auto;
901
- background: #fff;
902
- color: #222;
893
+ background: var(--inkkit-diagram-background, #fff);
894
+ color: var(--inkkit-diagram-text-color, #222);
903
895
  }
904
896
  .inkkit-root .inkkit-mermaid-preview svg {
905
897
  display: block;
@@ -907,7 +899,7 @@
907
899
  height: auto;
908
900
  }
909
901
  .inkkit-root .inkkit-mermaid-preview[data-state='error'] {
910
- color: #9b2424;
902
+ color: var(--inkkit-diagram-error-color, #9b2424);
911
903
  font-size: calc(var(--inkkit-font-size, 15px) * 0.9);
912
904
  }
913
905
 
package/dist/tables.js CHANGED
@@ -150,9 +150,31 @@ export function tableCommand(ctx, command, options) {
150
150
  view.dispatch(tr);
151
151
  return true;
152
152
  }
153
+ if (command === 'addRowBefore' || command === 'addRowAfter') {
154
+ if (!isInTable(view.state))
155
+ return false;
156
+ const { tableStart } = selectedRect(view.state);
157
+ const add = command === 'addRowBefore' ? addRowBefore : addRowAfter;
158
+ return add(view.state, (tr) => {
159
+ // GFM keeps alignment once per column, so a new row's cells take the
160
+ // header's; otherwise they read back aligned and the edit is refused.
161
+ const table = tr.doc.nodeAt(tableStart - 1);
162
+ const map = TableMap.get(table);
163
+ for (let row = 1; row < map.height; row++)
164
+ for (let col = 0; col < map.width; col++) {
165
+ const offset = map.map[row * map.width + col];
166
+ const cell = table.nodeAt(offset);
167
+ const { alignment } = table.nodeAt(map.map[col]).attrs;
168
+ if (cell.attrs.alignment !== alignment)
169
+ tr.setNodeMarkup(tableStart + offset, undefined, {
170
+ ...cell.attrs,
171
+ alignment,
172
+ });
173
+ }
174
+ view.dispatch(tr);
175
+ });
176
+ }
153
177
  const actions = {
154
- addRowBefore,
155
- addRowAfter,
156
178
  addColumnBefore,
157
179
  addColumnAfter,
158
180
  deleteRow,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,10 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "import": "./dist/index.js"
15
15
  },
16
+ "./core": {
17
+ "types": "./dist/core.d.ts",
18
+ "import": "./dist/core.js"
19
+ },
16
20
  "./style.css": "./dist/style.css"
17
21
  },
18
22
  "types": "./dist/index.d.ts",
@@ -23,7 +27,9 @@
23
27
  "README.md"
24
28
  ],
25
29
  "sideEffects": [
26
- "**/*.css"
30
+ "**/*.css",
31
+ "./dist/index.js",
32
+ "./src/index.ts"
27
33
  ],
28
34
  "publishConfig": {
29
35
  "access": "public",
@@ -43,10 +49,11 @@
43
49
  "build:playground": "vite build playground --config vite.config.ts",
44
50
  "test:package": "node scripts/test-package.mjs",
45
51
  "check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:package",
46
- "test:browser": "playwright test --project=chromium"
52
+ "test:browser": "playwright test --project=chromium",
53
+ "test:built-playground": "playwright test --config playwright.built.config.ts"
47
54
  },
48
55
  "dependencies": {
49
- "@milkdown/kit": "7.22.1",
56
+ "@milkdown/kit": "7.22.3",
50
57
  "dompurify": "3.4.16",
51
58
  "lowlight": "^3.3.0",
52
59
  "mdast-util-from-markdown": "^2.1.0",
@@ -66,6 +73,7 @@
66
73
  "micromark-extension-highlight-mark": "1.2.0",
67
74
  "micromark-util-normalize-identifier": "^2.0.1",
68
75
  "micromark-util-types": "^2.0.3",
76
+ "prosemirror-drop-indicator": "0.1.4",
69
77
  "prosemirror-search": "^1.1.1"
70
78
  },
71
79
  "devDependencies": {