@meowdown/core 0.68.3 → 0.70.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.
- package/dist/index.d.ts +51 -62
- package/dist/index.js +288 -238
- package/dist/style.css +454 -20
- package/package.json +8 -2
package/dist/index.js
CHANGED
|
@@ -40,6 +40,12 @@ import { normalizeIdentifier } from "micromark-util-normalize-identifier";
|
|
|
40
40
|
import { createListRenderingPlugin, createSafariInputMethodWorkaroundPlugin, createToggleCollapsedCommand, defaultAttributesGetter, enterCommand, findCheckboxInListItem, handleListMarkerMouseDown, joinListElements, listToDOM, unwrapListSlice, wrappingListInputRule } from "prosemirror-flat-list";
|
|
41
41
|
import { defineTableCellSpec, defineTableCommands, defineTableDropIndicator, defineTableEditingPlugin, defineTableHeaderCellSpec, defineTableRowSpec, defineTableSpec, deleteTable, isCellSelection } from "@prosekit/extensions/table";
|
|
42
42
|
import { closeHistory } from "@prosekit/pm/history";
|
|
43
|
+
import { fromSyndication } from "@post-embed/exporter/x/syndication";
|
|
44
|
+
import { XPostSchema, YouTubeVideoSchema } from "@post-embed/schema";
|
|
45
|
+
import { createLRU } from "lru.min";
|
|
46
|
+
import * as v from "valibot";
|
|
47
|
+
import { registerXPost } from "@post-embed/elements/x";
|
|
48
|
+
import { registerYouTubeVideo } from "@post-embed/elements/youtube";
|
|
43
49
|
import { registerResizableHandleElement, registerResizableRootElement } from "@prosekit/web/resizable";
|
|
44
50
|
import { InputRule } from "@prosekit/pm/inputrules";
|
|
45
51
|
|
|
@@ -1318,7 +1324,8 @@ function definePlainTextPaste() {
|
|
|
1318
1324
|
function docToMarkdown(node, options = {}) {
|
|
1319
1325
|
const out = new MdOut();
|
|
1320
1326
|
if (options.frontmatter) emitFrontmatter(node.attrs.frontmatter, out);
|
|
1321
|
-
|
|
1327
|
+
const child = node.childCount === 1 ? node.child(0) : void 0;
|
|
1328
|
+
if (!(child && isNodeOfType(child, "paragraph") && child.childCount === 0)) emit$1(node, out);
|
|
1322
1329
|
return out.finish();
|
|
1323
1330
|
}
|
|
1324
1331
|
/**
|
|
@@ -1373,6 +1380,7 @@ var MdOut = class {
|
|
|
1373
1380
|
this.pendingFirst = null;
|
|
1374
1381
|
this.atLineStart = true;
|
|
1375
1382
|
this.deferredBlankPrefix = null;
|
|
1383
|
+
this.contentEnd = 0;
|
|
1376
1384
|
}
|
|
1377
1385
|
/**
|
|
1378
1386
|
* Write `text`, opening each embedded line with the current line prefix.
|
|
@@ -1407,20 +1415,30 @@ var MdOut = class {
|
|
|
1407
1415
|
}
|
|
1408
1416
|
}
|
|
1409
1417
|
/**
|
|
1410
|
-
* End a block that owns no line of its own (an empty paragraph):
|
|
1411
|
-
* blank line
|
|
1412
|
-
* one,
|
|
1413
|
-
*
|
|
1414
|
-
*
|
|
1415
|
-
* blank line
|
|
1416
|
-
*
|
|
1418
|
+
* End a block that owns no line of its own (an empty paragraph): it is one
|
|
1419
|
+
* blank line. After another block, the blank line that block owes is this
|
|
1420
|
+
* one, and the next block is owed a fresh one, so each empty block adds a
|
|
1421
|
+
* blank line to the separator. With nothing owed (the start of the output)
|
|
1422
|
+
* the line is written directly and nothing is owed after it: a leading empty
|
|
1423
|
+
* block is a blank line, not a separator. A marker-bearing block (an empty
|
|
1424
|
+
* list item's `- `, an empty quote line's `>`) writes its marker as the line.
|
|
1417
1425
|
*/
|
|
1418
1426
|
closeEmptyBlock() {
|
|
1419
|
-
if (!this.atLineStart
|
|
1427
|
+
if (!this.atLineStart) {
|
|
1420
1428
|
this.closeBlock();
|
|
1421
1429
|
return;
|
|
1422
1430
|
}
|
|
1423
|
-
if (this.
|
|
1431
|
+
if (this.pendingFirst !== null) {
|
|
1432
|
+
this.emitDeferredBlankLine();
|
|
1433
|
+
const marker = this.pendingFirst;
|
|
1434
|
+
this.parts.push(marker.endsWith("] ") ? marker : marker.trimEnd(), "\n");
|
|
1435
|
+
this.pendingFirst = null;
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (this.deferredBlankPrefix === null) {
|
|
1439
|
+
this.parts.push(this.linePrefix.trimEnd(), "\n");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1424
1442
|
this.emitDeferredBlankLine();
|
|
1425
1443
|
this.deferredBlankPrefix = this.linePrefix;
|
|
1426
1444
|
}
|
|
@@ -1435,7 +1453,10 @@ var MdOut = class {
|
|
|
1435
1453
|
this.pendingFirst = null;
|
|
1436
1454
|
this.atLineStart = false;
|
|
1437
1455
|
}
|
|
1438
|
-
if (!this.atLineStart)
|
|
1456
|
+
if (!this.atLineStart) {
|
|
1457
|
+
this.parts.push("\n");
|
|
1458
|
+
this.contentEnd = this.parts.length;
|
|
1459
|
+
}
|
|
1439
1460
|
this.atLineStart = true;
|
|
1440
1461
|
this.deferredBlankPrefix = this.linePrefix;
|
|
1441
1462
|
}
|
|
@@ -1486,14 +1507,14 @@ var MdOut = class {
|
|
|
1486
1507
|
this.pendingFirst = firstLine !== null ? null : savedFirst;
|
|
1487
1508
|
}
|
|
1488
1509
|
finish() {
|
|
1489
|
-
const text = this.parts.join("");
|
|
1510
|
+
const text = this.parts.slice(0, this.contentEnd).join("");
|
|
1490
1511
|
let cut = text.length;
|
|
1491
1512
|
for (let i = text.length - 1; i >= 0; i--) {
|
|
1492
1513
|
const code = text.charCodeAt(i);
|
|
1493
1514
|
if (code === 10) cut = i;
|
|
1494
1515
|
else if (code !== 32 && code !== 9) break;
|
|
1495
1516
|
}
|
|
1496
|
-
return text.slice(0, cut) + "\n";
|
|
1517
|
+
return (cut === 0 ? "" : text.slice(0, cut) + "\n") + this.parts.slice(this.contentEnd).join("") || "\n";
|
|
1497
1518
|
}
|
|
1498
1519
|
emitDeferredBlankLine() {
|
|
1499
1520
|
const prefix = this.deferredBlankPrefix;
|
|
@@ -2829,8 +2850,8 @@ const emptyBatchSetMarkStep = new BatchSetMarkStep([]);
|
|
|
2829
2850
|
|
|
2830
2851
|
//#endregion
|
|
2831
2852
|
//#region src/extensions/magic-comment.ts
|
|
2832
|
-
const MAGIC_COMMENT_RE = /^<!--\s*(\{[
|
|
2833
|
-
const TRAILING_MAGIC_COMMENT_RE = /(?:<!--\s*\{[
|
|
2853
|
+
const MAGIC_COMMENT_RE = /^<!--\s*(\{[\s\S]*?\})\s*-->/;
|
|
2854
|
+
const TRAILING_MAGIC_COMMENT_RE = /(?:<!--\s*\{[\s\S]*?\}\s*-->)+$/;
|
|
2834
2855
|
/**
|
|
2835
2856
|
* Read the metadata out of a `<!-- {...} -->` comment, or `undefined` when the
|
|
2836
2857
|
* text is not a comment carrying at least one recognized field.
|
|
@@ -2848,21 +2869,28 @@ function parseMagicComment(comment) {
|
|
|
2848
2869
|
const width = toPositiveNumber(data.width);
|
|
2849
2870
|
const height = toPositiveNumber(data.height);
|
|
2850
2871
|
const noLink = data.noLink === true ? true : void 0;
|
|
2851
|
-
|
|
2872
|
+
const snapshot = isObject(data.snapshot) && !Array.isArray(data.snapshot) ? data.snapshot : void 0;
|
|
2873
|
+
if (!width && !height && !noLink && !snapshot) return;
|
|
2852
2874
|
return {
|
|
2853
2875
|
width,
|
|
2854
2876
|
height,
|
|
2855
|
-
noLink
|
|
2877
|
+
noLink,
|
|
2878
|
+
snapshot
|
|
2856
2879
|
};
|
|
2857
2880
|
}
|
|
2858
2881
|
function toPositiveNumber(value) {
|
|
2859
2882
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.round(value);
|
|
2860
2883
|
}
|
|
2861
2884
|
/**
|
|
2862
|
-
* The canonical comment meowdown writes for the metadata.
|
|
2885
|
+
* The canonical comment meowdown writes for the metadata. `--` inside a JSON
|
|
2886
|
+
* string becomes `--`: the inline comment rule of `@lezer/markdown`
|
|
2887
|
+
* (CommonMark 0.30) ends a comment at any `--`, so a snapshot text holding a
|
|
2888
|
+
* double dash would otherwise cut the comment short. Outside strings JSON
|
|
2889
|
+
* only writes `-` as a number sign, never doubled, so the escape keeps the
|
|
2890
|
+
* JSON valid and `JSON.parse` restores the text.
|
|
2863
2891
|
*/
|
|
2864
2892
|
function formatMagicComment(magic) {
|
|
2865
|
-
return `<!-- ${JSON.stringify(magic)} -->`;
|
|
2893
|
+
return `<!-- ${JSON.stringify(magic).replaceAll("--", String.raw`-\u002d`)} -->`;
|
|
2866
2894
|
}
|
|
2867
2895
|
/**
|
|
2868
2896
|
* Drop the trailing run of magic comments from the source text.
|
|
@@ -3476,6 +3504,7 @@ function walkImage(node, parentMarks, text, marks, out, options, context, traili
|
|
|
3476
3504
|
const title = resolution.title;
|
|
3477
3505
|
const width = trailing?.magic.width ?? null;
|
|
3478
3506
|
const height = trailing?.magic.height ?? null;
|
|
3507
|
+
const snapshot = trailing?.magic.snapshot ?? null;
|
|
3479
3508
|
const to = trailing?.to ?? node.to;
|
|
3480
3509
|
emit(out, node.from, to, [
|
|
3481
3510
|
...parentMarks,
|
|
@@ -3486,6 +3515,7 @@ function walkImage(node, parentMarks, text, marks, out, options, context, traili
|
|
|
3486
3515
|
title,
|
|
3487
3516
|
width,
|
|
3488
3517
|
height,
|
|
3518
|
+
snapshot,
|
|
3489
3519
|
syntax: null,
|
|
3490
3520
|
wikiTarget: null
|
|
3491
3521
|
})
|
|
@@ -3559,6 +3589,7 @@ function walkWikiEmbed(node, parentMarks, text, marks, out, options) {
|
|
|
3559
3589
|
title: "",
|
|
3560
3590
|
width: embed.width,
|
|
3561
3591
|
height: embed.height,
|
|
3592
|
+
snapshot: null,
|
|
3562
3593
|
syntax: "wikiEmbed",
|
|
3563
3594
|
wikiTarget: embed.target
|
|
3564
3595
|
})
|
|
@@ -3857,6 +3888,7 @@ function defineMdImage() {
|
|
|
3857
3888
|
title: { default: "" },
|
|
3858
3889
|
width: { default: null },
|
|
3859
3890
|
height: { default: null },
|
|
3891
|
+
snapshot: { default: null },
|
|
3860
3892
|
syntax: { default: null },
|
|
3861
3893
|
wikiTarget: { default: null }
|
|
3862
3894
|
},
|
|
@@ -6157,7 +6189,7 @@ function markdownToDoc(markdown, options = {}) {
|
|
|
6157
6189
|
frontmatterBody = body;
|
|
6158
6190
|
if (matchLength) rest = markdown.slice(matchLength);
|
|
6159
6191
|
}
|
|
6160
|
-
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest, 0);
|
|
6192
|
+
const blocks = collectBlocks(nodes, gfmBlockOnlyParser.parse(rest).cursor(), rest, 0, rest !== markdown);
|
|
6161
6193
|
return nodes.doc(frontmatterBody === void 0 ? {} : { frontmatter: frontmatterBody }, blocks);
|
|
6162
6194
|
}
|
|
6163
6195
|
/**
|
|
@@ -6175,13 +6207,19 @@ function matchFrontmatter(markdown) {
|
|
|
6175
6207
|
return [(match[1] ?? "").replace(/\r?\n$/, ""), match[0].length];
|
|
6176
6208
|
}
|
|
6177
6209
|
/**
|
|
6178
|
-
* Walk the
|
|
6179
|
-
*
|
|
6180
|
-
*
|
|
6210
|
+
* Walk the document's children, converting each block-level child and
|
|
6211
|
+
* flattening any node converter that returns multiple siblings (lists are the
|
|
6212
|
+
* main case). Blank lines before the first block and after the last one are
|
|
6213
|
+
* content too, one empty paragraph each; when a frontmatter block was peeled
|
|
6214
|
+
* off ahead of `text`, the first leading blank line is that block's separator.
|
|
6181
6215
|
*/
|
|
6182
|
-
function collectBlocks(nodes, cursor, text, column) {
|
|
6216
|
+
function collectBlocks(nodes, cursor, text, column, afterFrontmatter) {
|
|
6183
6217
|
const out = [];
|
|
6184
|
-
if (!cursor.firstChild())
|
|
6218
|
+
if (!cursor.firstChild()) {
|
|
6219
|
+
appendEmptyParagraphs(out, nodes, Math.max(1, countNewlines(text, 0, text.length)));
|
|
6220
|
+
return out;
|
|
6221
|
+
}
|
|
6222
|
+
appendEmptyParagraphs(out, nodes, countNewlines(text, 0, cursor.from) - (afterFrontmatter ? 1 : 0));
|
|
6185
6223
|
let previousTo;
|
|
6186
6224
|
do {
|
|
6187
6225
|
if (previousTo != null) appendGapParagraphs(out, nodes, text, previousTo, cursor.from);
|
|
@@ -6189,6 +6227,8 @@ function collectBlocks(nodes, cursor, text, column) {
|
|
|
6189
6227
|
appendBlocks(out, nodes, convertBlock(nodes, cursor, text, column));
|
|
6190
6228
|
} while (cursor.nextSibling());
|
|
6191
6229
|
cursor.parent();
|
|
6230
|
+
const end = text.endsWith("\n") ? text.length - 1 : text.length;
|
|
6231
|
+
appendEmptyParagraphs(out, nodes, countNewlines(text, previousTo, end));
|
|
6192
6232
|
return out;
|
|
6193
6233
|
}
|
|
6194
6234
|
/**
|
|
@@ -6218,9 +6258,15 @@ function appendBlocks(out, nodes, blocks) {
|
|
|
6218
6258
|
* of them (the previous block's own terminator plus one per blank line).
|
|
6219
6259
|
*/
|
|
6220
6260
|
function appendGapParagraphs(out, nodes, text, gapFrom, gapTo) {
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6261
|
+
appendEmptyParagraphs(out, nodes, countNewlines(text, gapFrom, gapTo) - 2);
|
|
6262
|
+
}
|
|
6263
|
+
function appendEmptyParagraphs(out, nodes, count) {
|
|
6264
|
+
for (let i = 0; i < count; i++) out.push(nodes.paragraph());
|
|
6265
|
+
}
|
|
6266
|
+
function countNewlines(text, from, to) {
|
|
6267
|
+
let count = 0;
|
|
6268
|
+
for (let i = from; i < to; i++) if (text.charCodeAt(i) === 10) count++;
|
|
6269
|
+
return count;
|
|
6224
6270
|
}
|
|
6225
6271
|
function convertBlock(nodes, cursor, text, column) {
|
|
6226
6272
|
switch (cursor.type.id) {
|
|
@@ -6459,30 +6505,44 @@ function convertHTMLComment(nodes, cursor, text, column) {
|
|
|
6459
6505
|
/**
|
|
6460
6506
|
* A blockquote's children start at column 0: every column an enclosing container
|
|
6461
6507
|
* wrote sits in front of the `> ` marker on the line, and comes off with it.
|
|
6508
|
+
*
|
|
6509
|
+
* Blank quote lines before the first block and after the last one are empty
|
|
6510
|
+
* paragraphs, like the document's own edges. The quote's range ends on its last
|
|
6511
|
+
* line rather than after it, so every newline in the trailing slice is a blank
|
|
6512
|
+
* line, and a quote made of markers only has one more line than newlines.
|
|
6462
6513
|
*/
|
|
6463
6514
|
function convertBlockquote(nodes, cursor, text) {
|
|
6464
6515
|
const content = [];
|
|
6516
|
+
const from = cursor.from;
|
|
6517
|
+
const to = cursor.to;
|
|
6518
|
+
let previousTo;
|
|
6465
6519
|
if (cursor.firstChild()) {
|
|
6466
|
-
let previousTo;
|
|
6467
6520
|
do {
|
|
6468
6521
|
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
6469
|
-
if (previousTo
|
|
6522
|
+
if (previousTo == null) appendEmptyParagraphs(content, nodes, countNewlines(text, from, cursor.from));
|
|
6523
|
+
else appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
6470
6524
|
previousTo = cursor.to;
|
|
6471
6525
|
appendBlocks(content, nodes, convertBlock(nodes, cursor, text, 0));
|
|
6472
6526
|
} while (cursor.nextSibling());
|
|
6473
6527
|
cursor.parent();
|
|
6474
6528
|
}
|
|
6529
|
+
appendEmptyParagraphs(content, nodes, previousTo == null ? countNewlines(text, from, to) + 1 : countNewlines(text, previousTo, to));
|
|
6475
6530
|
return nodes.blockquote(content);
|
|
6476
6531
|
}
|
|
6477
6532
|
function convertList(nodes, cursor, text, column, kind) {
|
|
6478
|
-
const
|
|
6533
|
+
const out = [];
|
|
6479
6534
|
if (cursor.firstChild()) {
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6535
|
+
let previousContentEnd;
|
|
6536
|
+
do {
|
|
6537
|
+
if (cursor.type.id !== LEZER_NODE_IDS.ListItem) continue;
|
|
6538
|
+
if (previousContentEnd != null) appendGapParagraphs(out, nodes, text, previousContentEnd, cursor.from);
|
|
6539
|
+
const [item, contentEnd] = convertListItem(nodes, cursor, text, column, kind);
|
|
6540
|
+
out.push(item);
|
|
6541
|
+
previousContentEnd = contentEnd;
|
|
6542
|
+
} while (cursor.nextSibling());
|
|
6483
6543
|
cursor.parent();
|
|
6484
6544
|
}
|
|
6485
|
-
return
|
|
6545
|
+
return out;
|
|
6486
6546
|
}
|
|
6487
6547
|
/**
|
|
6488
6548
|
* The marker style at `cursor`, plus the start number of an ordered item.
|
|
@@ -6536,6 +6596,19 @@ function convertTaskItem(nodes, cursor, text, column) {
|
|
|
6536
6596
|
paragraph
|
|
6537
6597
|
};
|
|
6538
6598
|
}
|
|
6599
|
+
/**
|
|
6600
|
+
* The gap between a list marker ending at `markTo` and the item's first content
|
|
6601
|
+
* at `contentFrom`. Only content that opens on the marker's own line measures a
|
|
6602
|
+
* gap; an item whose content starts on the next line takes the canonical single
|
|
6603
|
+
* space, the column its own continuation lines are indented to. A gap of 5+ is
|
|
6604
|
+
* indented code (a different node, so the content's column would be the code
|
|
6605
|
+
* block's), and 1 is the canonical default; only a 2-4 space gap is a faithful,
|
|
6606
|
+
* content-preserving variation.
|
|
6607
|
+
*/
|
|
6608
|
+
function measureMarkerGap(text, contentFrom, markTo, markEndColumn) {
|
|
6609
|
+
const gap = markTo != null && text.lastIndexOf("\n", contentFrom - 1) < markTo ? measureContentColumn(text, contentFrom) - markEndColumn : 1;
|
|
6610
|
+
return gap >= 2 && gap <= 4 ? gap : 1;
|
|
6611
|
+
}
|
|
6539
6612
|
function convertListItem(nodes, cursor, text, column, kind) {
|
|
6540
6613
|
const content = [];
|
|
6541
6614
|
let taskChecked;
|
|
@@ -6547,7 +6620,7 @@ function convertListItem(nodes, cursor, text, column, kind) {
|
|
|
6547
6620
|
let markEndColumn = 0;
|
|
6548
6621
|
let markerGap = 1;
|
|
6549
6622
|
let contentColumn = column + markWidth + markerGap;
|
|
6550
|
-
let
|
|
6623
|
+
let previousTo;
|
|
6551
6624
|
if (cursor.firstChild()) {
|
|
6552
6625
|
do {
|
|
6553
6626
|
if (cursor.type.id === LEZER_NODE_IDS.QuoteMark) continue;
|
|
@@ -6560,12 +6633,11 @@ function convertListItem(nodes, cursor, text, column, kind) {
|
|
|
6560
6633
|
markEndColumn = measureContentColumn(text, cursor.to);
|
|
6561
6634
|
continue;
|
|
6562
6635
|
}
|
|
6563
|
-
if (
|
|
6564
|
-
|
|
6565
|
-
const gap = markTo != null && text.lastIndexOf("\n", cursor.from - 1) < markTo ? measureContentColumn(text, cursor.from) - markEndColumn : 1;
|
|
6566
|
-
markerGap = gap >= 2 && gap <= 4 ? gap : 1;
|
|
6636
|
+
if (previousTo == null) {
|
|
6637
|
+
markerGap = measureMarkerGap(text, cursor.from, markTo, markEndColumn);
|
|
6567
6638
|
contentColumn = column + markWidth + markerGap;
|
|
6568
|
-
}
|
|
6639
|
+
} else appendGapParagraphs(content, nodes, text, previousTo, cursor.from);
|
|
6640
|
+
previousTo = cursor.to;
|
|
6569
6641
|
if (kind === "bullet" && cursor.type.id === LEZER_NODE_IDS.Task) {
|
|
6570
6642
|
const task = convertTaskItem(nodes, cursor, text, contentColumn);
|
|
6571
6643
|
taskChecked = task.checked;
|
|
@@ -6588,7 +6660,7 @@ function convertListItem(nodes, cursor, text, column, kind) {
|
|
|
6588
6660
|
taskMarker,
|
|
6589
6661
|
markerGap
|
|
6590
6662
|
};
|
|
6591
|
-
return nodes.list(attrs, content);
|
|
6663
|
+
return [nodes.list(attrs, content), previousTo ?? cursor.to];
|
|
6592
6664
|
}
|
|
6593
6665
|
function convertCodeBlock(nodes, cursor, text) {
|
|
6594
6666
|
const indented = cursor.type.id === LEZER_NODE_IDS.CodeBlock;
|
|
@@ -6841,105 +6913,38 @@ const codeBlockLanguages = languages.map((language) => ({
|
|
|
6841
6913
|
})).filter((language) => !excludeLanguages.has(language.label)).concat(extraLanguages).sort((a, b) => a.label.localeCompare(b.label));
|
|
6842
6914
|
|
|
6843
6915
|
//#endregion
|
|
6844
|
-
//#region src/
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
function prefersDarkColorScheme() {
|
|
6850
|
-
if (typeof window === "undefined") return false;
|
|
6851
|
-
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
6916
|
+
//#region src/extensions/paste.ts
|
|
6917
|
+
function getPastedText(event, slice) {
|
|
6918
|
+
const fromClipboard = event.clipboardData?.getData("text/plain");
|
|
6919
|
+
if (fromClipboard) return fromClipboard;
|
|
6920
|
+
return slice.content.textBetween(0, slice.content.size, "\n");
|
|
6852
6921
|
}
|
|
6853
6922
|
|
|
6854
6923
|
//#endregion
|
|
6855
|
-
//#region src/extensions/
|
|
6856
|
-
const
|
|
6924
|
+
//#region src/extensions/post-embed.ts
|
|
6925
|
+
const X_POST_HOSTS = /^(?:www\.|mobile\.)?(?:twitter\.com|x\.com)$/i;
|
|
6857
6926
|
const STATUS_ID = /\/status(?:es)?\/(\d+)/;
|
|
6858
|
-
|
|
6859
|
-
|
|
6927
|
+
const YOUTUBE_HOSTS = /^(?:www\.|m\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
|
|
6928
|
+
const YOUTU_BE_HOST = /^(?:www\.)?youtu\.be$/i;
|
|
6929
|
+
const VIDEO_ID = /^[\w-]{11}$/;
|
|
6930
|
+
function parseURL(src) {
|
|
6860
6931
|
try {
|
|
6861
|
-
|
|
6932
|
+
return new URL(src);
|
|
6862
6933
|
} catch {
|
|
6863
6934
|
return;
|
|
6864
6935
|
}
|
|
6865
|
-
if (!TWEET_HOSTS.test(url.hostname)) return void 0;
|
|
6866
|
-
return STATUS_ID.exec(url.pathname)?.[1];
|
|
6867
|
-
}
|
|
6868
|
-
const matchTweet = (src) => {
|
|
6869
|
-
const tweetId = parseTweetId(src);
|
|
6870
|
-
if (!tweetId) return;
|
|
6871
|
-
const theme = prefersDarkColorScheme() ? "dark" : "light";
|
|
6872
|
-
return {
|
|
6873
|
-
kind: "tweet",
|
|
6874
|
-
key: `tweet:${tweetId}`,
|
|
6875
|
-
src: `https://platform.twitter.com/embed/Tweet.html?id=${tweetId}&theme=${theme}&dnt=true`,
|
|
6876
|
-
title: "Tweet",
|
|
6877
|
-
className: "md-embed md-embed-tweet",
|
|
6878
|
-
testid: "tweet-embed"
|
|
6879
|
-
};
|
|
6880
|
-
};
|
|
6881
|
-
/**
|
|
6882
|
-
* `Tweet.html` reports its rendered height via `postMessage`; size the iframe to
|
|
6883
|
-
* fit and pass each reported height to `onHeight`. Returns a cleanup that
|
|
6884
|
-
* removes the listener. The cleanup also runs once the iframe leaves the DOM, so
|
|
6885
|
-
* the editor's DOM mark view (which has no destroy hook) is covered, while a
|
|
6886
|
-
* React caller can call it on unmount.
|
|
6887
|
-
*/
|
|
6888
|
-
function listenForTweetHeight(iframe, onHeight) {
|
|
6889
|
-
const onMessage = (event) => {
|
|
6890
|
-
if (event.source !== iframe.contentWindow) return;
|
|
6891
|
-
try {
|
|
6892
|
-
const height = event.data?.["twttr.embed"]?.params?.[0]?.height;
|
|
6893
|
-
if (typeof height === "number" && height > 0) {
|
|
6894
|
-
applyTweetHeight(iframe, height);
|
|
6895
|
-
onHeight?.(height);
|
|
6896
|
-
}
|
|
6897
|
-
} catch (error) {
|
|
6898
|
-
console.warn("[meowdown] failed to parse tweet resize message:", error);
|
|
6899
|
-
}
|
|
6900
|
-
};
|
|
6901
|
-
window.addEventListener("message", onMessage);
|
|
6902
|
-
let cleaned = false;
|
|
6903
|
-
const cleanup = () => {
|
|
6904
|
-
if (cleaned) return;
|
|
6905
|
-
cleaned = true;
|
|
6906
|
-
window.removeEventListener("message", onMessage);
|
|
6907
|
-
observer.disconnect();
|
|
6908
|
-
};
|
|
6909
|
-
const observer = new MutationObserver(() => {
|
|
6910
|
-
if (!iframe.isConnected) cleanup();
|
|
6911
|
-
});
|
|
6912
|
-
observer.observe(document.body, {
|
|
6913
|
-
childList: true,
|
|
6914
|
-
subtree: true
|
|
6915
|
-
});
|
|
6916
|
-
return cleanup;
|
|
6917
6936
|
}
|
|
6918
6937
|
/**
|
|
6919
|
-
*
|
|
6938
|
+
* The post id of an X status URL, or `undefined` for any other `src`.
|
|
6920
6939
|
*/
|
|
6921
|
-
function
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
}
|
|
6940
|
+
function parseXPostId(src) {
|
|
6941
|
+
const url = parseURL(src);
|
|
6942
|
+
if (!url || !X_POST_HOSTS.test(url.hostname)) return void 0;
|
|
6943
|
+
return STATUS_ID.exec(url.pathname)?.[1];
|
|
6926
6944
|
}
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
const YOUTUBE_HOSTS = /^(?:www\.|m\.)?(?:youtube\.com|youtube-nocookie\.com)$/i;
|
|
6931
|
-
const YOUTU_BE_HOST = /^(?:www\.)?youtu\.be$/i;
|
|
6932
|
-
const VIDEO_ID = /^[\w-]{11}$/;
|
|
6933
|
-
/**
|
|
6934
|
-
* Extract `{ videoId, startSeconds? }` from any watch/shorts/embed/live/`youtu.be` URL.
|
|
6935
|
-
*/
|
|
6936
|
-
function parseYouTube(src) {
|
|
6937
|
-
let url;
|
|
6938
|
-
try {
|
|
6939
|
-
url = new URL(src);
|
|
6940
|
-
} catch {
|
|
6941
|
-
return;
|
|
6942
|
-
}
|
|
6945
|
+
function isYouTubeVideo(src) {
|
|
6946
|
+
const url = parseURL(src);
|
|
6947
|
+
if (!url) return false;
|
|
6943
6948
|
let videoId = null;
|
|
6944
6949
|
if (YOUTU_BE_HOST.test(url.hostname)) videoId = url.pathname.slice(1);
|
|
6945
6950
|
else if (YOUTUBE_HOSTS.test(url.hostname)) {
|
|
@@ -6947,58 +6952,68 @@ function parseYouTube(src) {
|
|
|
6947
6952
|
if (url.pathname === "/watch") videoId = url.searchParams.get("v");
|
|
6948
6953
|
else if (firstSegment === "shorts" || firstSegment === "embed" || firstSegment === "live") videoId = secondSegment ?? null;
|
|
6949
6954
|
}
|
|
6950
|
-
|
|
6951
|
-
const timeParam = url.searchParams.get("start") ?? url.searchParams.get("t");
|
|
6952
|
-
const startSeconds = timeParam ? parseStartSeconds(timeParam) : void 0;
|
|
6953
|
-
return {
|
|
6954
|
-
videoId,
|
|
6955
|
-
startSeconds
|
|
6956
|
-
};
|
|
6955
|
+
return videoId !== null && VIDEO_ID.test(videoId);
|
|
6957
6956
|
}
|
|
6958
6957
|
/**
|
|
6959
|
-
*
|
|
6958
|
+
* The post-embed card an image `src` renders as, or `undefined` for a plain image.
|
|
6960
6959
|
*/
|
|
6961
|
-
function
|
|
6962
|
-
if (
|
|
6963
|
-
|
|
6964
|
-
if (!matched || !matched[1] && !matched[2] && !matched[3]) return void 0;
|
|
6965
|
-
return Number(matched[1] ?? 0) * 3600 + Number(matched[2] ?? 0) * 60 + Number(matched[3] ?? 0);
|
|
6960
|
+
function matchPostEmbed(src) {
|
|
6961
|
+
if (parseXPostId(src) !== void 0) return "x-post";
|
|
6962
|
+
if (isYouTubeVideo(src)) return "youtube-video";
|
|
6966
6963
|
}
|
|
6967
|
-
const matchYouTube = (src) => {
|
|
6968
|
-
const parsed = parseYouTube(src);
|
|
6969
|
-
if (!parsed) return;
|
|
6970
|
-
const query = parsed.startSeconds ? `?start=${parsed.startSeconds}` : "";
|
|
6971
|
-
return {
|
|
6972
|
-
kind: "youtube",
|
|
6973
|
-
key: `youtube:${parsed.videoId}:${parsed.startSeconds ?? 0}`,
|
|
6974
|
-
src: `https://www.youtube-nocookie.com/embed/${parsed.videoId}${query}`,
|
|
6975
|
-
title: "YouTube video",
|
|
6976
|
-
className: "md-embed md-embed-youtube",
|
|
6977
|
-
testid: "youtube-embed",
|
|
6978
|
-
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share; fullscreen",
|
|
6979
|
-
allowFullscreen: true
|
|
6980
|
-
};
|
|
6981
|
-
};
|
|
6982
|
-
|
|
6983
|
-
//#endregion
|
|
6984
|
-
//#region src/extensions/embed.ts
|
|
6985
|
-
const EMBED_MATCHERS = [matchYouTube, matchTweet];
|
|
6986
6964
|
/**
|
|
6987
|
-
*
|
|
6965
|
+
* Wrap a loader in a 64-entry LRU keyed by URL. A settled URL answers
|
|
6966
|
+
* synchronously, so a revisited document renders its cards in the first
|
|
6967
|
+
* frame; the first visit returns the in-flight load.
|
|
6988
6968
|
*/
|
|
6989
|
-
function
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
if (
|
|
6993
|
-
|
|
6969
|
+
function cached(load) {
|
|
6970
|
+
const cache = createLRU({ max: 64 });
|
|
6971
|
+
return (url) => {
|
|
6972
|
+
if (cache.has(url)) return cache.get(url);
|
|
6973
|
+
const pending = load(url).then((value) => {
|
|
6974
|
+
cache.set(url, value);
|
|
6975
|
+
return value;
|
|
6976
|
+
}, (error) => {
|
|
6977
|
+
cache.delete(url);
|
|
6978
|
+
throw error;
|
|
6979
|
+
});
|
|
6980
|
+
cache.set(url, pending);
|
|
6981
|
+
return pending;
|
|
6982
|
+
};
|
|
6994
6983
|
}
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
const
|
|
7000
|
-
if (
|
|
7001
|
-
|
|
6984
|
+
const X_POST_API = "https://react-tweet.vercel.app/api/tweet/";
|
|
6985
|
+
const defaultResolveXPost = cached(async (url) => {
|
|
6986
|
+
const id = parseXPostId(url);
|
|
6987
|
+
if (id === void 0) return;
|
|
6988
|
+
const response = await fetch(X_POST_API + id);
|
|
6989
|
+
if (!response.ok) return;
|
|
6990
|
+
const json = await response.json();
|
|
6991
|
+
return fromSyndication(json.data);
|
|
6992
|
+
});
|
|
6993
|
+
const YOUTUBE_OEMBED_API = "https://www.youtube.com/oembed?format=json&url=";
|
|
6994
|
+
const defaultResolveYouTubeVideo = cached(async (url) => {
|
|
6995
|
+
const response = await fetch(YOUTUBE_OEMBED_API + encodeURIComponent(url));
|
|
6996
|
+
if (!response.ok) return;
|
|
6997
|
+
return {
|
|
6998
|
+
url,
|
|
6999
|
+
...await response.json()
|
|
7000
|
+
};
|
|
7001
|
+
});
|
|
7002
|
+
const PostEmbedSnapshotSchema = v.variant("kind", [v.object({
|
|
7003
|
+
kind: v.literal("x-post"),
|
|
7004
|
+
data: XPostSchema
|
|
7005
|
+
}), v.object({
|
|
7006
|
+
kind: v.literal("youtube-video"),
|
|
7007
|
+
data: YouTubeVideoSchema
|
|
7008
|
+
})]);
|
|
7009
|
+
/**
|
|
7010
|
+
* The snapshot a card renders from a saved JSON object, or `undefined` when
|
|
7011
|
+
* the object does not validate. Unknown fields are dropped, so a persisted
|
|
7012
|
+
* snapshot is exactly what the card needs.
|
|
7013
|
+
*/
|
|
7014
|
+
function parsePostEmbedSnapshot(value) {
|
|
7015
|
+
const result = v.safeParse(PostEmbedSnapshotSchema, value);
|
|
7016
|
+
return result.success ? result.output : void 0;
|
|
7002
7017
|
}
|
|
7003
7018
|
|
|
7004
7019
|
//#endregion
|
|
@@ -7007,7 +7022,7 @@ const embedPasteKey = new PluginKey("meowdown-embed-paste");
|
|
|
7007
7022
|
function detectEmbedUrl(text) {
|
|
7008
7023
|
const trimmed = text.trim();
|
|
7009
7024
|
if (!trimmed || /\s/.test(trimmed)) return void 0;
|
|
7010
|
-
return
|
|
7025
|
+
return matchPostEmbed(trimmed) ? trimmed : void 0;
|
|
7011
7026
|
}
|
|
7012
7027
|
function insertEmbedFromPaste(view, url) {
|
|
7013
7028
|
const { from, to } = view.state.selection;
|
|
@@ -7739,28 +7754,6 @@ function defaultResolveImageUrl(src) {
|
|
|
7739
7754
|
*/
|
|
7740
7755
|
const MAX_DISPLAY_HEIGHT = 500;
|
|
7741
7756
|
/**
|
|
7742
|
-
* Build the iframe DOM for an embed descriptor and start its height listener.
|
|
7743
|
-
* A persisted tweet height seeds the iframe before `Tweet.html` reports the
|
|
7744
|
-
* real one, so a revisited tweet keeps its space instead of shifting layout.
|
|
7745
|
-
*/
|
|
7746
|
-
function buildEmbedIframe(embed, height, onHeight) {
|
|
7747
|
-
const iframe = document.createElement("iframe");
|
|
7748
|
-
iframe.src = embed.src;
|
|
7749
|
-
iframe.title = embed.title;
|
|
7750
|
-
iframe.className = embed.className;
|
|
7751
|
-
iframe.dataset.testid = embed.testid;
|
|
7752
|
-
iframe.loading = "lazy";
|
|
7753
|
-
iframe.referrerPolicy = "strict-origin-when-cross-origin";
|
|
7754
|
-
iframe.setAttribute("frameborder", "0");
|
|
7755
|
-
if (embed.allow) iframe.allow = embed.allow;
|
|
7756
|
-
if (embed.allowFullscreen) iframe.allowFullscreen = true;
|
|
7757
|
-
if (embed.kind === "tweet") {
|
|
7758
|
-
applyTweetHeight(iframe, height);
|
|
7759
|
-
listenForTweetHeight(iframe, onHeight);
|
|
7760
|
-
}
|
|
7761
|
-
return iframe;
|
|
7762
|
-
}
|
|
7763
|
-
/**
|
|
7764
7757
|
* Write a persisted display size onto a resizable resizable root.
|
|
7765
7758
|
*/
|
|
7766
7759
|
function applySize(root, width, height) {
|
|
@@ -7828,40 +7821,59 @@ function commitImageSize(view, content, rawWidth, rawHeight) {
|
|
|
7828
7821
|
}, true);
|
|
7829
7822
|
}
|
|
7830
7823
|
/**
|
|
7831
|
-
*
|
|
7832
|
-
*
|
|
7833
|
-
* writing those back would churn the document on every open.
|
|
7824
|
+
* Persist a resized width into the trailing magic comment. A card's height is
|
|
7825
|
+
* its content's, so any persisted height is dropped rather than carried over.
|
|
7834
7826
|
*/
|
|
7835
|
-
|
|
7827
|
+
function commitEmbedWidth(view, content, rawWidth) {
|
|
7828
|
+
const pos = view.posAtDOM(content, 0);
|
|
7829
|
+
const range = getMarkRangeAt(view.state, pos, "mdImage");
|
|
7830
|
+
if (!range) return;
|
|
7831
|
+
rewriteMagicComment(view, range, {
|
|
7832
|
+
width: Math.round(rawWidth),
|
|
7833
|
+
height: void 0
|
|
7834
|
+
}, true);
|
|
7835
|
+
}
|
|
7836
7836
|
/**
|
|
7837
|
-
* Persist
|
|
7838
|
-
*
|
|
7839
|
-
*
|
|
7837
|
+
* Persist a resolved snapshot into the trailing magic comment. The whole
|
|
7838
|
+
* `<!-- ... -->` range is rewritten, not just the comment: the
|
|
7839
|
+
* transaction stays out of history, and undoing the edit that inserted the
|
|
7840
|
+
* image must delete the snapshot with it. prosemirror-history maps the
|
|
7841
|
+
* inverse deletion through this step, and a position at the end of a
|
|
7842
|
+
* replaced range maps to the end of the replacement, so the mapped deletion
|
|
7843
|
+
* covers the comment. An insertion at the range end would survive that undo
|
|
7844
|
+
* as an orphan comment rendered as plain text.
|
|
7840
7845
|
*/
|
|
7841
|
-
function
|
|
7842
|
-
if (!view.editable || !content.isConnected) return;
|
|
7846
|
+
function commitSnapshot(view, content, src, snapshot) {
|
|
7843
7847
|
const pos = view.posAtDOM(content, 0);
|
|
7844
7848
|
const range = getMarkRangeAt(view.state, pos, "mdImage");
|
|
7845
7849
|
if (!range) return;
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7850
|
+
if (range.mark.attrs.src !== src) return;
|
|
7851
|
+
const current = view.state.doc.textBetween(range.from, range.to);
|
|
7852
|
+
const base = stripMagicComment(current);
|
|
7853
|
+
const comment = formatMagicComment({
|
|
7854
|
+
...parseMagicComment(current.slice(base.length)),
|
|
7855
|
+
snapshot
|
|
7856
|
+
});
|
|
7857
|
+
if (base + comment === current) return;
|
|
7858
|
+
view.dispatch(view.state.tr.insertText(base + comment, range.from, range.to).setMeta("addToHistory", false));
|
|
7849
7859
|
}
|
|
7850
7860
|
var ImageMarkView = class {
|
|
7851
7861
|
#dom;
|
|
7852
7862
|
#contentDOM;
|
|
7853
7863
|
#view;
|
|
7854
7864
|
#resolveImageUrl;
|
|
7855
|
-
#
|
|
7865
|
+
#resolveXPost;
|
|
7866
|
+
#resolveYouTubeVideo;
|
|
7856
7867
|
#attrs;
|
|
7857
7868
|
#resizableRoot;
|
|
7858
7869
|
#image;
|
|
7859
|
-
#
|
|
7870
|
+
#destroyed = false;
|
|
7860
7871
|
constructor(mark, view, options) {
|
|
7861
7872
|
this.#attrs = mark.attrs;
|
|
7862
7873
|
this.#view = view;
|
|
7863
7874
|
this.#resolveImageUrl = options.resolveImageUrl;
|
|
7864
|
-
this.#
|
|
7875
|
+
this.#resolveXPost = options.resolveXPost ?? defaultResolveXPost;
|
|
7876
|
+
this.#resolveYouTubeVideo = options.resolveYouTubeVideo ?? defaultResolveYouTubeVideo;
|
|
7865
7877
|
this.#dom = document.createElement("span");
|
|
7866
7878
|
this.#dom.className = "md-image-view md-atom-view";
|
|
7867
7879
|
this.#contentDOM = document.createElement("span");
|
|
@@ -7884,67 +7896,105 @@ var ImageMarkView = class {
|
|
|
7884
7896
|
update(mark) {
|
|
7885
7897
|
const next = mark.attrs;
|
|
7886
7898
|
const previous = this.#attrs;
|
|
7887
|
-
if (next.src !== previous.src) return false;
|
|
7899
|
+
if (next.src !== previous.src || next.snapshot !== previous.snapshot) return false;
|
|
7888
7900
|
this.#attrs = next;
|
|
7889
7901
|
if (this.#image && next.alt !== previous.alt) this.#image.alt = next.alt;
|
|
7890
7902
|
if (this.#resizableRoot && (next.width !== previous.width || next.height !== previous.height)) {
|
|
7891
7903
|
if (this.#image) applyImageDisplaySize(this.#resizableRoot, this.#image, next.width, next.height);
|
|
7892
7904
|
else applySize(this.#resizableRoot, next.width, next.height);
|
|
7893
7905
|
}
|
|
7894
|
-
if (this.#tweetIframe && next.height !== previous.height) applyTweetHeight(this.#tweetIframe, next.height);
|
|
7895
7906
|
return true;
|
|
7896
7907
|
}
|
|
7897
7908
|
ignoreMutation(mutation) {
|
|
7898
7909
|
return !this.#contentDOM.contains(mutation.target);
|
|
7899
7910
|
}
|
|
7911
|
+
destroy() {
|
|
7912
|
+
this.#destroyed = true;
|
|
7913
|
+
}
|
|
7900
7914
|
/**
|
|
7901
|
-
* Build the inline preview for the image `src`:
|
|
7915
|
+
* Build the inline preview for the image `src`: a post-embed card or a
|
|
7916
|
+
* resizable `<img>`.
|
|
7902
7917
|
*/
|
|
7903
7918
|
#renderPreview() {
|
|
7904
7919
|
const { src } = this.#attrs;
|
|
7905
|
-
const
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
|
|
7909
|
-
|
|
7910
|
-
|
|
7911
|
-
} : void 0;
|
|
7912
|
-
const iframe = buildEmbedIframe(embed, this.#attrs.height, onHeight);
|
|
7913
|
-
if (embed.kind === "tweet") this.#tweetIframe = iframe;
|
|
7914
|
-
wrapper.appendChild(embed.kind === "youtube" ? this.#buildResizableEmbed(iframe) : iframe);
|
|
7920
|
+
const wrapper = document.createElement("span");
|
|
7921
|
+
wrapper.className = "md-image-view-preview md-atom-view-preview";
|
|
7922
|
+
const kind = matchPostEmbed(src);
|
|
7923
|
+
if (kind) {
|
|
7924
|
+
wrapper.dataset.testid = `${kind}-embed`;
|
|
7925
|
+
wrapper.appendChild(this.#buildPostEmbed(kind, src));
|
|
7915
7926
|
return wrapper;
|
|
7916
7927
|
}
|
|
7917
7928
|
const url = (this.#resolveImageUrl ?? defaultResolveImageUrl)(src);
|
|
7918
7929
|
if (!url) return void 0;
|
|
7919
|
-
const wrapper = document.createElement("span");
|
|
7920
|
-
wrapper.className = "md-image-view-preview md-atom-view-preview";
|
|
7921
7930
|
wrapper.dataset.testid = "image-preview";
|
|
7922
7931
|
wrapper.appendChild(this.#buildResizableImage(url));
|
|
7923
7932
|
return wrapper;
|
|
7924
7933
|
}
|
|
7925
7934
|
/**
|
|
7926
|
-
* A
|
|
7927
|
-
*
|
|
7928
|
-
*
|
|
7929
|
-
*
|
|
7935
|
+
* A post-embed card renders a saved snapshot as is. Without one (or with
|
|
7936
|
+
* one that does not validate or names another kind) it loads through the
|
|
7937
|
+
* resolver, rendering its own pending and unavailable states, and the first
|
|
7938
|
+
* answer is written back into the source so the next open renders in the
|
|
7939
|
+
* first frame with no request.
|
|
7940
|
+
*/
|
|
7941
|
+
#buildPostEmbed(kind, src) {
|
|
7942
|
+
const saved = this.#attrs.snapshot == null ? void 0 : parsePostEmbedSnapshot(this.#attrs.snapshot);
|
|
7943
|
+
if (kind === "x-post") {
|
|
7944
|
+
registerXPost();
|
|
7945
|
+
const element = document.createElement("post-embed-x-post");
|
|
7946
|
+
element.data = saved?.kind === "x-post" ? saved.data : null;
|
|
7947
|
+
element.resolver = this.#persisting(kind, this.#resolveXPost);
|
|
7948
|
+
element.url = src;
|
|
7949
|
+
return element;
|
|
7950
|
+
}
|
|
7951
|
+
registerYouTubeVideo();
|
|
7952
|
+
const element = document.createElement("post-embed-youtube-video");
|
|
7953
|
+
element.playback = "inline";
|
|
7954
|
+
element.data = saved?.kind === "youtube-video" ? saved.data : null;
|
|
7955
|
+
element.resolver = this.#persisting(kind, this.#resolveYouTubeVideo);
|
|
7956
|
+
element.url = src;
|
|
7957
|
+
return this.#buildResizableVideo(element);
|
|
7958
|
+
}
|
|
7959
|
+
/**
|
|
7960
|
+
* Wrap a resolver so its first valid answer is persisted under `kind`.
|
|
7961
|
+
* post-embed only calls it while `data` is null, and logs a rejection
|
|
7962
|
+
* itself.
|
|
7930
7963
|
*/
|
|
7931
|
-
#
|
|
7964
|
+
#persisting(kind, resolver) {
|
|
7965
|
+
return (url) => {
|
|
7966
|
+
const result = resolver(url);
|
|
7967
|
+
Promise.resolve(result).then((value) => {
|
|
7968
|
+
if (this.#destroyed || value == null) return;
|
|
7969
|
+
const snapshot = parsePostEmbedSnapshot({
|
|
7970
|
+
kind,
|
|
7971
|
+
data: value
|
|
7972
|
+
});
|
|
7973
|
+
if (snapshot) commitSnapshot(this.#view, this.#contentDOM, url, snapshot);
|
|
7974
|
+
}, () => {});
|
|
7975
|
+
return result;
|
|
7976
|
+
};
|
|
7977
|
+
}
|
|
7978
|
+
/**
|
|
7979
|
+
* A resizable video card: the same resizable web component as images, but
|
|
7980
|
+
* only the dragged width is persisted, as `<!-- {"width":N} -->`; the card
|
|
7981
|
+
* keeps its content height.
|
|
7982
|
+
*/
|
|
7983
|
+
#buildResizableVideo(element) {
|
|
7932
7984
|
registerResizableRootElement();
|
|
7933
7985
|
registerResizableHandleElement();
|
|
7934
7986
|
const root = document.createElement("prosekit-resizable-root");
|
|
7935
7987
|
root.className = "md-embed-resizable";
|
|
7936
7988
|
root.dataset.testid = "embed-resizable";
|
|
7937
|
-
root.
|
|
7938
|
-
|
|
7939
|
-
root.appendChild(iframe);
|
|
7989
|
+
applySize(root, this.#attrs.width, null);
|
|
7990
|
+
root.appendChild(element);
|
|
7940
7991
|
const handle = document.createElement("prosekit-resizable-handle");
|
|
7941
7992
|
handle.className = "md-image-resize-handle";
|
|
7942
7993
|
handle.setAttribute("position", "bottom-right");
|
|
7943
7994
|
handle.addEventListener("click", (event) => event.stopPropagation());
|
|
7944
7995
|
root.appendChild(handle);
|
|
7945
7996
|
root.addEventListener("resizeEnd", (event) => {
|
|
7946
|
-
|
|
7947
|
-
commitImageSize(this.#view, this.#contentDOM, nextWidth, nextHeight);
|
|
7997
|
+
commitEmbedWidth(this.#view, this.#contentDOM, event.detail.width);
|
|
7948
7998
|
});
|
|
7949
7999
|
this.#resizableRoot = root;
|
|
7950
8000
|
return root;
|
|
@@ -8809,4 +8859,4 @@ function getVirtualElementFromRange(view, range) {
|
|
|
8809
8859
|
}
|
|
8810
8860
|
|
|
8811
8861
|
//#endregion
|
|
8812
|
-
export { EDITOR_KEY_BINDINGS, Priority, buildFileMarkdown, checkRoundTrip, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineCodeBlockSyntaxHighlight, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLComment, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkCommands, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMath, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, formatSizedWikiEmbed, getCodeTokens, getFileKind, getIsComposing, getLinkText, getLinkUnitAt, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunks, inlineTextToMarkChunksWithContext, insertLink, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isMarkOfType, isModEvent, isNodeOfType,
|
|
8862
|
+
export { EDITOR_KEY_BINDINGS, Priority, buildFileMarkdown, checkRoundTrip, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defaultResolveXPost, defaultResolveYouTubeVideo, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineCodeBlockSyntaxHighlight, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLComment, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkCommands, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMath, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, formatSizedWikiEmbed, getCodeTokens, getFileKind, getIsComposing, getLinkText, getLinkUnitAt, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunks, inlineTextToMarkChunksWithContext, insertLink, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isMarkOfType, isModEvent, isNodeOfType, isReferenceDefinitionNode, isSelectionInTableCell, loadKaTeX, markdownToDoc, matchPostEmbed, normalizeHref, parsePostEmbedSnapshot, parseWikiEmbed, parseXPostId, removeLink, renderMathInto, updateLink, wikiEmbedBasename, withPriority };
|