@aicayzer/inkkit 0.3.6 → 0.4.1

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.
@@ -6759,7 +6759,7 @@ function tokenizeThematicBreak(effects, ok, nok) {
6759
6759
  * } from 'micromark-util-types'
6760
6760
  */
6761
6761
  /** @type {Construct} */
6762
- var list$3 = {
6762
+ var list$4 = {
6763
6763
  continuation: { tokenize: tokenizeListContinuation },
6764
6764
  exit: tokenizeListEnd,
6765
6765
  name: "list",
@@ -6879,7 +6879,7 @@ function tokenizeListContinuation(effects, ok, nok) {
6879
6879
  function notInCurrentItem(code) {
6880
6880
  self.containerState._closeFlow = true;
6881
6881
  self.interrupt = void 0;
6882
- return factorySpace(effects, effects.attempt(list$3, ok, nok), "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? void 0 : 4)(code);
6882
+ return factorySpace(effects, effects.attempt(list$4, ok, nok), "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? void 0 : 4)(code);
6883
6883
  }
6884
6884
  }
6885
6885
  /**
@@ -7313,19 +7313,19 @@ var constructs_exports = /* @__PURE__ */ __exportAll({
7313
7313
  });
7314
7314
  /** @satisfies {Extension['document']} */
7315
7315
  var document$1 = {
7316
- [42]: list$3,
7317
- [43]: list$3,
7318
- [45]: list$3,
7319
- [48]: list$3,
7320
- [49]: list$3,
7321
- [50]: list$3,
7322
- [51]: list$3,
7323
- [52]: list$3,
7324
- [53]: list$3,
7325
- [54]: list$3,
7326
- [55]: list$3,
7327
- [56]: list$3,
7328
- [57]: list$3,
7316
+ [42]: list$4,
7317
+ [43]: list$4,
7318
+ [45]: list$4,
7319
+ [48]: list$4,
7320
+ [49]: list$4,
7321
+ [50]: list$4,
7322
+ [51]: list$4,
7323
+ [52]: list$4,
7324
+ [53]: list$4,
7325
+ [54]: list$4,
7326
+ [55]: list$4,
7327
+ [56]: list$4,
7328
+ [57]: list$4,
7329
7329
  [62]: blockQuote
7330
7330
  };
7331
7331
  /** @satisfies {Extension['contentInitial']} */
@@ -9452,10 +9452,10 @@ function configure(base, extension) {
9452
9452
  case "extensions": break;
9453
9453
  /* c8 ignore next 4 */
9454
9454
  case "unsafe":
9455
- list$2(base[key], extension[key]);
9455
+ list$3(base[key], extension[key]);
9456
9456
  break;
9457
9457
  case "join":
9458
- list$2(base[key], extension[key]);
9458
+ list$3(base[key], extension[key]);
9459
9459
  break;
9460
9460
  case "handlers":
9461
9461
  map$2(base[key], extension[key]);
@@ -9476,7 +9476,7 @@ function configure(base, extension) {
9476
9476
  * @returns {undefined}
9477
9477
  * Nothing.
9478
9478
  */
9479
- function list$2(left, right) {
9479
+ function list$3(left, right) {
9480
9480
  if (right) left.push(...right);
9481
9481
  }
9482
9482
  /**
@@ -11096,7 +11096,7 @@ function checkRule(state) {
11096
11096
  * @returns {string}
11097
11097
  * Serialized markdown.
11098
11098
  */
11099
- function list$1(node, parent, state, info) {
11099
+ function list$2(node, parent, state, info) {
11100
11100
  const exit = state.enter("list");
11101
11101
  const bulletCurrent = state.bulletCurrent;
11102
11102
  /** @type {string} */
@@ -11416,7 +11416,7 @@ var handle = {
11416
11416
  inlineCode,
11417
11417
  link: link$1,
11418
11418
  linkReference,
11419
- list: list$1,
11419
+ list: list$2,
11420
11420
  listItem,
11421
11421
  paragraph: paragraph$1,
11422
11422
  root: root$1,
@@ -12759,7 +12759,7 @@ function toMarkdown(tree, options) {
12759
12759
  configure(state, settings);
12760
12760
  if (state.options.tightDefinitions) state.join.push(joinDefinition);
12761
12761
  state.handle = zwitch("type", {
12762
- invalid,
12762
+ invalid: invalid$1,
12763
12763
  unknown,
12764
12764
  handlers: state.handlers
12765
12765
  });
@@ -12806,7 +12806,7 @@ function toMarkdown(tree, options) {
12806
12806
  * @throws {Error}
12807
12807
  * Always.
12808
12808
  */
12809
- function invalid(value) {
12809
+ function invalid$1(value) {
12810
12810
  throw new Error("Cannot handle value `" + value + "`, expected node");
12811
12811
  }
12812
12812
  /**
@@ -17808,7 +17808,7 @@ var DOMParser$1 = class DOMParser$1 {
17808
17808
  return schema.cached.domParser || (schema.cached.domParser = new DOMParser$1(schema, DOMParser$1.schemaRules(schema)));
17809
17809
  }
17810
17810
  };
17811
- var blockTags = {
17811
+ var blockTags$1 = {
17812
17812
  address: true,
17813
17813
  article: true,
17814
17814
  aside: true,
@@ -17904,7 +17904,7 @@ var NodeContext = class {
17904
17904
  inlineContext(node) {
17905
17905
  if (this.type) return this.type.inlineContent;
17906
17906
  if (this.content.length) return this.content[0].isInline;
17907
- return node.parentNode && !blockTags.hasOwnProperty(node.parentNode.nodeName.toLowerCase());
17907
+ return node.parentNode && !blockTags$1.hasOwnProperty(node.parentNode.nodeName.toLowerCase());
17908
17908
  }
17909
17909
  };
17910
17910
  var ParseContext = class {
@@ -17968,7 +17968,7 @@ var ParseContext = class {
17968
17968
  if (rule && rule.closeParent) this.open = Math.max(0, this.open - 1);
17969
17969
  else if (rule && rule.skip.nodeType) dom = rule.skip;
17970
17970
  let sync, oldNeedsBlock = this.needsBlock;
17971
- if (blockTags.hasOwnProperty(name)) {
17971
+ if (blockTags$1.hasOwnProperty(name)) {
17972
17972
  if (top.content.length && top.content[0].isInline && this.open) {
17973
17973
  this.open--;
17974
17974
  top = this.top;
@@ -28368,6 +28368,7 @@ var defaultLabels = {
28368
28368
  slashWarning: "Warning",
28369
28369
  slashCaution: "Caution",
28370
28370
  formattedEditor: "Formatted Markdown editor",
28371
+ richTextEditor: "Rich text editor",
28371
28372
  plainTextEditor: "Plain text editor",
28372
28373
  sourceEditor: "Markdown source editor",
28373
28374
  placeholder: "Start typing…",
@@ -28408,6 +28409,33 @@ function editorLabels(ctx) {
28408
28409
  }
28409
28410
  //#endregion
28410
28411
  //#region src/types.ts
28412
+ /** The closed list of what a conversion can lose. The API reference describes each one. */
28413
+ var formatLossCodes = [
28414
+ "heading",
28415
+ "code-block",
28416
+ "diagram",
28417
+ "quote",
28418
+ "callout",
28419
+ "task",
28420
+ "rule",
28421
+ "footnote",
28422
+ "comment",
28423
+ "highlight",
28424
+ "image",
28425
+ "file",
28426
+ "wiki-link",
28427
+ "literal",
28428
+ "link",
28429
+ "table-alignment",
28430
+ "list-start",
28431
+ "list-item-blocks",
28432
+ "list-depth",
28433
+ "link-underline",
28434
+ "invalid-character",
28435
+ "alignment",
28436
+ "mark-spaces",
28437
+ "empty-line"
28438
+ ];
28411
28439
  /** Every failure InkKit reports. Hosts branch on `code`; `message` is for logs. When the failure
28412
28440
  * started elsewhere, such as in an adapter, the host clipboard or the renderer, `cause` holds the
28413
28441
  * original error. */
@@ -28940,6 +28968,61 @@ function remarkLinkForms() {
28940
28968
  //#endregion
28941
28969
  //#region node_modules/.pnpm/prosemirror-schema-list@1.5.1/node_modules/prosemirror-schema-list/dist/index.js
28942
28970
  /**
28971
+ Returns a command function that wraps the selection in a list with
28972
+ the given type an attributes. If `dispatch` is null, only return a
28973
+ value to indicate whether this is possible, but don't actually
28974
+ perform the change.
28975
+ */
28976
+ function wrapInList(listType, attrs = null) {
28977
+ return function(state, dispatch) {
28978
+ let { $from, $to } = state.selection;
28979
+ let range = $from.blockRange($to);
28980
+ if (!range) return false;
28981
+ let tr = dispatch ? state.tr : null;
28982
+ if (!wrapRangeInList(tr, range, listType, attrs)) return false;
28983
+ if (dispatch) dispatch(tr.scrollIntoView());
28984
+ return true;
28985
+ };
28986
+ }
28987
+ /**
28988
+ Try to wrap the given node range in a list of the given type.
28989
+ Return `true` when this is possible, `false` otherwise. When `tr`
28990
+ is non-null, the wrapping is added to that transaction. When it is
28991
+ `null`, the function only queries whether the wrapping is
28992
+ possible.
28993
+ */
28994
+ function wrapRangeInList(tr, range, listType, attrs = null) {
28995
+ let doJoin = false, outerRange = range, doc = range.$from.doc;
28996
+ if (range.depth >= 2 && range.$from.node(range.depth - 1).type.compatibleContent(listType) && range.startIndex == 0) {
28997
+ if (range.$from.index(range.depth - 1) == 0) return false;
28998
+ let $insert = doc.resolve(range.start - 2);
28999
+ outerRange = new NodeRange($insert, $insert, range.depth);
29000
+ if (range.endIndex < range.parent.childCount) range = new NodeRange(range.$from, doc.resolve(range.$to.end(range.depth)), range.depth);
29001
+ doJoin = true;
29002
+ }
29003
+ let wrap = findWrapping(outerRange, listType, attrs, range);
29004
+ if (!wrap) return false;
29005
+ if (tr) doWrapInList(tr, range, wrap, doJoin, listType);
29006
+ return true;
29007
+ }
29008
+ function doWrapInList(tr, range, wrappers, joinBefore, listType) {
29009
+ let content = Fragment.empty;
29010
+ for (let i = wrappers.length - 1; i >= 0; i--) content = Fragment.from(wrappers[i].type.create(wrappers[i].attrs, content));
29011
+ tr.step(new ReplaceAroundStep(range.start - (joinBefore ? 2 : 0), range.end, range.start, range.end, new Slice(content, 0, 0), wrappers.length, true));
29012
+ let found = 0;
29013
+ for (let i = 0; i < wrappers.length; i++) if (wrappers[i].type == listType) found = i + 1;
29014
+ let splitDepth = wrappers.length - found;
29015
+ let splitPos = range.start + wrappers.length - (joinBefore ? 2 : 0), parent = range.parent;
29016
+ for (let i = range.startIndex, e = range.endIndex, first = true; i < e; i++, first = false) {
29017
+ if (!first && canSplit(tr.doc, splitPos, splitDepth)) {
29018
+ tr.split(splitPos, splitDepth);
29019
+ splitPos += 2 * splitDepth;
29020
+ }
29021
+ splitPos += parent.child(i).nodeSize;
29022
+ }
29023
+ return tr;
29024
+ }
29025
+ /**
28943
29026
  Build a command that splits a non-empty textblock at the top level
28944
29027
  of a list item by also splitting that list item.
28945
29028
  */
@@ -40191,26 +40274,32 @@ var quotes$1 = {
40191
40274
  "\"": ["“", "”"],
40192
40275
  "'": ["‘", "’"]
40193
40276
  };
40277
+ /** Input rules that are not Markdown syntax, which apply in every formatted document. */
40278
+ var formatNeutralRules = /* @__PURE__ */ new WeakSet();
40279
+ function formatNeutral(rule) {
40280
+ formatNeutralRules.add(rule);
40281
+ return rule;
40282
+ }
40194
40283
  var smartPunctuation = [
40195
40284
  smartPunctuationCtx,
40196
40285
  inputTracking,
40197
- $inputRule((ctx) => new InputRule(/["']$/, (state, match, start, end) => {
40286
+ $inputRule((ctx) => formatNeutral(new InputRule(/["']$/, (state, match, start, end) => {
40198
40287
  if (!applies(ctx.get(smartPunctuationCtx.key), state, match, start)) return null;
40199
40288
  const [opening, closing] = quotes$1[match[0]];
40200
40289
  return state.tr.insertText(opensHere(state, start) ? opening : closing, start, end).setMeta(smartRule, "quote");
40201
- }, { inCodeMark: false })),
40202
- $inputRule((ctx) => new InputRule(/--$/, (state, match, start, end) => {
40290
+ }, { inCodeMark: false }))),
40291
+ $inputRule((ctx) => formatNeutral(new InputRule(/--$/, (state, match, start, end) => {
40203
40292
  if (!applies(ctx.get(smartPunctuationCtx.key), state, match, start)) return null;
40204
40293
  if (input.getState(state)?.hyphen !== state.selection.from) return null;
40205
40294
  const before = textBefore(state, start);
40206
40295
  if (!before.trim() || before.endsWith("-") || /^\s*\|/.test(before)) return null;
40207
40296
  return state.tr.insertText(enDash, start, end).setMeta(smartRule, "en");
40208
- }, { inCodeMark: false })),
40209
- $inputRule((ctx) => new InputRule(/\u2013-$/, (state, match, start, end) => {
40297
+ }, { inCodeMark: false }))),
40298
+ $inputRule((ctx) => formatNeutral(new InputRule(/\u2013-$/, (state, match, start, end) => {
40210
40299
  if (!applies(ctx.get(smartPunctuationCtx.key), state, match, start)) return null;
40211
40300
  if (lastRule(state)?.transform.getMeta(smartRule) !== "en") return null;
40212
40301
  return state.tr.insertText(emDash, start, end).setMeta(smartRule, "em");
40213
- }, { inCodeMark: false }))
40302
+ }, { inCodeMark: false })))
40214
40303
  ].flat();
40215
40304
  /**
40216
40305
  * Undo straight after a smart quote or dash puts back the character that was typed, as Backspace
@@ -42702,6 +42791,16 @@ function insertBlocks(view, blocks, point) {
42702
42791
  view.dispatch(tr.setSelection(TextSelection.create(tr.doc, end)).scrollIntoView());
42703
42792
  }
42704
42793
  //#endregion
42794
+ //#region src/format-input.ts
42795
+ var markdown = {
42796
+ markdownInput: () => true,
42797
+ richText: () => void 0
42798
+ };
42799
+ var formatInputCtx = $ctx(markdown, "inkkitFormatInput");
42800
+ function formatInput(ctx) {
42801
+ return ctx.isInjected(formatInputCtx.key) ? ctx.get(formatInputCtx.key) : markdown;
42802
+ }
42803
+ //#endregion
42705
42804
  //#region src/literals.ts
42706
42805
  var unsupported = /* @__PURE__ */ new Set([
42707
42806
  "html",
@@ -43650,6 +43749,7 @@ function editedBlocks(before, after) {
43650
43749
  var autolinkLiterals = $prose((ctx) => new Plugin({
43651
43750
  key: new PluginKey("autolinkLiterals"),
43652
43751
  appendTransaction(transactions, oldState, newState) {
43752
+ if (!formatInput(ctx).markdownInput()) return null;
43653
43753
  if (!transactions.some((tr) => tr.docChanged)) return null;
43654
43754
  const base = newState.tr;
43655
43755
  const written = transactions.every(isWrittenEdit);
@@ -46013,8 +46113,8 @@ function shareFormatting(children) {
46013
46113
  };
46014
46114
  const previous = children[index - 1];
46015
46115
  const next = children[index + 1];
46016
- const before = previous && same(previous, formatting);
46017
- const after = next && same(next, formatting);
46116
+ const before = Boolean(previous && same(previous, formatting));
46117
+ const after = Boolean(next && same(next, formatting));
46018
46118
  if (!before && !after) continue;
46019
46119
  const merged = {
46020
46120
  ...formatting,
@@ -46271,6 +46371,26 @@ var preserveSpacerParagraphs = paragraphSchema.extendSchema((base) => (ctx) => {
46271
46371
  const schema = base(ctx);
46272
46372
  return {
46273
46373
  ...schema,
46374
+ attrs: {
46375
+ ...schema.attrs,
46376
+ align: { default: null }
46377
+ },
46378
+ parseDOM: [{ tag: "p" }],
46379
+ toDOM: (node) => {
46380
+ const [tag, attrs, hole] = schema.toDOM(node);
46381
+ return node.attrs.align ? [
46382
+ tag,
46383
+ {
46384
+ ...attrs,
46385
+ style: `text-align: ${String(node.attrs.align)}`
46386
+ },
46387
+ hole
46388
+ ] : [
46389
+ tag,
46390
+ attrs,
46391
+ hole
46392
+ ];
46393
+ },
46274
46394
  parseMarkdown: {
46275
46395
  ...schema.parseMarkdown,
46276
46396
  runner(state, node, type) {
@@ -46595,7 +46715,7 @@ function serialize(ctx, doc = ctx.get(editorViewCtx).state.doc) {
46595
46715
  }
46596
46716
  //#endregion
46597
46717
  //#region src/editor-selection.ts
46598
- var markNames = {
46718
+ var markNames$1 = {
46599
46719
  strong: "bold",
46600
46720
  emphasis: "italic",
46601
46721
  strike_through: "strikethrough",
@@ -46640,10 +46760,10 @@ function caretState(state) {
46640
46760
  const active = /* @__PURE__ */ new Set();
46641
46761
  const marks = empty ? state.storedMarks ?? $from.marks() : [];
46642
46762
  for (const mark of marks) {
46643
- const name = markNames[mark.type.name];
46763
+ const name = markNames$1[mark.type.name];
46644
46764
  if (name) active.add(name);
46645
46765
  }
46646
- if (!empty) for (const [name, mark] of Object.entries(markNames)) {
46766
+ if (!empty) for (const [name, mark] of Object.entries(markNames$1)) {
46647
46767
  const type = state.schema.marks[name];
46648
46768
  if (type && state.doc.rangeHasMark($from.pos, $to.pos, type)) active.add(mark);
46649
46769
  }
@@ -46685,9 +46805,10 @@ function blockAt(parent, $from) {
46685
46805
  }
46686
46806
  //#endregion
46687
46807
  //#region src/editor-links.ts
46688
- function checkDestination(ctx, destination, name, linkSchemes) {
46808
+ function checkDestination(ctx, destination, name, linkSchemes, markdown = true) {
46689
46809
  if (destination.trim() === "") throw invalidArgument(`${name} must not be empty`);
46690
46810
  if (!isSafeLinkTarget(destination, linkSchemes)) throw invalidArgument(`${name} must be relative, or an address with an allowed scheme (http, https, mailto, tel or one of linkSchemes), with no control characters`);
46811
+ if (!markdown) return;
46691
46812
  const { schema } = ctx.get(editorViewCtx).state;
46692
46813
  const written = serialize(ctx, schema.node("doc", null, [schema.node("paragraph", null, [schema.text("x", [schema.marks.link.create({ href: destination })])])]));
46693
46814
  let read;
@@ -47427,6 +47548,22 @@ var EditorClipboard = class {
47427
47548
  return;
47428
47549
  }
47429
47550
  const view = this.runtime.ctx().get(editorViewCtx);
47551
+ const rich = this.runtime.handler().richText;
47552
+ if (rich) {
47553
+ if (!event.clipboardData || view.state.selection.empty) return;
47554
+ event.preventDefault();
47555
+ event.stopPropagation();
47556
+ try {
47557
+ this.runtime.snapshot();
47558
+ const output = rich.copy(view.state, this.runtime.linkSchemes());
47559
+ event.clipboardData.setData("text/plain", output.text);
47560
+ event.clipboardData.setData("text/html", output.html);
47561
+ if (event.type === "cut" && this.runtime.writable()) view.dispatch(view.state.tr.deleteSelection().scrollIntoView());
47562
+ } catch (error) {
47563
+ this.runtime.report(error);
47564
+ }
47565
+ return;
47566
+ }
47430
47567
  const fragment = commentSelectionContent(view.state.doc, view.state.selection.from, view.state.selection.to, view.state.selection.content().content);
47431
47568
  const expanded = selectionContent(view.state.doc, fragment);
47432
47569
  let hasImages = false;
@@ -48760,6 +48897,7 @@ function runOffsets(nodes, text, from, to) {
48760
48897
  const offsets = [from];
48761
48898
  const links = [];
48762
48899
  const breaks = /* @__PURE__ */ new Set();
48900
+ const tokens = /* @__PURE__ */ new Map();
48763
48901
  let at = from;
48764
48902
  let afterBreak = false;
48765
48903
  for (const node of nodes) {
@@ -48772,10 +48910,9 @@ function runOffsets(nodes, text, from, to) {
48772
48910
  }
48773
48911
  if (start !== at) return void 0;
48774
48912
  const raw = text.slice(start, end);
48775
- if (node.type === "link") {
48776
- const label = inlineText(node);
48777
- const angle = raw === `<${label}>`;
48778
- if (raw !== label && !angle) return void 0;
48913
+ const label = node.type === "link" ? inlineText(node) : void 0;
48914
+ const angle = label !== void 0 && raw === `<${label}>`;
48915
+ if (label !== void 0 && (raw === label || angle)) {
48779
48916
  links.push([decoded.length, decoded.length + label.length]);
48780
48917
  decoded += label;
48781
48918
  for (let index = 1; index <= label.length; index++) offsets.push(index === label.length ? end : start + index + Number(angle));
@@ -48788,7 +48925,12 @@ function runOffsets(nodes, text, from, to) {
48788
48925
  breaks.add(decoded.length);
48789
48926
  decoded += "\n";
48790
48927
  offsets.push(end);
48791
- } else return void 0;
48928
+ } else {
48929
+ tokens.set(decoded.length, signature(node));
48930
+ links.push([decoded.length, decoded.length + 1]);
48931
+ decoded += "";
48932
+ offsets.push(end);
48933
+ }
48792
48934
  afterBreak = node.type === "break";
48793
48935
  at = end;
48794
48936
  }
@@ -48796,7 +48938,8 @@ function runOffsets(nodes, text, from, to) {
48796
48938
  decoded,
48797
48939
  offsets,
48798
48940
  links,
48799
- breaks
48941
+ breaks,
48942
+ tokens
48800
48943
  } : void 0;
48801
48944
  }
48802
48945
  function retainRun(before, after, source, canonical, [from, to], [newFrom, newTo]) {
@@ -48806,11 +48949,31 @@ function retainRun(before, after, source, canonical, [from, to], [newFrom, newTo
48806
48949
  const a = old.decoded;
48807
48950
  const b = now.decoded;
48808
48951
  let prefix = 0;
48809
- while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix] && old.breaks.has(prefix) === now.breaks.has(prefix)) prefix++;
48952
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix] && old.tokens.get(prefix) === now.tokens.get(prefix) && old.breaks.has(prefix) === now.breaks.has(prefix)) prefix++;
48810
48953
  let suffix = 0;
48811
- while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix] && old.breaks.has(a.length - 1 - suffix) === now.breaks.has(b.length - 1 - suffix)) suffix++;
48954
+ while (suffix < a.length - prefix && suffix < b.length - prefix && a[a.length - 1 - suffix] === b[b.length - 1 - suffix] && old.tokens.get(a.length - 1 - suffix) === now.tokens.get(b.length - 1 - suffix) && old.breaks.has(a.length - 1 - suffix) === now.breaks.has(b.length - 1 - suffix)) suffix++;
48955
+ const reference = /^&(?:#[xX][\da-fA-F]+|#\d+|[A-Za-z][A-Za-z\d]+);$/;
48956
+ const encodedBoundary = (oldIndex, newIndex) => reference.test(canonical.slice(now.offsets[newIndex], now.offsets[newIndex + 1])) && !reference.test(source.slice(old.offsets[oldIndex], old.offsets[oldIndex + 1]));
48812
48957
  for (let widened = true; widened;) {
48813
48958
  widened = false;
48959
+ if (prefix > 0 && encodedBoundary(prefix - 1, prefix - 1)) {
48960
+ prefix--;
48961
+ widened = true;
48962
+ }
48963
+ if (suffix > 0 && encodedBoundary(a.length - suffix, b.length - suffix)) {
48964
+ suffix--;
48965
+ widened = true;
48966
+ }
48967
+ for (const { offsets, decoded } of [old, now]) {
48968
+ while (prefix > 0 && offsets[prefix] === offsets[prefix + 1]) {
48969
+ prefix--;
48970
+ widened = true;
48971
+ }
48972
+ while (suffix > 0 && offsets[decoded.length - suffix] === offsets[decoded.length - suffix + 1]) {
48973
+ suffix--;
48974
+ widened = true;
48975
+ }
48976
+ }
48814
48977
  for (const [links, length] of [[old.links, a.length], [now.links, b.length]]) for (const [start, end] of links) if (start <= length - suffix && end >= prefix) {
48815
48978
  if (start < prefix) {
48816
48979
  prefix = start;
@@ -48988,7 +49151,7 @@ function correspondence(before, after) {
48988
49151
  if (work.budget < 0) throw tooManyBlocks();
48989
49152
  const anchors = uniqueAnchors(before, after, b, bEnd, a, aEnd);
48990
49153
  if (!anchors.length) {
48991
- align(before, after, b, bEnd, a, aEnd, result, work);
49154
+ align$1(before, after, b, bEnd, a, aEnd, result, work);
48992
49155
  continue;
48993
49156
  }
48994
49157
  for (const anchor of anchors) {
@@ -49053,7 +49216,7 @@ function uniqueAnchors(before, after, b, bEnd, a, aEnd) {
49053
49216
  }
49054
49217
  var INDEL = 2;
49055
49218
  var REPLACE = 3;
49056
- function align(before, after, b, bEnd, a, aEnd, result, work) {
49219
+ function align$1(before, after, b, bEnd, a, aEnd, result, work) {
49057
49220
  const rows = bEnd - b + 1;
49058
49221
  const columns = aEnd - a + 1;
49059
49222
  const offset = Math.abs(rows - columns);
@@ -51482,7 +51645,7 @@ function cleanSemantics(container, localIDs) {
51482
51645
  if (title) element.setAttribute("title", title);
51483
51646
  if (id && localIDs.has(id)) element.setAttribute("id", id);
51484
51647
  if (order && /^\d+$/.test(order)) element.setAttribute("start", order);
51485
- if (/^(left|center|right)$/.test(alignment) && /^(TH|TD)$/.test(element.tagName)) element.setAttribute("style", `text-align: ${alignment}`);
51648
+ if (/^(left|center|right)$/.test(alignment) && /^(TH|TD)$/.test(element.tagName) || /^(center|right|justify)$/.test(alignment) && element.tagName === "P") element.setAttribute("style", `text-align: ${alignment}`);
51486
51649
  }
51487
51650
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
51488
51651
  const comments = [];
@@ -51601,10 +51764,12 @@ var markdownFormat = {
51601
51764
  promise: "exact",
51602
51765
  literalOnly: false,
51603
51766
  sourceMode: true,
51767
+ formattedLabel: "formattedEditor",
51604
51768
  literalLabel: "sourceEditor",
51605
51769
  headings: true,
51606
51770
  nativeCopy: false,
51607
51771
  keepsInsertedLineEndings: true,
51772
+ markdownInput: true,
51608
51773
  load: (ctx, text) => Preservation.read(ctx, text),
51609
51774
  parse: (ctx, text) => Preservation.read(ctx, text),
51610
51775
  pendingExport: (ctx, text) => ctx.get(parserCtx)(text),
@@ -51642,6 +51807,1067 @@ var markdownFormat = {
51642
51807
  };
51643
51808
  }
51644
51809
  };
51810
+ /** The largest first number of an ordered list that RTF writes and reads back unchanged, as
51811
+ * measured with the host's converter. */
51812
+ var maximumListStart = 2147483647;
51813
+ /** Characters a run never holds: they would change the structure, stand for an attachment or end
51814
+ * the text when RTF is written. */
51815
+ var forbidden = /[\u0000\n\r\u000b\u000c\u0085\u2028\u2029\ufffc]/;
51816
+ var loneSurrogate = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
51817
+ /** Why a run cannot hold `text`, or undefined. Loading and editing share the rule. */
51818
+ function textProblem(text) {
51819
+ const bad = forbidden.exec(text);
51820
+ if (bad) return `text cannot hold ${scalarName(bad[0])}`;
51821
+ if (loneSurrogate.test(text)) return "text cannot hold a lone surrogate";
51822
+ }
51823
+ /** Why `href` cannot be a link's address, or undefined. Any address that is not empty and holds
51824
+ * no control character is kept, as the host's converter keeps it; loading, editing, paste and the
51825
+ * converter share the rule. Edits and paste also apply Inkkit's link safety. */
51826
+ function linkProblem(href) {
51827
+ if (!href) return "a link needs an address";
51828
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(href)) return "a link address cannot hold a control character";
51829
+ const problem = textProblem(href);
51830
+ return problem && `a link address ${problem.replace(/^text /, "")}`;
51831
+ }
51832
+ /** The canonical order of marks. */
51833
+ var markRank = {
51834
+ bold: 0,
51835
+ italic: 1,
51836
+ underline: 2,
51837
+ strikethrough: 3,
51838
+ superscript: 4,
51839
+ subscript: 5,
51840
+ code: 6,
51841
+ link: 7
51842
+ };
51843
+ function invalid(path, problem) {
51844
+ throw invalidArgument(`${path}: ${problem}`);
51845
+ }
51846
+ function record(value, path, keys) {
51847
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(path, "must be an object");
51848
+ for (const key of Object.keys(value)) if (!keys.includes(key)) invalid(`${path}.${key}`, "is not a known field");
51849
+ return value;
51850
+ }
51851
+ function list$1(value, path) {
51852
+ if (!Array.isArray(value)) invalid(path, "must be a list");
51853
+ return value;
51854
+ }
51855
+ function scalarName(text) {
51856
+ return `U+${text.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
51857
+ }
51858
+ /** Collects inline content in canonical form: adjacent runs with equal marks merge, and each run
51859
+ * is NFC after merging, because joining two NFC strings can leave a sequence that composes. */
51860
+ var InlineBuilder = class {
51861
+ content = [];
51862
+ marksKey;
51863
+ text(text, marks) {
51864
+ if (!text) return;
51865
+ const key = JSON.stringify(marks);
51866
+ const last = this.content.at(-1);
51867
+ if (last?.type === "text" && this.marksKey === key) {
51868
+ last.text += text;
51869
+ return;
51870
+ }
51871
+ this.content.push(marks.length ? {
51872
+ type: "text",
51873
+ text,
51874
+ marks: [...marks]
51875
+ } : {
51876
+ type: "text",
51877
+ text
51878
+ });
51879
+ this.marksKey = key;
51880
+ }
51881
+ break() {
51882
+ this.content.push({ type: "break" });
51883
+ this.marksKey = void 0;
51884
+ }
51885
+ finish() {
51886
+ for (const inline of this.content) if (inline.type === "text") inline.text = inline.text.normalize("NFC");
51887
+ return this.content;
51888
+ }
51889
+ };
51890
+ /** Marks in canonical order. A link's run never also has underline: a link's underline is how it
51891
+ * is drawn, so RTF readers report it on every link. */
51892
+ function canonicalMarks(marks) {
51893
+ const sorted = [...marks].sort((a, b) => markRank[a.type] - markRank[b.type]);
51894
+ return sorted.some((mark) => mark.type === "link") ? sorted.filter((mark) => mark.type !== "underline") : sorted;
51895
+ }
51896
+ function readMarks(value, path) {
51897
+ const sorted = [...list$1(value, path).map((entry, index) => {
51898
+ const at = `${path}[${index}]`;
51899
+ const mark = record(entry, at, ["type", "href"]);
51900
+ const type = mark.type;
51901
+ if (typeof type !== "string" || !(type in markRank)) invalid(`${at}.type`, `unknown mark type ${JSON.stringify(type)}`);
51902
+ if (type === "link") {
51903
+ if (typeof mark.href !== "string") invalid(`${at}.href`, "must be text");
51904
+ return {
51905
+ type: "link",
51906
+ href: mark.href
51907
+ };
51908
+ }
51909
+ if (!isOmitted(mark.href)) invalid(`${at}.href`, "only a link has an address");
51910
+ return { type };
51911
+ })].sort((a, b) => markRank[a.type] - markRank[b.type]);
51912
+ const types = /* @__PURE__ */ new Set();
51913
+ for (const mark of sorted) {
51914
+ if (types.has(mark.type)) invalid(path, `the ${mark.type} mark appears more than once`);
51915
+ types.add(mark.type);
51916
+ const problem = mark.type === "link" ? linkProblem(mark.href) : void 0;
51917
+ if (problem) invalid(path, problem);
51918
+ }
51919
+ if (types.has("superscript") && types.has("subscript")) invalid(path, "a run cannot be both superscript and subscript");
51920
+ return canonicalMarks(sorted);
51921
+ }
51922
+ function readInlines(value, path) {
51923
+ const builder = new InlineBuilder();
51924
+ list$1(value, path).forEach((entry, index) => {
51925
+ const at = `${path}[${index}]`;
51926
+ const inline = record(entry, at, [
51927
+ "type",
51928
+ "text",
51929
+ "marks"
51930
+ ]);
51931
+ if (inline.type === "break") {
51932
+ if (!isOmitted(inline.text) || !isOmitted(inline.marks)) invalid(at, "a break holds no text or marks");
51933
+ builder.break();
51934
+ return;
51935
+ }
51936
+ if (inline.type !== "text") invalid(`${at}.type`, `unknown inline type ${JSON.stringify(inline.type)}`);
51937
+ const text = inline.text;
51938
+ if (typeof text !== "string") invalid(`${at}.text`, "must be text");
51939
+ if (!text) invalid(`${at}.text`, "a text run cannot be empty");
51940
+ const problem = textProblem(text);
51941
+ if (problem) invalid(`${at}.text`, problem);
51942
+ builder.text(text, isOmitted(inline.marks) ? [] : readMarks(inline.marks, `${at}.marks`));
51943
+ });
51944
+ return builder.finish();
51945
+ }
51946
+ function readBlock(value, path, listDepth) {
51947
+ const block = record(value, path, [
51948
+ "type",
51949
+ "align",
51950
+ "content",
51951
+ "ordered",
51952
+ "start",
51953
+ "items",
51954
+ "rows"
51955
+ ]);
51956
+ const only = (...keys) => {
51957
+ for (const key of Object.keys(block)) if (key !== "type" && !keys.includes(key)) invalid(`${path}.${key}`, `a ${String(block.type)} has no ${key}`);
51958
+ };
51959
+ switch (block.type) {
51960
+ case "paragraph": {
51961
+ only("align", "content");
51962
+ const align = block.align;
51963
+ if (!isOmitted(align) && align !== "center" && align !== "right" && align !== "justify") invalid(`${path}.align`, "must be center, right or justify");
51964
+ const content = readInlines(block.content, `${path}.content`);
51965
+ return isOmitted(align) ? {
51966
+ type: "paragraph",
51967
+ content
51968
+ } : {
51969
+ type: "paragraph",
51970
+ align,
51971
+ content
51972
+ };
51973
+ }
51974
+ case "list":
51975
+ only("ordered", "start", "items");
51976
+ return readList(block, path, listDepth + 1);
51977
+ case "table": {
51978
+ only("rows");
51979
+ if (listDepth > 0) invalid(path, "a table cannot be inside a list item");
51980
+ const rows = list$1(block.rows, `${path}.rows`);
51981
+ if (!rows.length) invalid(`${path}.rows`, "a table needs at least one row");
51982
+ const width = list$1(rows[0], `${path}.rows[0]`).length;
51983
+ if (!width) invalid(`${path}.rows[0]`, "a table row needs at least one cell");
51984
+ return {
51985
+ type: "table",
51986
+ rows: rows.map((row, rowIndex) => {
51987
+ const at = `${path}.rows[${rowIndex}]`;
51988
+ const cells = list$1(row, at);
51989
+ if (cells.length !== width) invalid(at, `the row has ${cells.length} cells, but the first row has ${width}`);
51990
+ return cells.map((cell, cellIndex) => {
51991
+ const cellPath = `${at}[${cellIndex}]`;
51992
+ const content = record(cell, cellPath, ["content"]).content;
51993
+ return { content: readInlines(content, `${cellPath}.content`) };
51994
+ });
51995
+ })
51996
+ };
51997
+ }
51998
+ default: invalid(`${path}.type`, `unknown block type ${JSON.stringify(block.type)}`);
51999
+ }
52000
+ }
52001
+ function readList(block, path, depth) {
52002
+ if (depth > 9) invalid(path, `lists nest deeper than 9 levels`);
52003
+ if (typeof block.ordered !== "boolean") invalid(`${path}.ordered`, "must be true or false");
52004
+ let start;
52005
+ if (!isOmitted(block.start)) {
52006
+ if (!block.ordered) invalid(`${path}.start`, "only an ordered list has a start");
52007
+ if (!Number.isSafeInteger(block.start)) invalid(`${path}.start`, "must be a whole number");
52008
+ if (block.start < 1) invalid(`${path}.start`, "the start must be at least 1");
52009
+ if (block.start > 2147483647) invalid(`${path}.start`, `the start must be at most ${maximumListStart}`);
52010
+ if (block.start !== 1) start = block.start;
52011
+ }
52012
+ const entries = list$1(block.items, `${path}.items`);
52013
+ if (!entries.length) invalid(`${path}.items`, "a list needs at least one item");
52014
+ const items = entries.map((entry, itemIndex) => {
52015
+ const at = `${path}.items[${itemIndex}]`;
52016
+ const blocks = list$1(record(entry, at, ["blocks"]).blocks, `${at}.blocks`);
52017
+ const first = blocks[0];
52018
+ if (!first || typeof first !== "object" || first.type !== "paragraph") invalid(`${at}.blocks[0]`, "a list item must start with a paragraph");
52019
+ return { blocks: blocks.map((child, blockIndex) => {
52020
+ const childPath = `${at}.blocks[${blockIndex}]`;
52021
+ if (blockIndex > 0 && (!child || typeof child !== "object" || child.type !== "list")) invalid(childPath, "after its paragraph, a list item can hold only lists");
52022
+ return readBlock(child, childPath, depth);
52023
+ }) };
52024
+ });
52025
+ return start === void 0 ? {
52026
+ type: "list",
52027
+ ordered: block.ordered,
52028
+ items
52029
+ } : {
52030
+ type: "list",
52031
+ ordered: block.ordered,
52032
+ start,
52033
+ items
52034
+ };
52035
+ }
52036
+ /**
52037
+ * Checks `value` as a version 1 document and returns its canonical form: marks sorted, a link's
52038
+ * underline removed, adjacent equal runs merged, runs in NFC, and `start` of 1 left out. Throws
52039
+ * `invalid-argument` naming the first problem, under `path`.
52040
+ */
52041
+ function readRichTextDocument(value, path) {
52042
+ const document = record(value, path, ["version", "blocks"]);
52043
+ if (document.version !== 1) invalid(`${path}.version`, `version ${JSON.stringify(document.version)} is not supported; the version must be 1`);
52044
+ return {
52045
+ version: 1,
52046
+ blocks: list$1(document.blocks, `${path}.blocks`).map((block, index) => readBlock(block, `${path}.blocks[${index}]`, 0))
52047
+ };
52048
+ }
52049
+ /** The text that stands for a canonical document inside the editor: its JSON. Two documents are
52050
+ * the same exactly when their texts are. */
52051
+ function richTextText(document) {
52052
+ return JSON.stringify(document);
52053
+ }
52054
+ /** The document a text from `richTextText` stands for, as a new object. */
52055
+ function richTextFromText(text) {
52056
+ return JSON.parse(text);
52057
+ }
52058
+ /** Text from outside a document, such as a paste or another format, as canonical runs: a line or
52059
+ * paragraph separator inside a paragraph becomes a line break, and characters RTF cannot hold as
52060
+ * text become U+FFFD, so that no character disappears. */
52061
+ function appendText(builder, text, marks) {
52062
+ text.replace(/[\u0000\ufffc]/g, "�").replace(/[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/g, "�").split(/\r\n|[\n\r\u000b\u000c\u0085\u2028\u2029]/).forEach((line, index) => {
52063
+ if (index) builder.break();
52064
+ builder.text(line, marks);
52065
+ });
52066
+ }
52067
+ /** Typed or pasted text as paragraphs: each paragraph separator ends one, including a final one,
52068
+ * so inserting `'a\n'` leaves the caret in a new paragraph. A line separator is a line break.
52069
+ * Unlike the host's `RichTextDocument(plainText:)`, which reads a file, nothing is trimmed. */
52070
+ function textBlocks(text) {
52071
+ if (text === "") return [];
52072
+ return text.split(/\r\n|[\n\r\u000c\u0085\u2029]/).map((line) => {
52073
+ const builder = new InlineBuilder();
52074
+ appendText(builder, line, []);
52075
+ return {
52076
+ type: "paragraph",
52077
+ content: builder.finish()
52078
+ };
52079
+ });
52080
+ }
52081
+ //#endregion
52082
+ //#region src/formats/rich-text-prose.ts
52083
+ /** Editor marks by rich text mark, in canonical order. */
52084
+ var markNames = {
52085
+ bold: "strong",
52086
+ italic: "emphasis",
52087
+ underline: "underline",
52088
+ strikethrough: "strike_through",
52089
+ superscript: "superscript",
52090
+ subscript: "subscript",
52091
+ code: "inlineCode",
52092
+ link: "link"
52093
+ };
52094
+ var richMarks$1 = Object.fromEntries(Object.entries(markNames).map(([rich, name]) => [name, rich]));
52095
+ /** The editor nodes an RTF document can hold. */
52096
+ var allowedNodes = /* @__PURE__ */ new Set([
52097
+ "doc",
52098
+ "paragraph",
52099
+ "text",
52100
+ "hardbreak",
52101
+ "bullet_list",
52102
+ "ordered_list",
52103
+ "list_item",
52104
+ "table",
52105
+ "table_header_row",
52106
+ "table_row",
52107
+ "table_header",
52108
+ "table_cell"
52109
+ ]);
52110
+ function unrepresentable(what) {
52111
+ return new InkKitError("preservation", `The document holds ${what}, which an RTF document cannot hold`);
52112
+ }
52113
+ function proseMarks(schema, marks = []) {
52114
+ return marks.map((mark) => mark.type === "link" ? schema.marks.link.create({ href: mark.href }) : schema.marks[markNames[mark.type]].create());
52115
+ }
52116
+ function proseInlines(schema, content, around) {
52117
+ return content.map((inline) => {
52118
+ if (inline.type === "break") return schema.nodes.hardbreak.create({
52119
+ isInline: false,
52120
+ isHTML: false
52121
+ });
52122
+ let marks = [...around];
52123
+ for (const mark of proseMarks(schema, inline.marks)) marks = mark.addToSet(marks);
52124
+ return schema.text(inline.text, marks);
52125
+ });
52126
+ }
52127
+ function proseParagraph(schema, content, around, align = null) {
52128
+ return schema.nodes.paragraph.create({ align }, proseInlines(schema, content, around));
52129
+ }
52130
+ /** The editor block for a rich text block. Its text also takes `around`, the marks of text that a
52131
+ * paste replaces. */
52132
+ function proseBlock(schema, block, around = []) {
52133
+ switch (block.type) {
52134
+ case "paragraph": return proseParagraph(schema, block.content, around, block.align ?? null);
52135
+ case "list": {
52136
+ const start = block.start ?? 1;
52137
+ const items = block.items.map((item, index) => schema.nodes.list_item.create({
52138
+ label: block.ordered ? `${start + index}.` : "•",
52139
+ listType: block.ordered ? "ordered" : "bullet",
52140
+ spread: false,
52141
+ checked: null
52142
+ }, item.blocks.map((child) => proseBlock(schema, child, around))));
52143
+ return block.ordered ? schema.nodes.ordered_list.create({
52144
+ order: start,
52145
+ spread: false
52146
+ }, items) : schema.nodes.bullet_list.create({ spread: false }, items);
52147
+ }
52148
+ case "table": {
52149
+ const row = (cells, index) => {
52150
+ const header = index === 0;
52151
+ return schema.nodes[header ? "table_header_row" : "table_row"].create(null, cells.rows[index].map((cell) => schema.nodes[header ? "table_header" : "table_cell"].create({ alignment: null }, proseParagraph(schema, cell.content, around))));
52152
+ };
52153
+ return schema.nodes.table.create(null, block.rows.map((_, index) => row(block, index)));
52154
+ }
52155
+ }
52156
+ }
52157
+ /** The editor document for a canonical rich text document. */
52158
+ function richTextToProse(schema, document) {
52159
+ const blocks = document.blocks.map((block) => proseBlock(schema, block));
52160
+ if (!blocks.length) blocks.push(proseParagraph(schema, [], []));
52161
+ return schema.topNodeType.create(null, blocks);
52162
+ }
52163
+ function richMarksOf(marks) {
52164
+ const result = [];
52165
+ for (const mark of marks) {
52166
+ const type = richMarks$1[mark.type.name];
52167
+ if (!type) throw unrepresentable(`the ${mark.type.name} mark`);
52168
+ result.push(type === "link" ? {
52169
+ type: "link",
52170
+ href: String(mark.attrs.href ?? "")
52171
+ } : { type });
52172
+ }
52173
+ return canonicalMarks(result);
52174
+ }
52175
+ function richInlines(node) {
52176
+ const builder = new InlineBuilder();
52177
+ node.forEach((child) => {
52178
+ if (child.type.name === "hardbreak") builder.break();
52179
+ else if (child.isText) {
52180
+ const text = child.text;
52181
+ if (textProblem(text)) throw unrepresentable("a separator in its text");
52182
+ const marks = richMarksOf(child.marks);
52183
+ if (marks.some((mark) => mark.type === "link" && linkProblem(mark.href))) throw unrepresentable("a link without an address");
52184
+ builder.text(text, marks);
52185
+ } else throw unrepresentable(`a ${child.type.name}`);
52186
+ });
52187
+ return builder.finish();
52188
+ }
52189
+ function richParagraph(node, aligned) {
52190
+ if (node.type.name !== "paragraph") throw unrepresentable(`a ${node.type.name}`);
52191
+ const align = node.attrs.align;
52192
+ if (align != null && !aligned) throw unrepresentable("an aligned table cell");
52193
+ if (align != null && align !== "center" && align !== "right" && align !== "justify") throw unrepresentable(`${align} alignment`);
52194
+ const content = richInlines(node);
52195
+ return align == null ? {
52196
+ type: "paragraph",
52197
+ content
52198
+ } : {
52199
+ type: "paragraph",
52200
+ align,
52201
+ content
52202
+ };
52203
+ }
52204
+ function richBlock(node, listDepth) {
52205
+ switch (node.type.name) {
52206
+ case "paragraph": return richParagraph(node, true);
52207
+ case "bullet_list":
52208
+ case "ordered_list": {
52209
+ const depth = listDepth + 1;
52210
+ if (depth > 9) throw unrepresentable(`lists deeper than 9 levels`);
52211
+ const ordered = node.type.name === "ordered_list";
52212
+ const items = [];
52213
+ node.forEach((item) => {
52214
+ if (item.type.name !== "list_item") throw unrepresentable(`a ${item.type.name}`);
52215
+ if (item.attrs.checked != null) throw unrepresentable("a task");
52216
+ const blocks = [];
52217
+ item.forEach((child, _offset, index) => {
52218
+ if (index === 0) blocks.push(richParagraph(child, true));
52219
+ else if (/^(bullet|ordered)_list$/.test(child.type.name)) blocks.push(richBlock(child, depth));
52220
+ else throw unrepresentable(`a ${child.type.name} in a list item`);
52221
+ });
52222
+ items.push({ blocks });
52223
+ });
52224
+ const start = Number(node.attrs.order ?? 1);
52225
+ if (ordered && (!Number.isSafeInteger(start) || start < 1 || start > 2147483647)) throw unrepresentable(`a list that starts below 1 or above ${maximumListStart}`);
52226
+ return ordered && start !== 1 ? {
52227
+ type: "list",
52228
+ ordered,
52229
+ start,
52230
+ items
52231
+ } : {
52232
+ type: "list",
52233
+ ordered,
52234
+ items
52235
+ };
52236
+ }
52237
+ case "table": {
52238
+ if (listDepth > 0) throw unrepresentable("a table in a list");
52239
+ const rows = [];
52240
+ node.forEach((row) => {
52241
+ const cells = [];
52242
+ row.forEach((cell) => {
52243
+ if (cell.attrs.alignment != null || Number(cell.attrs.colspan ?? 1) !== 1 || Number(cell.attrs.rowspan ?? 1) !== 1) throw unrepresentable("an aligned or merged table cell");
52244
+ if (cell.childCount !== 1) throw unrepresentable("a cell of several blocks");
52245
+ const paragraph = richParagraph(cell.firstChild, false);
52246
+ cells.push({ content: paragraph.content });
52247
+ });
52248
+ rows.push(cells);
52249
+ });
52250
+ if (!rows.length || !rows[0].length || rows.some((row) => row.length !== rows[0].length)) throw unrepresentable("a table with uneven rows");
52251
+ return {
52252
+ type: "table",
52253
+ rows
52254
+ };
52255
+ }
52256
+ default: throw unrepresentable(`a ${node.type.name}`);
52257
+ }
52258
+ }
52259
+ /** The canonical rich text document for an editor document. Throws `preservation` when the
52260
+ * document holds something RTF cannot. The editor always holds a block, so its one empty
52261
+ * paragraph is an empty document when `emptyAsNone`, as it is when the document loaded empty. */
52262
+ function proseToRichText(doc, emptyAsNone = true) {
52263
+ const blocks = [];
52264
+ doc.forEach((node) => blocks.push(richBlock(node, 0)));
52265
+ if (emptyAsNone && blocks.length === 1 && blocks[0].type === "paragraph" && !blocks[0].align && !blocks[0].content.length) blocks.length = 0;
52266
+ return {
52267
+ version: 1,
52268
+ blocks
52269
+ };
52270
+ }
52271
+ /** Why `node` cannot be in an RTF document at `$depth` lists deep, or undefined. Checks the node
52272
+ * itself and its direct children, not its descendants. */
52273
+ function refusal(node, listDepth, inCell) {
52274
+ const name = node.type.name;
52275
+ if (!allowedNodes.has(name)) return name;
52276
+ for (const mark of node.marks) if (!richMarks$1[mark.type.name]) return mark.type.name;
52277
+ if (node.isText && textProblem(node.text)) return "text";
52278
+ const href = node.type.schema.marks.link?.isInSet(node.marks)?.attrs.href;
52279
+ if (href !== void 0 && linkProblem(String(href ?? ""))) return "link";
52280
+ if (name === "paragraph" && node.attrs.align != null && inCell) return "aligned cell";
52281
+ if (name === "list_item") {
52282
+ if (node.attrs.checked != null) return "task";
52283
+ for (let index = 1; index < node.childCount; index++) if (!/^(bullet|ordered)_list$/.test(node.child(index).type.name)) return "list item block";
52284
+ }
52285
+ if ((name === "bullet_list" || name === "ordered_list") && listDepth > 9) return "list depth";
52286
+ const order = Number(node.attrs.order);
52287
+ if (name === "ordered_list" && !(order >= 1 && order <= 2147483647)) return "list start";
52288
+ if (name === "table" && listDepth > 0) return "table in list";
52289
+ if ((name === "table_cell" || name === "table_header") && (node.attrs.alignment != null || Number(node.attrs.colspan ?? 1) !== 1 || Number(node.attrs.rowspan ?? 1) !== 1)) return "cell";
52290
+ }
52291
+ /** Whether the document a transaction reaches holds only what RTF can, read in the part it
52292
+ * changed and that part's ancestors. */
52293
+ function richTextAllows(tr, before) {
52294
+ if (!tr.docChanged) return true;
52295
+ const doc = tr.doc;
52296
+ const start = before.doc.content.findDiffStart(doc.content);
52297
+ if (start == null) return true;
52298
+ const end = before.doc.content.findDiffEnd(doc.content);
52299
+ const to = Math.max(start, end?.b ?? doc.content.size);
52300
+ let allowed = true;
52301
+ const $start = doc.resolve(start);
52302
+ let lists = 0;
52303
+ for (let depth = 1; depth <= $start.depth; depth++) {
52304
+ const node = $start.node(depth);
52305
+ if (/^(bullet|ordered)_list$/.test(node.type.name)) lists++;
52306
+ if (refusal(node, lists, false)) return false;
52307
+ }
52308
+ doc.nodesBetween(start, Math.min(to, doc.content.size), (node, pos) => {
52309
+ if (!allowed) return false;
52310
+ const $pos = doc.resolve(pos);
52311
+ let depth = /^(bullet|ordered)_list$/.test(node.type.name) ? 1 : 0;
52312
+ let inCell = false;
52313
+ for (let level = $pos.depth; level > 0; level--) {
52314
+ const name = $pos.node(level).type.name;
52315
+ if (/^(bullet|ordered)_list$/.test(name)) depth++;
52316
+ if (name === "table_cell" || name === "table_header") inCell = true;
52317
+ }
52318
+ if (refusal(node, depth, inCell)) allowed = false;
52319
+ return allowed;
52320
+ });
52321
+ return allowed;
52322
+ }
52323
+ /** The ranges of runs that have both a link and underline, which canonical form removes the
52324
+ * underline from, in the part of `doc` from `from` to `to`. */
52325
+ function linkedUnderlines(doc, from, to) {
52326
+ const ranges = [];
52327
+ const { link, underline } = doc.type.schema.marks;
52328
+ if (!link || !underline) return ranges;
52329
+ doc.nodesBetween(from, to, (node, pos) => {
52330
+ if (node.isText && link.isInSet(node.marks) && underline.isInSet(node.marks)) ranges.push({
52331
+ from: pos,
52332
+ to: pos + node.nodeSize
52333
+ });
52334
+ });
52335
+ return ranges;
52336
+ }
52337
+ /** Whether every character from `from` to `to`, or the caret's marks, is in a link. */
52338
+ function inLink(state) {
52339
+ const { link } = state.schema.marks;
52340
+ if (!link) return false;
52341
+ const { from, to, empty, $from } = state.selection;
52342
+ if (empty) return Boolean(link.isInSet(state.storedMarks ?? $from.marks()));
52343
+ let text = false;
52344
+ let all = true;
52345
+ state.doc.nodesBetween(from, to, (node) => {
52346
+ if (!node.isText) return true;
52347
+ text = true;
52348
+ if (!link.isInSet(node.marks)) all = false;
52349
+ return all;
52350
+ });
52351
+ return text && all;
52352
+ }
52353
+ //#endregion
52354
+ //#region src/formats/rich-text-import.ts
52355
+ var skipped = /* @__PURE__ */ new Set([
52356
+ "SCRIPT",
52357
+ "STYLE",
52358
+ "TEMPLATE",
52359
+ "NOSCRIPT",
52360
+ "IFRAME",
52361
+ "OBJECT",
52362
+ "EMBED",
52363
+ "HEAD",
52364
+ "TITLE",
52365
+ "META",
52366
+ "LINK",
52367
+ "SVG",
52368
+ "MATH",
52369
+ "IMG",
52370
+ "PICTURE",
52371
+ "VIDEO",
52372
+ "AUDIO",
52373
+ "CANVAS",
52374
+ "BUTTON",
52375
+ "INPUT",
52376
+ "SELECT",
52377
+ "TEXTAREA",
52378
+ "HR"
52379
+ ]);
52380
+ var blockTags = /* @__PURE__ */ new Set([
52381
+ "ADDRESS",
52382
+ "ARTICLE",
52383
+ "ASIDE",
52384
+ "BLOCKQUOTE",
52385
+ "BODY",
52386
+ "CENTER",
52387
+ "DD",
52388
+ "DETAILS",
52389
+ "DIV",
52390
+ "DL",
52391
+ "DT",
52392
+ "FIELDSET",
52393
+ "FIGCAPTION",
52394
+ "FIGURE",
52395
+ "FOOTER",
52396
+ "FORM",
52397
+ "H1",
52398
+ "H2",
52399
+ "H3",
52400
+ "H4",
52401
+ "H5",
52402
+ "H6",
52403
+ "HEADER",
52404
+ "HTML",
52405
+ "LI",
52406
+ "MAIN",
52407
+ "NAV",
52408
+ "OL",
52409
+ "P",
52410
+ "PRE",
52411
+ "SECTION",
52412
+ "SUMMARY",
52413
+ "TABLE",
52414
+ "UL"
52415
+ ]);
52416
+ function withMark(marks, mark) {
52417
+ if (marks.some((held) => held.type === mark.type)) return marks;
52418
+ const other = mark.type === "superscript" ? "subscript" : mark.type === "subscript" ? "superscript" : "";
52419
+ return canonicalMarks([...marks.filter((held) => held.type !== other), mark]);
52420
+ }
52421
+ function withoutMark(marks, type) {
52422
+ return marks.filter((mark) => mark.type !== type);
52423
+ }
52424
+ /** The marks an element adds to its text, from its tag and its inline style. */
52425
+ function elementMarks(element, marks, schemes) {
52426
+ const tag = element.tagName;
52427
+ if (tag === "B" || tag === "STRONG") marks = withMark(marks, { type: "bold" });
52428
+ if (tag === "I" || tag === "EM" || tag === "CITE" || tag === "DFN" || tag === "VAR") marks = withMark(marks, { type: "italic" });
52429
+ if (tag === "U" || tag === "INS") marks = withMark(marks, { type: "underline" });
52430
+ if (tag === "S" || tag === "DEL" || tag === "STRIKE") marks = withMark(marks, { type: "strikethrough" });
52431
+ if (tag === "SUP") marks = withMark(marks, { type: "superscript" });
52432
+ if (tag === "SUB") marks = withMark(marks, { type: "subscript" });
52433
+ if (tag === "CODE" || tag === "KBD" || tag === "SAMP" || tag === "TT") marks = withMark(marks, { type: "code" });
52434
+ if (tag === "A") {
52435
+ const href = element.getAttribute("href")?.trim();
52436
+ if (href && isSafeLinkTarget(href, schemes) && !linkProblem(href)) marks = withMark(withoutMark(marks, "link"), {
52437
+ type: "link",
52438
+ href
52439
+ });
52440
+ }
52441
+ const style = element instanceof HTMLElement ? element.style : void 0;
52442
+ if (style) {
52443
+ const weight = style.fontWeight;
52444
+ if (weight === "bold" || weight === "bolder" || Number(weight) >= 600) marks = withMark(marks, { type: "bold" });
52445
+ else if (weight === "normal" || weight && Number(weight) < 600) marks = withoutMark(marks, "bold");
52446
+ if (style.fontStyle === "italic" || style.fontStyle === "oblique") marks = withMark(marks, { type: "italic" });
52447
+ else if (style.fontStyle === "normal") marks = withoutMark(marks, "italic");
52448
+ const decoration = `${style.textDecoration} ${style.textDecorationLine}`;
52449
+ if (/underline/.test(decoration)) marks = withMark(marks, { type: "underline" });
52450
+ if (/line-through/.test(decoration)) marks = withMark(marks, { type: "strikethrough" });
52451
+ if (style.verticalAlign === "super") marks = withMark(marks, { type: "superscript" });
52452
+ if (style.verticalAlign === "sub") marks = withMark(marks, { type: "subscript" });
52453
+ }
52454
+ return canonicalMarks(marks);
52455
+ }
52456
+ function alignment(element) {
52457
+ const value = (element instanceof HTMLElement ? element.style.textAlign : "") || element.getAttribute("align") || (element.tagName === "CENTER" ? "center" : "");
52458
+ return value === "center" || value === "right" || value === "justify" ? value : void 0;
52459
+ }
52460
+ /** Reads blocks from a DOM, the way a person reads the page. */
52461
+ var Reader = class Reader {
52462
+ schemes;
52463
+ depth;
52464
+ blocks = [];
52465
+ inline;
52466
+ align;
52467
+ space;
52468
+ constructor(schemes, depth) {
52469
+ this.schemes = schemes;
52470
+ this.depth = depth;
52471
+ }
52472
+ /** Ends the open paragraph, if any. */
52473
+ flush() {
52474
+ if (!this.inline) return;
52475
+ const content = this.inline.finish();
52476
+ if (content.at(-1)?.type === "break") content.pop();
52477
+ this.blocks.push(this.align ? {
52478
+ type: "paragraph",
52479
+ align: this.align,
52480
+ content
52481
+ } : {
52482
+ type: "paragraph",
52483
+ content
52484
+ });
52485
+ this.inline = void 0;
52486
+ this.align = void 0;
52487
+ this.space = void 0;
52488
+ }
52489
+ open() {
52490
+ this.inline ??= new InlineBuilder();
52491
+ return this.inline;
52492
+ }
52493
+ text(value, marks, pre) {
52494
+ if (pre) {
52495
+ value.split(/\r\n|[\n\r]/).forEach((line, index) => {
52496
+ if (index) this.flush();
52497
+ appendText(this.open(), line, marks);
52498
+ });
52499
+ return;
52500
+ }
52501
+ const collapsed = value.replace(/[ \t\n\r\f]+/g, " ");
52502
+ if (!collapsed) return;
52503
+ if (collapsed === " ") {
52504
+ if (this.inline?.content.length) this.space ??= marks;
52505
+ return;
52506
+ }
52507
+ const builder = this.open();
52508
+ const empty = !builder.content.length;
52509
+ let text = collapsed;
52510
+ if (text.startsWith(" ")) {
52511
+ if (!empty) this.space ??= marks;
52512
+ text = text.slice(1);
52513
+ }
52514
+ if (!text) return;
52515
+ if (this.space) {
52516
+ builder.text(" ", this.space);
52517
+ this.space = void 0;
52518
+ }
52519
+ if (text.endsWith(" ")) {
52520
+ this.space = marks;
52521
+ text = text.slice(0, -1);
52522
+ }
52523
+ appendText(builder, text, marks);
52524
+ }
52525
+ lineBreak() {
52526
+ this.space = void 0;
52527
+ this.open().break();
52528
+ }
52529
+ /** Starts a paragraph that takes `element`'s alignment. */
52530
+ paragraph(element) {
52531
+ this.flush();
52532
+ this.align = alignment(element);
52533
+ }
52534
+ push(block) {
52535
+ this.flush();
52536
+ this.blocks.push(block);
52537
+ }
52538
+ read(node, marks, pre) {
52539
+ if (node.nodeType === Node.TEXT_NODE) {
52540
+ this.text(node.textContent ?? "", marks, pre);
52541
+ return;
52542
+ }
52543
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
52544
+ const element = node;
52545
+ const tag = element.tagName.toUpperCase();
52546
+ if (skipped.has(tag) || element instanceof HTMLElement && element.hidden) return;
52547
+ if (element instanceof HTMLElement && element.style.display === "none") return;
52548
+ if (tag === "BR") {
52549
+ this.lineBreak();
52550
+ return;
52551
+ }
52552
+ if (tag === "UL" || tag === "OL") {
52553
+ this.push(this.list(element, marks));
52554
+ return;
52555
+ }
52556
+ if (tag === "TABLE") {
52557
+ const table = this.depth === 0 && !spansRows(element);
52558
+ const rows = tableRows(element, marks, this.schemes, table);
52559
+ if (!rows.length) return;
52560
+ if (table) this.push({
52561
+ type: "table",
52562
+ rows
52563
+ });
52564
+ else for (const row of rows) {
52565
+ this.paragraph(element);
52566
+ this.open();
52567
+ row.forEach((cell, index) => {
52568
+ if (index) this.open().text(" ", []);
52569
+ for (const inline of cell.content) if (inline.type === "break") this.open().break();
52570
+ else this.open().text(inline.text, inline.marks ?? []);
52571
+ });
52572
+ this.flush();
52573
+ }
52574
+ return;
52575
+ }
52576
+ let inner = elementMarks(element, marks, this.schemes);
52577
+ if (/^H[1-6]$/.test(tag)) inner = withMark(inner, { type: "bold" });
52578
+ const block = blockTags.has(tag);
52579
+ if (block) this.paragraph(element);
52580
+ if (tag === "P" || /^H[1-6]$/.test(tag)) this.open();
52581
+ const preformatted = pre || tag === "PRE" || element instanceof HTMLElement && /^pre/.test(element.style.whiteSpace);
52582
+ if (tag === "PRE") inner = withMark(inner, { type: "code" });
52583
+ for (const child of element.childNodes) this.read(child, inner, preformatted);
52584
+ if (block) this.flush();
52585
+ }
52586
+ list(element, marks) {
52587
+ const ordered = element.tagName.toUpperCase() === "OL";
52588
+ const start = Number(element.getAttribute("start") ?? 1);
52589
+ const items = [];
52590
+ const inner = elementMarks(element, marks, this.schemes);
52591
+ for (const child of element.childNodes) {
52592
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName.toUpperCase() === "LI") {
52593
+ items.push({ blocks: this.item(child, inner) });
52594
+ continue;
52595
+ }
52596
+ const reader = new Reader(this.schemes, this.depth + 1);
52597
+ reader.read(child, inner, false);
52598
+ reader.flush();
52599
+ if (!reader.blocks.length) continue;
52600
+ const blocks = itemBlocks(reader.blocks, this.depth + 1);
52601
+ if (items.length && blocks[0].type === "paragraph" && !blocks[0].content.length) items.at(-1).blocks.push(...blocks.slice(1));
52602
+ else items.push({ blocks });
52603
+ }
52604
+ if (!items.length) items.push({ blocks: [{
52605
+ type: "paragraph",
52606
+ content: []
52607
+ }] });
52608
+ return ordered && Number.isSafeInteger(start) && start > 1 ? {
52609
+ type: "list",
52610
+ ordered,
52611
+ start: Math.min(start, maximumListStart),
52612
+ items
52613
+ } : {
52614
+ type: "list",
52615
+ ordered,
52616
+ items
52617
+ };
52618
+ }
52619
+ item(element, marks) {
52620
+ const reader = new Reader(this.schemes, this.depth + 1);
52621
+ const inner = elementMarks(element, marks, this.schemes);
52622
+ for (const child of element.childNodes) reader.read(child, inner, false);
52623
+ reader.flush();
52624
+ return itemBlocks(reader.blocks, this.depth + 1);
52625
+ }
52626
+ };
52627
+ /** A list item's blocks as RTF holds them: one paragraph, then lists. Further paragraphs join the
52628
+ * first as lines. Lists keep every level here; the paste lifts those deeper than RTF holds. */
52629
+ function itemBlocks(blocks, depth) {
52630
+ const builder = new InlineBuilder();
52631
+ let align;
52632
+ let lines = 0;
52633
+ const nested = [];
52634
+ const line = (content) => {
52635
+ if (lines++) builder.break();
52636
+ for (const inline of content) if (inline.type === "break") builder.break();
52637
+ else builder.text(inline.text, inline.marks ?? []);
52638
+ };
52639
+ const flatten = (block) => {
52640
+ if (block.type === "paragraph") line(block.content);
52641
+ else if (block.type === "list") for (const item of block.items) for (const child of item.blocks) flatten(child);
52642
+ else for (const row of block.rows) for (const cell of row) line(cell.content);
52643
+ };
52644
+ for (const block of blocks) if (block.type === "list") nested.push(block);
52645
+ else if (block.type === "paragraph" && !nested.length) {
52646
+ if (!lines) align = block.align;
52647
+ line(block.content);
52648
+ } else flatten(block);
52649
+ const content = builder.finish();
52650
+ return [align ? {
52651
+ type: "paragraph",
52652
+ align,
52653
+ content
52654
+ } : {
52655
+ type: "paragraph",
52656
+ content
52657
+ }, ...nested];
52658
+ }
52659
+ /** Whether a cell of `table` itself, not of a table inside it, spans more than one row. */
52660
+ function spansRows(table) {
52661
+ return [...table.querySelectorAll("td[rowspan], th[rowspan]")].some((cell) => cell.closest("table") === table && Number(cell.getAttribute("rowspan")) > 1);
52662
+ }
52663
+ function tableRows(table, marks, schemes, even) {
52664
+ const rows = [];
52665
+ for (const row of table.querySelectorAll("tr")) {
52666
+ if (row.closest("table") !== table) continue;
52667
+ const cells = [];
52668
+ for (const cell of row.children) {
52669
+ if (cell.tagName !== "TD" && cell.tagName !== "TH") continue;
52670
+ const reader = new Reader(schemes, 1);
52671
+ const inner = elementMarks(cell, marks, schemes);
52672
+ for (const child of cell.childNodes) reader.read(child, inner, false);
52673
+ reader.flush();
52674
+ const builder = new InlineBuilder();
52675
+ let lines = 0;
52676
+ const add = (block) => {
52677
+ if (block.type === "paragraph") {
52678
+ if (lines++) builder.break();
52679
+ for (const inline of block.content) if (inline.type === "break") builder.break();
52680
+ else builder.text(inline.text, inline.marks ?? []);
52681
+ } else if (block.type === "list") for (const item of block.items) item.blocks.forEach(add);
52682
+ else for (const nestedRow of block.rows) for (const nestedCell of nestedRow) add({
52683
+ type: "paragraph",
52684
+ content: nestedCell.content
52685
+ });
52686
+ };
52687
+ reader.blocks.forEach(add);
52688
+ const content = builder.finish();
52689
+ const span = Math.max(1, Math.min(100, Number(cell.getAttribute("colspan") ?? 1) || 1));
52690
+ cells.push({ content });
52691
+ for (let index = 1; index < span; index++) cells.push({ content: [] });
52692
+ }
52693
+ if (cells.length) rows.push(cells);
52694
+ }
52695
+ const width = even ? Math.max(0, ...rows.map((row) => row.length)) : 0;
52696
+ for (const row of rows) while (row.length < width) row.push({ content: [] });
52697
+ return rows;
52698
+ }
52699
+ /** Blocks for pasted HTML. */
52700
+ function htmlBlocks(html, schemes) {
52701
+ const parsed = new DOMParser().parseFromString(html, "text/html");
52702
+ const reader = new Reader(schemes, 0);
52703
+ reader.read(parsed.body, [], false);
52704
+ reader.flush();
52705
+ return reader.blocks;
52706
+ }
52707
+ /** Every block's text as lines of one paragraph, for a place that holds only one, such as a
52708
+ * table cell. */
52709
+ function asLines(blocks) {
52710
+ const builder = new InlineBuilder();
52711
+ let lines = 0;
52712
+ const add = (content) => {
52713
+ if (lines++) builder.break();
52714
+ for (const inline of content) if (inline.type === "break") builder.break();
52715
+ else builder.text(inline.text, inline.marks ?? []);
52716
+ };
52717
+ const visit = (block) => {
52718
+ if (block.type === "paragraph") add(block.content);
52719
+ else if (block.type === "list") block.items.forEach((item) => item.blocks.forEach(visit));
52720
+ else block.rows.forEach((row) => add(row.flatMap((cell, index) => index ? [{
52721
+ type: "text",
52722
+ text: " "
52723
+ }, ...cell.content] : cell.content)));
52724
+ };
52725
+ blocks.forEach(visit);
52726
+ return [{
52727
+ type: "paragraph",
52728
+ content: builder.finish()
52729
+ }];
52730
+ }
52731
+ /** `list`, at `depth` lists deep, with every level beyond the deepest RTF holds lifted to the
52732
+ * deepest: the items of a list that would be too deep become items of its parent list. */
52733
+ function liftDeepLevels(list, depth) {
52734
+ const flat = (deep) => deep.items.flatMap((item) => [{ blocks: [item.blocks[0]] }, ...item.blocks.slice(1).flatMap((nested) => flat(nested))]);
52735
+ return {
52736
+ ...list,
52737
+ items: list.items.flatMap((item) => {
52738
+ const [paragraph, ...nested] = item.blocks;
52739
+ const kept = [];
52740
+ const lifted = [];
52741
+ for (const child of nested) if (depth + 1 <= 9) kept.push(liftDeepLevels(child, depth + 1));
52742
+ else lifted.push(...flat(child));
52743
+ return [{ blocks: [paragraph, ...kept] }, ...lifted];
52744
+ })
52745
+ };
52746
+ }
52747
+ /**
52748
+ * A paste of several blocks into a list item, which holds one paragraph: each paragraph becomes
52749
+ * an item beside the one at the caret, at its level. The first joins the item at the caret, the
52750
+ * text after the caret moves to the last, and a pasted list nests in the item before it.
52751
+ */
52752
+ function listItemsSlice(schema, blocks, list, depth, marks) {
52753
+ const empty = {
52754
+ type: "paragraph",
52755
+ content: []
52756
+ };
52757
+ const items = [];
52758
+ for (const block of blocks) if (block.type === "paragraph") items.push({ blocks: [block] });
52759
+ else if (block.type === "list") {
52760
+ if (!items.length) items.push({ blocks: [empty] });
52761
+ items.at(-1).blocks.push(block);
52762
+ } else for (const line of asLines([block])) items.push({ blocks: [line] });
52763
+ if (!items.length || items.at(-1).blocks.length > 1) items.push({ blocks: [empty] });
52764
+ const ordered = list.type.name === "ordered_list";
52765
+ const nodes = liftDeepLevels({
52766
+ type: "list",
52767
+ ordered,
52768
+ items
52769
+ }, depth).items.map((item) => schema.nodes.list_item.create({
52770
+ label: ordered ? "1." : "•",
52771
+ listType: ordered ? "ordered" : "bullet",
52772
+ spread: false,
52773
+ checked: null
52774
+ }, item.blocks.map((block) => proseBlock(schema, block, marks))));
52775
+ return new Slice(Fragment.fromArray(nodes), 2, 2);
52776
+ }
52777
+ /**
52778
+ * The slice that `input` pastes into an RTF document at the selection. Explicit Markdown pastes as
52779
+ * its text, unparsed; HTML keeps what RTF can hold; plain text gives a paragraph for each line.
52780
+ * Plain text takes the marks of the text it replaces, as typing would.
52781
+ */
52782
+ function richTextPaste(input, state, schemes) {
52783
+ const { schema, selection } = state;
52784
+ const { $from, $to } = selection;
52785
+ const html = input.markdown == null && input.html ? htmlBlocks(input.html, schemes) : void 0;
52786
+ const plain = html == null;
52787
+ let blocks = html ?? textBlocks(input.markdown ?? input.text);
52788
+ if (html && !blocks.some((block) => block.type !== "paragraph" || block.content.length)) blocks = textBlocks(input.text);
52789
+ let cell = false;
52790
+ let lists = 0;
52791
+ let list;
52792
+ for (let depth = $from.depth; depth > 0; depth--) {
52793
+ const node = $from.node(depth);
52794
+ const name = node.type.name;
52795
+ if (name === "table_cell" || name === "table_header") cell = true;
52796
+ if (name === "bullet_list" || name === "ordered_list") {
52797
+ lists++;
52798
+ list ??= node;
52799
+ }
52800
+ }
52801
+ if (cell) blocks = asLines(blocks);
52802
+ else blocks = blocks.map((block) => block.type === "list" ? liftDeepLevels(block, lists + 1) : block);
52803
+ const marks = plain ? state.storedMarks ?? ($from.marksAcross($to) ?? $from.marks()).filter((mark) => mark.type.name !== "link") : [];
52804
+ if (list && !cell && !(blocks.length === 1 && blocks[0].type === "paragraph")) return listItemsSlice(schema, blocks, list, lists, marks);
52805
+ const nodes = blocks.map((block) => proseBlock(schema, block, marks));
52806
+ if (!nodes.length) return Slice.empty;
52807
+ const openStart = nodes[0].type.name === "paragraph" ? 1 : 0;
52808
+ const openEnd = nodes.at(-1).type.name === "paragraph" ? 1 : 0;
52809
+ return new Slice(Fragment.fromArray(nodes), openStart, openEnd);
52810
+ }
52811
+ //#endregion
52812
+ //#region src/formats/rich-text.ts
52813
+ function source(ctx, text) {
52814
+ const { schema } = ctx.get(editorViewCtx).state;
52815
+ const document = richTextFromText(text);
52816
+ const emptyAsNone = document.blocks.length === 0;
52817
+ return {
52818
+ baseline: richTextToProse(schema, document),
52819
+ serialize: (doc) => richTextText(proseToRichText(doc, emptyAsNone))
52820
+ };
52821
+ }
52822
+ /** Rich text, which the host converts to and from RTF. The editor holds the canonical JSON as the
52823
+ * document's text. It reaches no Markdown parsing or preservation;
52824
+ * `test/formats/boundary.test.ts` checks its imports. */
52825
+ var richTextFormat = {
52826
+ format: "rtf",
52827
+ promise: "structure",
52828
+ literalOnly: false,
52829
+ sourceMode: false,
52830
+ formattedLabel: "richTextEditor",
52831
+ literalLabel: "sourceEditor",
52832
+ headings: false,
52833
+ nativeCopy: false,
52834
+ keepsInsertedLineEndings: false,
52835
+ markdownInput: false,
52836
+ load: source,
52837
+ parse: source,
52838
+ pendingExport: () => void 0,
52839
+ literalPaste: (input) => input.text,
52840
+ richText: {
52841
+ allows: richTextAllows,
52842
+ linkedUnderlines,
52843
+ inLink,
52844
+ paste: richTextPaste,
52845
+ copy(state, linkSchemes) {
52846
+ const content = state.selection.content().content;
52847
+ return clipboardContent(content, state.schema, linkSchemes);
52848
+ }
52849
+ },
52850
+ async print({ doc, signal, output }) {
52851
+ return printableMarkdown(doc.content, output.ctx().get(editorViewCtx).state.schema, void 0, output.context(), void 0, signal);
52852
+ },
52853
+ async clipboard({ all, version, output, assertCurrent }) {
52854
+ const { state } = output.ctx().get(editorViewCtx);
52855
+ const slice = all ? new Slice(state.doc.content, 0, 0) : state.selection.content();
52856
+ const content = slice.content;
52857
+ const markdown = output.richTextMarkdown(slice);
52858
+ const { html } = clipboardContent(content, state.schema, output.linkSchemes());
52859
+ const text = clipboardText(content);
52860
+ assertCurrent();
52861
+ return {
52862
+ ...version,
52863
+ text,
52864
+ html,
52865
+ markdown,
52866
+ images: [],
52867
+ diagrams: []
52868
+ };
52869
+ }
52870
+ };
51645
52871
  //#endregion
51646
52872
  //#region src/formats/text.ts
51647
52873
  /** A paragraph holding `text`, or an empty one. */
@@ -51656,10 +52882,12 @@ var textFormat = {
51656
52882
  promise: "exact",
51657
52883
  literalOnly: true,
51658
52884
  sourceMode: false,
52885
+ formattedLabel: "formattedEditor",
51659
52886
  literalLabel: "plainTextEditor",
51660
52887
  headings: false,
51661
52888
  nativeCopy: true,
51662
52889
  keepsInsertedLineEndings: false,
52890
+ markdownInput: false,
51663
52891
  load: (ctx) => paragraph(ctx, ""),
51664
52892
  parse: paragraph,
51665
52893
  pendingExport: () => void 0,
@@ -51681,11 +52909,18 @@ var textFormat = {
51681
52909
  };
51682
52910
  //#endregion
51683
52911
  //#region src/formats/index.ts
51684
- /** Every format Inkkit can open, and the list a host is offered when it chooses none. */
51685
- var documentFormats = ["md", "txt"];
52912
+ /** Every format Inkkit can open. */
52913
+ var documentFormats = [
52914
+ "md",
52915
+ "txt",
52916
+ "rtf"
52917
+ ];
52918
+ /** The formats a host is offered when it chooses none. */
52919
+ var defaultFormats = ["md", "txt"];
51686
52920
  var formatHandlers = {
51687
52921
  md: markdownFormat,
51688
- txt: textFormat
52922
+ txt: textFormat,
52923
+ rtf: richTextFormat
51689
52924
  };
51690
52925
  //#endregion
51691
52926
  //#region src/editor-document.ts
@@ -51746,7 +52981,7 @@ var EditorDocument = class {
51746
52981
  this.sourcePreservations.set(provenance, preservation);
51747
52982
  }
51748
52983
  }
51749
- text = preservation.serialize(cleanSourceDoc(doc), this.carried);
52984
+ text = preservation.serialize(this.handler.richText ? doc : cleanSourceDoc(doc), this.carried);
51750
52985
  } catch (error) {
51751
52986
  throw coded(error, "preservation");
51752
52987
  }
@@ -51793,7 +53028,7 @@ var EditorDocument = class {
51793
53028
  */
51794
53029
  authoredSources(doc, positions) {
51795
53030
  const none = /* @__PURE__ */ new Map();
51796
- if (this.runtime.literal() || !positions.length) return none;
53031
+ if (this.runtime.literal() || this.handler.richText || !positions.length) return none;
51797
53032
  try {
51798
53033
  let marked = doc;
51799
53034
  for (let index = positions.length - 1; index >= 0; index--) {
@@ -52305,7 +53540,7 @@ var rejoinLists = $prose(() => keymap$3({ Backspace: (state, dispatch) => {
52305
53540
  const list = $from.node(-2);
52306
53541
  const sublist = container.maybeChild(1);
52307
53542
  const previous = list.maybeChild($from.index(-2) - 1)?.lastChild;
52308
- if (index !== 0 || container.childCount !== 2 || !sublist || !isList(sublist) || previous?.type !== sublist.type) return false;
53543
+ if (index !== 0 || container.childCount !== 2 || !sublist || !isList$1(sublist) || previous?.type !== sublist.type) return false;
52309
53544
  if (dispatch) {
52310
53545
  const at = $from.before(-1);
52311
53546
  const end = at - 2;
@@ -52317,7 +53552,7 @@ var rejoinLists = $prose(() => keymap$3({ Backspace: (state, dispatch) => {
52317
53552
  }
52318
53553
  const before = container.maybeChild(index - 1);
52319
53554
  const after = container.maybeChild(index + 1);
52320
- if (!before || !after || !isList(before) || after.type !== before.type) return false;
53555
+ if (!before || !after || !isList$1(before) || after.type !== before.type) return false;
52321
53556
  if (dispatch) {
52322
53557
  const at = $from.before();
52323
53558
  const tr = state.tr.delete(at, $from.after());
@@ -52390,21 +53625,21 @@ var movedListItems = $prose(() => new Plugin({
52390
53625
  }));
52391
53626
  var textInputPolicy = $prose(() => textInputPlugin());
52392
53627
  function outermostListDepth($pos) {
52393
- for (let depth = 1; depth <= $pos.depth; depth++) if (isList($pos.node(depth))) return depth;
53628
+ for (let depth = 1; depth <= $pos.depth; depth++) if (isList$1($pos.node(depth))) return depth;
52394
53629
  return null;
52395
53630
  }
52396
53631
  function innermostListDepth($pos) {
52397
- for (let depth = $pos.depth; depth >= 1; depth--) if (isList($pos.node(depth))) return depth;
53632
+ for (let depth = $pos.depth; depth >= 1; depth--) if (isList$1($pos.node(depth))) return depth;
52398
53633
  return null;
52399
53634
  }
52400
- function isList(node) {
53635
+ function isList$1(node) {
52401
53636
  return node.type.name === "bullet_list" || node.type.name === "ordered_list";
52402
53637
  }
52403
53638
  function joinLists(tr, start, index) {
52404
53639
  const $start = tr.doc.resolve(start);
52405
53640
  const before = $start.parent.maybeChild(index - 1);
52406
53641
  const after = $start.parent.maybeChild(index);
52407
- if (before && after && isList(before) && after.type === before.type) tr.join($start.posAtIndex(index));
53642
+ if (before && after && isList$1(before) && after.type === before.type) tr.join($start.posAtIndex(index));
52408
53643
  }
52409
53644
  //#endregion
52410
53645
  //#region src/editor-commands.ts
@@ -52412,7 +53647,67 @@ function headingLevel(command) {
52412
53647
  const match = /^heading([1-6])$/.exec(command);
52413
53648
  return match ? Number(match[1]) : void 0;
52414
53649
  }
52415
- function formatAvailability(editor, state, formatted) {
53650
+ /** What an RTF document cannot hold: headings, code blocks, quotes, tasks and highlight. Every
53651
+ * other command is available as in Markdown, and alignment only here. */
53652
+ var unavailableInRichText = [
53653
+ "heading1",
53654
+ "heading2",
53655
+ "heading3",
53656
+ "heading4",
53657
+ "heading5",
53658
+ "heading6",
53659
+ "paragraph",
53660
+ "highlight",
53661
+ "codeBlock",
53662
+ "quote",
53663
+ "taskList",
53664
+ "toggleTask"
53665
+ ];
53666
+ var alignments = {
53667
+ alignLeft: null,
53668
+ alignCenter: "center",
53669
+ alignRight: "right",
53670
+ alignJustify: "justify"
53671
+ };
53672
+ function isAlignCommand(command) {
53673
+ return command in alignments;
53674
+ }
53675
+ /** Calls `found` for each paragraph the selection touches that can take an alignment, which
53676
+ * those in table cells cannot, until it returns false. */
53677
+ function eachAlignable(state, found) {
53678
+ const { from, to } = state.selection;
53679
+ let going = true;
53680
+ state.doc.nodesBetween(from, to, (node, pos) => {
53681
+ if (!going || node.type.name === "table") return false;
53682
+ if (node.type.name === "paragraph") going = found(node, pos);
53683
+ return going && !node.isTextblock;
53684
+ });
53685
+ }
53686
+ function canAlign(state) {
53687
+ let any = false;
53688
+ eachAlignable(state, () => {
53689
+ any = true;
53690
+ return false;
53691
+ });
53692
+ return any;
53693
+ }
53694
+ /** The alignment of the paragraph that holds the start of the selection. */
53695
+ function caretAlignment(state) {
53696
+ const { parent } = state.selection.$from;
53697
+ return parent.type.name === "paragraph" && parent.attrs.align ? parent.attrs.align : "left";
53698
+ }
53699
+ function formatAvailability(editor, state, formatted, handler) {
53700
+ const available = markdownAvailability(editor, state, formatted);
53701
+ const rich = handler.richText;
53702
+ if (!rich) return available;
53703
+ for (const command of unavailableInRichText) available[command] = false;
53704
+ if (available.underline && rich.inLink(state)) available.underline = false;
53705
+ const align = formatted && canAlign(state);
53706
+ available.alignLeft = available.alignCenter = align;
53707
+ available.alignRight = available.alignJustify = align;
53708
+ return available;
53709
+ }
53710
+ function markdownAvailability(editor, state, formatted) {
52416
53711
  const manager = editor.ctx.get(commandsCtx);
52417
53712
  const check = (key, payload) => formatted && manager.get(key)(payload)(state);
52418
53713
  const canBecome = (type, level) => formatted && textblockType(editor, type, level)(state);
@@ -52439,9 +53734,27 @@ function formatAvailability(editor, state, formatted) {
52439
53734
  orderedList: formatted && (innermostListDepth(textBounds(state).$from) != null || check(wrapInOrderedListCommand.key)),
52440
53735
  taskList: formatted && (innermostListDepth(textBounds(state).$from) != null || check(wrapInBulletListCommand.key)),
52441
53736
  toggleTask: formatted && taskAtCaret(state) != null,
53737
+ alignLeft: false,
53738
+ alignCenter: false,
53739
+ alignRight: false,
53740
+ alignJustify: false,
52442
53741
  link: check(toggleLinkCommand.key, {})
52443
53742
  };
52444
53743
  }
53744
+ /** Sets the alignment of the paragraphs the selection touches. */
53745
+ function align(editor, command) {
53746
+ const view = editor.ctx.get(editorViewCtx);
53747
+ const value = alignments[command];
53748
+ const tr = view.state.tr;
53749
+ eachAlignable(view.state, (node, pos) => {
53750
+ if (node.attrs.align !== value) tr.setNodeMarkup(pos, void 0, {
53751
+ ...node.attrs,
53752
+ align: value
53753
+ });
53754
+ return true;
53755
+ });
53756
+ if (tr.docChanged) view.dispatch(tr);
53757
+ }
52445
53758
  function canQuote(editor, state) {
52446
53759
  const { $from, $to } = textBounds(state);
52447
53760
  if (quoted($from, $to)) {
@@ -52496,12 +53809,32 @@ function toggleInlineCode(editor) {
52496
53809
  if (!type.isInSet(selection.$from.marks())) view.dispatch(view.state.tr.addStoredMark(type.create()));
52497
53810
  else withMarkRunSelected(editor, type, run);
52498
53811
  }
52499
- function list(editor, kind) {
53812
+ function list(editor, kind, itemPerBlock) {
52500
53813
  const view = editor.ctx.get(editorViewCtx);
52501
53814
  const { state } = view;
52502
53815
  const { $from } = textBounds(state);
52503
53816
  const wanted = kind === "bulletList" ? "bullet_list" : "ordered_list";
52504
53817
  const depth = innermostListDepth($from);
53818
+ if (depth == null && itemPerBlock) {
53819
+ const ordered = wanted === "ordered_list";
53820
+ wrapInList(state.schema.nodes[wanted], ordered ? {
53821
+ order: 1,
53822
+ spread: false
53823
+ } : { spread: false })(state, (tr) => {
53824
+ tr.doc.nodesBetween(tr.mapping.map(state.selection.from), tr.mapping.map(state.selection.to), (node, pos) => {
53825
+ if (node.type.name !== "list_item") return true;
53826
+ tr.setNodeMarkup(pos, void 0, {
53827
+ ...node.attrs,
53828
+ listType: ordered ? "ordered" : "bullet",
53829
+ label: ordered ? "1." : "•",
53830
+ spread: false
53831
+ });
53832
+ return false;
53833
+ });
53834
+ view.dispatch(tr);
53835
+ });
53836
+ return;
53837
+ }
52505
53838
  if (depth == null) {
52506
53839
  const command = kind === "bulletList" ? wrapInBulletListCommand : wrapInOrderedListCommand;
52507
53840
  editor.action(callCommand(command.key));
@@ -52560,11 +53893,15 @@ function textblockType(editor, type, attr) {
52560
53893
  if (type === "heading") return setTextblockType(ctx, headingSchema.type(ctx), { level: Number(attr) });
52561
53894
  return setTextblockType(ctx, paragraphSchema.type(ctx));
52562
53895
  }
52563
- function applyFormat(editor, command, checked, link) {
53896
+ function applyFormat(editor, command, checked, link, itemPerBlock = false) {
52564
53897
  const view = editor.ctx.get(editorViewCtx);
52565
53898
  const run = (cmd, payload) => editor.action(callCommand(cmd, payload));
52566
53899
  const state = caretState(view.state);
52567
53900
  const become = (type, attr) => textblockType(editor, type, attr)(view.state, view.dispatch);
53901
+ if (isAlignCommand(command)) {
53902
+ align(editor, command);
53903
+ return;
53904
+ }
52568
53905
  const level = headingLevel(command);
52569
53906
  if (level != null) {
52570
53907
  if (state.block.type === "heading" && state.block.level === level) become("paragraph");
@@ -52608,7 +53945,7 @@ function applyFormat(editor, command, checked, link) {
52608
53945
  break;
52609
53946
  case "bulletList":
52610
53947
  case "orderedList":
52611
- list(editor, command);
53948
+ list(editor, command, itemPerBlock);
52612
53949
  break;
52613
53950
  case "taskList":
52614
53951
  toggleTaskList(editor.ctx);
@@ -55583,6 +56920,7 @@ async function createMilkdownEditor(root, options, runtime) {
55583
56920
  ctx.set(rootCtx, root);
55584
56921
  ctx.set(authoredSourcesCtx.key, (doc, positions) => runtime.authoredSources(doc, positions));
55585
56922
  ctx.set(smartPunctuationCtx.key, () => runtime.smartPunctuation());
56923
+ ctx.set(formatInputCtx.key, runtime.format);
55586
56924
  ctx.set(linkSchemesCtx.key, runtime.linkSchemes);
55587
56925
  ctx.update(labelsCtx.key, () => ({
55588
56926
  ...defaultLabels,
@@ -55632,7 +56970,7 @@ async function createMilkdownEditor(root, options, runtime) {
55632
56970
  return true;
55633
56971
  }
55634
56972
  }));
55635
- }).use(labelsCtx).use(linkSchemesCtx).use(runtime.plugins.paste).use(visibleClipboard).use(runtime.plugins.suggestions).use(runtime.plugins.keymap).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => runtime.visibleRect())).use(tablePlugins).use(tableBlock).use(runtime.plugins.change).use(runtime.plugins.historyInput).use(history).use(clipboard).use(cursor).use(taskListPlugin((operation) => runtime.mutate(operation))).use(undoTypedRule).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(runtime.copy ? codeCopyPlugin((text) => runtime.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-wulvkYDg.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview(runtime.diagrams, (error) => runtime.report(error))).use(selectionPlugin).use($prose(() => search())).use(runtime.plugins.policy).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
56973
+ }).use(labelsCtx).use(linkSchemesCtx).use(formatInputCtx).use(richTextGuard).use(runtime.plugins.paste).use(visibleClipboard).use(runtime.plugins.suggestions).use(runtime.plugins.keymap).use(createDialect(Boolean(options.images || options.files), Boolean(options.wikiLinks), Boolean(options.files))).use(codeControlCtx).use(codeControlsPlugin(() => runtime.visibleRect())).use(tablePlugins).use(tableBlock).use(runtime.plugins.change).use(runtime.plugins.historyInput).use(history).use(clipboard).use(cursor).use(taskListPlugin((operation) => runtime.mutate(operation))).use(undoTypedRule).use(quoteBackspace).use(calloutEnter).use(literalEnter).use(literalReread).use(listItemBackspace).use(nestedItemEnter).use(rejoinLists).use(movedListItems).use(textInputPolicy).use(runtime.copy ? codeCopyPlugin((text) => runtime.copy?.(text)) : []).use(placeholderPlugin).use(options.rendering?.codeHighlighting === false ? [] : (await import("./highlight-CqtvWKCp.js")).highlightPlugin).use(options.rendering?.diagramPreview === false || !diagramsAvailable() ? [] : mermaidPreview(runtime.diagrams, (error) => runtime.report(error))).use(selectionPlugin).use($prose(() => search())).use(runtime.plugins.policy).use(options.wikiLinks ? wikiLinkView(options.wikiLinks, {
55636
56974
  capture: () => runtime.capture(),
55637
56975
  active: () => runtime.active(),
55638
56976
  labels: {
@@ -55648,8 +56986,46 @@ async function createMilkdownEditor(root, options, runtime) {
55648
56986
  ...options.labels
55649
56987
  },
55650
56988
  error: (error) => runtime.report(error)
55651
- }, options.files, runtime.canMutate) : []).create();
56989
+ }, options.files, runtime.canMutate) : []).create().then((editor) => {
56990
+ markdownInputRules(editor.ctx);
56991
+ return editor;
56992
+ });
56993
+ }
56994
+ /** Markdown input rules, such as `# ` for a heading, do nothing in a format that is not Markdown.
56995
+ * Rules that are not Markdown syntax, such as smart punctuation, keep applying. */
56996
+ function markdownInputRules(ctx) {
56997
+ for (const rule of ctx.get(inputRulesCtx)) {
56998
+ if (formatNeutralRules.has(rule)) continue;
56999
+ const held = rule;
57000
+ const handler = held.handler;
57001
+ held.handler = (state, match, start, end) => formatInput(ctx).markdownInput() ? handler(state, match, start, end) : null;
57002
+ }
55652
57003
  }
57004
+ /**
57005
+ * In a format that holds less than Markdown, such as RTF, refuses any transaction that would bring
57006
+ * in a node, mark or structure the format cannot hold, so a disallowed edit changes nothing. A
57007
+ * link's underline, which RTF does not keep, is removed in the same step.
57008
+ */
57009
+ var richTextGuard = $prose((ctx) => new Plugin({
57010
+ key: new PluginKey("inkkitRichTextGuard"),
57011
+ filterTransaction(tr, state) {
57012
+ const rich = formatInput(ctx).richText();
57013
+ return !rich || rich.allows(tr, state);
57014
+ },
57015
+ appendTransaction(transactions, oldState, newState) {
57016
+ const rich = formatInput(ctx).richText();
57017
+ if (!rich || !transactions.some((tr) => tr.docChanged)) return null;
57018
+ const start = oldState.doc.content.findDiffStart(newState.doc.content);
57019
+ if (start == null) return null;
57020
+ const end = oldState.doc.content.findDiffEnd(newState.doc.content).b;
57021
+ const ranges = rich.linkedUnderlines(newState.doc, start, Math.max(start, end));
57022
+ if (!ranges.length) return null;
57023
+ const tr = newState.tr;
57024
+ for (const { from, to } of ranges) tr.removeMark(from, to, newState.schema.marks.underline);
57025
+ if (newState.storedMarks) tr.setStoredMarks(newState.storedMarks);
57026
+ return tr;
57027
+ }
57028
+ }));
55653
57029
  function createDocumentState(ctx, doc) {
55654
57030
  return EditorState.create(ctx.get(editorStateOptionsCtx)({
55655
57031
  schema: ctx.get(schemaCtx),
@@ -55662,6 +57038,402 @@ function initialDocumentSelection(state) {
55662
57038
  return setSearchState(state.tr.setSelection(end), new SearchQuery({ search: "" }));
55663
57039
  }
55664
57040
  //#endregion
57041
+ //#region src/formats/convert.ts
57042
+ var Losses = class {
57043
+ counts = /* @__PURE__ */ new Map();
57044
+ add(code) {
57045
+ this.counts.set(code, (this.counts.get(code) ?? 0) + 1);
57046
+ }
57047
+ list() {
57048
+ return Object.freeze(formatLossCodes.filter((code) => this.counts.has(code)).map((code) => Object.freeze({
57049
+ code,
57050
+ count: this.counts.get(code)
57051
+ })));
57052
+ }
57053
+ };
57054
+ var richMarks = {
57055
+ strong: "bold",
57056
+ emphasis: "italic",
57057
+ underline: "underline",
57058
+ strike_through: "strikethrough",
57059
+ superscript: "superscript",
57060
+ subscript: "subscript",
57061
+ inlineCode: "code",
57062
+ link: "link"
57063
+ };
57064
+ var isList = (node) => node.type.name === "bullet_list" || node.type.name === "ordered_list";
57065
+ /** Reads a formatted Markdown document as rich text, counting what RTF cannot hold. */
57066
+ var MarkdownReader = class {
57067
+ lost = new Losses();
57068
+ /** Appends text, counting characters RTF can't hold, which become U+FFFD. */
57069
+ append(builder, text, marks) {
57070
+ if (/[\u0000\ufffc]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(text)) this.lost.add("invalid-character");
57071
+ appendText(builder, text, marks);
57072
+ }
57073
+ marks(marks, extra) {
57074
+ const result = [...extra];
57075
+ for (const mark of marks) {
57076
+ const type = richMarks[mark.type.name];
57077
+ if (!type) {
57078
+ if (mark.type.name === "highlight") this.lost.add("highlight");
57079
+ continue;
57080
+ }
57081
+ if (result.some((held) => held.type === type)) continue;
57082
+ if (type === "link") {
57083
+ const href = String(mark.attrs.href ?? "");
57084
+ if (linkProblem(href)) {
57085
+ this.lost.add("link");
57086
+ continue;
57087
+ }
57088
+ result.push({
57089
+ type: "link",
57090
+ href
57091
+ });
57092
+ } else result.push({ type });
57093
+ }
57094
+ if (result.some((mark) => mark.type === "link") && result.some((mark) => mark.type === "underline")) this.lost.add("link-underline");
57095
+ return canonicalMarks(result);
57096
+ }
57097
+ /** Appends a textblock's inline content. */
57098
+ inline(node, builder, extra = []) {
57099
+ node.forEach((child) => {
57100
+ const name = child.type.name;
57101
+ if (child.isText) this.append(builder, child.text, this.marks(child.marks, extra));
57102
+ else if (name === "hardbreak") builder.break();
57103
+ else if (name === "footnote_reference") {
57104
+ this.lost.add("footnote");
57105
+ this.append(builder, `[${String(child.attrs.label)}]`, this.marks(child.marks, extra));
57106
+ } else if (name === "inkkit_wiki_link") {
57107
+ this.lost.add("wiki-link");
57108
+ this.append(builder, String(child.attrs.label || child.attrs.target || child.attrs.raw || ""), this.marks(child.marks, extra));
57109
+ } else if (name === "image") this.lost.add(child.attrs.inkkitFileTarget || child.attrs.inkkitFileRaw ? "file" : "image");
57110
+ else if (name === "comment_inline") this.lost.add("comment");
57111
+ else if (name === "html") {
57112
+ this.lost.add("literal");
57113
+ this.append(builder, String(child.attrs.value ?? ""), this.marks(child.marks, extra));
57114
+ } else if (child.isInline) this.inline(child, builder, extra);
57115
+ });
57116
+ }
57117
+ paragraph(node, extra = []) {
57118
+ const builder = new InlineBuilder();
57119
+ this.inline(node, builder, extra);
57120
+ return {
57121
+ type: "paragraph",
57122
+ content: builder.finish()
57123
+ };
57124
+ }
57125
+ lines(text, marks) {
57126
+ return text.split(/\r\n|[\n\r]/).map((line) => {
57127
+ const builder = new InlineBuilder();
57128
+ this.append(builder, line, marks);
57129
+ return {
57130
+ type: "paragraph",
57131
+ content: builder.finish()
57132
+ };
57133
+ });
57134
+ }
57135
+ /** The blocks for `node`, `depth` lists deep. */
57136
+ blocks(node, depth) {
57137
+ switch (node.type.name) {
57138
+ case "paragraph": return [this.paragraph(node)];
57139
+ case "heading":
57140
+ this.lost.add("heading");
57141
+ return [this.paragraph(node, [{ type: "bold" }])];
57142
+ case "code_block":
57143
+ this.lost.add(String(node.attrs.language).toLowerCase() === "mermaid" ? "diagram" : "code-block");
57144
+ return this.lines(node.textContent, [{ type: "code" }]);
57145
+ case "literal_markdown":
57146
+ this.lost.add("literal");
57147
+ return this.lines(node.textContent, []);
57148
+ case "blockquote":
57149
+ case "inkkit_callout": {
57150
+ this.lost.add(node.type.name === "blockquote" ? "quote" : "callout");
57151
+ const blocks = [];
57152
+ const title = node.attrs.title;
57153
+ if (title) blocks.push(...this.lines(title, [{ type: "bold" }]));
57154
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57155
+ return blocks;
57156
+ }
57157
+ case "footnote_definition": {
57158
+ this.lost.add("footnote");
57159
+ const blocks = [];
57160
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57161
+ const label = `[${String(node.attrs.label)}] `;
57162
+ const first = blocks[0];
57163
+ const builder = new InlineBuilder();
57164
+ builder.text(first?.type === "paragraph" ? label : label.trim(), []);
57165
+ if (first?.type === "paragraph") {
57166
+ for (const inline of first.content) if (inline.type === "break") builder.break();
57167
+ else builder.text(inline.text, inline.marks ?? []);
57168
+ blocks[0] = {
57169
+ ...first,
57170
+ content: builder.finish()
57171
+ };
57172
+ } else blocks.unshift({
57173
+ type: "paragraph",
57174
+ content: builder.finish()
57175
+ });
57176
+ return blocks;
57177
+ }
57178
+ case "bullet_list":
57179
+ case "ordered_list": return [this.list(node, depth + 1)];
57180
+ case "table": return depth > 0 ? this.tableLines(node) : [this.table(node)];
57181
+ case "hr":
57182
+ this.lost.add("rule");
57183
+ return [];
57184
+ case "comment_block":
57185
+ this.lost.add("comment");
57186
+ return [];
57187
+ case "image_block":
57188
+ this.lost.add(node.attrs.inkkitFileTarget || node.attrs.inkkitFileRaw ? "file" : "image");
57189
+ return [];
57190
+ case "reference_definition": return [];
57191
+ default: {
57192
+ if (node.isTextblock) return [this.paragraph(node)];
57193
+ const blocks = [];
57194
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57195
+ return blocks;
57196
+ }
57197
+ }
57198
+ }
57199
+ table(node) {
57200
+ let aligned = false;
57201
+ const rows = [];
57202
+ node.forEach((row) => {
57203
+ const cells = [];
57204
+ row.forEach((cell) => {
57205
+ if (cell.attrs.alignment != null) aligned = true;
57206
+ const builder = new InlineBuilder();
57207
+ cell.forEach((child, _offset, index) => {
57208
+ if (index) builder.break();
57209
+ this.inline(child, builder);
57210
+ });
57211
+ cells.push({ content: builder.finish() });
57212
+ });
57213
+ rows.push(cells);
57214
+ });
57215
+ if (aligned) this.lost.add("table-alignment");
57216
+ return {
57217
+ type: "table",
57218
+ rows
57219
+ };
57220
+ }
57221
+ /** A table inside a list item, as lines of tab-separated cells. */
57222
+ tableLines(node) {
57223
+ return this.table(node).rows.map((row) => {
57224
+ const builder = new InlineBuilder();
57225
+ row.forEach((cell, index) => {
57226
+ if (index) builder.text(" ", []);
57227
+ for (const inline of cell.content) if (inline.type === "break") builder.break();
57228
+ else builder.text(inline.text, inline.marks ?? []);
57229
+ });
57230
+ return {
57231
+ type: "paragraph",
57232
+ content: builder.finish()
57233
+ };
57234
+ });
57235
+ }
57236
+ list(node, depth) {
57237
+ const ordered = node.type.name === "ordered_list";
57238
+ let start = Number(node.attrs.order ?? 1);
57239
+ if (ordered && !(Number.isSafeInteger(start) && start >= 1)) {
57240
+ this.lost.add("list-start");
57241
+ start = 1;
57242
+ } else if (ordered && start > 2147483647) {
57243
+ this.lost.add("list-start");
57244
+ start = maximumListStart;
57245
+ }
57246
+ const items = [];
57247
+ node.forEach((item) => {
57248
+ if (item.attrs.checked != null) this.lost.add("task");
57249
+ const builder = new InlineBuilder();
57250
+ let lines = 0;
57251
+ const nested = [];
57252
+ let flattened = false;
57253
+ const line = (content) => {
57254
+ if (lines++) builder.break();
57255
+ for (const inline of content) if (inline.type === "break") builder.break();
57256
+ else builder.text(inline.text, inline.marks ?? []);
57257
+ };
57258
+ const flatten = (block) => {
57259
+ if (block.type === "paragraph") line(block.content);
57260
+ else if (block.type === "list") for (const listItem of block.items) listItem.blocks.forEach(flatten);
57261
+ else for (const row of block.rows) for (const cell of row) line(cell.content);
57262
+ };
57263
+ item.forEach((child, _offset, index) => {
57264
+ if (isList(child)) {
57265
+ if (depth < 9) nested.push(...this.blocks(child, depth));
57266
+ else {
57267
+ this.lost.add("list-depth");
57268
+ this.blocks(child, depth).forEach(flatten);
57269
+ }
57270
+ return;
57271
+ }
57272
+ for (const block of this.blocks(child, depth)) {
57273
+ if (index === 0 && !lines && block.type === "paragraph") {
57274
+ line(block.content);
57275
+ continue;
57276
+ }
57277
+ flattened = true;
57278
+ flatten(block);
57279
+ }
57280
+ });
57281
+ if (flattened) this.lost.add("list-item-blocks");
57282
+ items.push({ blocks: [{
57283
+ type: "paragraph",
57284
+ content: builder.finish()
57285
+ }, ...nested] });
57286
+ });
57287
+ return ordered && start !== 1 ? {
57288
+ type: "list",
57289
+ ordered,
57290
+ start,
57291
+ items
57292
+ } : {
57293
+ type: "list",
57294
+ ordered,
57295
+ items
57296
+ };
57297
+ }
57298
+ };
57299
+ /** A formatted Markdown document as rich text, with what RTF could not hold. */
57300
+ function markdownToRichText(doc) {
57301
+ const reader = new MarkdownReader();
57302
+ const blocks = [];
57303
+ doc.forEach((node) => blocks.push(...reader.blocks(node, 0)));
57304
+ if (blocks.length === 1 && blocks[0].type === "paragraph" && !blocks[0].content.length) blocks.length = 0;
57305
+ return {
57306
+ document: {
57307
+ version: 1,
57308
+ blocks
57309
+ },
57310
+ lost: reader.lost.list()
57311
+ };
57312
+ }
57313
+ /** A rich text document as Markdown, with what Markdown could not hold: alignment. Underline,
57314
+ * superscript and subscript are written as `<u>`, `<sup>` and `<sub>`. */
57315
+ function richTextToMarkdown(ctx, document) {
57316
+ const lost = new Losses();
57317
+ const { schema } = ctx.get(editorViewCtx).state;
57318
+ const ready = markdownReady(document, lost);
57319
+ const text = serialize(ctx, htmlBreaksInCells(richTextToProse(schema, ready)));
57320
+ let reread;
57321
+ try {
57322
+ reread = markdownToRichText(ctx.get(parserCtx)(text)).document;
57323
+ } catch {
57324
+ reread = void 0;
57325
+ }
57326
+ if (!reread || !sameDocument(reread, ready)) throw new InkKitError("preservation", "The document could not be written as Markdown that reads back the same");
57327
+ return {
57328
+ text,
57329
+ lost: lost.list()
57330
+ };
57331
+ }
57332
+ /** Marks that show on a space: moving a space out of one changes what the reader sees. */
57333
+ var visibleOnSpace = /* @__PURE__ */ new Set([
57334
+ "underline",
57335
+ "strikethrough",
57336
+ "link"
57337
+ ]);
57338
+ /**
57339
+ * `content` as Markdown can write it. An emphasis, strikethrough, link or HTML mark can't start or
57340
+ * end with a space, so leading and trailing spaces move outside the run, and a run of only spaces
57341
+ * loses its marks. A code span keeps its spaces. Counts `mark-spaces` where a visible mark moved.
57342
+ */
57343
+ function markdownInlines(content, lost) {
57344
+ const builder = new InlineBuilder();
57345
+ for (const inline of content) {
57346
+ if (inline.type === "break") {
57347
+ builder.break();
57348
+ continue;
57349
+ }
57350
+ let marks = inline.marks ?? [];
57351
+ if (/^ +$/.test(inline.text) && marks.some((mark) => mark.type === "code")) {
57352
+ const kept = marks.filter((mark) => mark.type === "code");
57353
+ if (marks.some((mark) => mark.type !== "code" && visibleOnSpace.has(mark.type))) lost.add("mark-spaces");
57354
+ marks = kept;
57355
+ }
57356
+ const [, lead, core, trail] = /^( *)([^]*?)( *)$/.exec(inline.text);
57357
+ if (!marks.length || marks.some((mark) => mark.type === "code") || !lead && !trail) {
57358
+ builder.text(inline.text, marks);
57359
+ continue;
57360
+ }
57361
+ if (marks.some((mark) => visibleOnSpace.has(mark.type))) lost.add("mark-spaces");
57362
+ builder.text(lead, []);
57363
+ builder.text(core, marks);
57364
+ builder.text(trail, []);
57365
+ }
57366
+ return builder.finish();
57367
+ }
57368
+ /**
57369
+ * `document` as Markdown can hold it, with what that drops counted in `lost`: alignment, a mark's
57370
+ * edge spaces, a paragraph's only line break, which Markdown reads back as an empty paragraph, and
57371
+ * empty paragraphs at the end, which Markdown doesn't keep.
57372
+ */
57373
+ function markdownReady(document, lost) {
57374
+ const block = (item) => {
57375
+ if (item.type === "paragraph") {
57376
+ if (item.align) lost.add("alignment");
57377
+ let content = markdownInlines(item.content, lost);
57378
+ if (content.length === 1 && content[0].type === "break") {
57379
+ lost.add("empty-line");
57380
+ content = [];
57381
+ }
57382
+ return {
57383
+ type: "paragraph",
57384
+ content
57385
+ };
57386
+ }
57387
+ if (item.type === "list") return {
57388
+ ...item,
57389
+ items: item.items.map((listItem) => ({ blocks: listItem.blocks.map(block) }))
57390
+ };
57391
+ return {
57392
+ type: "table",
57393
+ rows: item.rows.map((row) => row.map((cell) => ({ content: markdownInlines(cell.content, lost) })))
57394
+ };
57395
+ };
57396
+ const blocks = document.blocks.map(block);
57397
+ while (blocks.length > 1 && blocks.at(-1).type === "paragraph" && !blocks.at(-1).content.length) {
57398
+ blocks.pop();
57399
+ lost.add("empty-line");
57400
+ }
57401
+ return {
57402
+ version: 1,
57403
+ blocks
57404
+ };
57405
+ }
57406
+ /** Whether two documents are the same, counting one empty paragraph as an empty document. */
57407
+ function sameDocument(a, b) {
57408
+ const text = (document) => JSON.stringify(document.blocks.length === 1 && document.blocks[0].type === "paragraph" && !document.blocks[0].content.length ? [] : document.blocks);
57409
+ return text(a) === text(b);
57410
+ }
57411
+ /** Markdown for content copied from a rich text document. Unlike a conversion it reports no
57412
+ * losses and isn't read back: a copy may lose alignment. */
57413
+ function richContentMarkdown(ctx, content) {
57414
+ const { schema } = ctx.get(editorViewCtx).state;
57415
+ const empty = schema.topNodeType.createAndFill();
57416
+ let doc = new Transform(empty).replace(0, empty.content.size, content).doc;
57417
+ try {
57418
+ doc = richTextToProse(schema, markdownReady(proseToRichText(doc, false), new Losses()));
57419
+ } catch {}
57420
+ return serialize(ctx, htmlBreaksInCells(doc));
57421
+ }
57422
+ /** `node` with each line break in a table cell written as `<br />`, since a cell is one line of
57423
+ * Markdown. */
57424
+ function htmlBreaksInCells(node, inCell = false) {
57425
+ if (node.type.name === "hardbreak") return inCell ? node.type.create({
57426
+ ...node.attrs,
57427
+ isHTML: true
57428
+ }, null, node.marks) : node;
57429
+ if (node.isLeaf) return node;
57430
+ const role = node.type.spec.tableRole;
57431
+ const cell = inCell || role === "cell" || role === "header_cell";
57432
+ const children = [];
57433
+ node.forEach((child) => children.push(htmlBreaksInCells(child, cell)));
57434
+ return node.copy(Fragment.fromArray(children));
57435
+ }
57436
+ //#endregion
55665
57437
  //#region src/paste-html.ts
55666
57438
  var rich = /* @__PURE__ */ new Set([
55667
57439
  "P",
@@ -56772,6 +58544,14 @@ var PasteController = class {
56772
58544
  transformPasted: (slice, view, plain) => this.claimed || plain || view.dragging ? slice : importableSlice(this.options.ctx(), slice),
56773
58545
  handlePaste: (view, event) => {
56774
58546
  this.claimed = false;
58547
+ if (this.options.richText?.() && event.clipboardData && view.editable) {
58548
+ const data = event.clipboardData;
58549
+ this.run({
58550
+ text: data.getData("text/plain"),
58551
+ html: data.getData("text/html") || void 0
58552
+ }).catch((error) => options.onError?.(error));
58553
+ return true;
58554
+ }
56775
58555
  const claim = claimsPaste(view, event);
56776
58556
  if (claim === false) return false;
56777
58557
  const data = event.clipboardData;
@@ -56876,6 +58656,17 @@ var PasteController = class {
56876
58656
  },
56877
58657
  controller: new AbortController()
56878
58658
  };
58659
+ const rich = this.options.richText?.();
58660
+ if (rich) {
58661
+ const anchor = operation;
58662
+ const state = view.state;
58663
+ const slice = input.plainText ? rich.paste({ text: input.text }, state, this.options.linkSchemes ?? /* @__PURE__ */ new Set()) : rich.paste(input, state, this.options.linkSchemes ?? /* @__PURE__ */ new Set());
58664
+ const tr = state.tr.replaceRange(anchor.from, anchor.to, slice);
58665
+ const end = tr.doc.resolve(tr.mapping.map(anchor.to, 1));
58666
+ this.dispatch(view, tr.setSelection(Selection.near(end, -1)).scrollIntoView());
58667
+ if (slice.size && view.state.doc === state.doc) throw new InkKitError("preservation", "The paste was not applied because the RTF document cannot hold it there");
58668
+ return;
58669
+ }
56879
58670
  const inCode = () => view.state.doc.resolve(this.anchor(operation).from).parent.type.spec.code;
56880
58671
  this.active = operation;
56881
58672
  this.options.onPendingChanged?.();
@@ -57502,6 +59293,10 @@ var formatCommands = [
57502
59293
  "orderedList",
57503
59294
  "taskList",
57504
59295
  "toggleTask",
59296
+ "alignLeft",
59297
+ "alignCenter",
59298
+ "alignRight",
59299
+ "alignJustify",
57505
59300
  "link"
57506
59301
  ];
57507
59302
  /** Checks a format call's options against its command. */
@@ -57685,16 +59480,49 @@ function hostEvents(value) {
57685
59480
  };
57686
59481
  } });
57687
59482
  }
57688
- /** Checks a load's input. A format the host did not offer is refused like an unknown one. */
59483
+ /** Checks a load's input. A format the host did not offer is refused like an unknown one, and an
59484
+ * RTF document is checked and put in canonical form. */
57689
59485
  function documentInput(value, offered) {
57690
59486
  const input = expectRecord(value, "input");
59487
+ const documentId = expectString(input.documentId, "input.documentId");
59488
+ const generation = expectGeneration(input.generation, "input.generation");
59489
+ const format = expectOneOf(input.format, offered, "input.format");
59490
+ if (format === "rtf") {
59491
+ if (!isOmitted(input.text)) throw invalidArgument("input.text must be left out for an RTF document");
59492
+ return {
59493
+ documentId,
59494
+ generation,
59495
+ format,
59496
+ text: richTextText(readRichTextDocument(input.document, "input.document"))
59497
+ };
59498
+ }
59499
+ if (!isOmitted(input.document)) throw invalidArgument(`input.document must be left out for ${format}`);
57691
59500
  return {
57692
- documentId: expectString(input.documentId, "input.documentId"),
57693
- generation: expectGeneration(input.generation, "input.generation"),
57694
- format: expectOneOf(input.format, offered, "input.format"),
59501
+ documentId,
59502
+ generation,
59503
+ format,
57695
59504
  text: expectString(input.text, "input.text")
57696
59505
  };
57697
59506
  }
59507
+ /** The public snapshot of the editor's text for a document. */
59508
+ function publicSnapshot(saved) {
59509
+ const { documentId, generation, revision, format, text, dirty } = saved;
59510
+ return format === "rtf" ? {
59511
+ documentId,
59512
+ generation,
59513
+ revision,
59514
+ format,
59515
+ document: richTextFromText(text),
59516
+ dirty
59517
+ } : {
59518
+ documentId,
59519
+ generation,
59520
+ revision,
59521
+ format,
59522
+ text,
59523
+ dirty
59524
+ };
59525
+ }
57698
59526
  /** A range's shape. Whether its offsets fit the snapshot is `invalid-range`, checked later. */
57699
59527
  function textRange(value, name) {
57700
59528
  const range = expectRecord(value, name);
@@ -57818,6 +59646,7 @@ var InkKitEditor = class InkKitEditor {
57818
59646
  output = new EditorOutput({
57819
59647
  ctx: () => this.editor.ctx,
57820
59648
  exportDocument: (text) => this.document.exportDocument(text),
59649
+ richTextMarkdown: (content) => richContentMarkdown(this.editor.ctx, content),
57821
59650
  diagrams: () => this.diagrams,
57822
59651
  root: () => this.root,
57823
59652
  handler: () => this.document.handler,
@@ -57837,7 +59666,7 @@ var InkKitEditor = class InkKitEditor {
57837
59666
  element: () => this.source.element,
57838
59667
  replaceSelection: (text) => this.source.replaceLiteralSelection(text),
57839
59668
  writable: () => this.writable,
57840
- snapshot: () => this.snapshot(),
59669
+ snapshot: () => this.savedText(),
57841
59670
  prepareSelection: () => this.clipboardSnapshot("selection"),
57842
59671
  capture: () => this.session.capture(),
57843
59672
  assertCut: (captured) => this.assertCut(captured),
@@ -57917,7 +59746,7 @@ var InkKitEditor = class InkKitEditor {
57917
59746
  };
57918
59747
  } });
57919
59748
  this.linkSchemes = new Set(options.linkSchemes);
57920
- this.offered = options.formats ?? documentFormats;
59749
+ this.offered = options.formats ?? defaultFormats;
57921
59750
  this.formatList = Object.freeze(this.offered.map((format) => Object.freeze({
57922
59751
  format,
57923
59752
  promise: formatHandlers[format].promise
@@ -57934,6 +59763,9 @@ var InkKitEditor = class InkKitEditor {
57934
59763
  }
57935
59764
  /** What `changed` reports for the document's new text. */
57936
59765
  changeSnapshot(text) {
59766
+ return publicSnapshot(this.saved(text));
59767
+ }
59768
+ saved(text) {
57937
59769
  return {
57938
59770
  ...this.session.version(),
57939
59771
  format: this.document.format,
@@ -57941,6 +59773,11 @@ var InkKitEditor = class InkKitEditor {
57941
59773
  dirty: text !== this.document.original
57942
59774
  };
57943
59775
  }
59776
+ /** The current text, as `snapshot` checks and reads it. */
59777
+ savedText(expectedGeneration) {
59778
+ this.assertCurrent(expectedGeneration);
59779
+ return this.saved(this.document.currentText());
59780
+ }
57944
59781
  /** Throws unless the document `captured` describes is still current, alive, not composing and
57945
59782
  * has no import pending. */
57946
59783
  assertCaptured(captured, aborted = false) {
@@ -58146,7 +59983,7 @@ var InkKitEditor = class InkKitEditor {
58146
59983
  this.publishCommandState();
58147
59984
  }
58148
59985
  applyInputPolicy() {
58149
- applyInputPolicy(this.editor.ctx.get(editorViewCtx), this.source.element, this.inputPreferences, this.options.labels?.formattedEditor ?? defaultLabels.formattedEditor, {
59986
+ applyInputPolicy(this.editor.ctx.get(editorViewCtx), this.source.element, this.inputPreferences, this.options.labels?.[this.document.handler.formattedLabel] ?? defaultLabels[this.document.handler.formattedLabel], {
58150
59987
  writable: () => this.writable,
58151
59988
  literal: () => this.literalSurface
58152
59989
  });
@@ -58163,8 +60000,16 @@ var InkKitEditor = class InkKitEditor {
58163
60000
  const { state } = this.editor.ctx.get(editorViewCtx);
58164
60001
  const mutable = this.writable && !this.composing && !this.pasteController.pending;
58165
60002
  const formatted = mutable && !this.literalSurface;
58166
- const format = formatAvailability(this.editor, state, formatted);
60003
+ const rich = this.document.handler.richText != null;
60004
+ const format = formatAvailability(this.editor, state, formatted, this.document.handler);
58167
60005
  const tables = formatted ? tableAvailability(this.editor.ctx) : Object.fromEntries(tableCommands.map((name) => [name, false]));
60006
+ if (rich) tables.alignLeft = tables.alignCenter = tables.alignRight = false;
60007
+ const caret = this.literalSurface ? {
60008
+ marks: [],
60009
+ block: { type: "paragraph" },
60010
+ quoted: false
60011
+ } : caretState(state);
60012
+ if (rich) caret.align = caretAlignment(state);
58168
60013
  return {
58169
60014
  ...this.session.version(),
58170
60015
  format: this.document.format,
@@ -58172,11 +60017,7 @@ var InkKitEditor = class InkKitEditor {
58172
60017
  editable: this.writable,
58173
60018
  composing: this.composing,
58174
60019
  pending: this.pasteController.pending,
58175
- caret: this.literalSurface ? {
58176
- marks: [],
58177
- block: { type: "paragraph" },
58178
- quoted: false
58179
- } : caretState(state),
60020
+ caret,
58180
60021
  table: this.literalSurface ? void 0 : tableContext(state),
58181
60022
  commands: {
58182
60023
  undo: mutable && this.history.canUndo(state),
@@ -58184,12 +60025,12 @@ var InkKitEditor = class InkKitEditor {
58184
60025
  insertText: mutable,
58185
60026
  paste: mutable,
58186
60027
  replace: mutable,
58187
- replaceSource: mutable,
58188
- insertImages: formatted && this.canInsertImages,
58189
- appendImages: formatted && this.canInsertImages,
60028
+ replaceSource: mutable && !rich,
60029
+ insertImages: formatted && !rich && this.canInsertImages,
60030
+ appendImages: formatted && !rich && this.canInsertImages,
58190
60031
  insertPaths: mutable,
58191
- insertFootnote: formatted,
58192
- editReferenceDefinition: formatted && referenceDefinitions(state.doc).size > 0,
60032
+ insertFootnote: formatted && !rich,
60033
+ editReferenceDefinition: formatted && !rich && referenceDefinitions(state.doc).size > 0,
58193
60034
  format,
58194
60035
  table: tables,
58195
60036
  moveBlockUp: formatted && canMoveBlock(state, "up"),
@@ -58197,9 +60038,10 @@ var InkKitEditor = class InkKitEditor {
58197
60038
  }
58198
60039
  };
58199
60040
  }
58200
- /** An image adapter shows and exports an inserted reference, and so does a file adapter. */
60041
+ /** An image adapter shows and exports an inserted reference, and so does a file adapter. An RTF
60042
+ * document holds no images. */
58201
60043
  get canInsertImages() {
58202
- return !!this.options.images || !!this.options.files;
60044
+ return (!!this.options.images || !!this.options.files) && !this.document.handler.richText;
58203
60045
  }
58204
60046
  historyInputPlugin = createHistoryInputPlugin({
58205
60047
  writable: () => this.writable,
@@ -58238,7 +60080,7 @@ var InkKitEditor = class InkKitEditor {
58238
60080
  this.history.settleSource();
58239
60081
  this.history.modeChanged();
58240
60082
  },
58241
- text: () => this.snapshot().text,
60083
+ text: () => this.savedText().text,
58242
60084
  changed: () => {
58243
60085
  this.mode = mode;
58244
60086
  this.session.modeChanged();
@@ -58270,6 +60112,7 @@ var InkKitEditor = class InkKitEditor {
58270
60112
  this.assertAlive();
58271
60113
  expectString(text, "text");
58272
60114
  this.assertMutation(expectedGeneration);
60115
+ if (this.document.handler.richText) throw invalidArgument("An RTF document has no source to replace");
58273
60116
  const changed = this.history.applySource(text, true);
58274
60117
  if (changed) this.clipboard.clear();
58275
60118
  return changed;
@@ -58284,22 +60127,52 @@ var InkKitEditor = class InkKitEditor {
58284
60127
  this.assertMutation(expectedGeneration);
58285
60128
  return this.history.redo();
58286
60129
  }
58287
- /** The complete current text, which is what a host saves. It throws rather than return partial
58288
- * text. */
60130
+ /** The complete current document, which is what a host saves. It throws rather than return a
60131
+ * partial document. */
58289
60132
  snapshot(expectedGeneration) {
58290
- this.assertCurrent(expectedGeneration);
58291
- const text = this.document.currentText();
58292
- return {
58293
- ...this.session.version(),
58294
- format: this.document.format,
58295
- text,
58296
- dirty: text !== this.document.original
60133
+ return publicSnapshot(this.savedText(expectedGeneration));
60134
+ }
60135
+ /**
60136
+ * The loaded document converted to `format`, which must be offered, with what the conversion
60137
+ * lost. It changes nothing: load the result as a new generation. Markdown converts to RTF and
60138
+ * RTF to Markdown; the same format returns a copy. Throws `invalid-argument` for any other pair.
60139
+ */
60140
+ convertDocument(format, expectedGeneration) {
60141
+ this.assertAlive();
60142
+ expectOneOf(format, this.offered, "format");
60143
+ const saved = this.savedText(expectedGeneration);
60144
+ const from = saved.format;
60145
+ if (from === format) return from === "rtf" ? {
60146
+ format: "rtf",
60147
+ document: richTextFromText(saved.text),
60148
+ lost: Object.freeze([])
60149
+ } : {
60150
+ format: from,
60151
+ text: saved.text,
60152
+ lost: Object.freeze([])
58297
60153
  };
60154
+ if (from === "md" && format === "rtf") {
60155
+ const { document, lost } = markdownToRichText(this.document.exportDocument(saved.text));
60156
+ return {
60157
+ format,
60158
+ document,
60159
+ lost
60160
+ };
60161
+ }
60162
+ if (from === "rtf" && format === "md") {
60163
+ const { text, lost } = richTextToMarkdown(this.editor.ctx, richTextFromText(saved.text));
60164
+ return {
60165
+ format,
60166
+ text,
60167
+ lost
60168
+ };
60169
+ }
60170
+ throw invalidArgument(`Inkkit does not convert ${from} to ${format}; ${from === "txt" || format === "txt" ? "the host converts plain text" : "convert through another format"}`);
58298
60171
  }
58299
60172
  /** The whole document as standalone HTML for printing. Rejects rather than return partial
58300
60173
  * output. */
58301
60174
  async printableSnapshot(expectedGeneration) {
58302
- const snapshot = this.snapshot(expectedGeneration);
60175
+ const snapshot = this.savedText(expectedGeneration);
58303
60176
  const captured = this.session.capture();
58304
60177
  return this.output.printable(snapshot, captured);
58305
60178
  }
@@ -58308,7 +60181,7 @@ var InkKitEditor = class InkKitEditor {
58308
60181
  async clipboardSnapshot(scope, expectedGeneration) {
58309
60182
  this.assertAlive();
58310
60183
  const all = (isOmitted(scope) ? "document" : expectOneOf(scope, clipboardScopes, "scope")) === "document";
58311
- const snapshot = this.snapshot(expectedGeneration);
60184
+ const snapshot = this.savedText(expectedGeneration);
58312
60185
  const captured = this.session.capture();
58313
60186
  return this.output.clipboard(snapshot, captured, all);
58314
60187
  }
@@ -58342,12 +60215,25 @@ var InkKitEditor = class InkKitEditor {
58342
60215
  expectString(text, "text");
58343
60216
  this.assertMutation(expectedGeneration);
58344
60217
  const before = this.session.capture();
60218
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
58345
60219
  this.insertPlainText(text);
60220
+ if (text && this.refusedRichEdit(doc)) throw new InkKitError("preservation", "The text was not pasted because the RTF document cannot hold it there");
58346
60221
  return !before.isCurrent();
58347
60222
  }
60223
+ /** Whether an RTF edit that should have changed `before` was refused. */
60224
+ refusedRichEdit(before) {
60225
+ return this.document.handler.richText != null && !this.literalSurface && this.editor.ctx.get(editorViewCtx).state.doc === before;
60226
+ }
58348
60227
  insertPlainText(text) {
58349
60228
  if (this.literalSurface) this.source.replaceLiteralSelection(text, true, this.document.handler.keepsInsertedLineEndings);
58350
- else insertPlainText(this.editor.ctx.get(editorViewCtx), text);
60229
+ else if (this.document.handler.richText) {
60230
+ const view = this.editor.ctx.get(editorViewCtx);
60231
+ const slice = this.document.handler.richText.paste({ text }, view.state, this.linkSchemes);
60232
+ const lines = /[\r\n\u000c\u0085\u2028\u2029\u000b]/.test(text);
60233
+ const tr = view.state.tr.replaceSelection(slice).scrollIntoView();
60234
+ view.dispatch(lines ? closeHistory(tr) : tr);
60235
+ if (lines) view.dispatch(closeHistory(view.state.tr).setMeta("addToHistory", false));
60236
+ } else insertPlainText(this.editor.ctx.get(editorViewCtx), text);
58351
60237
  }
58352
60238
  /** Runs a table command and returns whether it applied. False in source and TXT modes, outside a
58353
60239
  * table for commands that need one, and at a boundary a move can't cross. */
@@ -58421,15 +60307,13 @@ var InkKitEditor = class InkKitEditor {
58421
60307
  }
58422
60308
  } catch (error) {
58423
60309
  view.updateState(view.state);
58424
- this.refuse(new InkKitError("preservation", `The edit was not applied because its Markdown could not be preserved: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
60310
+ this.refuse(new InkKitError("preservation", this.document.handler.richText ? `The edit was not applied because the document could not be saved as RTF: ${error instanceof Error ? error.message : String(error)}` : `The edit was not applied because its Markdown could not be preserved: ${error instanceof Error ? error.message : String(error)}`, { cause: error }));
58425
60311
  return;
58426
60312
  }
58427
60313
  }
58428
60314
  this.history.didApply(view.state, next);
58429
60315
  view.updateState(next);
58430
60316
  }
58431
- /** Builds the editor in `root`. It has no document until `loadDocument`. Rejects with
58432
- * `invalid-argument` for a bad root, handler or option. */
58433
60317
  static async mount(root, events, options) {
58434
60318
  if (!(root instanceof HTMLElement)) throw invalidArgument("root must be an HTML element");
58435
60319
  events = hostEvents(events);
@@ -58448,7 +60332,9 @@ var InkKitEditor = class InkKitEditor {
58448
60332
  commit: (apply) => instance.facadeCall(apply),
58449
60333
  literalText: () => instance.literalSurface,
58450
60334
  editable: () => instance.writable && !instance.literalSurface,
58451
- onPendingChanged: () => instance.publishCommandState()
60335
+ onPendingChanged: () => instance.publishCommandState(),
60336
+ richText: () => instance.document.handler.richText,
60337
+ linkSchemes: instance.linkSchemes
58452
60338
  });
58453
60339
  instance.suggestions = createEditorSuggestions({
58454
60340
  root,
@@ -58476,6 +60362,10 @@ var InkKitEditor = class InkKitEditor {
58476
60362
  historyInput: instance.historyInputPlugin,
58477
60363
  policy: instance.policyPlugin
58478
60364
  },
60365
+ format: {
60366
+ markdownInput: () => instance.document.handler.markdownInput,
60367
+ richText: () => instance.document.handler.richText
60368
+ },
58479
60369
  authoredSources: (doc, positions) => instance.document.authoredSources(doc, positions),
58480
60370
  smartPunctuation: () => instance.inputPreferences.smartPunctuation === true,
58481
60371
  linkSchemes: instance.linkSchemes,
@@ -58537,9 +60427,9 @@ var InkKitEditor = class InkKitEditor {
58537
60427
  }
58538
60428
  /** Replaces the document. The generation must be greater than the loaded one. A failed load
58539
60429
  * changes nothing. */
58540
- loadDocument(input) {
60430
+ loadDocument(document) {
58541
60431
  this.assertAlive();
58542
- input = documentInput(input, this.offered);
60432
+ const input = documentInput(document, this.offered);
58543
60433
  this.session.checkLoad(input.generation, "input.generation");
58544
60434
  const handler = formatHandlers[input.format];
58545
60435
  const markdown = handler.literalOnly ? "" : input.text;
@@ -58547,7 +60437,7 @@ var InkKitEditor = class InkKitEditor {
58547
60437
  try {
58548
60438
  source = handler.load(this.editor.ctx, markdown);
58549
60439
  } catch (error) {
58550
- throw new InkKitError("preservation", "The document was not loaded because its Markdown could not be shown without losing part of it", { cause: error });
60440
+ throw coded(error, "preservation", "The document was not loaded because its Markdown could not be shown without losing part of it");
58551
60441
  }
58552
60442
  const container = this.geometry.container;
58553
60443
  const top = container.scrollTop, left = container.scrollLeft;
@@ -58564,7 +60454,7 @@ var InkKitEditor = class InkKitEditor {
58564
60454
  this.source.reset();
58565
60455
  this.search.reset();
58566
60456
  this.document.handler = handler;
58567
- this.source.setSourceValue(input.text);
60457
+ this.source.setSourceValue(handler.richText ? "" : input.text);
58568
60458
  this.source.element.setSelectionRange(this.source.element.value.length, this.source.element.value.length);
58569
60459
  this.load(markdown, source);
58570
60460
  this.document.original = input.text;
@@ -58583,9 +60473,9 @@ var InkKitEditor = class InkKitEditor {
58583
60473
  /** Replaces the document with a newer version of it, keeping the selection, focus, scroll
58584
60474
  * position and Markdown editing mode. With `expectedGeneration`, the loaded document must be
58585
60475
  * that generation. */
58586
- reloadDocument(input, expectedGeneration) {
60476
+ reloadDocument(document, expectedGeneration) {
58587
60477
  this.assertAlive();
58588
- input = documentInput(input, this.offered);
60478
+ const input = documentInput(document, this.offered);
58589
60479
  if (!isOmitted(expectedGeneration)) this.assertReady(expectedGeneration);
58590
60480
  this.session.checkLoad(input.generation, "input.generation");
58591
60481
  const view = this.editor.ctx.get(editorViewCtx);
@@ -58595,7 +60485,7 @@ var InkKitEditor = class InkKitEditor {
58595
60485
  const saved = captureReloadSurface(view, this.source.element, this.geometry.container, wasLiteral);
58596
60486
  this.reportingSuppressed = true;
58597
60487
  try {
58598
- this.loadDocument(input);
60488
+ this.loadDocument(document);
58599
60489
  if (previous.sourceMode && this.document.handler.sourceMode && previousMode === "source") this.setEditingMode("source", input.generation);
58600
60490
  restoreReloadSurface(view, this.source.element, () => this.literalSurface, saved);
58601
60491
  } finally {
@@ -58730,7 +60620,8 @@ var InkKitEditor = class InkKitEditor {
58730
60620
  expectString(search, "search");
58731
60621
  expectString(replacement, "replacement");
58732
60622
  this.assertMutation(expectedGeneration);
58733
- return this.search.replace(search, replacement, false) > 0;
60623
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60624
+ return this.search.replace(search, replacement, false) > 0 && (replacement === search || !this.refusedRichEdit(doc));
58734
60625
  }
58735
60626
  /** Replaces every non-overlapping match once and returns how many. */
58736
60627
  replaceAll(search, replacement, expectedGeneration) {
@@ -58738,7 +60629,9 @@ var InkKitEditor = class InkKitEditor {
58738
60629
  expectString(search, "search");
58739
60630
  expectString(replacement, "replacement");
58740
60631
  this.assertMutation(expectedGeneration);
58741
- return this.search.replace(search, replacement, true);
60632
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60633
+ const count = this.search.replace(search, replacement, true);
60634
+ return count && replacement !== search && this.refusedRichEdit(doc) ? 0 : count;
58742
60635
  }
58743
60636
  outlineContext() {
58744
60637
  const { documentId, generation, revision, key } = this.session.capture();
@@ -58781,11 +60674,10 @@ var InkKitEditor = class InkKitEditor {
58781
60674
  this.source.replaceLiteralSelection(text, false);
58782
60675
  return true;
58783
60676
  }
58784
- if (/[\r\n]/.test(text)) {
58785
- this.pasteAsPlainText(text);
58786
- return true;
58787
- }
58788
- return insertNativeText(this.editor.ctx.get(editorViewCtx), text);
60677
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60678
+ if (/[\r\n]/.test(text)) this.insertPlainText(text);
60679
+ else if (!insertNativeText(this.editor.ctx.get(editorViewCtx), text)) return false;
60680
+ return !(text && this.refusedRichEdit(doc));
58789
60681
  }
58790
60682
  /**
58791
60683
  * A key the host's native input received, run through InkKit's key handling. Returns whether
@@ -58964,12 +60856,14 @@ var InkKitEditor = class InkKitEditor {
58964
60856
  format(command, options, expectedGeneration) {
58965
60857
  this.assertAlive();
58966
60858
  const checked = formatOptions(command, options);
60859
+ if (checked?.destination !== void 0 && this.document.handler.richText) checked.destination = checked.destination.trim();
58967
60860
  if (checked?.destination !== void 0) this.checkDestination(checked.destination, "options.destination");
58968
60861
  this.assertMutation(expectedGeneration);
58969
60862
  if (this.literalSurface) return false;
58970
60863
  const view = this.editor.ctx.get(editorViewCtx);
58971
60864
  const before = view.state;
58972
- applyFormat(this.editor, command, checked, (destination) => this.link(destination));
60865
+ if ((this.document.handler.richText || command.startsWith("align")) && !this.commandState().commands.format[command]) return false;
60866
+ applyFormat(this.editor, command, checked, (destination) => this.link(destination), this.document.handler.richText != null);
58973
60867
  this.focus();
58974
60868
  const after = view.state;
58975
60869
  const marks = (state) => state.storedMarks ?? state.selection.$from.marks();
@@ -58977,11 +60871,14 @@ var InkKitEditor = class InkKitEditor {
58977
60871
  }
58978
60872
  /**
58979
60873
  * Refuses a link destination that is empty, that opens anything but a web, mail or phone address
58980
- * or a scheme the host listed, or that the Markdown writer cannot save so that it reads back the
58981
- * same.
60874
+ * or a scheme the host listed, or, in Markdown, that the writer cannot save so that it reads back
60875
+ * the same. In RTF, it also refuses what the rich text link rule refuses.
58982
60876
  */
58983
60877
  checkDestination(destination, name) {
58984
- checkDestination(this.editor.ctx, destination, name, this.linkSchemes);
60878
+ const rich = this.document.handler.richText != null;
60879
+ checkDestination(this.editor.ctx, destination, name, this.linkSchemes, !rich);
60880
+ const problem = rich ? linkProblem(destination) : void 0;
60881
+ if (problem) throw invalidArgument(`${name}: ${problem}`);
58985
60882
  }
58986
60883
  /**
58987
60884
  * With a destination, sets the link: on the whole link that holds the caret or the selection,
@@ -59000,7 +60897,7 @@ var InkKitEditor = class InkKitEditor {
59000
60897
  this.checkDestination(expectString(destination, "destination"), "destination");
59001
60898
  optionalString(title, "title");
59002
60899
  this.assertMutation(expectedGeneration);
59003
- if (this.literalSurface) return false;
60900
+ if (this.literalSurface || this.document.handler.richText) return false;
59004
60901
  return editReferenceDefinition(this.editor.ctx.get(editorViewCtx), label, destination, title);
59005
60902
  }
59006
60903
  /** Inserts a footnote reference, and a definition when the label has none, and returns whether
@@ -59010,8 +60907,8 @@ var InkKitEditor = class InkKitEditor {
59010
60907
  const given = optionalString(label, "label");
59011
60908
  if (given !== void 0 && !footnoteLabel(given)) throw invalidArgument("A footnote label must be 1 to 999 characters without whitespace, brackets or backslashes");
59012
60909
  this.assertMutation(expectedGeneration);
59013
- if (this.literalSurface) return false;
59014
- insertFootnote(this.editor.ctx.get(editorViewCtx), label, () => this.snapshot().text);
60910
+ if (this.literalSurface || this.document.handler.richText) return false;
60911
+ insertFootnote(this.editor.ctx.get(editorViewCtx), label, () => this.savedText().text);
59015
60912
  this.focus();
59016
60913
  return true;
59017
60914
  }
@@ -59031,6 +60928,7 @@ var InkKitEditor = class InkKitEditor {
59031
60928
  "loadDocument",
59032
60929
  "reloadDocument",
59033
60930
  "snapshot",
60931
+ "convertDocument",
59034
60932
  "printableSnapshot",
59035
60933
  "clipboardSnapshot",
59036
60934
  "paste",
@@ -59094,13 +60992,13 @@ var InkKitEditor = class InkKitEditor {
59094
60992
  });
59095
60993
  }
59096
60994
  const mount = InkKitEditor.mount;
59097
- InkKitEditor.mount = async (...args) => {
60995
+ InkKitEditor.mount = (async (...args) => {
59098
60996
  try {
59099
60997
  return await mount(...args);
59100
60998
  } catch (error) {
59101
60999
  throw coded(error);
59102
61000
  }
59103
- };
61001
+ });
59104
61002
  }
59105
61003
  };
59106
61004
  //#endregion