@aicayzer/inkkit 0.0.2 → 0.0.4

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/preserve.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parserCtx, remarkCtx } from '@milkdown/kit/core';
2
2
  import { serialize } from './dialect.js';
3
+ import { restoreCodeEndings, finalCodeEnding } from './code-fences.js';
3
4
  function signature(node) {
4
5
  return JSON.stringify(node, (key, value) => ['position', 'data', 'url', 'title', 'marker'].includes(key)
5
6
  ? undefined
@@ -10,7 +11,7 @@ function ownShape(node) {
10
11
  ? undefined
11
12
  : value);
12
13
  }
13
- function retainText(raw, before, after) {
14
+ function retainText(raw, before, after, quoteIndent = false) {
14
15
  before = before.replace(/\r\n?/g, '\n');
15
16
  after = after.replace(/\r\n?/g, '\n');
16
17
  let prefix = 0;
@@ -26,6 +27,14 @@ function retainText(raw, before, after) {
26
27
  const offsets = [0];
27
28
  let decoded = '';
28
29
  for (let index = 0; index < raw.length;) {
30
+ if (index > 0 && raw[index - 1] === '\n') {
31
+ const gutter = (quoteIndent ? /^(?:[ \t]*>[ \t]*)+/ : /^(?:[ \t]*>[ \t]?)+/).exec(raw.slice(index));
32
+ if (gutter) {
33
+ index += gutter[0].length;
34
+ offsets[offsets.length - 1] = index;
35
+ continue;
36
+ }
37
+ }
29
38
  const escaped = /^\\([!"#$%&'()*+,\-./:;<=>?@[\]\\^_`{|}~])/.exec(raw.slice(index));
30
39
  const entity = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);/.exec(raw.slice(index));
31
40
  let value;
@@ -53,11 +62,22 @@ function retainText(raw, before, after) {
53
62
  for (let part = 0; part < value.length; part++)
54
63
  offsets.push(index);
55
64
  }
56
- if (decoded !== before)
57
- return after;
65
+ if (decoded !== before) {
66
+ // Continuation indentation is excluded from a quoted paragraph's text.
67
+ return quoteIndent ? after : retainText(raw, before, after, true);
68
+ }
58
69
  const inserted = after
59
70
  .slice(prefix, after.length - suffix)
60
- .replace(/[\\`*_[\]<>]/g, '\\$&');
71
+ .split('')
72
+ .map((character, index) => {
73
+ const at = prefix + index;
74
+ const delimiter = (character === '%' || character === '=') &&
75
+ (after[at - 1] === character || after[at + 1] === character);
76
+ return delimiter || /[\\`*_[\]<>]/.test(character)
77
+ ? `\\${character}`
78
+ : character;
79
+ })
80
+ .join('');
61
81
  return (raw.slice(0, offsets[prefix]) +
62
82
  inserted +
63
83
  raw.slice(offsets[before.length - suffix]));
@@ -96,6 +116,40 @@ function retainSource(before, after, source, canonical, lineEnding) {
96
116
  if (start == null || end == null || newStart == null || newEnd == null)
97
117
  throw new PreservationError('A source token has no location');
98
118
  const raw = source.slice(start, end);
119
+ if (before.type === 'code' &&
120
+ after.type === 'code' &&
121
+ before.lang === after.lang) {
122
+ const opening = /^([ \t>]*)(`{3,}|~{3,})([^\r\n]*)(\r?\n)/.exec(raw);
123
+ if (opening) {
124
+ const lines = raw.slice(opening[0].length).split(/\r?\n/);
125
+ const closing = /^([ \t>]*)(`{3,}|~{3,})([ \t]*)$/.exec(lines.at(-1) ?? '');
126
+ const originalLine = (before.value ?? '').replace(/\r\n?/g, '\n').split('\n')[0] ?? '';
127
+ const first = lines[0] ?? '';
128
+ const gutter = first.endsWith(originalLine)
129
+ ? first.slice(0, first.length - originalLine.length)
130
+ : opening[1];
131
+ const body = (after.value ?? '').replace(/\r\n?/g, '\n').split('\n');
132
+ const character = opening[2][0];
133
+ let length = opening[2].length;
134
+ for (const line of body) {
135
+ const run = new RegExp(`^ {0,3}(${character}+)[ \\t]*$`).exec(line);
136
+ if (run)
137
+ length = Math.max(length, run[1].length + 1);
138
+ }
139
+ const fence = character.repeat(length);
140
+ return (opening[1] +
141
+ fence +
142
+ opening[3] +
143
+ lineEnding +
144
+ body.map((line) => gutter + line).join(lineEnding) +
145
+ (closing
146
+ ? lineEnding +
147
+ closing[1] +
148
+ character.repeat(Math.max(length, closing[2].length)) +
149
+ closing[3]
150
+ : ''));
151
+ }
152
+ }
99
153
  if (before.type === 'definition' &&
100
154
  after.type === 'definition' &&
101
155
  before.label === after.label &&
@@ -174,7 +228,7 @@ export class PreservationError extends Error {
174
228
  }
175
229
  }
176
230
  function form(text) {
177
- return text.replace(/\n+$/, '');
231
+ return text.replace(/\r\n?/g, '\n').replace(/\n+$/, '');
178
232
  }
179
233
  function blockForm(ctx, node) {
180
234
  // An empty document is empty Markdown, but an empty block inside a document
@@ -200,7 +254,7 @@ function semanticSignature(doc) {
200
254
  continue;
201
255
  const token = {
202
256
  ...child,
203
- text,
257
+ text: text.replace(/\r\n?/g, '\n'),
204
258
  marks: index === 1 ? child.marks : undefined,
205
259
  };
206
260
  const previous = tokens.at(-1);
@@ -218,10 +272,12 @@ function semanticSignature(doc) {
218
272
  [
219
273
  'referenceContent',
220
274
  'referenceType',
275
+ 'listPlaceholder',
221
276
  'marker',
222
277
  'id',
223
278
  'label',
224
279
  'spread',
280
+ 'authoredFence',
225
281
  ].includes(key)
226
282
  ? undefined
227
283
  : value));
@@ -378,16 +434,17 @@ export class Preservation {
378
434
  output += retainSource(block.ast, positionalNode(currentRawTree, currentTree.children[index]), body, canonical, this.lineEnding);
379
435
  }
380
436
  else
381
- output += text.replaceAll('\n', this.lineEnding);
437
+ output += restoreCodeEndings(this.ctx, doc.type.create(null, [doc.child(index)]), text.replaceAll('\n', this.lineEnding));
382
438
  previous = position;
383
439
  });
384
440
  if (previous === this.blocks.length - 1 && this.blocks.length)
385
441
  output += this.source.slice(this.blocks[previous].end);
386
442
  else if (output)
387
- output += this.lineEnding;
443
+ output += finalCodeEnding(doc) ?? this.lineEnding;
388
444
  const parse = this.ctx.get(parserCtx);
389
445
  const reopened = parse(output);
390
- if (serialize(this.ctx, reopened) === canonical &&
446
+ if (serialize(this.ctx, reopened).replace(/\r\n?/g, '\n') ===
447
+ canonical.replace(/\r\n?/g, '\n') &&
391
448
  semanticSignature(reopened) === semanticSignature(doc))
392
449
  return output;
393
450
  throw new PreservationError('The edited Markdown cannot be reopened without changing its content');
@@ -1,6 +1,7 @@
1
1
  import { Fragment } from '@milkdown/kit/prose/model';
2
2
  import { serialize } from './dialect.js';
3
3
  import { normaliseLabel } from './references.js';
4
+ import { shareableFragment } from './comments.js';
4
5
  function key(node) {
5
6
  if (node.type.name === 'reference_definition')
6
7
  return `link:${node.attrs.identifier}`;
@@ -70,6 +71,8 @@ export function selectionMarkdown(ctx, doc, content) {
70
71
  return serialize(ctx, doc.type.create(null, selectionContent(doc, content)));
71
72
  }
72
73
  export function referenceMetadata(ctx, doc, content) {
74
+ doc = doc.copy(shareableFragment(doc.content));
75
+ content = shareableFragment(content);
73
76
  let references = false, index = 0;
74
77
  const rewrite = (node) => {
75
78
  if (key(node) ||
@@ -121,7 +124,8 @@ function allLabels(doc) {
121
124
  }
122
125
  const rawReferences = /(?<![\\!])\[([^\]]+)\](?:\[([^\]]*)\])?(?!\()/g;
123
126
  function isCode(node) {
124
- return (node.type.name === 'code_block' ||
127
+ return (node.type.name.startsWith('comment_') ||
128
+ node.type.name === 'code_block' ||
125
129
  node.marks.some((mark) => mark.type.name === 'inlineCode'));
126
130
  }
127
131
  // Rename only the incoming document. Definitions belong to the insertion's undo transaction.
package/dist/style.css CHANGED
@@ -562,3 +562,109 @@ body {
562
562
  font-size: 0.8em;
563
563
  opacity: 0.7;
564
564
  }
565
+
566
+ .ProseMirror mark {
567
+ background: color-mix(in srgb, #e5c041 38%, transparent);
568
+ color: inherit;
569
+ }
570
+
571
+ .ProseMirror .inkkit-callout {
572
+ --callout-color: var(--accent);
573
+ border-inline-start: 3px solid var(--callout-color);
574
+ padding: 0.65rem 1rem;
575
+ background: color-mix(in srgb, var(--callout-color) 7%, transparent);
576
+ }
577
+
578
+ .ProseMirror [data-inkkit-callout='TIP'] {
579
+ --callout-color: #30a46c;
580
+ }
581
+ .ProseMirror [data-inkkit-callout='IMPORTANT'] {
582
+ --callout-color: #8e4ec6;
583
+ }
584
+ .ProseMirror [data-inkkit-callout='WARNING'] {
585
+ --callout-color: #d28b00;
586
+ }
587
+ .ProseMirror [data-inkkit-callout='CAUTION'] {
588
+ --callout-color: #e5484d;
589
+ }
590
+
591
+ .inkkit-callout-header {
592
+ display: flex;
593
+ align-items: baseline;
594
+ gap: 0.75rem;
595
+ }
596
+
597
+ [data-inkkit-callout-toggle] {
598
+ color: var(--muted);
599
+ background: transparent;
600
+ border: 0;
601
+ padding: 0.25rem;
602
+ cursor: pointer;
603
+ }
604
+
605
+ .inkkit-callout[data-inkkit-folded='true'] > .inkkit-callout-body {
606
+ display: none;
607
+ }
608
+
609
+ .inkkit-comment {
610
+ display: none;
611
+ }
612
+
613
+ [data-inkkit-comments-visible='true'] .inkkit-comment {
614
+ display: inline;
615
+ white-space: pre-wrap;
616
+ font-family: var(--mono);
617
+ color: var(--muted);
618
+ background: var(--code-bg);
619
+ }
620
+
621
+ [data-inkkit-comments-visible='true'] .inkkit-comment-block {
622
+ display: block;
623
+ padding: 0.5rem;
624
+ }
625
+
626
+ @media print {
627
+ html,
628
+ body,
629
+ #editor,
630
+ .ProseMirror {
631
+ height: auto;
632
+ min-height: 0;
633
+ overflow: visible;
634
+ }
635
+
636
+ .milkdown-table-block .table-wrapper,
637
+ .ProseMirror pre {
638
+ overflow: visible;
639
+ }
640
+
641
+ .inkkit-comment,
642
+ [data-inkkit-comments-visible='true'] .inkkit-comment,
643
+ [data-inkkit-callout-toggle],
644
+ .ProseMirror pre .copy,
645
+ .milkdown-table-block .handle,
646
+ .milkdown-table-block .button-group,
647
+ .milkdown-table-block .line-handle {
648
+ display: none !important;
649
+ }
650
+
651
+ .inkkit-callout[data-inkkit-folded='true'] > .inkkit-callout-body {
652
+ display: block !important;
653
+ }
654
+ }
655
+ /* Diagram previews are presentation; the authored code remains editable. */
656
+ .inkkit-mermaid-preview {
657
+ padding: 16px;
658
+ overflow: auto;
659
+ background: #fff;
660
+ color: #222;
661
+ }
662
+ .inkkit-mermaid-preview svg {
663
+ display: block;
664
+ max-width: 100%;
665
+ height: auto;
666
+ }
667
+ .inkkit-mermaid-preview[data-state='error'] {
668
+ color: #9b2424;
669
+ font-size: 0.9em;
670
+ }
package/dist/types.d.ts CHANGED
@@ -14,7 +14,7 @@ export interface DocumentSnapshot extends DocumentInput {
14
14
  revision: number;
15
15
  dirty: boolean;
16
16
  }
17
- export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable';
17
+ export type EditorErrorCode = 'not-ready' | 'stale-document' | 'composition' | 'operation-pending' | 'destroyed' | 'preservation' | 'image-unavailable' | 'diagram-unavailable';
18
18
  export declare class InkKitError extends Error {
19
19
  readonly code: EditorErrorCode;
20
20
  constructor(code: EditorErrorCode, message: string);
@@ -57,4 +57,10 @@ export interface ClipboardOutput {
57
57
  html: string;
58
58
  markdown: string;
59
59
  images: ClipboardImage[];
60
+ diagrams?: ClipboardDiagram[];
61
+ }
62
+ export interface ClipboardDiagram {
63
+ source: string;
64
+ image?: PortableImage;
65
+ error?: string;
60
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aicayzer/inkkit",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "A preserving Markdown editor for offline web views and the web",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,19 +46,25 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@milkdown/kit": "7.22.1",
49
+ "dompurify": "3.4.16",
49
50
  "lowlight": "^3.3.0",
51
+ "mdast-util-from-markdown": "^2.1.0",
50
52
  "mdast-util-gfm-autolink-literal": "^2.0.1",
51
53
  "mdast-util-gfm-footnote": "^2.1.0",
52
54
  "mdast-util-gfm-strikethrough": "^2.0.0",
53
55
  "mdast-util-gfm-table": "^2.0.0",
54
56
  "mdast-util-gfm-task-list-item": "^2.0.0",
57
+ "mdast-util-highlight-mark": "1.2.2",
55
58
  "mdast-util-to-markdown": "2.2.0",
59
+ "mermaid": "12.1.0",
56
60
  "micromark-extension-gfm-autolink-literal": "^2.1.0",
57
61
  "micromark-extension-gfm-footnote": "^2.1.0",
58
62
  "micromark-extension-gfm-strikethrough": "^2.1.0",
59
63
  "micromark-extension-gfm-table": "^2.1.1",
60
64
  "micromark-extension-gfm-task-list-item": "^2.1.0",
65
+ "micromark-extension-highlight-mark": "1.2.0",
61
66
  "micromark-util-normalize-identifier": "^2.0.1",
67
+ "micromark-util-types": "^2.0.3",
62
68
  "prosemirror-search": "^1.1.1"
63
69
  },
64
70
  "devDependencies": {