@aicayzer/inkkit 0.3.6 → 0.4.0

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;
@@ -48988,7 +49125,7 @@ function correspondence(before, after) {
48988
49125
  if (work.budget < 0) throw tooManyBlocks();
48989
49126
  const anchors = uniqueAnchors(before, after, b, bEnd, a, aEnd);
48990
49127
  if (!anchors.length) {
48991
- align(before, after, b, bEnd, a, aEnd, result, work);
49128
+ align$1(before, after, b, bEnd, a, aEnd, result, work);
48992
49129
  continue;
48993
49130
  }
48994
49131
  for (const anchor of anchors) {
@@ -49053,7 +49190,7 @@ function uniqueAnchors(before, after, b, bEnd, a, aEnd) {
49053
49190
  }
49054
49191
  var INDEL = 2;
49055
49192
  var REPLACE = 3;
49056
- function align(before, after, b, bEnd, a, aEnd, result, work) {
49193
+ function align$1(before, after, b, bEnd, a, aEnd, result, work) {
49057
49194
  const rows = bEnd - b + 1;
49058
49195
  const columns = aEnd - a + 1;
49059
49196
  const offset = Math.abs(rows - columns);
@@ -51482,7 +51619,7 @@ function cleanSemantics(container, localIDs) {
51482
51619
  if (title) element.setAttribute("title", title);
51483
51620
  if (id && localIDs.has(id)) element.setAttribute("id", id);
51484
51621
  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}`);
51622
+ 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
51623
  }
51487
51624
  const walker = document.createTreeWalker(container, NodeFilter.SHOW_COMMENT);
51488
51625
  const comments = [];
@@ -51601,10 +51738,12 @@ var markdownFormat = {
51601
51738
  promise: "exact",
51602
51739
  literalOnly: false,
51603
51740
  sourceMode: true,
51741
+ formattedLabel: "formattedEditor",
51604
51742
  literalLabel: "sourceEditor",
51605
51743
  headings: true,
51606
51744
  nativeCopy: false,
51607
51745
  keepsInsertedLineEndings: true,
51746
+ markdownInput: true,
51608
51747
  load: (ctx, text) => Preservation.read(ctx, text),
51609
51748
  parse: (ctx, text) => Preservation.read(ctx, text),
51610
51749
  pendingExport: (ctx, text) => ctx.get(parserCtx)(text),
@@ -51642,6 +51781,1067 @@ var markdownFormat = {
51642
51781
  };
51643
51782
  }
51644
51783
  };
51784
+ /** The largest first number of an ordered list that RTF writes and reads back unchanged, as
51785
+ * measured with the host's converter. */
51786
+ var maximumListStart = 2147483647;
51787
+ /** Characters a run never holds: they would change the structure, stand for an attachment or end
51788
+ * the text when RTF is written. */
51789
+ var forbidden = /[\u0000\n\r\u000b\u000c\u0085\u2028\u2029\ufffc]/;
51790
+ var loneSurrogate = /[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/;
51791
+ /** Why a run cannot hold `text`, or undefined. Loading and editing share the rule. */
51792
+ function textProblem(text) {
51793
+ const bad = forbidden.exec(text);
51794
+ if (bad) return `text cannot hold ${scalarName(bad[0])}`;
51795
+ if (loneSurrogate.test(text)) return "text cannot hold a lone surrogate";
51796
+ }
51797
+ /** Why `href` cannot be a link's address, or undefined. Any address that is not empty and holds
51798
+ * no control character is kept, as the host's converter keeps it; loading, editing, paste and the
51799
+ * converter share the rule. Edits and paste also apply Inkkit's link safety. */
51800
+ function linkProblem(href) {
51801
+ if (!href) return "a link needs an address";
51802
+ if (/[\u0000-\u001f\u007f-\u009f]/.test(href)) return "a link address cannot hold a control character";
51803
+ const problem = textProblem(href);
51804
+ return problem && `a link address ${problem.replace(/^text /, "")}`;
51805
+ }
51806
+ /** The canonical order of marks. */
51807
+ var markRank = {
51808
+ bold: 0,
51809
+ italic: 1,
51810
+ underline: 2,
51811
+ strikethrough: 3,
51812
+ superscript: 4,
51813
+ subscript: 5,
51814
+ code: 6,
51815
+ link: 7
51816
+ };
51817
+ function invalid(path, problem) {
51818
+ throw invalidArgument(`${path}: ${problem}`);
51819
+ }
51820
+ function record(value, path, keys) {
51821
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(path, "must be an object");
51822
+ for (const key of Object.keys(value)) if (!keys.includes(key)) invalid(`${path}.${key}`, "is not a known field");
51823
+ return value;
51824
+ }
51825
+ function list$1(value, path) {
51826
+ if (!Array.isArray(value)) invalid(path, "must be a list");
51827
+ return value;
51828
+ }
51829
+ function scalarName(text) {
51830
+ return `U+${text.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}`;
51831
+ }
51832
+ /** Collects inline content in canonical form: adjacent runs with equal marks merge, and each run
51833
+ * is NFC after merging, because joining two NFC strings can leave a sequence that composes. */
51834
+ var InlineBuilder = class {
51835
+ content = [];
51836
+ marksKey;
51837
+ text(text, marks) {
51838
+ if (!text) return;
51839
+ const key = JSON.stringify(marks);
51840
+ const last = this.content.at(-1);
51841
+ if (last?.type === "text" && this.marksKey === key) {
51842
+ last.text += text;
51843
+ return;
51844
+ }
51845
+ this.content.push(marks.length ? {
51846
+ type: "text",
51847
+ text,
51848
+ marks: [...marks]
51849
+ } : {
51850
+ type: "text",
51851
+ text
51852
+ });
51853
+ this.marksKey = key;
51854
+ }
51855
+ break() {
51856
+ this.content.push({ type: "break" });
51857
+ this.marksKey = void 0;
51858
+ }
51859
+ finish() {
51860
+ for (const inline of this.content) if (inline.type === "text") inline.text = inline.text.normalize("NFC");
51861
+ return this.content;
51862
+ }
51863
+ };
51864
+ /** Marks in canonical order. A link's run never also has underline: a link's underline is how it
51865
+ * is drawn, so RTF readers report it on every link. */
51866
+ function canonicalMarks(marks) {
51867
+ const sorted = [...marks].sort((a, b) => markRank[a.type] - markRank[b.type]);
51868
+ return sorted.some((mark) => mark.type === "link") ? sorted.filter((mark) => mark.type !== "underline") : sorted;
51869
+ }
51870
+ function readMarks(value, path) {
51871
+ const sorted = [...list$1(value, path).map((entry, index) => {
51872
+ const at = `${path}[${index}]`;
51873
+ const mark = record(entry, at, ["type", "href"]);
51874
+ const type = mark.type;
51875
+ if (typeof type !== "string" || !(type in markRank)) invalid(`${at}.type`, `unknown mark type ${JSON.stringify(type)}`);
51876
+ if (type === "link") {
51877
+ if (typeof mark.href !== "string") invalid(`${at}.href`, "must be text");
51878
+ return {
51879
+ type: "link",
51880
+ href: mark.href
51881
+ };
51882
+ }
51883
+ if (!isOmitted(mark.href)) invalid(`${at}.href`, "only a link has an address");
51884
+ return { type };
51885
+ })].sort((a, b) => markRank[a.type] - markRank[b.type]);
51886
+ const types = /* @__PURE__ */ new Set();
51887
+ for (const mark of sorted) {
51888
+ if (types.has(mark.type)) invalid(path, `the ${mark.type} mark appears more than once`);
51889
+ types.add(mark.type);
51890
+ const problem = mark.type === "link" ? linkProblem(mark.href) : void 0;
51891
+ if (problem) invalid(path, problem);
51892
+ }
51893
+ if (types.has("superscript") && types.has("subscript")) invalid(path, "a run cannot be both superscript and subscript");
51894
+ return canonicalMarks(sorted);
51895
+ }
51896
+ function readInlines(value, path) {
51897
+ const builder = new InlineBuilder();
51898
+ list$1(value, path).forEach((entry, index) => {
51899
+ const at = `${path}[${index}]`;
51900
+ const inline = record(entry, at, [
51901
+ "type",
51902
+ "text",
51903
+ "marks"
51904
+ ]);
51905
+ if (inline.type === "break") {
51906
+ if (!isOmitted(inline.text) || !isOmitted(inline.marks)) invalid(at, "a break holds no text or marks");
51907
+ builder.break();
51908
+ return;
51909
+ }
51910
+ if (inline.type !== "text") invalid(`${at}.type`, `unknown inline type ${JSON.stringify(inline.type)}`);
51911
+ const text = inline.text;
51912
+ if (typeof text !== "string") invalid(`${at}.text`, "must be text");
51913
+ if (!text) invalid(`${at}.text`, "a text run cannot be empty");
51914
+ const problem = textProblem(text);
51915
+ if (problem) invalid(`${at}.text`, problem);
51916
+ builder.text(text, isOmitted(inline.marks) ? [] : readMarks(inline.marks, `${at}.marks`));
51917
+ });
51918
+ return builder.finish();
51919
+ }
51920
+ function readBlock(value, path, listDepth) {
51921
+ const block = record(value, path, [
51922
+ "type",
51923
+ "align",
51924
+ "content",
51925
+ "ordered",
51926
+ "start",
51927
+ "items",
51928
+ "rows"
51929
+ ]);
51930
+ const only = (...keys) => {
51931
+ for (const key of Object.keys(block)) if (key !== "type" && !keys.includes(key)) invalid(`${path}.${key}`, `a ${String(block.type)} has no ${key}`);
51932
+ };
51933
+ switch (block.type) {
51934
+ case "paragraph": {
51935
+ only("align", "content");
51936
+ const align = block.align;
51937
+ if (!isOmitted(align) && align !== "center" && align !== "right" && align !== "justify") invalid(`${path}.align`, "must be center, right or justify");
51938
+ const content = readInlines(block.content, `${path}.content`);
51939
+ return isOmitted(align) ? {
51940
+ type: "paragraph",
51941
+ content
51942
+ } : {
51943
+ type: "paragraph",
51944
+ align,
51945
+ content
51946
+ };
51947
+ }
51948
+ case "list":
51949
+ only("ordered", "start", "items");
51950
+ return readList(block, path, listDepth + 1);
51951
+ case "table": {
51952
+ only("rows");
51953
+ if (listDepth > 0) invalid(path, "a table cannot be inside a list item");
51954
+ const rows = list$1(block.rows, `${path}.rows`);
51955
+ if (!rows.length) invalid(`${path}.rows`, "a table needs at least one row");
51956
+ const width = list$1(rows[0], `${path}.rows[0]`).length;
51957
+ if (!width) invalid(`${path}.rows[0]`, "a table row needs at least one cell");
51958
+ return {
51959
+ type: "table",
51960
+ rows: rows.map((row, rowIndex) => {
51961
+ const at = `${path}.rows[${rowIndex}]`;
51962
+ const cells = list$1(row, at);
51963
+ if (cells.length !== width) invalid(at, `the row has ${cells.length} cells, but the first row has ${width}`);
51964
+ return cells.map((cell, cellIndex) => {
51965
+ const cellPath = `${at}[${cellIndex}]`;
51966
+ const content = record(cell, cellPath, ["content"]).content;
51967
+ return { content: readInlines(content, `${cellPath}.content`) };
51968
+ });
51969
+ })
51970
+ };
51971
+ }
51972
+ default: invalid(`${path}.type`, `unknown block type ${JSON.stringify(block.type)}`);
51973
+ }
51974
+ }
51975
+ function readList(block, path, depth) {
51976
+ if (depth > 9) invalid(path, `lists nest deeper than 9 levels`);
51977
+ if (typeof block.ordered !== "boolean") invalid(`${path}.ordered`, "must be true or false");
51978
+ let start;
51979
+ if (!isOmitted(block.start)) {
51980
+ if (!block.ordered) invalid(`${path}.start`, "only an ordered list has a start");
51981
+ if (!Number.isSafeInteger(block.start)) invalid(`${path}.start`, "must be a whole number");
51982
+ if (block.start < 1) invalid(`${path}.start`, "the start must be at least 1");
51983
+ if (block.start > 2147483647) invalid(`${path}.start`, `the start must be at most ${maximumListStart}`);
51984
+ if (block.start !== 1) start = block.start;
51985
+ }
51986
+ const entries = list$1(block.items, `${path}.items`);
51987
+ if (!entries.length) invalid(`${path}.items`, "a list needs at least one item");
51988
+ const items = entries.map((entry, itemIndex) => {
51989
+ const at = `${path}.items[${itemIndex}]`;
51990
+ const blocks = list$1(record(entry, at, ["blocks"]).blocks, `${at}.blocks`);
51991
+ const first = blocks[0];
51992
+ if (!first || typeof first !== "object" || first.type !== "paragraph") invalid(`${at}.blocks[0]`, "a list item must start with a paragraph");
51993
+ return { blocks: blocks.map((child, blockIndex) => {
51994
+ const childPath = `${at}.blocks[${blockIndex}]`;
51995
+ if (blockIndex > 0 && (!child || typeof child !== "object" || child.type !== "list")) invalid(childPath, "after its paragraph, a list item can hold only lists");
51996
+ return readBlock(child, childPath, depth);
51997
+ }) };
51998
+ });
51999
+ return start === void 0 ? {
52000
+ type: "list",
52001
+ ordered: block.ordered,
52002
+ items
52003
+ } : {
52004
+ type: "list",
52005
+ ordered: block.ordered,
52006
+ start,
52007
+ items
52008
+ };
52009
+ }
52010
+ /**
52011
+ * Checks `value` as a version 1 document and returns its canonical form: marks sorted, a link's
52012
+ * underline removed, adjacent equal runs merged, runs in NFC, and `start` of 1 left out. Throws
52013
+ * `invalid-argument` naming the first problem, under `path`.
52014
+ */
52015
+ function readRichTextDocument(value, path) {
52016
+ const document = record(value, path, ["version", "blocks"]);
52017
+ if (document.version !== 1) invalid(`${path}.version`, `version ${JSON.stringify(document.version)} is not supported; the version must be 1`);
52018
+ return {
52019
+ version: 1,
52020
+ blocks: list$1(document.blocks, `${path}.blocks`).map((block, index) => readBlock(block, `${path}.blocks[${index}]`, 0))
52021
+ };
52022
+ }
52023
+ /** The text that stands for a canonical document inside the editor: its JSON. Two documents are
52024
+ * the same exactly when their texts are. */
52025
+ function richTextText(document) {
52026
+ return JSON.stringify(document);
52027
+ }
52028
+ /** The document a text from `richTextText` stands for, as a new object. */
52029
+ function richTextFromText(text) {
52030
+ return JSON.parse(text);
52031
+ }
52032
+ /** Text from outside a document, such as a paste or another format, as canonical runs: a line or
52033
+ * paragraph separator inside a paragraph becomes a line break, and characters RTF cannot hold as
52034
+ * text become U+FFFD, so that no character disappears. */
52035
+ function appendText(builder, text, marks) {
52036
+ 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) => {
52037
+ if (index) builder.break();
52038
+ builder.text(line, marks);
52039
+ });
52040
+ }
52041
+ /** Typed or pasted text as paragraphs: each paragraph separator ends one, including a final one,
52042
+ * so inserting `'a\n'` leaves the caret in a new paragraph. A line separator is a line break.
52043
+ * Unlike the host's `RichTextDocument(plainText:)`, which reads a file, nothing is trimmed. */
52044
+ function textBlocks(text) {
52045
+ if (text === "") return [];
52046
+ return text.split(/\r\n|[\n\r\u000c\u0085\u2029]/).map((line) => {
52047
+ const builder = new InlineBuilder();
52048
+ appendText(builder, line, []);
52049
+ return {
52050
+ type: "paragraph",
52051
+ content: builder.finish()
52052
+ };
52053
+ });
52054
+ }
52055
+ //#endregion
52056
+ //#region src/formats/rich-text-prose.ts
52057
+ /** Editor marks by rich text mark, in canonical order. */
52058
+ var markNames = {
52059
+ bold: "strong",
52060
+ italic: "emphasis",
52061
+ underline: "underline",
52062
+ strikethrough: "strike_through",
52063
+ superscript: "superscript",
52064
+ subscript: "subscript",
52065
+ code: "inlineCode",
52066
+ link: "link"
52067
+ };
52068
+ var richMarks$1 = Object.fromEntries(Object.entries(markNames).map(([rich, name]) => [name, rich]));
52069
+ /** The editor nodes an RTF document can hold. */
52070
+ var allowedNodes = /* @__PURE__ */ new Set([
52071
+ "doc",
52072
+ "paragraph",
52073
+ "text",
52074
+ "hardbreak",
52075
+ "bullet_list",
52076
+ "ordered_list",
52077
+ "list_item",
52078
+ "table",
52079
+ "table_header_row",
52080
+ "table_row",
52081
+ "table_header",
52082
+ "table_cell"
52083
+ ]);
52084
+ function unrepresentable(what) {
52085
+ return new InkKitError("preservation", `The document holds ${what}, which an RTF document cannot hold`);
52086
+ }
52087
+ function proseMarks(schema, marks = []) {
52088
+ return marks.map((mark) => mark.type === "link" ? schema.marks.link.create({ href: mark.href }) : schema.marks[markNames[mark.type]].create());
52089
+ }
52090
+ function proseInlines(schema, content, around) {
52091
+ return content.map((inline) => {
52092
+ if (inline.type === "break") return schema.nodes.hardbreak.create({
52093
+ isInline: false,
52094
+ isHTML: false
52095
+ });
52096
+ let marks = [...around];
52097
+ for (const mark of proseMarks(schema, inline.marks)) marks = mark.addToSet(marks);
52098
+ return schema.text(inline.text, marks);
52099
+ });
52100
+ }
52101
+ function proseParagraph(schema, content, around, align = null) {
52102
+ return schema.nodes.paragraph.create({ align }, proseInlines(schema, content, around));
52103
+ }
52104
+ /** The editor block for a rich text block. Its text also takes `around`, the marks of text that a
52105
+ * paste replaces. */
52106
+ function proseBlock(schema, block, around = []) {
52107
+ switch (block.type) {
52108
+ case "paragraph": return proseParagraph(schema, block.content, around, block.align ?? null);
52109
+ case "list": {
52110
+ const start = block.start ?? 1;
52111
+ const items = block.items.map((item, index) => schema.nodes.list_item.create({
52112
+ label: block.ordered ? `${start + index}.` : "•",
52113
+ listType: block.ordered ? "ordered" : "bullet",
52114
+ spread: false,
52115
+ checked: null
52116
+ }, item.blocks.map((child) => proseBlock(schema, child, around))));
52117
+ return block.ordered ? schema.nodes.ordered_list.create({
52118
+ order: start,
52119
+ spread: false
52120
+ }, items) : schema.nodes.bullet_list.create({ spread: false }, items);
52121
+ }
52122
+ case "table": {
52123
+ const row = (cells, index) => {
52124
+ const header = index === 0;
52125
+ 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))));
52126
+ };
52127
+ return schema.nodes.table.create(null, block.rows.map((_, index) => row(block, index)));
52128
+ }
52129
+ }
52130
+ }
52131
+ /** The editor document for a canonical rich text document. */
52132
+ function richTextToProse(schema, document) {
52133
+ const blocks = document.blocks.map((block) => proseBlock(schema, block));
52134
+ if (!blocks.length) blocks.push(proseParagraph(schema, [], []));
52135
+ return schema.topNodeType.create(null, blocks);
52136
+ }
52137
+ function richMarksOf(marks) {
52138
+ const result = [];
52139
+ for (const mark of marks) {
52140
+ const type = richMarks$1[mark.type.name];
52141
+ if (!type) throw unrepresentable(`the ${mark.type.name} mark`);
52142
+ result.push(type === "link" ? {
52143
+ type: "link",
52144
+ href: String(mark.attrs.href ?? "")
52145
+ } : { type });
52146
+ }
52147
+ return canonicalMarks(result);
52148
+ }
52149
+ function richInlines(node) {
52150
+ const builder = new InlineBuilder();
52151
+ node.forEach((child) => {
52152
+ if (child.type.name === "hardbreak") builder.break();
52153
+ else if (child.isText) {
52154
+ const text = child.text;
52155
+ if (textProblem(text)) throw unrepresentable("a separator in its text");
52156
+ const marks = richMarksOf(child.marks);
52157
+ if (marks.some((mark) => mark.type === "link" && linkProblem(mark.href))) throw unrepresentable("a link without an address");
52158
+ builder.text(text, marks);
52159
+ } else throw unrepresentable(`a ${child.type.name}`);
52160
+ });
52161
+ return builder.finish();
52162
+ }
52163
+ function richParagraph(node, aligned) {
52164
+ if (node.type.name !== "paragraph") throw unrepresentable(`a ${node.type.name}`);
52165
+ const align = node.attrs.align;
52166
+ if (align != null && !aligned) throw unrepresentable("an aligned table cell");
52167
+ if (align != null && align !== "center" && align !== "right" && align !== "justify") throw unrepresentable(`${align} alignment`);
52168
+ const content = richInlines(node);
52169
+ return align == null ? {
52170
+ type: "paragraph",
52171
+ content
52172
+ } : {
52173
+ type: "paragraph",
52174
+ align,
52175
+ content
52176
+ };
52177
+ }
52178
+ function richBlock(node, listDepth) {
52179
+ switch (node.type.name) {
52180
+ case "paragraph": return richParagraph(node, true);
52181
+ case "bullet_list":
52182
+ case "ordered_list": {
52183
+ const depth = listDepth + 1;
52184
+ if (depth > 9) throw unrepresentable(`lists deeper than 9 levels`);
52185
+ const ordered = node.type.name === "ordered_list";
52186
+ const items = [];
52187
+ node.forEach((item) => {
52188
+ if (item.type.name !== "list_item") throw unrepresentable(`a ${item.type.name}`);
52189
+ if (item.attrs.checked != null) throw unrepresentable("a task");
52190
+ const blocks = [];
52191
+ item.forEach((child, _offset, index) => {
52192
+ if (index === 0) blocks.push(richParagraph(child, true));
52193
+ else if (/^(bullet|ordered)_list$/.test(child.type.name)) blocks.push(richBlock(child, depth));
52194
+ else throw unrepresentable(`a ${child.type.name} in a list item`);
52195
+ });
52196
+ items.push({ blocks });
52197
+ });
52198
+ const start = Number(node.attrs.order ?? 1);
52199
+ if (ordered && (!Number.isSafeInteger(start) || start < 1 || start > 2147483647)) throw unrepresentable(`a list that starts below 1 or above ${maximumListStart}`);
52200
+ return ordered && start !== 1 ? {
52201
+ type: "list",
52202
+ ordered,
52203
+ start,
52204
+ items
52205
+ } : {
52206
+ type: "list",
52207
+ ordered,
52208
+ items
52209
+ };
52210
+ }
52211
+ case "table": {
52212
+ if (listDepth > 0) throw unrepresentable("a table in a list");
52213
+ const rows = [];
52214
+ node.forEach((row) => {
52215
+ const cells = [];
52216
+ row.forEach((cell) => {
52217
+ 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");
52218
+ if (cell.childCount !== 1) throw unrepresentable("a cell of several blocks");
52219
+ const paragraph = richParagraph(cell.firstChild, false);
52220
+ cells.push({ content: paragraph.content });
52221
+ });
52222
+ rows.push(cells);
52223
+ });
52224
+ if (!rows.length || !rows[0].length || rows.some((row) => row.length !== rows[0].length)) throw unrepresentable("a table with uneven rows");
52225
+ return {
52226
+ type: "table",
52227
+ rows
52228
+ };
52229
+ }
52230
+ default: throw unrepresentable(`a ${node.type.name}`);
52231
+ }
52232
+ }
52233
+ /** The canonical rich text document for an editor document. Throws `preservation` when the
52234
+ * document holds something RTF cannot. The editor always holds a block, so its one empty
52235
+ * paragraph is an empty document when `emptyAsNone`, as it is when the document loaded empty. */
52236
+ function proseToRichText(doc, emptyAsNone = true) {
52237
+ const blocks = [];
52238
+ doc.forEach((node) => blocks.push(richBlock(node, 0)));
52239
+ if (emptyAsNone && blocks.length === 1 && blocks[0].type === "paragraph" && !blocks[0].align && !blocks[0].content.length) blocks.length = 0;
52240
+ return {
52241
+ version: 1,
52242
+ blocks
52243
+ };
52244
+ }
52245
+ /** Why `node` cannot be in an RTF document at `$depth` lists deep, or undefined. Checks the node
52246
+ * itself and its direct children, not its descendants. */
52247
+ function refusal(node, listDepth, inCell) {
52248
+ const name = node.type.name;
52249
+ if (!allowedNodes.has(name)) return name;
52250
+ for (const mark of node.marks) if (!richMarks$1[mark.type.name]) return mark.type.name;
52251
+ if (node.isText && textProblem(node.text)) return "text";
52252
+ const href = node.type.schema.marks.link?.isInSet(node.marks)?.attrs.href;
52253
+ if (href !== void 0 && linkProblem(String(href ?? ""))) return "link";
52254
+ if (name === "paragraph" && node.attrs.align != null && inCell) return "aligned cell";
52255
+ if (name === "list_item") {
52256
+ if (node.attrs.checked != null) return "task";
52257
+ for (let index = 1; index < node.childCount; index++) if (!/^(bullet|ordered)_list$/.test(node.child(index).type.name)) return "list item block";
52258
+ }
52259
+ if ((name === "bullet_list" || name === "ordered_list") && listDepth > 9) return "list depth";
52260
+ const order = Number(node.attrs.order);
52261
+ if (name === "ordered_list" && !(order >= 1 && order <= 2147483647)) return "list start";
52262
+ if (name === "table" && listDepth > 0) return "table in list";
52263
+ 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";
52264
+ }
52265
+ /** Whether the document a transaction reaches holds only what RTF can, read in the part it
52266
+ * changed and that part's ancestors. */
52267
+ function richTextAllows(tr, before) {
52268
+ if (!tr.docChanged) return true;
52269
+ const doc = tr.doc;
52270
+ const start = before.doc.content.findDiffStart(doc.content);
52271
+ if (start == null) return true;
52272
+ const end = before.doc.content.findDiffEnd(doc.content);
52273
+ const to = Math.max(start, end?.b ?? doc.content.size);
52274
+ let allowed = true;
52275
+ const $start = doc.resolve(start);
52276
+ let lists = 0;
52277
+ for (let depth = 1; depth <= $start.depth; depth++) {
52278
+ const node = $start.node(depth);
52279
+ if (/^(bullet|ordered)_list$/.test(node.type.name)) lists++;
52280
+ if (refusal(node, lists, false)) return false;
52281
+ }
52282
+ doc.nodesBetween(start, Math.min(to, doc.content.size), (node, pos) => {
52283
+ if (!allowed) return false;
52284
+ const $pos = doc.resolve(pos);
52285
+ let depth = /^(bullet|ordered)_list$/.test(node.type.name) ? 1 : 0;
52286
+ let inCell = false;
52287
+ for (let level = $pos.depth; level > 0; level--) {
52288
+ const name = $pos.node(level).type.name;
52289
+ if (/^(bullet|ordered)_list$/.test(name)) depth++;
52290
+ if (name === "table_cell" || name === "table_header") inCell = true;
52291
+ }
52292
+ if (refusal(node, depth, inCell)) allowed = false;
52293
+ return allowed;
52294
+ });
52295
+ return allowed;
52296
+ }
52297
+ /** The ranges of runs that have both a link and underline, which canonical form removes the
52298
+ * underline from, in the part of `doc` from `from` to `to`. */
52299
+ function linkedUnderlines(doc, from, to) {
52300
+ const ranges = [];
52301
+ const { link, underline } = doc.type.schema.marks;
52302
+ if (!link || !underline) return ranges;
52303
+ doc.nodesBetween(from, to, (node, pos) => {
52304
+ if (node.isText && link.isInSet(node.marks) && underline.isInSet(node.marks)) ranges.push({
52305
+ from: pos,
52306
+ to: pos + node.nodeSize
52307
+ });
52308
+ });
52309
+ return ranges;
52310
+ }
52311
+ /** Whether every character from `from` to `to`, or the caret's marks, is in a link. */
52312
+ function inLink(state) {
52313
+ const { link } = state.schema.marks;
52314
+ if (!link) return false;
52315
+ const { from, to, empty, $from } = state.selection;
52316
+ if (empty) return Boolean(link.isInSet(state.storedMarks ?? $from.marks()));
52317
+ let text = false;
52318
+ let all = true;
52319
+ state.doc.nodesBetween(from, to, (node) => {
52320
+ if (!node.isText) return true;
52321
+ text = true;
52322
+ if (!link.isInSet(node.marks)) all = false;
52323
+ return all;
52324
+ });
52325
+ return text && all;
52326
+ }
52327
+ //#endregion
52328
+ //#region src/formats/rich-text-import.ts
52329
+ var skipped = /* @__PURE__ */ new Set([
52330
+ "SCRIPT",
52331
+ "STYLE",
52332
+ "TEMPLATE",
52333
+ "NOSCRIPT",
52334
+ "IFRAME",
52335
+ "OBJECT",
52336
+ "EMBED",
52337
+ "HEAD",
52338
+ "TITLE",
52339
+ "META",
52340
+ "LINK",
52341
+ "SVG",
52342
+ "MATH",
52343
+ "IMG",
52344
+ "PICTURE",
52345
+ "VIDEO",
52346
+ "AUDIO",
52347
+ "CANVAS",
52348
+ "BUTTON",
52349
+ "INPUT",
52350
+ "SELECT",
52351
+ "TEXTAREA",
52352
+ "HR"
52353
+ ]);
52354
+ var blockTags = /* @__PURE__ */ new Set([
52355
+ "ADDRESS",
52356
+ "ARTICLE",
52357
+ "ASIDE",
52358
+ "BLOCKQUOTE",
52359
+ "BODY",
52360
+ "CENTER",
52361
+ "DD",
52362
+ "DETAILS",
52363
+ "DIV",
52364
+ "DL",
52365
+ "DT",
52366
+ "FIELDSET",
52367
+ "FIGCAPTION",
52368
+ "FIGURE",
52369
+ "FOOTER",
52370
+ "FORM",
52371
+ "H1",
52372
+ "H2",
52373
+ "H3",
52374
+ "H4",
52375
+ "H5",
52376
+ "H6",
52377
+ "HEADER",
52378
+ "HTML",
52379
+ "LI",
52380
+ "MAIN",
52381
+ "NAV",
52382
+ "OL",
52383
+ "P",
52384
+ "PRE",
52385
+ "SECTION",
52386
+ "SUMMARY",
52387
+ "TABLE",
52388
+ "UL"
52389
+ ]);
52390
+ function withMark(marks, mark) {
52391
+ if (marks.some((held) => held.type === mark.type)) return marks;
52392
+ const other = mark.type === "superscript" ? "subscript" : mark.type === "subscript" ? "superscript" : "";
52393
+ return canonicalMarks([...marks.filter((held) => held.type !== other), mark]);
52394
+ }
52395
+ function withoutMark(marks, type) {
52396
+ return marks.filter((mark) => mark.type !== type);
52397
+ }
52398
+ /** The marks an element adds to its text, from its tag and its inline style. */
52399
+ function elementMarks(element, marks, schemes) {
52400
+ const tag = element.tagName;
52401
+ if (tag === "B" || tag === "STRONG") marks = withMark(marks, { type: "bold" });
52402
+ if (tag === "I" || tag === "EM" || tag === "CITE" || tag === "DFN" || tag === "VAR") marks = withMark(marks, { type: "italic" });
52403
+ if (tag === "U" || tag === "INS") marks = withMark(marks, { type: "underline" });
52404
+ if (tag === "S" || tag === "DEL" || tag === "STRIKE") marks = withMark(marks, { type: "strikethrough" });
52405
+ if (tag === "SUP") marks = withMark(marks, { type: "superscript" });
52406
+ if (tag === "SUB") marks = withMark(marks, { type: "subscript" });
52407
+ if (tag === "CODE" || tag === "KBD" || tag === "SAMP" || tag === "TT") marks = withMark(marks, { type: "code" });
52408
+ if (tag === "A") {
52409
+ const href = element.getAttribute("href")?.trim();
52410
+ if (href && isSafeLinkTarget(href, schemes) && !linkProblem(href)) marks = withMark(withoutMark(marks, "link"), {
52411
+ type: "link",
52412
+ href
52413
+ });
52414
+ }
52415
+ const style = element instanceof HTMLElement ? element.style : void 0;
52416
+ if (style) {
52417
+ const weight = style.fontWeight;
52418
+ if (weight === "bold" || weight === "bolder" || Number(weight) >= 600) marks = withMark(marks, { type: "bold" });
52419
+ else if (weight === "normal" || weight && Number(weight) < 600) marks = withoutMark(marks, "bold");
52420
+ if (style.fontStyle === "italic" || style.fontStyle === "oblique") marks = withMark(marks, { type: "italic" });
52421
+ else if (style.fontStyle === "normal") marks = withoutMark(marks, "italic");
52422
+ const decoration = `${style.textDecoration} ${style.textDecorationLine}`;
52423
+ if (/underline/.test(decoration)) marks = withMark(marks, { type: "underline" });
52424
+ if (/line-through/.test(decoration)) marks = withMark(marks, { type: "strikethrough" });
52425
+ if (style.verticalAlign === "super") marks = withMark(marks, { type: "superscript" });
52426
+ if (style.verticalAlign === "sub") marks = withMark(marks, { type: "subscript" });
52427
+ }
52428
+ return canonicalMarks(marks);
52429
+ }
52430
+ function alignment(element) {
52431
+ const value = (element instanceof HTMLElement ? element.style.textAlign : "") || element.getAttribute("align") || (element.tagName === "CENTER" ? "center" : "");
52432
+ return value === "center" || value === "right" || value === "justify" ? value : void 0;
52433
+ }
52434
+ /** Reads blocks from a DOM, the way a person reads the page. */
52435
+ var Reader = class Reader {
52436
+ schemes;
52437
+ depth;
52438
+ blocks = [];
52439
+ inline;
52440
+ align;
52441
+ space;
52442
+ constructor(schemes, depth) {
52443
+ this.schemes = schemes;
52444
+ this.depth = depth;
52445
+ }
52446
+ /** Ends the open paragraph, if any. */
52447
+ flush() {
52448
+ if (!this.inline) return;
52449
+ const content = this.inline.finish();
52450
+ if (content.at(-1)?.type === "break") content.pop();
52451
+ this.blocks.push(this.align ? {
52452
+ type: "paragraph",
52453
+ align: this.align,
52454
+ content
52455
+ } : {
52456
+ type: "paragraph",
52457
+ content
52458
+ });
52459
+ this.inline = void 0;
52460
+ this.align = void 0;
52461
+ this.space = void 0;
52462
+ }
52463
+ open() {
52464
+ this.inline ??= new InlineBuilder();
52465
+ return this.inline;
52466
+ }
52467
+ text(value, marks, pre) {
52468
+ if (pre) {
52469
+ value.split(/\r\n|[\n\r]/).forEach((line, index) => {
52470
+ if (index) this.flush();
52471
+ appendText(this.open(), line, marks);
52472
+ });
52473
+ return;
52474
+ }
52475
+ const collapsed = value.replace(/[ \t\n\r\f]+/g, " ");
52476
+ if (!collapsed) return;
52477
+ if (collapsed === " ") {
52478
+ if (this.inline?.content.length) this.space ??= marks;
52479
+ return;
52480
+ }
52481
+ const builder = this.open();
52482
+ const empty = !builder.content.length;
52483
+ let text = collapsed;
52484
+ if (text.startsWith(" ")) {
52485
+ if (!empty) this.space ??= marks;
52486
+ text = text.slice(1);
52487
+ }
52488
+ if (!text) return;
52489
+ if (this.space) {
52490
+ builder.text(" ", this.space);
52491
+ this.space = void 0;
52492
+ }
52493
+ if (text.endsWith(" ")) {
52494
+ this.space = marks;
52495
+ text = text.slice(0, -1);
52496
+ }
52497
+ appendText(builder, text, marks);
52498
+ }
52499
+ lineBreak() {
52500
+ this.space = void 0;
52501
+ this.open().break();
52502
+ }
52503
+ /** Starts a paragraph that takes `element`'s alignment. */
52504
+ paragraph(element) {
52505
+ this.flush();
52506
+ this.align = alignment(element);
52507
+ }
52508
+ push(block) {
52509
+ this.flush();
52510
+ this.blocks.push(block);
52511
+ }
52512
+ read(node, marks, pre) {
52513
+ if (node.nodeType === Node.TEXT_NODE) {
52514
+ this.text(node.textContent ?? "", marks, pre);
52515
+ return;
52516
+ }
52517
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
52518
+ const element = node;
52519
+ const tag = element.tagName.toUpperCase();
52520
+ if (skipped.has(tag) || element instanceof HTMLElement && element.hidden) return;
52521
+ if (element instanceof HTMLElement && element.style.display === "none") return;
52522
+ if (tag === "BR") {
52523
+ this.lineBreak();
52524
+ return;
52525
+ }
52526
+ if (tag === "UL" || tag === "OL") {
52527
+ this.push(this.list(element, marks));
52528
+ return;
52529
+ }
52530
+ if (tag === "TABLE") {
52531
+ const table = this.depth === 0 && !spansRows(element);
52532
+ const rows = tableRows(element, marks, this.schemes, table);
52533
+ if (!rows.length) return;
52534
+ if (table) this.push({
52535
+ type: "table",
52536
+ rows
52537
+ });
52538
+ else for (const row of rows) {
52539
+ this.paragraph(element);
52540
+ this.open();
52541
+ row.forEach((cell, index) => {
52542
+ if (index) this.open().text(" ", []);
52543
+ for (const inline of cell.content) if (inline.type === "break") this.open().break();
52544
+ else this.open().text(inline.text, inline.marks ?? []);
52545
+ });
52546
+ this.flush();
52547
+ }
52548
+ return;
52549
+ }
52550
+ let inner = elementMarks(element, marks, this.schemes);
52551
+ if (/^H[1-6]$/.test(tag)) inner = withMark(inner, { type: "bold" });
52552
+ const block = blockTags.has(tag);
52553
+ if (block) this.paragraph(element);
52554
+ if (tag === "P" || /^H[1-6]$/.test(tag)) this.open();
52555
+ const preformatted = pre || tag === "PRE" || element instanceof HTMLElement && /^pre/.test(element.style.whiteSpace);
52556
+ if (tag === "PRE") inner = withMark(inner, { type: "code" });
52557
+ for (const child of element.childNodes) this.read(child, inner, preformatted);
52558
+ if (block) this.flush();
52559
+ }
52560
+ list(element, marks) {
52561
+ const ordered = element.tagName.toUpperCase() === "OL";
52562
+ const start = Number(element.getAttribute("start") ?? 1);
52563
+ const items = [];
52564
+ const inner = elementMarks(element, marks, this.schemes);
52565
+ for (const child of element.childNodes) {
52566
+ if (child.nodeType === Node.ELEMENT_NODE && child.tagName.toUpperCase() === "LI") {
52567
+ items.push({ blocks: this.item(child, inner) });
52568
+ continue;
52569
+ }
52570
+ const reader = new Reader(this.schemes, this.depth + 1);
52571
+ reader.read(child, inner, false);
52572
+ reader.flush();
52573
+ if (!reader.blocks.length) continue;
52574
+ const blocks = itemBlocks(reader.blocks, this.depth + 1);
52575
+ if (items.length && blocks[0].type === "paragraph" && !blocks[0].content.length) items.at(-1).blocks.push(...blocks.slice(1));
52576
+ else items.push({ blocks });
52577
+ }
52578
+ if (!items.length) items.push({ blocks: [{
52579
+ type: "paragraph",
52580
+ content: []
52581
+ }] });
52582
+ return ordered && Number.isSafeInteger(start) && start > 1 ? {
52583
+ type: "list",
52584
+ ordered,
52585
+ start: Math.min(start, maximumListStart),
52586
+ items
52587
+ } : {
52588
+ type: "list",
52589
+ ordered,
52590
+ items
52591
+ };
52592
+ }
52593
+ item(element, marks) {
52594
+ const reader = new Reader(this.schemes, this.depth + 1);
52595
+ const inner = elementMarks(element, marks, this.schemes);
52596
+ for (const child of element.childNodes) reader.read(child, inner, false);
52597
+ reader.flush();
52598
+ return itemBlocks(reader.blocks, this.depth + 1);
52599
+ }
52600
+ };
52601
+ /** A list item's blocks as RTF holds them: one paragraph, then lists. Further paragraphs join the
52602
+ * first as lines. Lists keep every level here; the paste lifts those deeper than RTF holds. */
52603
+ function itemBlocks(blocks, depth) {
52604
+ const builder = new InlineBuilder();
52605
+ let align;
52606
+ let lines = 0;
52607
+ const nested = [];
52608
+ const line = (content) => {
52609
+ if (lines++) builder.break();
52610
+ for (const inline of content) if (inline.type === "break") builder.break();
52611
+ else builder.text(inline.text, inline.marks ?? []);
52612
+ };
52613
+ const flatten = (block) => {
52614
+ if (block.type === "paragraph") line(block.content);
52615
+ else if (block.type === "list") for (const item of block.items) for (const child of item.blocks) flatten(child);
52616
+ else for (const row of block.rows) for (const cell of row) line(cell.content);
52617
+ };
52618
+ for (const block of blocks) if (block.type === "list") nested.push(block);
52619
+ else if (block.type === "paragraph" && !nested.length) {
52620
+ if (!lines) align = block.align;
52621
+ line(block.content);
52622
+ } else flatten(block);
52623
+ const content = builder.finish();
52624
+ return [align ? {
52625
+ type: "paragraph",
52626
+ align,
52627
+ content
52628
+ } : {
52629
+ type: "paragraph",
52630
+ content
52631
+ }, ...nested];
52632
+ }
52633
+ /** Whether a cell of `table` itself, not of a table inside it, spans more than one row. */
52634
+ function spansRows(table) {
52635
+ return [...table.querySelectorAll("td[rowspan], th[rowspan]")].some((cell) => cell.closest("table") === table && Number(cell.getAttribute("rowspan")) > 1);
52636
+ }
52637
+ function tableRows(table, marks, schemes, even) {
52638
+ const rows = [];
52639
+ for (const row of table.querySelectorAll("tr")) {
52640
+ if (row.closest("table") !== table) continue;
52641
+ const cells = [];
52642
+ for (const cell of row.children) {
52643
+ if (cell.tagName !== "TD" && cell.tagName !== "TH") continue;
52644
+ const reader = new Reader(schemes, 1);
52645
+ const inner = elementMarks(cell, marks, schemes);
52646
+ for (const child of cell.childNodes) reader.read(child, inner, false);
52647
+ reader.flush();
52648
+ const builder = new InlineBuilder();
52649
+ let lines = 0;
52650
+ const add = (block) => {
52651
+ if (block.type === "paragraph") {
52652
+ if (lines++) builder.break();
52653
+ for (const inline of block.content) if (inline.type === "break") builder.break();
52654
+ else builder.text(inline.text, inline.marks ?? []);
52655
+ } else if (block.type === "list") for (const item of block.items) item.blocks.forEach(add);
52656
+ else for (const nestedRow of block.rows) for (const nestedCell of nestedRow) add({
52657
+ type: "paragraph",
52658
+ content: nestedCell.content
52659
+ });
52660
+ };
52661
+ reader.blocks.forEach(add);
52662
+ const content = builder.finish();
52663
+ const span = Math.max(1, Math.min(100, Number(cell.getAttribute("colspan") ?? 1) || 1));
52664
+ cells.push({ content });
52665
+ for (let index = 1; index < span; index++) cells.push({ content: [] });
52666
+ }
52667
+ if (cells.length) rows.push(cells);
52668
+ }
52669
+ const width = even ? Math.max(0, ...rows.map((row) => row.length)) : 0;
52670
+ for (const row of rows) while (row.length < width) row.push({ content: [] });
52671
+ return rows;
52672
+ }
52673
+ /** Blocks for pasted HTML. */
52674
+ function htmlBlocks(html, schemes) {
52675
+ const parsed = new DOMParser().parseFromString(html, "text/html");
52676
+ const reader = new Reader(schemes, 0);
52677
+ reader.read(parsed.body, [], false);
52678
+ reader.flush();
52679
+ return reader.blocks;
52680
+ }
52681
+ /** Every block's text as lines of one paragraph, for a place that holds only one, such as a
52682
+ * table cell. */
52683
+ function asLines(blocks) {
52684
+ const builder = new InlineBuilder();
52685
+ let lines = 0;
52686
+ const add = (content) => {
52687
+ if (lines++) builder.break();
52688
+ for (const inline of content) if (inline.type === "break") builder.break();
52689
+ else builder.text(inline.text, inline.marks ?? []);
52690
+ };
52691
+ const visit = (block) => {
52692
+ if (block.type === "paragraph") add(block.content);
52693
+ else if (block.type === "list") block.items.forEach((item) => item.blocks.forEach(visit));
52694
+ else block.rows.forEach((row) => add(row.flatMap((cell, index) => index ? [{
52695
+ type: "text",
52696
+ text: " "
52697
+ }, ...cell.content] : cell.content)));
52698
+ };
52699
+ blocks.forEach(visit);
52700
+ return [{
52701
+ type: "paragraph",
52702
+ content: builder.finish()
52703
+ }];
52704
+ }
52705
+ /** `list`, at `depth` lists deep, with every level beyond the deepest RTF holds lifted to the
52706
+ * deepest: the items of a list that would be too deep become items of its parent list. */
52707
+ function liftDeepLevels(list, depth) {
52708
+ const flat = (deep) => deep.items.flatMap((item) => [{ blocks: [item.blocks[0]] }, ...item.blocks.slice(1).flatMap((nested) => flat(nested))]);
52709
+ return {
52710
+ ...list,
52711
+ items: list.items.flatMap((item) => {
52712
+ const [paragraph, ...nested] = item.blocks;
52713
+ const kept = [];
52714
+ const lifted = [];
52715
+ for (const child of nested) if (depth + 1 <= 9) kept.push(liftDeepLevels(child, depth + 1));
52716
+ else lifted.push(...flat(child));
52717
+ return [{ blocks: [paragraph, ...kept] }, ...lifted];
52718
+ })
52719
+ };
52720
+ }
52721
+ /**
52722
+ * A paste of several blocks into a list item, which holds one paragraph: each paragraph becomes
52723
+ * an item beside the one at the caret, at its level. The first joins the item at the caret, the
52724
+ * text after the caret moves to the last, and a pasted list nests in the item before it.
52725
+ */
52726
+ function listItemsSlice(schema, blocks, list, depth, marks) {
52727
+ const empty = {
52728
+ type: "paragraph",
52729
+ content: []
52730
+ };
52731
+ const items = [];
52732
+ for (const block of blocks) if (block.type === "paragraph") items.push({ blocks: [block] });
52733
+ else if (block.type === "list") {
52734
+ if (!items.length) items.push({ blocks: [empty] });
52735
+ items.at(-1).blocks.push(block);
52736
+ } else for (const line of asLines([block])) items.push({ blocks: [line] });
52737
+ if (!items.length || items.at(-1).blocks.length > 1) items.push({ blocks: [empty] });
52738
+ const ordered = list.type.name === "ordered_list";
52739
+ const nodes = liftDeepLevels({
52740
+ type: "list",
52741
+ ordered,
52742
+ items
52743
+ }, depth).items.map((item) => schema.nodes.list_item.create({
52744
+ label: ordered ? "1." : "•",
52745
+ listType: ordered ? "ordered" : "bullet",
52746
+ spread: false,
52747
+ checked: null
52748
+ }, item.blocks.map((block) => proseBlock(schema, block, marks))));
52749
+ return new Slice(Fragment.fromArray(nodes), 2, 2);
52750
+ }
52751
+ /**
52752
+ * The slice that `input` pastes into an RTF document at the selection. Explicit Markdown pastes as
52753
+ * its text, unparsed; HTML keeps what RTF can hold; plain text gives a paragraph for each line.
52754
+ * Plain text takes the marks of the text it replaces, as typing would.
52755
+ */
52756
+ function richTextPaste(input, state, schemes) {
52757
+ const { schema, selection } = state;
52758
+ const { $from, $to } = selection;
52759
+ const html = input.markdown == null && input.html ? htmlBlocks(input.html, schemes) : void 0;
52760
+ const plain = html == null;
52761
+ let blocks = html ?? textBlocks(input.markdown ?? input.text);
52762
+ if (html && !blocks.some((block) => block.type !== "paragraph" || block.content.length)) blocks = textBlocks(input.text);
52763
+ let cell = false;
52764
+ let lists = 0;
52765
+ let list;
52766
+ for (let depth = $from.depth; depth > 0; depth--) {
52767
+ const node = $from.node(depth);
52768
+ const name = node.type.name;
52769
+ if (name === "table_cell" || name === "table_header") cell = true;
52770
+ if (name === "bullet_list" || name === "ordered_list") {
52771
+ lists++;
52772
+ list ??= node;
52773
+ }
52774
+ }
52775
+ if (cell) blocks = asLines(blocks);
52776
+ else blocks = blocks.map((block) => block.type === "list" ? liftDeepLevels(block, lists + 1) : block);
52777
+ const marks = plain ? state.storedMarks ?? ($from.marksAcross($to) ?? $from.marks()).filter((mark) => mark.type.name !== "link") : [];
52778
+ if (list && !cell && !(blocks.length === 1 && blocks[0].type === "paragraph")) return listItemsSlice(schema, blocks, list, lists, marks);
52779
+ const nodes = blocks.map((block) => proseBlock(schema, block, marks));
52780
+ if (!nodes.length) return Slice.empty;
52781
+ const openStart = nodes[0].type.name === "paragraph" ? 1 : 0;
52782
+ const openEnd = nodes.at(-1).type.name === "paragraph" ? 1 : 0;
52783
+ return new Slice(Fragment.fromArray(nodes), openStart, openEnd);
52784
+ }
52785
+ //#endregion
52786
+ //#region src/formats/rich-text.ts
52787
+ function source(ctx, text) {
52788
+ const { schema } = ctx.get(editorViewCtx).state;
52789
+ const document = richTextFromText(text);
52790
+ const emptyAsNone = document.blocks.length === 0;
52791
+ return {
52792
+ baseline: richTextToProse(schema, document),
52793
+ serialize: (doc) => richTextText(proseToRichText(doc, emptyAsNone))
52794
+ };
52795
+ }
52796
+ /** Rich text, which the host converts to and from RTF. The editor holds the canonical JSON as the
52797
+ * document's text. It reaches no Markdown parsing or preservation;
52798
+ * `test/formats/boundary.test.ts` checks its imports. */
52799
+ var richTextFormat = {
52800
+ format: "rtf",
52801
+ promise: "structure",
52802
+ literalOnly: false,
52803
+ sourceMode: false,
52804
+ formattedLabel: "richTextEditor",
52805
+ literalLabel: "sourceEditor",
52806
+ headings: false,
52807
+ nativeCopy: false,
52808
+ keepsInsertedLineEndings: false,
52809
+ markdownInput: false,
52810
+ load: source,
52811
+ parse: source,
52812
+ pendingExport: () => void 0,
52813
+ literalPaste: (input) => input.text,
52814
+ richText: {
52815
+ allows: richTextAllows,
52816
+ linkedUnderlines,
52817
+ inLink,
52818
+ paste: richTextPaste,
52819
+ copy(state, linkSchemes) {
52820
+ const content = state.selection.content().content;
52821
+ return clipboardContent(content, state.schema, linkSchemes);
52822
+ }
52823
+ },
52824
+ async print({ doc, signal, output }) {
52825
+ return printableMarkdown(doc.content, output.ctx().get(editorViewCtx).state.schema, void 0, output.context(), void 0, signal);
52826
+ },
52827
+ async clipboard({ all, version, output, assertCurrent }) {
52828
+ const { state } = output.ctx().get(editorViewCtx);
52829
+ const slice = all ? new Slice(state.doc.content, 0, 0) : state.selection.content();
52830
+ const content = slice.content;
52831
+ const markdown = output.richTextMarkdown(slice);
52832
+ const { html } = clipboardContent(content, state.schema, output.linkSchemes());
52833
+ const text = clipboardText(content);
52834
+ assertCurrent();
52835
+ return {
52836
+ ...version,
52837
+ text,
52838
+ html,
52839
+ markdown,
52840
+ images: [],
52841
+ diagrams: []
52842
+ };
52843
+ }
52844
+ };
51645
52845
  //#endregion
51646
52846
  //#region src/formats/text.ts
51647
52847
  /** A paragraph holding `text`, or an empty one. */
@@ -51656,10 +52856,12 @@ var textFormat = {
51656
52856
  promise: "exact",
51657
52857
  literalOnly: true,
51658
52858
  sourceMode: false,
52859
+ formattedLabel: "formattedEditor",
51659
52860
  literalLabel: "plainTextEditor",
51660
52861
  headings: false,
51661
52862
  nativeCopy: true,
51662
52863
  keepsInsertedLineEndings: false,
52864
+ markdownInput: false,
51663
52865
  load: (ctx) => paragraph(ctx, ""),
51664
52866
  parse: paragraph,
51665
52867
  pendingExport: () => void 0,
@@ -51681,11 +52883,18 @@ var textFormat = {
51681
52883
  };
51682
52884
  //#endregion
51683
52885
  //#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"];
52886
+ /** Every format Inkkit can open. */
52887
+ var documentFormats = [
52888
+ "md",
52889
+ "txt",
52890
+ "rtf"
52891
+ ];
52892
+ /** The formats a host is offered when it chooses none. */
52893
+ var defaultFormats = ["md", "txt"];
51686
52894
  var formatHandlers = {
51687
52895
  md: markdownFormat,
51688
- txt: textFormat
52896
+ txt: textFormat,
52897
+ rtf: richTextFormat
51689
52898
  };
51690
52899
  //#endregion
51691
52900
  //#region src/editor-document.ts
@@ -51746,7 +52955,7 @@ var EditorDocument = class {
51746
52955
  this.sourcePreservations.set(provenance, preservation);
51747
52956
  }
51748
52957
  }
51749
- text = preservation.serialize(cleanSourceDoc(doc), this.carried);
52958
+ text = preservation.serialize(this.handler.richText ? doc : cleanSourceDoc(doc), this.carried);
51750
52959
  } catch (error) {
51751
52960
  throw coded(error, "preservation");
51752
52961
  }
@@ -51793,7 +53002,7 @@ var EditorDocument = class {
51793
53002
  */
51794
53003
  authoredSources(doc, positions) {
51795
53004
  const none = /* @__PURE__ */ new Map();
51796
- if (this.runtime.literal() || !positions.length) return none;
53005
+ if (this.runtime.literal() || this.handler.richText || !positions.length) return none;
51797
53006
  try {
51798
53007
  let marked = doc;
51799
53008
  for (let index = positions.length - 1; index >= 0; index--) {
@@ -52305,7 +53514,7 @@ var rejoinLists = $prose(() => keymap$3({ Backspace: (state, dispatch) => {
52305
53514
  const list = $from.node(-2);
52306
53515
  const sublist = container.maybeChild(1);
52307
53516
  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;
53517
+ if (index !== 0 || container.childCount !== 2 || !sublist || !isList$1(sublist) || previous?.type !== sublist.type) return false;
52309
53518
  if (dispatch) {
52310
53519
  const at = $from.before(-1);
52311
53520
  const end = at - 2;
@@ -52317,7 +53526,7 @@ var rejoinLists = $prose(() => keymap$3({ Backspace: (state, dispatch) => {
52317
53526
  }
52318
53527
  const before = container.maybeChild(index - 1);
52319
53528
  const after = container.maybeChild(index + 1);
52320
- if (!before || !after || !isList(before) || after.type !== before.type) return false;
53529
+ if (!before || !after || !isList$1(before) || after.type !== before.type) return false;
52321
53530
  if (dispatch) {
52322
53531
  const at = $from.before();
52323
53532
  const tr = state.tr.delete(at, $from.after());
@@ -52390,21 +53599,21 @@ var movedListItems = $prose(() => new Plugin({
52390
53599
  }));
52391
53600
  var textInputPolicy = $prose(() => textInputPlugin());
52392
53601
  function outermostListDepth($pos) {
52393
- for (let depth = 1; depth <= $pos.depth; depth++) if (isList($pos.node(depth))) return depth;
53602
+ for (let depth = 1; depth <= $pos.depth; depth++) if (isList$1($pos.node(depth))) return depth;
52394
53603
  return null;
52395
53604
  }
52396
53605
  function innermostListDepth($pos) {
52397
- for (let depth = $pos.depth; depth >= 1; depth--) if (isList($pos.node(depth))) return depth;
53606
+ for (let depth = $pos.depth; depth >= 1; depth--) if (isList$1($pos.node(depth))) return depth;
52398
53607
  return null;
52399
53608
  }
52400
- function isList(node) {
53609
+ function isList$1(node) {
52401
53610
  return node.type.name === "bullet_list" || node.type.name === "ordered_list";
52402
53611
  }
52403
53612
  function joinLists(tr, start, index) {
52404
53613
  const $start = tr.doc.resolve(start);
52405
53614
  const before = $start.parent.maybeChild(index - 1);
52406
53615
  const after = $start.parent.maybeChild(index);
52407
- if (before && after && isList(before) && after.type === before.type) tr.join($start.posAtIndex(index));
53616
+ if (before && after && isList$1(before) && after.type === before.type) tr.join($start.posAtIndex(index));
52408
53617
  }
52409
53618
  //#endregion
52410
53619
  //#region src/editor-commands.ts
@@ -52412,7 +53621,67 @@ function headingLevel(command) {
52412
53621
  const match = /^heading([1-6])$/.exec(command);
52413
53622
  return match ? Number(match[1]) : void 0;
52414
53623
  }
52415
- function formatAvailability(editor, state, formatted) {
53624
+ /** What an RTF document cannot hold: headings, code blocks, quotes, tasks and highlight. Every
53625
+ * other command is available as in Markdown, and alignment only here. */
53626
+ var unavailableInRichText = [
53627
+ "heading1",
53628
+ "heading2",
53629
+ "heading3",
53630
+ "heading4",
53631
+ "heading5",
53632
+ "heading6",
53633
+ "paragraph",
53634
+ "highlight",
53635
+ "codeBlock",
53636
+ "quote",
53637
+ "taskList",
53638
+ "toggleTask"
53639
+ ];
53640
+ var alignments = {
53641
+ alignLeft: null,
53642
+ alignCenter: "center",
53643
+ alignRight: "right",
53644
+ alignJustify: "justify"
53645
+ };
53646
+ function isAlignCommand(command) {
53647
+ return command in alignments;
53648
+ }
53649
+ /** Calls `found` for each paragraph the selection touches that can take an alignment, which
53650
+ * those in table cells cannot, until it returns false. */
53651
+ function eachAlignable(state, found) {
53652
+ const { from, to } = state.selection;
53653
+ let going = true;
53654
+ state.doc.nodesBetween(from, to, (node, pos) => {
53655
+ if (!going || node.type.name === "table") return false;
53656
+ if (node.type.name === "paragraph") going = found(node, pos);
53657
+ return going && !node.isTextblock;
53658
+ });
53659
+ }
53660
+ function canAlign(state) {
53661
+ let any = false;
53662
+ eachAlignable(state, () => {
53663
+ any = true;
53664
+ return false;
53665
+ });
53666
+ return any;
53667
+ }
53668
+ /** The alignment of the paragraph that holds the start of the selection. */
53669
+ function caretAlignment(state) {
53670
+ const { parent } = state.selection.$from;
53671
+ return parent.type.name === "paragraph" && parent.attrs.align ? parent.attrs.align : "left";
53672
+ }
53673
+ function formatAvailability(editor, state, formatted, handler) {
53674
+ const available = markdownAvailability(editor, state, formatted);
53675
+ const rich = handler.richText;
53676
+ if (!rich) return available;
53677
+ for (const command of unavailableInRichText) available[command] = false;
53678
+ if (available.underline && rich.inLink(state)) available.underline = false;
53679
+ const align = formatted && canAlign(state);
53680
+ available.alignLeft = available.alignCenter = align;
53681
+ available.alignRight = available.alignJustify = align;
53682
+ return available;
53683
+ }
53684
+ function markdownAvailability(editor, state, formatted) {
52416
53685
  const manager = editor.ctx.get(commandsCtx);
52417
53686
  const check = (key, payload) => formatted && manager.get(key)(payload)(state);
52418
53687
  const canBecome = (type, level) => formatted && textblockType(editor, type, level)(state);
@@ -52439,9 +53708,27 @@ function formatAvailability(editor, state, formatted) {
52439
53708
  orderedList: formatted && (innermostListDepth(textBounds(state).$from) != null || check(wrapInOrderedListCommand.key)),
52440
53709
  taskList: formatted && (innermostListDepth(textBounds(state).$from) != null || check(wrapInBulletListCommand.key)),
52441
53710
  toggleTask: formatted && taskAtCaret(state) != null,
53711
+ alignLeft: false,
53712
+ alignCenter: false,
53713
+ alignRight: false,
53714
+ alignJustify: false,
52442
53715
  link: check(toggleLinkCommand.key, {})
52443
53716
  };
52444
53717
  }
53718
+ /** Sets the alignment of the paragraphs the selection touches. */
53719
+ function align(editor, command) {
53720
+ const view = editor.ctx.get(editorViewCtx);
53721
+ const value = alignments[command];
53722
+ const tr = view.state.tr;
53723
+ eachAlignable(view.state, (node, pos) => {
53724
+ if (node.attrs.align !== value) tr.setNodeMarkup(pos, void 0, {
53725
+ ...node.attrs,
53726
+ align: value
53727
+ });
53728
+ return true;
53729
+ });
53730
+ if (tr.docChanged) view.dispatch(tr);
53731
+ }
52445
53732
  function canQuote(editor, state) {
52446
53733
  const { $from, $to } = textBounds(state);
52447
53734
  if (quoted($from, $to)) {
@@ -52496,12 +53783,32 @@ function toggleInlineCode(editor) {
52496
53783
  if (!type.isInSet(selection.$from.marks())) view.dispatch(view.state.tr.addStoredMark(type.create()));
52497
53784
  else withMarkRunSelected(editor, type, run);
52498
53785
  }
52499
- function list(editor, kind) {
53786
+ function list(editor, kind, itemPerBlock) {
52500
53787
  const view = editor.ctx.get(editorViewCtx);
52501
53788
  const { state } = view;
52502
53789
  const { $from } = textBounds(state);
52503
53790
  const wanted = kind === "bulletList" ? "bullet_list" : "ordered_list";
52504
53791
  const depth = innermostListDepth($from);
53792
+ if (depth == null && itemPerBlock) {
53793
+ const ordered = wanted === "ordered_list";
53794
+ wrapInList(state.schema.nodes[wanted], ordered ? {
53795
+ order: 1,
53796
+ spread: false
53797
+ } : { spread: false })(state, (tr) => {
53798
+ tr.doc.nodesBetween(tr.mapping.map(state.selection.from), tr.mapping.map(state.selection.to), (node, pos) => {
53799
+ if (node.type.name !== "list_item") return true;
53800
+ tr.setNodeMarkup(pos, void 0, {
53801
+ ...node.attrs,
53802
+ listType: ordered ? "ordered" : "bullet",
53803
+ label: ordered ? "1." : "•",
53804
+ spread: false
53805
+ });
53806
+ return false;
53807
+ });
53808
+ view.dispatch(tr);
53809
+ });
53810
+ return;
53811
+ }
52505
53812
  if (depth == null) {
52506
53813
  const command = kind === "bulletList" ? wrapInBulletListCommand : wrapInOrderedListCommand;
52507
53814
  editor.action(callCommand(command.key));
@@ -52560,11 +53867,15 @@ function textblockType(editor, type, attr) {
52560
53867
  if (type === "heading") return setTextblockType(ctx, headingSchema.type(ctx), { level: Number(attr) });
52561
53868
  return setTextblockType(ctx, paragraphSchema.type(ctx));
52562
53869
  }
52563
- function applyFormat(editor, command, checked, link) {
53870
+ function applyFormat(editor, command, checked, link, itemPerBlock = false) {
52564
53871
  const view = editor.ctx.get(editorViewCtx);
52565
53872
  const run = (cmd, payload) => editor.action(callCommand(cmd, payload));
52566
53873
  const state = caretState(view.state);
52567
53874
  const become = (type, attr) => textblockType(editor, type, attr)(view.state, view.dispatch);
53875
+ if (isAlignCommand(command)) {
53876
+ align(editor, command);
53877
+ return;
53878
+ }
52568
53879
  const level = headingLevel(command);
52569
53880
  if (level != null) {
52570
53881
  if (state.block.type === "heading" && state.block.level === level) become("paragraph");
@@ -52608,7 +53919,7 @@ function applyFormat(editor, command, checked, link) {
52608
53919
  break;
52609
53920
  case "bulletList":
52610
53921
  case "orderedList":
52611
- list(editor, command);
53922
+ list(editor, command, itemPerBlock);
52612
53923
  break;
52613
53924
  case "taskList":
52614
53925
  toggleTaskList(editor.ctx);
@@ -55583,6 +56894,7 @@ async function createMilkdownEditor(root, options, runtime) {
55583
56894
  ctx.set(rootCtx, root);
55584
56895
  ctx.set(authoredSourcesCtx.key, (doc, positions) => runtime.authoredSources(doc, positions));
55585
56896
  ctx.set(smartPunctuationCtx.key, () => runtime.smartPunctuation());
56897
+ ctx.set(formatInputCtx.key, runtime.format);
55586
56898
  ctx.set(linkSchemesCtx.key, runtime.linkSchemes);
55587
56899
  ctx.update(labelsCtx.key, () => ({
55588
56900
  ...defaultLabels,
@@ -55632,7 +56944,7 @@ async function createMilkdownEditor(root, options, runtime) {
55632
56944
  return true;
55633
56945
  }
55634
56946
  }));
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, {
56947
+ }).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-4lYN38Zo.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
56948
  capture: () => runtime.capture(),
55637
56949
  active: () => runtime.active(),
55638
56950
  labels: {
@@ -55648,8 +56960,46 @@ async function createMilkdownEditor(root, options, runtime) {
55648
56960
  ...options.labels
55649
56961
  },
55650
56962
  error: (error) => runtime.report(error)
55651
- }, options.files, runtime.canMutate) : []).create();
56963
+ }, options.files, runtime.canMutate) : []).create().then((editor) => {
56964
+ markdownInputRules(editor.ctx);
56965
+ return editor;
56966
+ });
56967
+ }
56968
+ /** Markdown input rules, such as `# ` for a heading, do nothing in a format that is not Markdown.
56969
+ * Rules that are not Markdown syntax, such as smart punctuation, keep applying. */
56970
+ function markdownInputRules(ctx) {
56971
+ for (const rule of ctx.get(inputRulesCtx)) {
56972
+ if (formatNeutralRules.has(rule)) continue;
56973
+ const held = rule;
56974
+ const handler = held.handler;
56975
+ held.handler = (state, match, start, end) => formatInput(ctx).markdownInput() ? handler(state, match, start, end) : null;
56976
+ }
55652
56977
  }
56978
+ /**
56979
+ * In a format that holds less than Markdown, such as RTF, refuses any transaction that would bring
56980
+ * in a node, mark or structure the format cannot hold, so a disallowed edit changes nothing. A
56981
+ * link's underline, which RTF does not keep, is removed in the same step.
56982
+ */
56983
+ var richTextGuard = $prose((ctx) => new Plugin({
56984
+ key: new PluginKey("inkkitRichTextGuard"),
56985
+ filterTransaction(tr, state) {
56986
+ const rich = formatInput(ctx).richText();
56987
+ return !rich || rich.allows(tr, state);
56988
+ },
56989
+ appendTransaction(transactions, oldState, newState) {
56990
+ const rich = formatInput(ctx).richText();
56991
+ if (!rich || !transactions.some((tr) => tr.docChanged)) return null;
56992
+ const start = oldState.doc.content.findDiffStart(newState.doc.content);
56993
+ if (start == null) return null;
56994
+ const end = oldState.doc.content.findDiffEnd(newState.doc.content).b;
56995
+ const ranges = rich.linkedUnderlines(newState.doc, start, Math.max(start, end));
56996
+ if (!ranges.length) return null;
56997
+ const tr = newState.tr;
56998
+ for (const { from, to } of ranges) tr.removeMark(from, to, newState.schema.marks.underline);
56999
+ if (newState.storedMarks) tr.setStoredMarks(newState.storedMarks);
57000
+ return tr;
57001
+ }
57002
+ }));
55653
57003
  function createDocumentState(ctx, doc) {
55654
57004
  return EditorState.create(ctx.get(editorStateOptionsCtx)({
55655
57005
  schema: ctx.get(schemaCtx),
@@ -55662,6 +57012,402 @@ function initialDocumentSelection(state) {
55662
57012
  return setSearchState(state.tr.setSelection(end), new SearchQuery({ search: "" }));
55663
57013
  }
55664
57014
  //#endregion
57015
+ //#region src/formats/convert.ts
57016
+ var Losses = class {
57017
+ counts = /* @__PURE__ */ new Map();
57018
+ add(code) {
57019
+ this.counts.set(code, (this.counts.get(code) ?? 0) + 1);
57020
+ }
57021
+ list() {
57022
+ return Object.freeze(formatLossCodes.filter((code) => this.counts.has(code)).map((code) => Object.freeze({
57023
+ code,
57024
+ count: this.counts.get(code)
57025
+ })));
57026
+ }
57027
+ };
57028
+ var richMarks = {
57029
+ strong: "bold",
57030
+ emphasis: "italic",
57031
+ underline: "underline",
57032
+ strike_through: "strikethrough",
57033
+ superscript: "superscript",
57034
+ subscript: "subscript",
57035
+ inlineCode: "code",
57036
+ link: "link"
57037
+ };
57038
+ var isList = (node) => node.type.name === "bullet_list" || node.type.name === "ordered_list";
57039
+ /** Reads a formatted Markdown document as rich text, counting what RTF cannot hold. */
57040
+ var MarkdownReader = class {
57041
+ lost = new Losses();
57042
+ /** Appends text, counting characters RTF can't hold, which become U+FFFD. */
57043
+ append(builder, text, marks) {
57044
+ if (/[\u0000\ufffc]|[\ud800-\udbff](?![\udc00-\udfff])|(?<![\ud800-\udbff])[\udc00-\udfff]/.test(text)) this.lost.add("invalid-character");
57045
+ appendText(builder, text, marks);
57046
+ }
57047
+ marks(marks, extra) {
57048
+ const result = [...extra];
57049
+ for (const mark of marks) {
57050
+ const type = richMarks[mark.type.name];
57051
+ if (!type) {
57052
+ if (mark.type.name === "highlight") this.lost.add("highlight");
57053
+ continue;
57054
+ }
57055
+ if (result.some((held) => held.type === type)) continue;
57056
+ if (type === "link") {
57057
+ const href = String(mark.attrs.href ?? "");
57058
+ if (linkProblem(href)) {
57059
+ this.lost.add("link");
57060
+ continue;
57061
+ }
57062
+ result.push({
57063
+ type: "link",
57064
+ href
57065
+ });
57066
+ } else result.push({ type });
57067
+ }
57068
+ if (result.some((mark) => mark.type === "link") && result.some((mark) => mark.type === "underline")) this.lost.add("link-underline");
57069
+ return canonicalMarks(result);
57070
+ }
57071
+ /** Appends a textblock's inline content. */
57072
+ inline(node, builder, extra = []) {
57073
+ node.forEach((child) => {
57074
+ const name = child.type.name;
57075
+ if (child.isText) this.append(builder, child.text, this.marks(child.marks, extra));
57076
+ else if (name === "hardbreak") builder.break();
57077
+ else if (name === "footnote_reference") {
57078
+ this.lost.add("footnote");
57079
+ this.append(builder, `[${String(child.attrs.label)}]`, this.marks(child.marks, extra));
57080
+ } else if (name === "inkkit_wiki_link") {
57081
+ this.lost.add("wiki-link");
57082
+ this.append(builder, String(child.attrs.label || child.attrs.target || child.attrs.raw || ""), this.marks(child.marks, extra));
57083
+ } else if (name === "image") this.lost.add(child.attrs.inkkitFileTarget || child.attrs.inkkitFileRaw ? "file" : "image");
57084
+ else if (name === "comment_inline") this.lost.add("comment");
57085
+ else if (name === "html") {
57086
+ this.lost.add("literal");
57087
+ this.append(builder, String(child.attrs.value ?? ""), this.marks(child.marks, extra));
57088
+ } else if (child.isInline) this.inline(child, builder, extra);
57089
+ });
57090
+ }
57091
+ paragraph(node, extra = []) {
57092
+ const builder = new InlineBuilder();
57093
+ this.inline(node, builder, extra);
57094
+ return {
57095
+ type: "paragraph",
57096
+ content: builder.finish()
57097
+ };
57098
+ }
57099
+ lines(text, marks) {
57100
+ return text.split(/\r\n|[\n\r]/).map((line) => {
57101
+ const builder = new InlineBuilder();
57102
+ this.append(builder, line, marks);
57103
+ return {
57104
+ type: "paragraph",
57105
+ content: builder.finish()
57106
+ };
57107
+ });
57108
+ }
57109
+ /** The blocks for `node`, `depth` lists deep. */
57110
+ blocks(node, depth) {
57111
+ switch (node.type.name) {
57112
+ case "paragraph": return [this.paragraph(node)];
57113
+ case "heading":
57114
+ this.lost.add("heading");
57115
+ return [this.paragraph(node, [{ type: "bold" }])];
57116
+ case "code_block":
57117
+ this.lost.add(String(node.attrs.language).toLowerCase() === "mermaid" ? "diagram" : "code-block");
57118
+ return this.lines(node.textContent, [{ type: "code" }]);
57119
+ case "literal_markdown":
57120
+ this.lost.add("literal");
57121
+ return this.lines(node.textContent, []);
57122
+ case "blockquote":
57123
+ case "inkkit_callout": {
57124
+ this.lost.add(node.type.name === "blockquote" ? "quote" : "callout");
57125
+ const blocks = [];
57126
+ const title = node.attrs.title;
57127
+ if (title) blocks.push(...this.lines(title, [{ type: "bold" }]));
57128
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57129
+ return blocks;
57130
+ }
57131
+ case "footnote_definition": {
57132
+ this.lost.add("footnote");
57133
+ const blocks = [];
57134
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57135
+ const label = `[${String(node.attrs.label)}] `;
57136
+ const first = blocks[0];
57137
+ const builder = new InlineBuilder();
57138
+ builder.text(first?.type === "paragraph" ? label : label.trim(), []);
57139
+ if (first?.type === "paragraph") {
57140
+ for (const inline of first.content) if (inline.type === "break") builder.break();
57141
+ else builder.text(inline.text, inline.marks ?? []);
57142
+ blocks[0] = {
57143
+ ...first,
57144
+ content: builder.finish()
57145
+ };
57146
+ } else blocks.unshift({
57147
+ type: "paragraph",
57148
+ content: builder.finish()
57149
+ });
57150
+ return blocks;
57151
+ }
57152
+ case "bullet_list":
57153
+ case "ordered_list": return [this.list(node, depth + 1)];
57154
+ case "table": return depth > 0 ? this.tableLines(node) : [this.table(node)];
57155
+ case "hr":
57156
+ this.lost.add("rule");
57157
+ return [];
57158
+ case "comment_block":
57159
+ this.lost.add("comment");
57160
+ return [];
57161
+ case "image_block":
57162
+ this.lost.add(node.attrs.inkkitFileTarget || node.attrs.inkkitFileRaw ? "file" : "image");
57163
+ return [];
57164
+ case "reference_definition": return [];
57165
+ default: {
57166
+ if (node.isTextblock) return [this.paragraph(node)];
57167
+ const blocks = [];
57168
+ node.forEach((child) => blocks.push(...this.blocks(child, depth)));
57169
+ return blocks;
57170
+ }
57171
+ }
57172
+ }
57173
+ table(node) {
57174
+ let aligned = false;
57175
+ const rows = [];
57176
+ node.forEach((row) => {
57177
+ const cells = [];
57178
+ row.forEach((cell) => {
57179
+ if (cell.attrs.alignment != null) aligned = true;
57180
+ const builder = new InlineBuilder();
57181
+ cell.forEach((child, _offset, index) => {
57182
+ if (index) builder.break();
57183
+ this.inline(child, builder);
57184
+ });
57185
+ cells.push({ content: builder.finish() });
57186
+ });
57187
+ rows.push(cells);
57188
+ });
57189
+ if (aligned) this.lost.add("table-alignment");
57190
+ return {
57191
+ type: "table",
57192
+ rows
57193
+ };
57194
+ }
57195
+ /** A table inside a list item, as lines of tab-separated cells. */
57196
+ tableLines(node) {
57197
+ return this.table(node).rows.map((row) => {
57198
+ const builder = new InlineBuilder();
57199
+ row.forEach((cell, index) => {
57200
+ if (index) builder.text(" ", []);
57201
+ for (const inline of cell.content) if (inline.type === "break") builder.break();
57202
+ else builder.text(inline.text, inline.marks ?? []);
57203
+ });
57204
+ return {
57205
+ type: "paragraph",
57206
+ content: builder.finish()
57207
+ };
57208
+ });
57209
+ }
57210
+ list(node, depth) {
57211
+ const ordered = node.type.name === "ordered_list";
57212
+ let start = Number(node.attrs.order ?? 1);
57213
+ if (ordered && !(Number.isSafeInteger(start) && start >= 1)) {
57214
+ this.lost.add("list-start");
57215
+ start = 1;
57216
+ } else if (ordered && start > 2147483647) {
57217
+ this.lost.add("list-start");
57218
+ start = maximumListStart;
57219
+ }
57220
+ const items = [];
57221
+ node.forEach((item) => {
57222
+ if (item.attrs.checked != null) this.lost.add("task");
57223
+ const builder = new InlineBuilder();
57224
+ let lines = 0;
57225
+ const nested = [];
57226
+ let flattened = false;
57227
+ const line = (content) => {
57228
+ if (lines++) builder.break();
57229
+ for (const inline of content) if (inline.type === "break") builder.break();
57230
+ else builder.text(inline.text, inline.marks ?? []);
57231
+ };
57232
+ const flatten = (block) => {
57233
+ if (block.type === "paragraph") line(block.content);
57234
+ else if (block.type === "list") for (const listItem of block.items) listItem.blocks.forEach(flatten);
57235
+ else for (const row of block.rows) for (const cell of row) line(cell.content);
57236
+ };
57237
+ item.forEach((child, _offset, index) => {
57238
+ if (isList(child)) {
57239
+ if (depth < 9) nested.push(...this.blocks(child, depth));
57240
+ else {
57241
+ this.lost.add("list-depth");
57242
+ this.blocks(child, depth).forEach(flatten);
57243
+ }
57244
+ return;
57245
+ }
57246
+ for (const block of this.blocks(child, depth)) {
57247
+ if (index === 0 && !lines && block.type === "paragraph") {
57248
+ line(block.content);
57249
+ continue;
57250
+ }
57251
+ flattened = true;
57252
+ flatten(block);
57253
+ }
57254
+ });
57255
+ if (flattened) this.lost.add("list-item-blocks");
57256
+ items.push({ blocks: [{
57257
+ type: "paragraph",
57258
+ content: builder.finish()
57259
+ }, ...nested] });
57260
+ });
57261
+ return ordered && start !== 1 ? {
57262
+ type: "list",
57263
+ ordered,
57264
+ start,
57265
+ items
57266
+ } : {
57267
+ type: "list",
57268
+ ordered,
57269
+ items
57270
+ };
57271
+ }
57272
+ };
57273
+ /** A formatted Markdown document as rich text, with what RTF could not hold. */
57274
+ function markdownToRichText(doc) {
57275
+ const reader = new MarkdownReader();
57276
+ const blocks = [];
57277
+ doc.forEach((node) => blocks.push(...reader.blocks(node, 0)));
57278
+ if (blocks.length === 1 && blocks[0].type === "paragraph" && !blocks[0].content.length) blocks.length = 0;
57279
+ return {
57280
+ document: {
57281
+ version: 1,
57282
+ blocks
57283
+ },
57284
+ lost: reader.lost.list()
57285
+ };
57286
+ }
57287
+ /** A rich text document as Markdown, with what Markdown could not hold: alignment. Underline,
57288
+ * superscript and subscript are written as `<u>`, `<sup>` and `<sub>`. */
57289
+ function richTextToMarkdown(ctx, document) {
57290
+ const lost = new Losses();
57291
+ const { schema } = ctx.get(editorViewCtx).state;
57292
+ const ready = markdownReady(document, lost);
57293
+ const text = serialize(ctx, htmlBreaksInCells(richTextToProse(schema, ready)));
57294
+ let reread;
57295
+ try {
57296
+ reread = markdownToRichText(ctx.get(parserCtx)(text)).document;
57297
+ } catch {
57298
+ reread = void 0;
57299
+ }
57300
+ if (!reread || !sameDocument(reread, ready)) throw new InkKitError("preservation", "The document could not be written as Markdown that reads back the same");
57301
+ return {
57302
+ text,
57303
+ lost: lost.list()
57304
+ };
57305
+ }
57306
+ /** Marks that show on a space: moving a space out of one changes what the reader sees. */
57307
+ var visibleOnSpace = /* @__PURE__ */ new Set([
57308
+ "underline",
57309
+ "strikethrough",
57310
+ "link"
57311
+ ]);
57312
+ /**
57313
+ * `content` as Markdown can write it. An emphasis, strikethrough, link or HTML mark can't start or
57314
+ * end with a space, so leading and trailing spaces move outside the run, and a run of only spaces
57315
+ * loses its marks. A code span keeps its spaces. Counts `mark-spaces` where a visible mark moved.
57316
+ */
57317
+ function markdownInlines(content, lost) {
57318
+ const builder = new InlineBuilder();
57319
+ for (const inline of content) {
57320
+ if (inline.type === "break") {
57321
+ builder.break();
57322
+ continue;
57323
+ }
57324
+ let marks = inline.marks ?? [];
57325
+ if (/^ +$/.test(inline.text) && marks.some((mark) => mark.type === "code")) {
57326
+ const kept = marks.filter((mark) => mark.type === "code");
57327
+ if (marks.some((mark) => mark.type !== "code" && visibleOnSpace.has(mark.type))) lost.add("mark-spaces");
57328
+ marks = kept;
57329
+ }
57330
+ const [, lead, core, trail] = /^( *)([^]*?)( *)$/.exec(inline.text);
57331
+ if (!marks.length || marks.some((mark) => mark.type === "code") || !lead && !trail) {
57332
+ builder.text(inline.text, marks);
57333
+ continue;
57334
+ }
57335
+ if (marks.some((mark) => visibleOnSpace.has(mark.type))) lost.add("mark-spaces");
57336
+ builder.text(lead, []);
57337
+ builder.text(core, marks);
57338
+ builder.text(trail, []);
57339
+ }
57340
+ return builder.finish();
57341
+ }
57342
+ /**
57343
+ * `document` as Markdown can hold it, with what that drops counted in `lost`: alignment, a mark's
57344
+ * edge spaces, a paragraph's only line break, which Markdown reads back as an empty paragraph, and
57345
+ * empty paragraphs at the end, which Markdown doesn't keep.
57346
+ */
57347
+ function markdownReady(document, lost) {
57348
+ const block = (item) => {
57349
+ if (item.type === "paragraph") {
57350
+ if (item.align) lost.add("alignment");
57351
+ let content = markdownInlines(item.content, lost);
57352
+ if (content.length === 1 && content[0].type === "break") {
57353
+ lost.add("empty-line");
57354
+ content = [];
57355
+ }
57356
+ return {
57357
+ type: "paragraph",
57358
+ content
57359
+ };
57360
+ }
57361
+ if (item.type === "list") return {
57362
+ ...item,
57363
+ items: item.items.map((listItem) => ({ blocks: listItem.blocks.map(block) }))
57364
+ };
57365
+ return {
57366
+ type: "table",
57367
+ rows: item.rows.map((row) => row.map((cell) => ({ content: markdownInlines(cell.content, lost) })))
57368
+ };
57369
+ };
57370
+ const blocks = document.blocks.map(block);
57371
+ while (blocks.length > 1 && blocks.at(-1).type === "paragraph" && !blocks.at(-1).content.length) {
57372
+ blocks.pop();
57373
+ lost.add("empty-line");
57374
+ }
57375
+ return {
57376
+ version: 1,
57377
+ blocks
57378
+ };
57379
+ }
57380
+ /** Whether two documents are the same, counting one empty paragraph as an empty document. */
57381
+ function sameDocument(a, b) {
57382
+ const text = (document) => JSON.stringify(document.blocks.length === 1 && document.blocks[0].type === "paragraph" && !document.blocks[0].content.length ? [] : document.blocks);
57383
+ return text(a) === text(b);
57384
+ }
57385
+ /** Markdown for content copied from a rich text document. Unlike a conversion it reports no
57386
+ * losses and isn't read back: a copy may lose alignment. */
57387
+ function richContentMarkdown(ctx, content) {
57388
+ const { schema } = ctx.get(editorViewCtx).state;
57389
+ const empty = schema.topNodeType.createAndFill();
57390
+ let doc = new Transform(empty).replace(0, empty.content.size, content).doc;
57391
+ try {
57392
+ doc = richTextToProse(schema, markdownReady(proseToRichText(doc, false), new Losses()));
57393
+ } catch {}
57394
+ return serialize(ctx, htmlBreaksInCells(doc));
57395
+ }
57396
+ /** `node` with each line break in a table cell written as `<br />`, since a cell is one line of
57397
+ * Markdown. */
57398
+ function htmlBreaksInCells(node, inCell = false) {
57399
+ if (node.type.name === "hardbreak") return inCell ? node.type.create({
57400
+ ...node.attrs,
57401
+ isHTML: true
57402
+ }, null, node.marks) : node;
57403
+ if (node.isLeaf) return node;
57404
+ const role = node.type.spec.tableRole;
57405
+ const cell = inCell || role === "cell" || role === "header_cell";
57406
+ const children = [];
57407
+ node.forEach((child) => children.push(htmlBreaksInCells(child, cell)));
57408
+ return node.copy(Fragment.fromArray(children));
57409
+ }
57410
+ //#endregion
55665
57411
  //#region src/paste-html.ts
55666
57412
  var rich = /* @__PURE__ */ new Set([
55667
57413
  "P",
@@ -56772,6 +58518,14 @@ var PasteController = class {
56772
58518
  transformPasted: (slice, view, plain) => this.claimed || plain || view.dragging ? slice : importableSlice(this.options.ctx(), slice),
56773
58519
  handlePaste: (view, event) => {
56774
58520
  this.claimed = false;
58521
+ if (this.options.richText?.() && event.clipboardData && view.editable) {
58522
+ const data = event.clipboardData;
58523
+ this.run({
58524
+ text: data.getData("text/plain"),
58525
+ html: data.getData("text/html") || void 0
58526
+ }).catch((error) => options.onError?.(error));
58527
+ return true;
58528
+ }
56775
58529
  const claim = claimsPaste(view, event);
56776
58530
  if (claim === false) return false;
56777
58531
  const data = event.clipboardData;
@@ -56876,6 +58630,17 @@ var PasteController = class {
56876
58630
  },
56877
58631
  controller: new AbortController()
56878
58632
  };
58633
+ const rich = this.options.richText?.();
58634
+ if (rich) {
58635
+ const anchor = operation;
58636
+ const state = view.state;
58637
+ 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());
58638
+ const tr = state.tr.replaceRange(anchor.from, anchor.to, slice);
58639
+ const end = tr.doc.resolve(tr.mapping.map(anchor.to, 1));
58640
+ this.dispatch(view, tr.setSelection(Selection.near(end, -1)).scrollIntoView());
58641
+ 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");
58642
+ return;
58643
+ }
56879
58644
  const inCode = () => view.state.doc.resolve(this.anchor(operation).from).parent.type.spec.code;
56880
58645
  this.active = operation;
56881
58646
  this.options.onPendingChanged?.();
@@ -57502,6 +59267,10 @@ var formatCommands = [
57502
59267
  "orderedList",
57503
59268
  "taskList",
57504
59269
  "toggleTask",
59270
+ "alignLeft",
59271
+ "alignCenter",
59272
+ "alignRight",
59273
+ "alignJustify",
57505
59274
  "link"
57506
59275
  ];
57507
59276
  /** Checks a format call's options against its command. */
@@ -57685,16 +59454,49 @@ function hostEvents(value) {
57685
59454
  };
57686
59455
  } });
57687
59456
  }
57688
- /** Checks a load's input. A format the host did not offer is refused like an unknown one. */
59457
+ /** Checks a load's input. A format the host did not offer is refused like an unknown one, and an
59458
+ * RTF document is checked and put in canonical form. */
57689
59459
  function documentInput(value, offered) {
57690
59460
  const input = expectRecord(value, "input");
59461
+ const documentId = expectString(input.documentId, "input.documentId");
59462
+ const generation = expectGeneration(input.generation, "input.generation");
59463
+ const format = expectOneOf(input.format, offered, "input.format");
59464
+ if (format === "rtf") {
59465
+ if (!isOmitted(input.text)) throw invalidArgument("input.text must be left out for an RTF document");
59466
+ return {
59467
+ documentId,
59468
+ generation,
59469
+ format,
59470
+ text: richTextText(readRichTextDocument(input.document, "input.document"))
59471
+ };
59472
+ }
59473
+ if (!isOmitted(input.document)) throw invalidArgument(`input.document must be left out for ${format}`);
57691
59474
  return {
57692
- documentId: expectString(input.documentId, "input.documentId"),
57693
- generation: expectGeneration(input.generation, "input.generation"),
57694
- format: expectOneOf(input.format, offered, "input.format"),
59475
+ documentId,
59476
+ generation,
59477
+ format,
57695
59478
  text: expectString(input.text, "input.text")
57696
59479
  };
57697
59480
  }
59481
+ /** The public snapshot of the editor's text for a document. */
59482
+ function publicSnapshot(saved) {
59483
+ const { documentId, generation, revision, format, text, dirty } = saved;
59484
+ return format === "rtf" ? {
59485
+ documentId,
59486
+ generation,
59487
+ revision,
59488
+ format,
59489
+ document: richTextFromText(text),
59490
+ dirty
59491
+ } : {
59492
+ documentId,
59493
+ generation,
59494
+ revision,
59495
+ format,
59496
+ text,
59497
+ dirty
59498
+ };
59499
+ }
57698
59500
  /** A range's shape. Whether its offsets fit the snapshot is `invalid-range`, checked later. */
57699
59501
  function textRange(value, name) {
57700
59502
  const range = expectRecord(value, name);
@@ -57818,6 +59620,7 @@ var InkKitEditor = class InkKitEditor {
57818
59620
  output = new EditorOutput({
57819
59621
  ctx: () => this.editor.ctx,
57820
59622
  exportDocument: (text) => this.document.exportDocument(text),
59623
+ richTextMarkdown: (content) => richContentMarkdown(this.editor.ctx, content),
57821
59624
  diagrams: () => this.diagrams,
57822
59625
  root: () => this.root,
57823
59626
  handler: () => this.document.handler,
@@ -57837,7 +59640,7 @@ var InkKitEditor = class InkKitEditor {
57837
59640
  element: () => this.source.element,
57838
59641
  replaceSelection: (text) => this.source.replaceLiteralSelection(text),
57839
59642
  writable: () => this.writable,
57840
- snapshot: () => this.snapshot(),
59643
+ snapshot: () => this.savedText(),
57841
59644
  prepareSelection: () => this.clipboardSnapshot("selection"),
57842
59645
  capture: () => this.session.capture(),
57843
59646
  assertCut: (captured) => this.assertCut(captured),
@@ -57917,7 +59720,7 @@ var InkKitEditor = class InkKitEditor {
57917
59720
  };
57918
59721
  } });
57919
59722
  this.linkSchemes = new Set(options.linkSchemes);
57920
- this.offered = options.formats ?? documentFormats;
59723
+ this.offered = options.formats ?? defaultFormats;
57921
59724
  this.formatList = Object.freeze(this.offered.map((format) => Object.freeze({
57922
59725
  format,
57923
59726
  promise: formatHandlers[format].promise
@@ -57934,6 +59737,9 @@ var InkKitEditor = class InkKitEditor {
57934
59737
  }
57935
59738
  /** What `changed` reports for the document's new text. */
57936
59739
  changeSnapshot(text) {
59740
+ return publicSnapshot(this.saved(text));
59741
+ }
59742
+ saved(text) {
57937
59743
  return {
57938
59744
  ...this.session.version(),
57939
59745
  format: this.document.format,
@@ -57941,6 +59747,11 @@ var InkKitEditor = class InkKitEditor {
57941
59747
  dirty: text !== this.document.original
57942
59748
  };
57943
59749
  }
59750
+ /** The current text, as `snapshot` checks and reads it. */
59751
+ savedText(expectedGeneration) {
59752
+ this.assertCurrent(expectedGeneration);
59753
+ return this.saved(this.document.currentText());
59754
+ }
57944
59755
  /** Throws unless the document `captured` describes is still current, alive, not composing and
57945
59756
  * has no import pending. */
57946
59757
  assertCaptured(captured, aborted = false) {
@@ -58146,7 +59957,7 @@ var InkKitEditor = class InkKitEditor {
58146
59957
  this.publishCommandState();
58147
59958
  }
58148
59959
  applyInputPolicy() {
58149
- applyInputPolicy(this.editor.ctx.get(editorViewCtx), this.source.element, this.inputPreferences, this.options.labels?.formattedEditor ?? defaultLabels.formattedEditor, {
59960
+ applyInputPolicy(this.editor.ctx.get(editorViewCtx), this.source.element, this.inputPreferences, this.options.labels?.[this.document.handler.formattedLabel] ?? defaultLabels[this.document.handler.formattedLabel], {
58150
59961
  writable: () => this.writable,
58151
59962
  literal: () => this.literalSurface
58152
59963
  });
@@ -58163,8 +59974,16 @@ var InkKitEditor = class InkKitEditor {
58163
59974
  const { state } = this.editor.ctx.get(editorViewCtx);
58164
59975
  const mutable = this.writable && !this.composing && !this.pasteController.pending;
58165
59976
  const formatted = mutable && !this.literalSurface;
58166
- const format = formatAvailability(this.editor, state, formatted);
59977
+ const rich = this.document.handler.richText != null;
59978
+ const format = formatAvailability(this.editor, state, formatted, this.document.handler);
58167
59979
  const tables = formatted ? tableAvailability(this.editor.ctx) : Object.fromEntries(tableCommands.map((name) => [name, false]));
59980
+ if (rich) tables.alignLeft = tables.alignCenter = tables.alignRight = false;
59981
+ const caret = this.literalSurface ? {
59982
+ marks: [],
59983
+ block: { type: "paragraph" },
59984
+ quoted: false
59985
+ } : caretState(state);
59986
+ if (rich) caret.align = caretAlignment(state);
58168
59987
  return {
58169
59988
  ...this.session.version(),
58170
59989
  format: this.document.format,
@@ -58172,11 +59991,7 @@ var InkKitEditor = class InkKitEditor {
58172
59991
  editable: this.writable,
58173
59992
  composing: this.composing,
58174
59993
  pending: this.pasteController.pending,
58175
- caret: this.literalSurface ? {
58176
- marks: [],
58177
- block: { type: "paragraph" },
58178
- quoted: false
58179
- } : caretState(state),
59994
+ caret,
58180
59995
  table: this.literalSurface ? void 0 : tableContext(state),
58181
59996
  commands: {
58182
59997
  undo: mutable && this.history.canUndo(state),
@@ -58184,12 +59999,12 @@ var InkKitEditor = class InkKitEditor {
58184
59999
  insertText: mutable,
58185
60000
  paste: mutable,
58186
60001
  replace: mutable,
58187
- replaceSource: mutable,
58188
- insertImages: formatted && this.canInsertImages,
58189
- appendImages: formatted && this.canInsertImages,
60002
+ replaceSource: mutable && !rich,
60003
+ insertImages: formatted && !rich && this.canInsertImages,
60004
+ appendImages: formatted && !rich && this.canInsertImages,
58190
60005
  insertPaths: mutable,
58191
- insertFootnote: formatted,
58192
- editReferenceDefinition: formatted && referenceDefinitions(state.doc).size > 0,
60006
+ insertFootnote: formatted && !rich,
60007
+ editReferenceDefinition: formatted && !rich && referenceDefinitions(state.doc).size > 0,
58193
60008
  format,
58194
60009
  table: tables,
58195
60010
  moveBlockUp: formatted && canMoveBlock(state, "up"),
@@ -58197,9 +60012,10 @@ var InkKitEditor = class InkKitEditor {
58197
60012
  }
58198
60013
  };
58199
60014
  }
58200
- /** An image adapter shows and exports an inserted reference, and so does a file adapter. */
60015
+ /** An image adapter shows and exports an inserted reference, and so does a file adapter. An RTF
60016
+ * document holds no images. */
58201
60017
  get canInsertImages() {
58202
- return !!this.options.images || !!this.options.files;
60018
+ return (!!this.options.images || !!this.options.files) && !this.document.handler.richText;
58203
60019
  }
58204
60020
  historyInputPlugin = createHistoryInputPlugin({
58205
60021
  writable: () => this.writable,
@@ -58238,7 +60054,7 @@ var InkKitEditor = class InkKitEditor {
58238
60054
  this.history.settleSource();
58239
60055
  this.history.modeChanged();
58240
60056
  },
58241
- text: () => this.snapshot().text,
60057
+ text: () => this.savedText().text,
58242
60058
  changed: () => {
58243
60059
  this.mode = mode;
58244
60060
  this.session.modeChanged();
@@ -58270,6 +60086,7 @@ var InkKitEditor = class InkKitEditor {
58270
60086
  this.assertAlive();
58271
60087
  expectString(text, "text");
58272
60088
  this.assertMutation(expectedGeneration);
60089
+ if (this.document.handler.richText) throw invalidArgument("An RTF document has no source to replace");
58273
60090
  const changed = this.history.applySource(text, true);
58274
60091
  if (changed) this.clipboard.clear();
58275
60092
  return changed;
@@ -58284,22 +60101,52 @@ var InkKitEditor = class InkKitEditor {
58284
60101
  this.assertMutation(expectedGeneration);
58285
60102
  return this.history.redo();
58286
60103
  }
58287
- /** The complete current text, which is what a host saves. It throws rather than return partial
58288
- * text. */
60104
+ /** The complete current document, which is what a host saves. It throws rather than return a
60105
+ * partial document. */
58289
60106
  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
60107
+ return publicSnapshot(this.savedText(expectedGeneration));
60108
+ }
60109
+ /**
60110
+ * The loaded document converted to `format`, which must be offered, with what the conversion
60111
+ * lost. It changes nothing: load the result as a new generation. Markdown converts to RTF and
60112
+ * RTF to Markdown; the same format returns a copy. Throws `invalid-argument` for any other pair.
60113
+ */
60114
+ convertDocument(format, expectedGeneration) {
60115
+ this.assertAlive();
60116
+ expectOneOf(format, this.offered, "format");
60117
+ const saved = this.savedText(expectedGeneration);
60118
+ const from = saved.format;
60119
+ if (from === format) return from === "rtf" ? {
60120
+ format: "rtf",
60121
+ document: richTextFromText(saved.text),
60122
+ lost: Object.freeze([])
60123
+ } : {
60124
+ format: from,
60125
+ text: saved.text,
60126
+ lost: Object.freeze([])
58297
60127
  };
60128
+ if (from === "md" && format === "rtf") {
60129
+ const { document, lost } = markdownToRichText(this.document.exportDocument(saved.text));
60130
+ return {
60131
+ format,
60132
+ document,
60133
+ lost
60134
+ };
60135
+ }
60136
+ if (from === "rtf" && format === "md") {
60137
+ const { text, lost } = richTextToMarkdown(this.editor.ctx, richTextFromText(saved.text));
60138
+ return {
60139
+ format,
60140
+ text,
60141
+ lost
60142
+ };
60143
+ }
60144
+ throw invalidArgument(`Inkkit does not convert ${from} to ${format}; ${from === "txt" || format === "txt" ? "the host converts plain text" : "convert through another format"}`);
58298
60145
  }
58299
60146
  /** The whole document as standalone HTML for printing. Rejects rather than return partial
58300
60147
  * output. */
58301
60148
  async printableSnapshot(expectedGeneration) {
58302
- const snapshot = this.snapshot(expectedGeneration);
60149
+ const snapshot = this.savedText(expectedGeneration);
58303
60150
  const captured = this.session.capture();
58304
60151
  return this.output.printable(snapshot, captured);
58305
60152
  }
@@ -58308,7 +60155,7 @@ var InkKitEditor = class InkKitEditor {
58308
60155
  async clipboardSnapshot(scope, expectedGeneration) {
58309
60156
  this.assertAlive();
58310
60157
  const all = (isOmitted(scope) ? "document" : expectOneOf(scope, clipboardScopes, "scope")) === "document";
58311
- const snapshot = this.snapshot(expectedGeneration);
60158
+ const snapshot = this.savedText(expectedGeneration);
58312
60159
  const captured = this.session.capture();
58313
60160
  return this.output.clipboard(snapshot, captured, all);
58314
60161
  }
@@ -58342,12 +60189,25 @@ var InkKitEditor = class InkKitEditor {
58342
60189
  expectString(text, "text");
58343
60190
  this.assertMutation(expectedGeneration);
58344
60191
  const before = this.session.capture();
60192
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
58345
60193
  this.insertPlainText(text);
60194
+ if (text && this.refusedRichEdit(doc)) throw new InkKitError("preservation", "The text was not pasted because the RTF document cannot hold it there");
58346
60195
  return !before.isCurrent();
58347
60196
  }
60197
+ /** Whether an RTF edit that should have changed `before` was refused. */
60198
+ refusedRichEdit(before) {
60199
+ return this.document.handler.richText != null && !this.literalSurface && this.editor.ctx.get(editorViewCtx).state.doc === before;
60200
+ }
58348
60201
  insertPlainText(text) {
58349
60202
  if (this.literalSurface) this.source.replaceLiteralSelection(text, true, this.document.handler.keepsInsertedLineEndings);
58350
- else insertPlainText(this.editor.ctx.get(editorViewCtx), text);
60203
+ else if (this.document.handler.richText) {
60204
+ const view = this.editor.ctx.get(editorViewCtx);
60205
+ const slice = this.document.handler.richText.paste({ text }, view.state, this.linkSchemes);
60206
+ const lines = /[\r\n\u000c\u0085\u2028\u2029\u000b]/.test(text);
60207
+ const tr = view.state.tr.replaceSelection(slice).scrollIntoView();
60208
+ view.dispatch(lines ? closeHistory(tr) : tr);
60209
+ if (lines) view.dispatch(closeHistory(view.state.tr).setMeta("addToHistory", false));
60210
+ } else insertPlainText(this.editor.ctx.get(editorViewCtx), text);
58351
60211
  }
58352
60212
  /** Runs a table command and returns whether it applied. False in source and TXT modes, outside a
58353
60213
  * table for commands that need one, and at a boundary a move can't cross. */
@@ -58421,15 +60281,13 @@ var InkKitEditor = class InkKitEditor {
58421
60281
  }
58422
60282
  } catch (error) {
58423
60283
  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 }));
60284
+ 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
60285
  return;
58426
60286
  }
58427
60287
  }
58428
60288
  this.history.didApply(view.state, next);
58429
60289
  view.updateState(next);
58430
60290
  }
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
60291
  static async mount(root, events, options) {
58434
60292
  if (!(root instanceof HTMLElement)) throw invalidArgument("root must be an HTML element");
58435
60293
  events = hostEvents(events);
@@ -58448,7 +60306,9 @@ var InkKitEditor = class InkKitEditor {
58448
60306
  commit: (apply) => instance.facadeCall(apply),
58449
60307
  literalText: () => instance.literalSurface,
58450
60308
  editable: () => instance.writable && !instance.literalSurface,
58451
- onPendingChanged: () => instance.publishCommandState()
60309
+ onPendingChanged: () => instance.publishCommandState(),
60310
+ richText: () => instance.document.handler.richText,
60311
+ linkSchemes: instance.linkSchemes
58452
60312
  });
58453
60313
  instance.suggestions = createEditorSuggestions({
58454
60314
  root,
@@ -58476,6 +60336,10 @@ var InkKitEditor = class InkKitEditor {
58476
60336
  historyInput: instance.historyInputPlugin,
58477
60337
  policy: instance.policyPlugin
58478
60338
  },
60339
+ format: {
60340
+ markdownInput: () => instance.document.handler.markdownInput,
60341
+ richText: () => instance.document.handler.richText
60342
+ },
58479
60343
  authoredSources: (doc, positions) => instance.document.authoredSources(doc, positions),
58480
60344
  smartPunctuation: () => instance.inputPreferences.smartPunctuation === true,
58481
60345
  linkSchemes: instance.linkSchemes,
@@ -58537,9 +60401,9 @@ var InkKitEditor = class InkKitEditor {
58537
60401
  }
58538
60402
  /** Replaces the document. The generation must be greater than the loaded one. A failed load
58539
60403
  * changes nothing. */
58540
- loadDocument(input) {
60404
+ loadDocument(document) {
58541
60405
  this.assertAlive();
58542
- input = documentInput(input, this.offered);
60406
+ const input = documentInput(document, this.offered);
58543
60407
  this.session.checkLoad(input.generation, "input.generation");
58544
60408
  const handler = formatHandlers[input.format];
58545
60409
  const markdown = handler.literalOnly ? "" : input.text;
@@ -58547,7 +60411,7 @@ var InkKitEditor = class InkKitEditor {
58547
60411
  try {
58548
60412
  source = handler.load(this.editor.ctx, markdown);
58549
60413
  } 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 });
60414
+ throw coded(error, "preservation", "The document was not loaded because its Markdown could not be shown without losing part of it");
58551
60415
  }
58552
60416
  const container = this.geometry.container;
58553
60417
  const top = container.scrollTop, left = container.scrollLeft;
@@ -58564,7 +60428,7 @@ var InkKitEditor = class InkKitEditor {
58564
60428
  this.source.reset();
58565
60429
  this.search.reset();
58566
60430
  this.document.handler = handler;
58567
- this.source.setSourceValue(input.text);
60431
+ this.source.setSourceValue(handler.richText ? "" : input.text);
58568
60432
  this.source.element.setSelectionRange(this.source.element.value.length, this.source.element.value.length);
58569
60433
  this.load(markdown, source);
58570
60434
  this.document.original = input.text;
@@ -58583,9 +60447,9 @@ var InkKitEditor = class InkKitEditor {
58583
60447
  /** Replaces the document with a newer version of it, keeping the selection, focus, scroll
58584
60448
  * position and Markdown editing mode. With `expectedGeneration`, the loaded document must be
58585
60449
  * that generation. */
58586
- reloadDocument(input, expectedGeneration) {
60450
+ reloadDocument(document, expectedGeneration) {
58587
60451
  this.assertAlive();
58588
- input = documentInput(input, this.offered);
60452
+ const input = documentInput(document, this.offered);
58589
60453
  if (!isOmitted(expectedGeneration)) this.assertReady(expectedGeneration);
58590
60454
  this.session.checkLoad(input.generation, "input.generation");
58591
60455
  const view = this.editor.ctx.get(editorViewCtx);
@@ -58595,7 +60459,7 @@ var InkKitEditor = class InkKitEditor {
58595
60459
  const saved = captureReloadSurface(view, this.source.element, this.geometry.container, wasLiteral);
58596
60460
  this.reportingSuppressed = true;
58597
60461
  try {
58598
- this.loadDocument(input);
60462
+ this.loadDocument(document);
58599
60463
  if (previous.sourceMode && this.document.handler.sourceMode && previousMode === "source") this.setEditingMode("source", input.generation);
58600
60464
  restoreReloadSurface(view, this.source.element, () => this.literalSurface, saved);
58601
60465
  } finally {
@@ -58730,7 +60594,8 @@ var InkKitEditor = class InkKitEditor {
58730
60594
  expectString(search, "search");
58731
60595
  expectString(replacement, "replacement");
58732
60596
  this.assertMutation(expectedGeneration);
58733
- return this.search.replace(search, replacement, false) > 0;
60597
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60598
+ return this.search.replace(search, replacement, false) > 0 && (replacement === search || !this.refusedRichEdit(doc));
58734
60599
  }
58735
60600
  /** Replaces every non-overlapping match once and returns how many. */
58736
60601
  replaceAll(search, replacement, expectedGeneration) {
@@ -58738,7 +60603,9 @@ var InkKitEditor = class InkKitEditor {
58738
60603
  expectString(search, "search");
58739
60604
  expectString(replacement, "replacement");
58740
60605
  this.assertMutation(expectedGeneration);
58741
- return this.search.replace(search, replacement, true);
60606
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60607
+ const count = this.search.replace(search, replacement, true);
60608
+ return count && replacement !== search && this.refusedRichEdit(doc) ? 0 : count;
58742
60609
  }
58743
60610
  outlineContext() {
58744
60611
  const { documentId, generation, revision, key } = this.session.capture();
@@ -58781,11 +60648,10 @@ var InkKitEditor = class InkKitEditor {
58781
60648
  this.source.replaceLiteralSelection(text, false);
58782
60649
  return true;
58783
60650
  }
58784
- if (/[\r\n]/.test(text)) {
58785
- this.pasteAsPlainText(text);
58786
- return true;
58787
- }
58788
- return insertNativeText(this.editor.ctx.get(editorViewCtx), text);
60651
+ const doc = this.editor.ctx.get(editorViewCtx).state.doc;
60652
+ if (/[\r\n]/.test(text)) this.insertPlainText(text);
60653
+ else if (!insertNativeText(this.editor.ctx.get(editorViewCtx), text)) return false;
60654
+ return !(text && this.refusedRichEdit(doc));
58789
60655
  }
58790
60656
  /**
58791
60657
  * A key the host's native input received, run through InkKit's key handling. Returns whether
@@ -58964,12 +60830,14 @@ var InkKitEditor = class InkKitEditor {
58964
60830
  format(command, options, expectedGeneration) {
58965
60831
  this.assertAlive();
58966
60832
  const checked = formatOptions(command, options);
60833
+ if (checked?.destination !== void 0 && this.document.handler.richText) checked.destination = checked.destination.trim();
58967
60834
  if (checked?.destination !== void 0) this.checkDestination(checked.destination, "options.destination");
58968
60835
  this.assertMutation(expectedGeneration);
58969
60836
  if (this.literalSurface) return false;
58970
60837
  const view = this.editor.ctx.get(editorViewCtx);
58971
60838
  const before = view.state;
58972
- applyFormat(this.editor, command, checked, (destination) => this.link(destination));
60839
+ if ((this.document.handler.richText || command.startsWith("align")) && !this.commandState().commands.format[command]) return false;
60840
+ applyFormat(this.editor, command, checked, (destination) => this.link(destination), this.document.handler.richText != null);
58973
60841
  this.focus();
58974
60842
  const after = view.state;
58975
60843
  const marks = (state) => state.storedMarks ?? state.selection.$from.marks();
@@ -58977,11 +60845,14 @@ var InkKitEditor = class InkKitEditor {
58977
60845
  }
58978
60846
  /**
58979
60847
  * 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.
60848
+ * or a scheme the host listed, or, in Markdown, that the writer cannot save so that it reads back
60849
+ * the same. In RTF, it also refuses what the rich text link rule refuses.
58982
60850
  */
58983
60851
  checkDestination(destination, name) {
58984
- checkDestination(this.editor.ctx, destination, name, this.linkSchemes);
60852
+ const rich = this.document.handler.richText != null;
60853
+ checkDestination(this.editor.ctx, destination, name, this.linkSchemes, !rich);
60854
+ const problem = rich ? linkProblem(destination) : void 0;
60855
+ if (problem) throw invalidArgument(`${name}: ${problem}`);
58985
60856
  }
58986
60857
  /**
58987
60858
  * With a destination, sets the link: on the whole link that holds the caret or the selection,
@@ -59000,7 +60871,7 @@ var InkKitEditor = class InkKitEditor {
59000
60871
  this.checkDestination(expectString(destination, "destination"), "destination");
59001
60872
  optionalString(title, "title");
59002
60873
  this.assertMutation(expectedGeneration);
59003
- if (this.literalSurface) return false;
60874
+ if (this.literalSurface || this.document.handler.richText) return false;
59004
60875
  return editReferenceDefinition(this.editor.ctx.get(editorViewCtx), label, destination, title);
59005
60876
  }
59006
60877
  /** Inserts a footnote reference, and a definition when the label has none, and returns whether
@@ -59010,8 +60881,8 @@ var InkKitEditor = class InkKitEditor {
59010
60881
  const given = optionalString(label, "label");
59011
60882
  if (given !== void 0 && !footnoteLabel(given)) throw invalidArgument("A footnote label must be 1 to 999 characters without whitespace, brackets or backslashes");
59012
60883
  this.assertMutation(expectedGeneration);
59013
- if (this.literalSurface) return false;
59014
- insertFootnote(this.editor.ctx.get(editorViewCtx), label, () => this.snapshot().text);
60884
+ if (this.literalSurface || this.document.handler.richText) return false;
60885
+ insertFootnote(this.editor.ctx.get(editorViewCtx), label, () => this.savedText().text);
59015
60886
  this.focus();
59016
60887
  return true;
59017
60888
  }
@@ -59031,6 +60902,7 @@ var InkKitEditor = class InkKitEditor {
59031
60902
  "loadDocument",
59032
60903
  "reloadDocument",
59033
60904
  "snapshot",
60905
+ "convertDocument",
59034
60906
  "printableSnapshot",
59035
60907
  "clipboardSnapshot",
59036
60908
  "paste",
@@ -59094,13 +60966,13 @@ var InkKitEditor = class InkKitEditor {
59094
60966
  });
59095
60967
  }
59096
60968
  const mount = InkKitEditor.mount;
59097
- InkKitEditor.mount = async (...args) => {
60969
+ InkKitEditor.mount = (async (...args) => {
59098
60970
  try {
59099
60971
  return await mount(...args);
59100
60972
  } catch (error) {
59101
60973
  throw coded(error);
59102
60974
  }
59103
- };
60975
+ });
59104
60976
  }
59105
60977
  };
59106
60978
  //#endregion