@4399ywkf/editor 0.6.0 → 0.8.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/README.md +161 -2
- package/dist/{DocView-XQMJUUVK.js → DocView-WEKRSKRU.js} +6 -4
- package/dist/DocView-WEKRSKRU.js.map +1 -0
- package/dist/SheetView-SDBRSTVS.js +3 -0
- package/dist/{SheetView-DSADIZNS.js.map → SheetView-SDBRSTVS.js.map} +1 -1
- package/dist/chunk-5HMKRNIH.js +27 -0
- package/dist/chunk-5HMKRNIH.js.map +1 -0
- package/dist/chunk-DGUNVCOM.js +711 -0
- package/dist/chunk-DGUNVCOM.js.map +1 -0
- package/dist/chunk-HPSPOM2O.js +128 -0
- package/dist/chunk-HPSPOM2O.js.map +1 -0
- package/dist/chunk-KRP5V7VF.js +130 -0
- package/dist/chunk-KRP5V7VF.js.map +1 -0
- package/dist/chunk-SBTKDJAL.js +107 -0
- package/dist/chunk-SBTKDJAL.js.map +1 -0
- package/dist/{chunk-IUDLMN53.js → chunk-SGLNT6Z2.js} +2416 -176
- package/dist/chunk-SGLNT6Z2.js.map +1 -0
- package/dist/{chunk-L6ZR5HHW.js → chunk-SYCK6Y5F.js} +15 -10
- package/dist/chunk-SYCK6Y5F.js.map +1 -0
- package/dist/chunk-WF7PIIFR.js +21 -0
- package/dist/chunk-WF7PIIFR.js.map +1 -0
- package/dist/chunk-ZY5KLA7T.js +258 -0
- package/dist/chunk-ZY5KLA7T.js.map +1 -0
- package/dist/code-types-Dqmit3xx.d.ts +65 -0
- package/dist/{types-C37MAt8o.d.ts → csv-parse-D0zTBvDB.d.ts} +39 -1
- package/dist/csv.d.ts +6 -3
- package/dist/csv.js +6 -125
- package/dist/csv.js.map +1 -1
- package/dist/doc-runtime.d.ts +1 -1
- package/dist/doc-runtime.js +7 -1
- package/dist/doc-runtime.js.map +1 -1
- package/dist/doc.d.ts +113 -6
- package/dist/doc.js +2 -1
- package/dist/doc.js.map +1 -1
- package/dist/docx.d.ts +15 -0
- package/dist/docx.js +116 -8
- package/dist/docx.js.map +1 -1
- package/dist/import-GVZDLWA2.js +6 -0
- package/dist/import-GVZDLWA2.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/pdf.d.ts +3 -1
- package/dist/pptx.d.ts +3 -1
- package/dist/{registry-BBL8yeIT.d.ts → registry-CYKivRDh.d.ts} +13 -1
- package/dist/sheet-runtime.d.ts +212 -0
- package/dist/sheet-runtime.js +416 -0
- package/dist/sheet-runtime.js.map +1 -0
- package/dist/sheet.d.ts +9 -5
- package/dist/sheet.js +1 -1
- package/dist/style.css +421 -111
- package/dist/xlsx.d.ts +118 -7
- package/dist/xlsx.js +7 -78
- package/dist/xlsx.js.map +1 -1
- package/package.json +11 -5
- package/dist/DocView-XQMJUUVK.js.map +0 -1
- package/dist/SheetView-DSADIZNS.js +0 -3
- package/dist/chunk-32WQX3NR.js +0 -37
- package/dist/chunk-32WQX3NR.js.map +0 -1
- package/dist/chunk-IUDLMN53.js.map +0 -1
- package/dist/chunk-L6ZR5HHW.js.map +0 -1
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { useTiptapEditor, isExtensionAvailable, isNodeInSchema, Button, cn, getSelectionBoundingRect, isSelectionValid, getSelectedNodesOfType, selectCurrentBlockContent, Popover, PopoverTrigger, PopoverContent, isValidPosition, updateNodesAttr, parseShortcutKeys, isMarkInSchema, isNodeTypeSelected, SR_ONLY, sanitizeUrl, selectionWithinConvertibleTypes, getSelectedBlockNodes, findNodePosition, clamp, focusNextNode, getAvatar, getUrlParam, handleImageUpload, getNodeDisplayName, isTextSelectionValid, AIButton, getElementOverflowPosition } from './chunk-QMYT5WXO.js';
|
|
2
|
+
import { createCodeHighlighter, getCodeBlockTitle, resolveShikiTheme } from './chunk-ZY5KLA7T.js';
|
|
2
3
|
import * as React from 'react';
|
|
3
4
|
import { createContext, memo, forwardRef, useState, useCallback, useRef, useEffect, useMemo, useLayoutEffect, useContext, Children, isValidElement, cloneElement } from 'react';
|
|
4
5
|
import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
|
|
5
6
|
import { PluginKey, Plugin, TextSelection, Selection, NodeSelection } from '@tiptap/pm/state';
|
|
6
7
|
import { Node, mergeAttributes, ReactNodeViewRenderer, Extension as Extension$1, NodeViewWrapper, NodeViewContent, useEditorState, EditorContext, EditorContent, useEditor, isNodeSelection } from '@tiptap/react';
|
|
7
|
-
import { Extension,
|
|
8
|
+
import { Extension, mergeAttributes as mergeAttributes$1, Node as Node$1, generateJSON, generateHTML } from '@tiptap/core';
|
|
8
9
|
import { Fragment, Slice } from '@tiptap/pm/model';
|
|
10
|
+
import { CodeBlock } from '@tiptap/extension-code-block';
|
|
11
|
+
import { MarkdownManager, Markdown } from '@tiptap/markdown';
|
|
12
|
+
import { Marked } from 'marked';
|
|
9
13
|
import { shift, flip, offset, useMergeRefs, useFloating, autoUpdate, useTransitionStyles, useDismiss, useInteractions, FloatingPortal, size } from '@floating-ui/react';
|
|
10
14
|
import { Doc } from 'yjs';
|
|
11
15
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
|
12
16
|
import throttle from 'lodash.throttle';
|
|
13
17
|
import { createPortal } from 'react-dom';
|
|
14
18
|
import { StarterKit } from '@tiptap/starter-kit';
|
|
15
|
-
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
|
|
16
|
-
import { createLowlight, common } from 'lowlight';
|
|
17
19
|
import { Mention } from '@tiptap/extension-mention';
|
|
18
20
|
import { TaskList, TaskItem } from '@tiptap/extension-list';
|
|
19
21
|
import { TextStyle, FontSize, FontFamily, Color } from '@tiptap/extension-text-style';
|
|
@@ -31,11 +33,12 @@ import { Emoji, gitHubEmojis } from '@tiptap/extension-emoji';
|
|
|
31
33
|
import { TableOfContents, getHierarchicalIndexes } from '@tiptap/extension-table-of-contents';
|
|
32
34
|
import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule';
|
|
33
35
|
import { Image as Image$1 } from '@tiptap/extension-image';
|
|
36
|
+
import { DecorationSet, Decoration } from '@tiptap/pm/view';
|
|
37
|
+
import { closeHistory } from '@tiptap/pm/history';
|
|
34
38
|
import { Table } from '@tiptap/extension-table/table';
|
|
35
39
|
import { TableCell, TableHeader, TableRow } from '@tiptap/extension-table';
|
|
36
40
|
import { columnResizing, tableEditing, cellAround, TableView, CellSelection, moveTableRow, moveTableColumn, TableMap, findTable, mergeCells, splitCell, selectedRect, deleteCellSelection, selectionCell, isInTable, columnResizingPluginKey, deleteRow, deleteColumn, toggleHeader, addRowBefore, addRowAfter, addColumnBefore, addColumnAfter, rowIsHeader, columnIsHeader } from '@tiptap/pm/tables';
|
|
37
41
|
import { canJoin, Mapping } from '@tiptap/pm/transform';
|
|
38
|
-
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
|
39
42
|
import * as Ariakit2 from '@ariakit/react';
|
|
40
43
|
import { useHotkeys } from 'react-hotkeys-hook';
|
|
41
44
|
import '@base-ui/react/merge-props';
|
|
@@ -1347,6 +1350,1670 @@ var PasteDropMedia = Extension$1.create({
|
|
|
1347
1350
|
];
|
|
1348
1351
|
}
|
|
1349
1352
|
});
|
|
1353
|
+
|
|
1354
|
+
// src/components/tiptap-node/collapsible-node/markdown-container.ts
|
|
1355
|
+
var OPENER_RE = /^ {0,3}(:{3,})[ \t]*([\w-]+)[ \t]*(.*?)[ \t]*$/;
|
|
1356
|
+
var CLOSER_RE = /^ {0,3}(:{3,})[ \t]*$/;
|
|
1357
|
+
var FENCE_OPEN_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
1358
|
+
function parseContainerOpener(line) {
|
|
1359
|
+
const m = OPENER_RE.exec(line);
|
|
1360
|
+
if (!m) return null;
|
|
1361
|
+
return { colons: m[1].length, name: m[2], info: m[3] };
|
|
1362
|
+
}
|
|
1363
|
+
function parseContainerCloser(line) {
|
|
1364
|
+
const m = CLOSER_RE.exec(line);
|
|
1365
|
+
return m ? m[1].length : null;
|
|
1366
|
+
}
|
|
1367
|
+
function matchFenceOpen(line) {
|
|
1368
|
+
const m = FENCE_OPEN_RE.exec(line);
|
|
1369
|
+
if (!m) return null;
|
|
1370
|
+
const char = m[2][0];
|
|
1371
|
+
if (char === "`" && m[3].includes("`")) return null;
|
|
1372
|
+
return { char, length: m[2].length, indent: m[1].length, info: m[3] };
|
|
1373
|
+
}
|
|
1374
|
+
function isFenceClose(line, fence) {
|
|
1375
|
+
const re = new RegExp(`^ {0,3}${fence.char === "`" ? "`" : "~"}{${fence.length},}[ \\t]*$`);
|
|
1376
|
+
return re.test(line);
|
|
1377
|
+
}
|
|
1378
|
+
function scanContainer(lines) {
|
|
1379
|
+
const open = parseContainerOpener(lines[0] ?? "");
|
|
1380
|
+
if (!open) return null;
|
|
1381
|
+
const stack = [open.colons];
|
|
1382
|
+
let fence = null;
|
|
1383
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
1384
|
+
const line = lines[i];
|
|
1385
|
+
if (fence) {
|
|
1386
|
+
if (isFenceClose(line, fence)) fence = null;
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
const fenceOpen = matchFenceOpen(line);
|
|
1390
|
+
if (fenceOpen) {
|
|
1391
|
+
fence = fenceOpen;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
const closer = parseContainerCloser(line);
|
|
1395
|
+
if (closer !== null) {
|
|
1396
|
+
const top = stack[stack.length - 1];
|
|
1397
|
+
if (top <= closer) {
|
|
1398
|
+
stack.pop();
|
|
1399
|
+
if (stack.length === 0) {
|
|
1400
|
+
return {
|
|
1401
|
+
name: open.name,
|
|
1402
|
+
info: open.info,
|
|
1403
|
+
colons: open.colons,
|
|
1404
|
+
bodyLines: lines.slice(1, i),
|
|
1405
|
+
lineCount: i + 1
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
const nested = parseContainerOpener(line);
|
|
1412
|
+
if (nested) stack.push(nested.colons);
|
|
1413
|
+
}
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
var ALL_CONTAINER_NAMES = [
|
|
1417
|
+
"details",
|
|
1418
|
+
"info",
|
|
1419
|
+
"tip",
|
|
1420
|
+
"warning",
|
|
1421
|
+
"danger",
|
|
1422
|
+
"param",
|
|
1423
|
+
"code-group"
|
|
1424
|
+
];
|
|
1425
|
+
function containsCompleteContainer(text, names = ALL_CONTAINER_NAMES) {
|
|
1426
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
1427
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
1428
|
+
const open = parseContainerOpener(lines[i]);
|
|
1429
|
+
if (!open || !names.includes(open.name)) continue;
|
|
1430
|
+
if (scanContainer(lines.slice(i))) return true;
|
|
1431
|
+
}
|
|
1432
|
+
return false;
|
|
1433
|
+
}
|
|
1434
|
+
var REQUIRED_WORDS = /* @__PURE__ */ new Set(["必选", "required"]);
|
|
1435
|
+
var DEFAULT_WORDS = /* @__PURE__ */ new Set(["默认", "default"]);
|
|
1436
|
+
function tokenizeInfo(info) {
|
|
1437
|
+
const out = [];
|
|
1438
|
+
const re = /`([^`]*)`|(\S+)/g;
|
|
1439
|
+
let m = re.exec(info);
|
|
1440
|
+
while (m) {
|
|
1441
|
+
if (m[1] !== void 0) out.push({ kind: "code", value: m[1] });
|
|
1442
|
+
else out.push({ kind: "word", value: m[2] });
|
|
1443
|
+
m = re.exec(info);
|
|
1444
|
+
}
|
|
1445
|
+
return out;
|
|
1446
|
+
}
|
|
1447
|
+
function parseParamInfo(info) {
|
|
1448
|
+
const result = { name: "", type: null, required: false, default: null };
|
|
1449
|
+
const tokens = tokenizeInfo(info);
|
|
1450
|
+
let expectDefault = false;
|
|
1451
|
+
for (const token of tokens) {
|
|
1452
|
+
if (expectDefault) {
|
|
1453
|
+
result.default = token.value;
|
|
1454
|
+
expectDefault = false;
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
if (token.kind === "code") {
|
|
1458
|
+
if (result.type === null) result.type = token.value;
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (REQUIRED_WORDS.has(token.value)) {
|
|
1462
|
+
result.required = true;
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (DEFAULT_WORDS.has(token.value)) {
|
|
1466
|
+
expectDefault = true;
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
if (!result.name) result.name = token.value;
|
|
1470
|
+
}
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
function formatParamInfo(attrs) {
|
|
1474
|
+
const parts = [];
|
|
1475
|
+
const name = attrs.name.trim();
|
|
1476
|
+
if (name) parts.push(name.replace(/\s+/g, "-"));
|
|
1477
|
+
if (attrs.type) parts.push(`\`${attrs.type.replace(/`/g, "'")}\``);
|
|
1478
|
+
if (attrs.required) parts.push("必选");
|
|
1479
|
+
if (attrs.default !== null && attrs.default !== void 0)
|
|
1480
|
+
parts.push(`默认 \`${attrs.default.replace(/`/g, "'")}\``);
|
|
1481
|
+
return parts.join(" ");
|
|
1482
|
+
}
|
|
1483
|
+
function containerColons(body) {
|
|
1484
|
+
let longest = 2;
|
|
1485
|
+
for (const line of body.split("\n")) {
|
|
1486
|
+
const m = /^ {0,3}(:{3,})/.exec(line);
|
|
1487
|
+
if (m) longest = Math.max(longest, m[1].length);
|
|
1488
|
+
}
|
|
1489
|
+
return longest + 1;
|
|
1490
|
+
}
|
|
1491
|
+
function renderContainerMarkdown(name, info, body) {
|
|
1492
|
+
const fence = ":".repeat(containerColons(body));
|
|
1493
|
+
const head = info ? `${fence} ${name} ${info}` : `${fence} ${name}`;
|
|
1494
|
+
return body ? `${head}
|
|
1495
|
+
${body}
|
|
1496
|
+
${fence}` : `${head}
|
|
1497
|
+
${fence}`;
|
|
1498
|
+
}
|
|
1499
|
+
function createContainerTokenizer(tokenName, names) {
|
|
1500
|
+
const startRe = new RegExp(`^ {0,3}:{3,}[ \\t]*(?:${names.join("|")})(?![\\w-])`, "m");
|
|
1501
|
+
return {
|
|
1502
|
+
name: tokenName,
|
|
1503
|
+
level: "block",
|
|
1504
|
+
start: (src) => {
|
|
1505
|
+
const m = startRe.exec(src);
|
|
1506
|
+
return m ? m.index : -1;
|
|
1507
|
+
},
|
|
1508
|
+
tokenize: (src, _tokens, lexer) => {
|
|
1509
|
+
const lines = src.split("\n");
|
|
1510
|
+
const open = parseContainerOpener(lines[0] ?? "");
|
|
1511
|
+
if (!open || !names.includes(open.name)) return void 0;
|
|
1512
|
+
const scanned = scanContainer(lines);
|
|
1513
|
+
if (!scanned) return void 0;
|
|
1514
|
+
const body = scanned.bodyLines.join("\n");
|
|
1515
|
+
return {
|
|
1516
|
+
type: tokenName,
|
|
1517
|
+
raw: lines.slice(0, scanned.lineCount).join("\n"),
|
|
1518
|
+
name: scanned.name,
|
|
1519
|
+
info: scanned.info,
|
|
1520
|
+
body,
|
|
1521
|
+
tokens: body.trim() ? lexer.blockTokens(body) : []
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
// src/components/tiptap-node/code-block-node/code-group-markdown.ts
|
|
1528
|
+
var CODE_GROUP_TOKEN = "codeGroup";
|
|
1529
|
+
var OPENER_RE2 = /^ {0,3}:::[ \t]*code-group[ \t]*$/;
|
|
1530
|
+
var BLANK_RE = /^[ \t]*$/;
|
|
1531
|
+
function parseFenceInfo(info) {
|
|
1532
|
+
const trimmed = info.trim();
|
|
1533
|
+
if (!trimmed) return { language: null, label: null };
|
|
1534
|
+
let i = 0;
|
|
1535
|
+
let language = "";
|
|
1536
|
+
while (i < trimmed.length && !/[\s[]/.test(trimmed[i])) {
|
|
1537
|
+
language += trimmed[i];
|
|
1538
|
+
i += 1;
|
|
1539
|
+
}
|
|
1540
|
+
while (i < trimmed.length && /\s/.test(trimmed[i])) i += 1;
|
|
1541
|
+
let label = null;
|
|
1542
|
+
if (trimmed[i] === "[") {
|
|
1543
|
+
i += 1;
|
|
1544
|
+
let text = "";
|
|
1545
|
+
let closed = false;
|
|
1546
|
+
while (i < trimmed.length) {
|
|
1547
|
+
const ch = trimmed[i];
|
|
1548
|
+
if (ch === "\\" && i + 1 < trimmed.length) {
|
|
1549
|
+
text += trimmed[i + 1];
|
|
1550
|
+
i += 2;
|
|
1551
|
+
continue;
|
|
1552
|
+
}
|
|
1553
|
+
if (ch === "]") {
|
|
1554
|
+
closed = true;
|
|
1555
|
+
break;
|
|
1556
|
+
}
|
|
1557
|
+
text += ch;
|
|
1558
|
+
i += 1;
|
|
1559
|
+
}
|
|
1560
|
+
if (closed) label = text.trim() || null;
|
|
1561
|
+
}
|
|
1562
|
+
return { language: language || null, label };
|
|
1563
|
+
}
|
|
1564
|
+
function escapeFenceLabel(label) {
|
|
1565
|
+
return label.replace(/[\\[\]]/g, (ch) => `\\${ch}`).replace(/[\r\n]+/g, " ");
|
|
1566
|
+
}
|
|
1567
|
+
function formatFenceInfo(language, label) {
|
|
1568
|
+
const parts = [];
|
|
1569
|
+
const lang = (language ?? "").trim();
|
|
1570
|
+
if (lang && !/[\s[\]`]/.test(lang)) parts.push(lang);
|
|
1571
|
+
const text = (label ?? "").trim();
|
|
1572
|
+
if (text) parts.push(`[${escapeFenceLabel(text)}]`);
|
|
1573
|
+
return parts.join(" ");
|
|
1574
|
+
}
|
|
1575
|
+
function chooseFence(source) {
|
|
1576
|
+
let longest = 0;
|
|
1577
|
+
for (const run of source.match(/`+/g) ?? []) longest = Math.max(longest, run.length);
|
|
1578
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
1579
|
+
}
|
|
1580
|
+
function fenceInfoFromRaw(raw) {
|
|
1581
|
+
const firstLine = raw.split("\n", 1)[0] ?? "";
|
|
1582
|
+
const m = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(firstLine);
|
|
1583
|
+
return m ? m[2] : null;
|
|
1584
|
+
}
|
|
1585
|
+
function scanFenceBlocks(lines) {
|
|
1586
|
+
const blocks = [];
|
|
1587
|
+
let i = 0;
|
|
1588
|
+
while (i < lines.length) {
|
|
1589
|
+
const line = lines[i];
|
|
1590
|
+
if (BLANK_RE.test(line)) {
|
|
1591
|
+
i += 1;
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const fence = matchFenceOpen(line);
|
|
1595
|
+
if (!fence) return null;
|
|
1596
|
+
const content = [];
|
|
1597
|
+
let j = i + 1;
|
|
1598
|
+
let closed = false;
|
|
1599
|
+
while (j < lines.length) {
|
|
1600
|
+
if (isFenceClose(lines[j], fence)) {
|
|
1601
|
+
closed = true;
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
content.push(stripIndent(lines[j], fence.indent));
|
|
1605
|
+
j += 1;
|
|
1606
|
+
}
|
|
1607
|
+
if (!closed) return null;
|
|
1608
|
+
const { language, label } = parseFenceInfo(fence.info);
|
|
1609
|
+
blocks.push({ language, label, source: content.join("\n") });
|
|
1610
|
+
i = j + 1;
|
|
1611
|
+
}
|
|
1612
|
+
return blocks;
|
|
1613
|
+
}
|
|
1614
|
+
function scanCodeGroupPartial(lines) {
|
|
1615
|
+
const blocks = [];
|
|
1616
|
+
let i = 1;
|
|
1617
|
+
while (i < lines.length) {
|
|
1618
|
+
const line = lines[i];
|
|
1619
|
+
if (BLANK_RE.test(line)) {
|
|
1620
|
+
i += 1;
|
|
1621
|
+
continue;
|
|
1622
|
+
}
|
|
1623
|
+
const fence = matchFenceOpen(line);
|
|
1624
|
+
if (!fence) return { status: "invalid" };
|
|
1625
|
+
const content = [];
|
|
1626
|
+
let j = i + 1;
|
|
1627
|
+
let closed = false;
|
|
1628
|
+
while (j < lines.length) {
|
|
1629
|
+
if (isFenceClose(lines[j], fence)) {
|
|
1630
|
+
closed = true;
|
|
1631
|
+
break;
|
|
1632
|
+
}
|
|
1633
|
+
content.push(stripIndent(lines[j], fence.indent));
|
|
1634
|
+
j += 1;
|
|
1635
|
+
}
|
|
1636
|
+
if (!closed) return { status: "partial", blocks };
|
|
1637
|
+
const { language, label } = parseFenceInfo(fence.info);
|
|
1638
|
+
blocks.push({ language, label, source: content.join("\n") });
|
|
1639
|
+
i = j + 1;
|
|
1640
|
+
}
|
|
1641
|
+
return { status: "partial", blocks };
|
|
1642
|
+
}
|
|
1643
|
+
function scanCodeGroupLines(lines) {
|
|
1644
|
+
if (lines.length === 0 || !OPENER_RE2.test(lines[0])) return { status: "invalid" };
|
|
1645
|
+
const container = scanContainer(lines);
|
|
1646
|
+
if (container && container.name === "code-group") {
|
|
1647
|
+
const blocks = scanFenceBlocks(container.bodyLines);
|
|
1648
|
+
if (!blocks || blocks.length === 0) return { status: "invalid" };
|
|
1649
|
+
return { status: "complete", blocks, lineCount: container.lineCount };
|
|
1650
|
+
}
|
|
1651
|
+
return scanCodeGroupPartial(lines);
|
|
1652
|
+
}
|
|
1653
|
+
function stripIndent(line, indent) {
|
|
1654
|
+
let n = 0;
|
|
1655
|
+
while (n < indent && line[n] === " ") n += 1;
|
|
1656
|
+
return line.slice(n);
|
|
1657
|
+
}
|
|
1658
|
+
function scanCodeGroup(src) {
|
|
1659
|
+
const lines = src.split("\n");
|
|
1660
|
+
const result = scanCodeGroupLines(lines);
|
|
1661
|
+
if (result.status !== "complete") return null;
|
|
1662
|
+
return { raw: lines.slice(0, result.lineCount).join("\n"), blocks: result.blocks };
|
|
1663
|
+
}
|
|
1664
|
+
var OPENER_START_RE = /^ {0,3}:::[ \t]*code-group[ \t]*$/m;
|
|
1665
|
+
var codeGroupTokenizer = {
|
|
1666
|
+
name: CODE_GROUP_TOKEN,
|
|
1667
|
+
level: "block",
|
|
1668
|
+
// 让 `::: code-group` 能打断前面的段落(与 fence 行为一致)
|
|
1669
|
+
start: (src) => {
|
|
1670
|
+
const m = OPENER_START_RE.exec(src);
|
|
1671
|
+
return m ? m.index : -1;
|
|
1672
|
+
},
|
|
1673
|
+
tokenize: (src) => {
|
|
1674
|
+
const scanned = scanCodeGroup(src);
|
|
1675
|
+
if (!scanned) return void 0;
|
|
1676
|
+
return { type: CODE_GROUP_TOKEN, raw: scanned.raw, blocks: scanned.blocks, tokens: [] };
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
function codeBlockNode(helpers, block) {
|
|
1680
|
+
return helpers.createNode(
|
|
1681
|
+
"codeBlock",
|
|
1682
|
+
{ language: block.language, label: block.label },
|
|
1683
|
+
block.source ? [helpers.createTextNode(block.source)] : []
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
function parseCodeGroupToken(token, helpers) {
|
|
1687
|
+
const blocks = token.blocks ?? [];
|
|
1688
|
+
if (blocks.length === 0) return [];
|
|
1689
|
+
return helpers.createNode(
|
|
1690
|
+
CODE_GROUP_TOKEN,
|
|
1691
|
+
{},
|
|
1692
|
+
blocks.map((b) => codeBlockNode(helpers, b))
|
|
1693
|
+
);
|
|
1694
|
+
}
|
|
1695
|
+
function parseCodeBlockToken(token, helpers) {
|
|
1696
|
+
const raw = token.raw ?? "";
|
|
1697
|
+
const info = fenceInfoFromRaw(raw);
|
|
1698
|
+
if (info === null && token.codeBlockStyle !== "indented") return [];
|
|
1699
|
+
const { language, label } = info === null ? { language: null, label: null } : parseFenceInfo(info);
|
|
1700
|
+
const text = typeof token.text === "string" ? token.text : "";
|
|
1701
|
+
return helpers.createNode(
|
|
1702
|
+
"codeBlock",
|
|
1703
|
+
{ language, label },
|
|
1704
|
+
text ? [helpers.createTextNode(text)] : []
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
function codeBlockSourceFromJSON(node) {
|
|
1708
|
+
return (node.content ?? []).map((n) => n.type === "text" ? n.text ?? "" : "").join("");
|
|
1709
|
+
}
|
|
1710
|
+
function renderCodeBlockMarkdown(node) {
|
|
1711
|
+
const attrs = node.attrs ?? {};
|
|
1712
|
+
const source = codeBlockSourceFromJSON(node);
|
|
1713
|
+
const fence = chooseFence(source);
|
|
1714
|
+
const info = formatFenceInfo(attrs.language, attrs.label);
|
|
1715
|
+
return `${fence}${info}
|
|
1716
|
+
${source}
|
|
1717
|
+
${fence}`;
|
|
1718
|
+
}
|
|
1719
|
+
function renderCodeGroupMarkdown(node, _helpers) {
|
|
1720
|
+
const children = (node.content ?? []).filter((c) => c.type === "codeBlock");
|
|
1721
|
+
const body = children.map(renderCodeBlockMarkdown).join("\n\n");
|
|
1722
|
+
return `::: code-group
|
|
1723
|
+
${body}
|
|
1724
|
+
:::`;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// src/components/tiptap-node/code-block-node/code-block-schema.ts
|
|
1728
|
+
var LANGUAGE_CLASS_PREFIX = "language-";
|
|
1729
|
+
function parseLanguageFromElement(element) {
|
|
1730
|
+
const direct = element.getAttribute("data-language");
|
|
1731
|
+
if (direct) return direct;
|
|
1732
|
+
const code = element.tagName === "CODE" ? element : element.querySelector("code");
|
|
1733
|
+
const candidates = [element, code].filter((el) => Boolean(el));
|
|
1734
|
+
for (const el of candidates) {
|
|
1735
|
+
for (const cls of Array.from(el.classList)) {
|
|
1736
|
+
if (cls.startsWith(LANGUAGE_CLASS_PREFIX)) {
|
|
1737
|
+
const lang = cls.slice(LANGUAGE_CLASS_PREFIX.length);
|
|
1738
|
+
if (lang) return lang;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
function getCodeBlockSource(node) {
|
|
1745
|
+
return node.textContent;
|
|
1746
|
+
}
|
|
1747
|
+
var CodeBlockSchema = CodeBlock.extend({
|
|
1748
|
+
// Markdown:fence info 解析 `语言 [标签]`,输出时按源码挑足够长的 fence
|
|
1749
|
+
markdownTokenName: "code",
|
|
1750
|
+
parseMarkdown: parseCodeBlockToken,
|
|
1751
|
+
renderMarkdown: (node) => renderCodeBlockMarkdown(node),
|
|
1752
|
+
addAttributes() {
|
|
1753
|
+
return {
|
|
1754
|
+
language: {
|
|
1755
|
+
default: null,
|
|
1756
|
+
parseHTML: (element) => parseLanguageFromElement(element),
|
|
1757
|
+
// pre 上的 data-language 与 code 上的 class 由节点级 renderHTML 统一产出
|
|
1758
|
+
rendered: false
|
|
1759
|
+
},
|
|
1760
|
+
label: {
|
|
1761
|
+
default: null,
|
|
1762
|
+
parseHTML: (element) => element.getAttribute("data-label") || null,
|
|
1763
|
+
rendered: false
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
},
|
|
1767
|
+
parseHTML() {
|
|
1768
|
+
return [
|
|
1769
|
+
{
|
|
1770
|
+
tag: "pre",
|
|
1771
|
+
preserveWhitespace: "full",
|
|
1772
|
+
// 高亮后的 <span> 只取文本;pre 里可能被别的编辑器塞进 <code> 之外的包装,
|
|
1773
|
+
// 有 <code> 就以它为内容根,否则整段文本
|
|
1774
|
+
contentElement: (node) => {
|
|
1775
|
+
const el = node;
|
|
1776
|
+
return el.querySelector("code") ?? el;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
];
|
|
1780
|
+
},
|
|
1781
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
1782
|
+
const { language, label } = node.attrs;
|
|
1783
|
+
return [
|
|
1784
|
+
"pre",
|
|
1785
|
+
mergeAttributes$1(this.options.HTMLAttributes, HTMLAttributes, {
|
|
1786
|
+
"data-language": language || null,
|
|
1787
|
+
"data-label": label || null
|
|
1788
|
+
}),
|
|
1789
|
+
["code", { class: language ? `${LANGUAGE_CLASS_PREFIX}${language}` : null }, 0]
|
|
1790
|
+
];
|
|
1791
|
+
}
|
|
1792
|
+
});
|
|
1793
|
+
var COPY_FEEDBACK_MS = 1600;
|
|
1794
|
+
async function copyText(text) {
|
|
1795
|
+
try {
|
|
1796
|
+
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
1797
|
+
await navigator.clipboard.writeText(text);
|
|
1798
|
+
return true;
|
|
1799
|
+
}
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
try {
|
|
1803
|
+
if (typeof document === "undefined") return false;
|
|
1804
|
+
const area = document.createElement("textarea");
|
|
1805
|
+
area.value = text;
|
|
1806
|
+
area.setAttribute("readonly", "");
|
|
1807
|
+
area.style.position = "fixed";
|
|
1808
|
+
area.style.opacity = "0";
|
|
1809
|
+
document.body.appendChild(area);
|
|
1810
|
+
area.select();
|
|
1811
|
+
const ok = document.execCommand("copy");
|
|
1812
|
+
document.body.removeChild(area);
|
|
1813
|
+
return ok;
|
|
1814
|
+
} catch {
|
|
1815
|
+
return false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
function useCopyFeedback() {
|
|
1819
|
+
const [state, setState] = useState("idle");
|
|
1820
|
+
const timer = useRef(null);
|
|
1821
|
+
useEffect(
|
|
1822
|
+
() => () => {
|
|
1823
|
+
if (timer.current) clearTimeout(timer.current);
|
|
1824
|
+
},
|
|
1825
|
+
[]
|
|
1826
|
+
);
|
|
1827
|
+
const copy = (text) => {
|
|
1828
|
+
void copyText(text).then((ok) => {
|
|
1829
|
+
setState(ok ? "copied" : "failed");
|
|
1830
|
+
if (timer.current) clearTimeout(timer.current);
|
|
1831
|
+
timer.current = setTimeout(() => setState("idle"), COPY_FEEDBACK_MS);
|
|
1832
|
+
});
|
|
1833
|
+
};
|
|
1834
|
+
return [state, copy];
|
|
1835
|
+
}
|
|
1836
|
+
var COPY_LABELS = {
|
|
1837
|
+
idle: "复制",
|
|
1838
|
+
copied: "已复制",
|
|
1839
|
+
failed: "复制失败"
|
|
1840
|
+
};
|
|
1841
|
+
function useHighlighter(editor) {
|
|
1842
|
+
const storage = editor.storage;
|
|
1843
|
+
return storage.codeHighlight?.highlighter ?? null;
|
|
1844
|
+
}
|
|
1845
|
+
function CodeBlockNodeView(props) {
|
|
1846
|
+
const { node, editor } = props;
|
|
1847
|
+
const attrs = node.attrs;
|
|
1848
|
+
const highlighter = useHighlighter(editor);
|
|
1849
|
+
const title = getCodeBlockTitle(
|
|
1850
|
+
attrs,
|
|
1851
|
+
(input) => highlighter?.normalizeLanguage(input) ?? (input ?? "").trim().toLowerCase()
|
|
1852
|
+
);
|
|
1853
|
+
const [copyState, copy] = useCopyFeedback();
|
|
1854
|
+
return /* @__PURE__ */ jsxs(
|
|
1855
|
+
NodeViewWrapper,
|
|
1856
|
+
{
|
|
1857
|
+
className: "tiptap-code-block",
|
|
1858
|
+
"data-language": attrs.language ?? void 0,
|
|
1859
|
+
"data-label": attrs.label ?? void 0,
|
|
1860
|
+
children: [
|
|
1861
|
+
/* @__PURE__ */ jsxs("div", { className: "tiptap-code-block__header", contentEditable: false, children: [
|
|
1862
|
+
/* @__PURE__ */ jsx("span", { className: "tiptap-code-block__title", children: title }),
|
|
1863
|
+
/* @__PURE__ */ jsx(
|
|
1864
|
+
"button",
|
|
1865
|
+
{
|
|
1866
|
+
type: "button",
|
|
1867
|
+
className: "tiptap-code-block__copy",
|
|
1868
|
+
"data-state": copyState,
|
|
1869
|
+
"aria-live": "polite",
|
|
1870
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
1871
|
+
onClick: () => copy(getCodeBlockSource(node)),
|
|
1872
|
+
children: COPY_LABELS[copyState]
|
|
1873
|
+
}
|
|
1874
|
+
)
|
|
1875
|
+
] }),
|
|
1876
|
+
/* @__PURE__ */ jsx(
|
|
1877
|
+
NodeViewContent,
|
|
1878
|
+
{
|
|
1879
|
+
as: "pre",
|
|
1880
|
+
className: "tiptap-code-block__pre",
|
|
1881
|
+
style: { whiteSpace: "pre" }
|
|
1882
|
+
}
|
|
1883
|
+
)
|
|
1884
|
+
]
|
|
1885
|
+
}
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/components/tiptap-node/code-block-node/code-block-node-extension.ts
|
|
1890
|
+
var CodeBlockExtension = CodeBlockSchema.extend({
|
|
1891
|
+
addNodeView() {
|
|
1892
|
+
return ReactNodeViewRenderer(CodeBlockNodeView, {
|
|
1893
|
+
contentDOMElementTag: "code"
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
var CODE_GROUP_NAME = CODE_GROUP_TOKEN;
|
|
1898
|
+
var CODE_GROUP_ATTR = "data-code-group";
|
|
1899
|
+
function getCodeGroupLabel(child, normalizeLanguage) {
|
|
1900
|
+
return getCodeBlockTitle(child.attrs, normalizeLanguage);
|
|
1901
|
+
}
|
|
1902
|
+
function getCodeGroupSource(child) {
|
|
1903
|
+
return child.textContent;
|
|
1904
|
+
}
|
|
1905
|
+
function getCodeGroupTabKey(child, index) {
|
|
1906
|
+
const id = child.attrs.id;
|
|
1907
|
+
return id ? `id:${id}` : `index:${index}`;
|
|
1908
|
+
}
|
|
1909
|
+
function getCodeGroupTabs(group, normalizeLanguage) {
|
|
1910
|
+
const tabs = [];
|
|
1911
|
+
group.forEach((child, _offset, index) => {
|
|
1912
|
+
tabs.push({
|
|
1913
|
+
key: getCodeGroupTabKey(child, index),
|
|
1914
|
+
index,
|
|
1915
|
+
label: getCodeGroupLabel(child, normalizeLanguage),
|
|
1916
|
+
language: child.attrs.language ?? null,
|
|
1917
|
+
source: getCodeGroupSource(child)
|
|
1918
|
+
});
|
|
1919
|
+
});
|
|
1920
|
+
return tabs;
|
|
1921
|
+
}
|
|
1922
|
+
function findCodeGroupDepth($pos) {
|
|
1923
|
+
for (let depth = $pos.depth; depth > 0; depth -= 1) {
|
|
1924
|
+
if ($pos.node(depth).type.name === CODE_GROUP_NAME) return depth;
|
|
1925
|
+
}
|
|
1926
|
+
return null;
|
|
1927
|
+
}
|
|
1928
|
+
function exitCodeGroup(tr, editor) {
|
|
1929
|
+
const { $from } = tr.selection;
|
|
1930
|
+
const depth = findCodeGroupDepth($from);
|
|
1931
|
+
if (depth === null) return false;
|
|
1932
|
+
const after = $from.after(depth);
|
|
1933
|
+
const nodeAfter = tr.doc.nodeAt(after);
|
|
1934
|
+
if (nodeAfter) {
|
|
1935
|
+
tr.setSelection(Selection.near(tr.doc.resolve(after)));
|
|
1936
|
+
return true;
|
|
1937
|
+
}
|
|
1938
|
+
const paragraph = editor.schema.nodes.paragraph;
|
|
1939
|
+
if (!paragraph) return false;
|
|
1940
|
+
tr.insert(after, paragraph.create());
|
|
1941
|
+
tr.setSelection(TextSelection.create(tr.doc, after + 1));
|
|
1942
|
+
return true;
|
|
1943
|
+
}
|
|
1944
|
+
function elementChildren(el) {
|
|
1945
|
+
const out = [];
|
|
1946
|
+
const kids = el.childNodes;
|
|
1947
|
+
for (let i = 0; i < kids.length; i += 1) {
|
|
1948
|
+
const n = kids.item(i);
|
|
1949
|
+
if (n && n.nodeType === 1) out.push(n);
|
|
1950
|
+
}
|
|
1951
|
+
return out;
|
|
1952
|
+
}
|
|
1953
|
+
var CodeGroupSchema = Node$1.create({
|
|
1954
|
+
name: CODE_GROUP_NAME,
|
|
1955
|
+
group: "block",
|
|
1956
|
+
content: "codeBlock+",
|
|
1957
|
+
isolating: true,
|
|
1958
|
+
defining: true,
|
|
1959
|
+
selectable: true,
|
|
1960
|
+
// 快捷键要抢在 codeBlock(默认 100)前面
|
|
1961
|
+
priority: 110,
|
|
1962
|
+
// Markdown:`::: code-group … :::` 块级 tokenizer 与解析 / 输出
|
|
1963
|
+
markdownTokenName: CODE_GROUP_TOKEN,
|
|
1964
|
+
markdownTokenizer: codeGroupTokenizer,
|
|
1965
|
+
parseMarkdown: parseCodeGroupToken,
|
|
1966
|
+
renderMarkdown: renderCodeGroupMarkdown,
|
|
1967
|
+
parseHTML() {
|
|
1968
|
+
return [
|
|
1969
|
+
{
|
|
1970
|
+
tag: `div[${CODE_GROUP_ATTR}]`,
|
|
1971
|
+
getAttrs: (element) => {
|
|
1972
|
+
const kids = elementChildren(element);
|
|
1973
|
+
if (kids.length === 0) return false;
|
|
1974
|
+
return kids.every((k) => k.tagName.toLowerCase() === "pre") ? null : false;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
];
|
|
1978
|
+
},
|
|
1979
|
+
renderHTML({ HTMLAttributes }) {
|
|
1980
|
+
return ["div", mergeAttributes$1(HTMLAttributes, { [CODE_GROUP_ATTR]: "" }), 0];
|
|
1981
|
+
},
|
|
1982
|
+
addKeyboardShortcuts() {
|
|
1983
|
+
const inGroupChild = (editor) => {
|
|
1984
|
+
const { $from, empty } = editor.state.selection;
|
|
1985
|
+
if (!empty) return false;
|
|
1986
|
+
if ($from.parent.type.name !== "codeBlock") return false;
|
|
1987
|
+
return findCodeGroupDepth($from) !== null;
|
|
1988
|
+
};
|
|
1989
|
+
return {
|
|
1990
|
+
// 三连回车:与官方 CodeBlock 相同的触发条件,但退出的是整个分组
|
|
1991
|
+
Enter: ({ editor }) => {
|
|
1992
|
+
if (!inGroupChild(editor)) return false;
|
|
1993
|
+
const { $from } = editor.state.selection;
|
|
1994
|
+
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
|
1995
|
+
if (!isAtEnd || !$from.parent.textContent.endsWith("\n\n")) return false;
|
|
1996
|
+
return editor.commands.command(({ tr }) => {
|
|
1997
|
+
tr.delete($from.pos - 2, $from.pos);
|
|
1998
|
+
return exitCodeGroup(tr, editor);
|
|
1999
|
+
});
|
|
2000
|
+
},
|
|
2001
|
+
// 末尾 ArrowDown:最后一个标签的末尾退出分组;中间标签交给官方处理(进入下一个子块)
|
|
2002
|
+
ArrowDown: ({ editor }) => {
|
|
2003
|
+
if (!inGroupChild(editor)) return false;
|
|
2004
|
+
const { $from } = editor.state.selection;
|
|
2005
|
+
const isAtEnd = $from.parentOffset === $from.parent.nodeSize - 2;
|
|
2006
|
+
if (!isAtEnd) return false;
|
|
2007
|
+
const depth = findCodeGroupDepth($from);
|
|
2008
|
+
if (depth === null) return false;
|
|
2009
|
+
const group = $from.node(depth);
|
|
2010
|
+
const isLastChild = $from.index(depth) === group.childCount - 1;
|
|
2011
|
+
if (!isLastChild) return false;
|
|
2012
|
+
return editor.commands.command(({ tr }) => exitCodeGroup(tr, editor));
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
|
|
2018
|
+
// src/components/tiptap-node/code-block-node/code-group-view-state.ts
|
|
2019
|
+
function resolveActiveIndex(keys, activeKey, previousIndex) {
|
|
2020
|
+
if (keys.length === 0) return 0;
|
|
2021
|
+
if (activeKey !== null) {
|
|
2022
|
+
const found = keys.indexOf(activeKey);
|
|
2023
|
+
if (found >= 0) return found;
|
|
2024
|
+
}
|
|
2025
|
+
return Math.min(Math.max(previousIndex, 0), keys.length - 1);
|
|
2026
|
+
}
|
|
2027
|
+
function tabKeyAction(key, focusedIndex, count) {
|
|
2028
|
+
if (count <= 0) return null;
|
|
2029
|
+
switch (key) {
|
|
2030
|
+
case "ArrowRight":
|
|
2031
|
+
return { type: "focus", index: (focusedIndex + 1) % count };
|
|
2032
|
+
case "ArrowLeft":
|
|
2033
|
+
return { type: "focus", index: (focusedIndex - 1 + count) % count };
|
|
2034
|
+
case "Home":
|
|
2035
|
+
return { type: "focus", index: 0 };
|
|
2036
|
+
case "End":
|
|
2037
|
+
return { type: "focus", index: count - 1 };
|
|
2038
|
+
case "Enter":
|
|
2039
|
+
case " ":
|
|
2040
|
+
case "Spacebar":
|
|
2041
|
+
return { type: "activate" };
|
|
2042
|
+
default:
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
function childContentRange(group, groupPos, index) {
|
|
2047
|
+
if (index < 0 || index >= group.childCount) return null;
|
|
2048
|
+
let offset6 = groupPos + 1;
|
|
2049
|
+
for (let i = 0; i < index; i += 1) offset6 += group.child(i).nodeSize;
|
|
2050
|
+
const child = group.child(index);
|
|
2051
|
+
return { from: offset6 + 1, to: offset6 + 1 + child.content.size };
|
|
2052
|
+
}
|
|
2053
|
+
function childIndexAtPos(group, groupPos, pos) {
|
|
2054
|
+
let offset6 = groupPos + 1;
|
|
2055
|
+
for (let i = 0; i < group.childCount; i += 1) {
|
|
2056
|
+
const child = group.child(i);
|
|
2057
|
+
const from = offset6 + 1;
|
|
2058
|
+
const to = from + child.content.size;
|
|
2059
|
+
if (pos >= from && pos <= to) return i;
|
|
2060
|
+
offset6 += child.nodeSize;
|
|
2061
|
+
}
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
function childEndPos(group, groupPos, index) {
|
|
2065
|
+
return childContentRange(group, groupPos, index)?.to ?? null;
|
|
2066
|
+
}
|
|
2067
|
+
var PANEL_ATTR = "data-code-panel";
|
|
2068
|
+
function useHighlighter2(editor) {
|
|
2069
|
+
const storage = editor.storage;
|
|
2070
|
+
return storage.codeHighlight?.highlighter ?? null;
|
|
2071
|
+
}
|
|
2072
|
+
function CodeGroupNodeView(props) {
|
|
2073
|
+
const { node, editor, getPos } = props;
|
|
2074
|
+
const highlighter = useHighlighter2(editor);
|
|
2075
|
+
const normalize = useCallback(
|
|
2076
|
+
(input) => highlighter?.normalizeLanguage(input) ?? (input ?? "").trim().toLowerCase(),
|
|
2077
|
+
[highlighter]
|
|
2078
|
+
);
|
|
2079
|
+
const tabs = useMemo(() => getCodeGroupTabs(node, normalize), [node, normalize]);
|
|
2080
|
+
const keys = useMemo(() => tabs.map((t) => t.key), [tabs]);
|
|
2081
|
+
const [activeKey, setActiveKey] = useState(() => keys[0] ?? null);
|
|
2082
|
+
const [previousIndex, setPreviousIndex] = useState(0);
|
|
2083
|
+
const activeIndex = resolveActiveIndex(keys, activeKey, previousIndex);
|
|
2084
|
+
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
2085
|
+
const [copyState, copy] = useCopyFeedback();
|
|
2086
|
+
const panelsRef = useRef(null);
|
|
2087
|
+
const tabRefs = useRef([]);
|
|
2088
|
+
const activeIndexRef = useRef(activeIndex);
|
|
2089
|
+
activeIndexRef.current = activeIndex;
|
|
2090
|
+
const activate = useCallback(
|
|
2091
|
+
(index) => {
|
|
2092
|
+
if (index < 0 || index >= keys.length) return;
|
|
2093
|
+
const current = activeIndexRef.current;
|
|
2094
|
+
if (index !== current) {
|
|
2095
|
+
const pos = getPos();
|
|
2096
|
+
if (typeof pos === "number") {
|
|
2097
|
+
const { selection, doc } = editor.state;
|
|
2098
|
+
const group = doc.nodeAt(pos);
|
|
2099
|
+
if (group && childIndexAtPos(group, pos, selection.from) === current) {
|
|
2100
|
+
const target = childEndPos(group, pos, index);
|
|
2101
|
+
if (target !== null) {
|
|
2102
|
+
editor.view.dispatch(editor.state.tr.setSelection(TextSelection.create(doc, target)));
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
setActiveKey(keys[index]);
|
|
2108
|
+
setPreviousIndex(index);
|
|
2109
|
+
setFocusedIndex(index);
|
|
2110
|
+
},
|
|
2111
|
+
[editor, getPos, keys]
|
|
2112
|
+
);
|
|
2113
|
+
const syncPanels = useCallback(() => {
|
|
2114
|
+
const panels = panelsRef.current;
|
|
2115
|
+
if (!panels) return;
|
|
2116
|
+
const container = panels.querySelector("[data-node-view-content-react]");
|
|
2117
|
+
if (!container) return;
|
|
2118
|
+
const children = Array.from(container.children);
|
|
2119
|
+
children.forEach((child, index) => {
|
|
2120
|
+
const active = index === activeIndexRef.current;
|
|
2121
|
+
child.setAttribute(PANEL_ATTR, active ? "active" : "hidden");
|
|
2122
|
+
child.setAttribute("role", "tabpanel");
|
|
2123
|
+
if (active) child.removeAttribute("aria-hidden");
|
|
2124
|
+
else child.setAttribute("aria-hidden", "true");
|
|
2125
|
+
});
|
|
2126
|
+
}, []);
|
|
2127
|
+
useLayoutEffect(() => {
|
|
2128
|
+
syncPanels();
|
|
2129
|
+
});
|
|
2130
|
+
useEffect(() => {
|
|
2131
|
+
const panels = panelsRef.current;
|
|
2132
|
+
if (!panels || typeof MutationObserver === "undefined") return;
|
|
2133
|
+
const observer = new MutationObserver(() => syncPanels());
|
|
2134
|
+
observer.observe(panels, { childList: true, subtree: true });
|
|
2135
|
+
return () => observer.disconnect();
|
|
2136
|
+
}, [syncPanels]);
|
|
2137
|
+
useEffect(() => {
|
|
2138
|
+
const onSelection = () => {
|
|
2139
|
+
const pos = getPos();
|
|
2140
|
+
if (typeof pos !== "number") return;
|
|
2141
|
+
const group = editor.state.doc.nodeAt(pos);
|
|
2142
|
+
if (!group || group.type !== node.type) return;
|
|
2143
|
+
const index = childIndexAtPos(group, pos, editor.state.selection.from);
|
|
2144
|
+
if (index === null || index === activeIndexRef.current) return;
|
|
2145
|
+
setActiveKey(getCodeGroupTabKey(group.child(index), index));
|
|
2146
|
+
setPreviousIndex(index);
|
|
2147
|
+
};
|
|
2148
|
+
editor.on("selectionUpdate", onSelection);
|
|
2149
|
+
return () => {
|
|
2150
|
+
editor.off("selectionUpdate", onSelection);
|
|
2151
|
+
};
|
|
2152
|
+
}, [editor, getPos, node.type]);
|
|
2153
|
+
const onKeyDown = (event) => {
|
|
2154
|
+
const action = tabKeyAction(event.key, focusedIndex, keys.length);
|
|
2155
|
+
if (!action) return;
|
|
2156
|
+
event.preventDefault();
|
|
2157
|
+
if (action.type === "focus") {
|
|
2158
|
+
setFocusedIndex(action.index);
|
|
2159
|
+
tabRefs.current[action.index]?.focus();
|
|
2160
|
+
} else {
|
|
2161
|
+
activate(focusedIndex);
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
const activeTab = tabs[activeIndex];
|
|
2165
|
+
const groupId = node.attrs.id ?? "code-group";
|
|
2166
|
+
return /* @__PURE__ */ jsxs(NodeViewWrapper, { className: "tiptap-code-group", "data-code-group": "", children: [
|
|
2167
|
+
/* @__PURE__ */ jsxs("div", { className: "tiptap-code-group__bar", contentEditable: false, children: [
|
|
2168
|
+
/* @__PURE__ */ jsx(
|
|
2169
|
+
"div",
|
|
2170
|
+
{
|
|
2171
|
+
className: "tiptap-code-group__tabs",
|
|
2172
|
+
role: "tablist",
|
|
2173
|
+
"aria-label": "代码分组",
|
|
2174
|
+
onKeyDown,
|
|
2175
|
+
children: tabs.map((tab, index) => /* @__PURE__ */ jsx(
|
|
2176
|
+
"button",
|
|
2177
|
+
{
|
|
2178
|
+
ref: (el) => {
|
|
2179
|
+
tabRefs.current[index] = el;
|
|
2180
|
+
},
|
|
2181
|
+
type: "button",
|
|
2182
|
+
role: "tab",
|
|
2183
|
+
id: `${groupId}-tab-${index}`,
|
|
2184
|
+
className: "tiptap-code-group__tab",
|
|
2185
|
+
"aria-selected": index === activeIndex,
|
|
2186
|
+
tabIndex: index === focusedIndex ? 0 : -1,
|
|
2187
|
+
"data-active": index === activeIndex ? "" : void 0,
|
|
2188
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2189
|
+
onClick: () => activate(index),
|
|
2190
|
+
onFocus: () => setFocusedIndex(index),
|
|
2191
|
+
children: tab.label
|
|
2192
|
+
},
|
|
2193
|
+
tab.key
|
|
2194
|
+
))
|
|
2195
|
+
}
|
|
2196
|
+
),
|
|
2197
|
+
/* @__PURE__ */ jsx(
|
|
2198
|
+
"button",
|
|
2199
|
+
{
|
|
2200
|
+
type: "button",
|
|
2201
|
+
className: "tiptap-code-group__copy",
|
|
2202
|
+
"data-state": copyState,
|
|
2203
|
+
"aria-live": "polite",
|
|
2204
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
2205
|
+
onClick: () => copy(activeTab?.source ?? ""),
|
|
2206
|
+
children: COPY_LABELS[copyState]
|
|
2207
|
+
}
|
|
2208
|
+
)
|
|
2209
|
+
] }),
|
|
2210
|
+
/* @__PURE__ */ jsx("div", { ref: panelsRef, className: "tiptap-code-group__body", children: /* @__PURE__ */ jsx(NodeViewContent, { as: "div", className: "tiptap-code-group__panels" }) })
|
|
2211
|
+
] });
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// src/components/tiptap-node/code-block-node/code-group-node-extension.ts
|
|
2215
|
+
var CodeGroupExtension = CodeGroupSchema.extend({
|
|
2216
|
+
addNodeView() {
|
|
2217
|
+
return ReactNodeViewRenderer(CodeGroupNodeView);
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
var CONTENT_FORMATS = ["json", "html", "markdown"];
|
|
2221
|
+
function isContentFormat(value) {
|
|
2222
|
+
return typeof value === "string" && CONTENT_FORMATS.includes(value);
|
|
2223
|
+
}
|
|
2224
|
+
function freshMarked() {
|
|
2225
|
+
return new Marked();
|
|
2226
|
+
}
|
|
2227
|
+
function createMarkdownExtension() {
|
|
2228
|
+
return Markdown.configure({ marked: freshMarked() });
|
|
2229
|
+
}
|
|
2230
|
+
function createMarkdownManager(extensions) {
|
|
2231
|
+
return new MarkdownManager({ extensions, marked: freshMarked() });
|
|
2232
|
+
}
|
|
2233
|
+
var normalizeNewlines = (s) => s.replace(/\r\n?/g, "\n");
|
|
2234
|
+
function parseEditorContent(content, format, extensions) {
|
|
2235
|
+
if (content === void 0 || content === null || content === "") return { ok: true, json: null };
|
|
2236
|
+
if (format !== void 0 && !isContentFormat(format)) {
|
|
2237
|
+
return {
|
|
2238
|
+
ok: false,
|
|
2239
|
+
error: `contentFormat 只接受 "json" | "html" | "markdown",收到 ${JSON.stringify(format)}`
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
const isString = typeof content === "string";
|
|
2243
|
+
const isObject = typeof content === "object";
|
|
2244
|
+
const resolved = format ?? (isString ? "html" : "json");
|
|
2245
|
+
if (resolved === "json") {
|
|
2246
|
+
if (!isObject)
|
|
2247
|
+
return { ok: false, error: `contentFormat="json" 需要传 JSON 对象,收到 ${typeof content}` };
|
|
2248
|
+
return { ok: true, json: content };
|
|
2249
|
+
}
|
|
2250
|
+
if (!isString)
|
|
2251
|
+
return { ok: false, error: `contentFormat="${resolved}" 需要传字符串,收到 ${typeof content}` };
|
|
2252
|
+
try {
|
|
2253
|
+
if (resolved === "markdown") {
|
|
2254
|
+
return { ok: true, json: createMarkdownManager(extensions).parse(normalizeNewlines(content)) };
|
|
2255
|
+
}
|
|
2256
|
+
return { ok: true, json: generateJSON(content, extensions) };
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
return {
|
|
2259
|
+
ok: false,
|
|
2260
|
+
error: `${resolved} 内容解析失败:${error?.message ?? String(error)}`
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function serializeEditorMarkdown(json, extensions) {
|
|
2265
|
+
return createMarkdownManager(extensions).serialize(json);
|
|
2266
|
+
}
|
|
2267
|
+
function getEditorMarkdown(editor) {
|
|
2268
|
+
const manager = editor.markdown;
|
|
2269
|
+
if (manager) return manager.serialize(editor.getJSON());
|
|
2270
|
+
return serializeEditorMarkdown(editor.getJSON(), editor.extensionManager.extensions);
|
|
2271
|
+
}
|
|
2272
|
+
var escapeAttr = (s) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
2273
|
+
function collectCodeBlocks(node, out = []) {
|
|
2274
|
+
if (node.type === "codeBlock") {
|
|
2275
|
+
out.push(node);
|
|
2276
|
+
return out;
|
|
2277
|
+
}
|
|
2278
|
+
for (const child of node.content ?? []) collectCodeBlocks(child, out);
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
function codeBlockText(node) {
|
|
2282
|
+
return (node.content ?? []).map((n) => n.type === "text" ? n.text ?? "" : "").join("");
|
|
2283
|
+
}
|
|
2284
|
+
async function renderHighlightedHTML(source, options = {}) {
|
|
2285
|
+
const isEditor = typeof source.getJSON === "function";
|
|
2286
|
+
const json = isEditor ? source.getJSON() : source;
|
|
2287
|
+
const extensions = options.extensions ?? (isEditor ? source.extensionManager.extensions : void 0);
|
|
2288
|
+
if (!extensions) throw new Error("renderHighlightedHTML:传 JSON 时必须提供 extensions");
|
|
2289
|
+
const editorHighlighter = isEditor ? source.storage.codeHighlight?.highlighter : void 0;
|
|
2290
|
+
const highlighter = options.highlighter ?? editorHighlighter ?? createCodeHighlighter(options);
|
|
2291
|
+
const theme = options.theme === void 0 || options.theme === "light" || options.theme === "dark" ? resolveShikiTheme(options.shikiTheme, options.theme === "dark") : options.theme;
|
|
2292
|
+
const blocks = collectCodeBlocks(json);
|
|
2293
|
+
const rendered = await Promise.all(
|
|
2294
|
+
blocks.map(async (block) => {
|
|
2295
|
+
const attrs = block.attrs ?? {};
|
|
2296
|
+
const result = await highlighter.highlight(codeBlockText(block), attrs.language, theme);
|
|
2297
|
+
const extra = (attrs.language ? ` data-language="${escapeAttr(attrs.language)}"` : "") + (attrs.label ? ` data-label="${escapeAttr(attrs.label)}"` : "");
|
|
2298
|
+
return result.html.replace(/^<pre\b/, `<pre${extra}`);
|
|
2299
|
+
})
|
|
2300
|
+
);
|
|
2301
|
+
let index = 0;
|
|
2302
|
+
const semantic = generateHTML(json, extensions);
|
|
2303
|
+
return semantic.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/g, (match) => {
|
|
2304
|
+
const next = rendered[index];
|
|
2305
|
+
index += 1;
|
|
2306
|
+
return next ?? match;
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// src/components/tiptap-node/collapsible-node/collapsible-view-state.ts
|
|
2311
|
+
function contentRange(node, pos) {
|
|
2312
|
+
return { from: pos + 1, to: pos + 1 + node.content.size };
|
|
2313
|
+
}
|
|
2314
|
+
function isSelectionInside(node, pos, selectionFrom) {
|
|
2315
|
+
const { from, to } = contentRange(node, pos);
|
|
2316
|
+
return selectionFrom >= from && selectionFrom <= to;
|
|
2317
|
+
}
|
|
2318
|
+
function positionAfter(node, pos) {
|
|
2319
|
+
return pos + node.nodeSize;
|
|
2320
|
+
}
|
|
2321
|
+
function apiParamPath($pos, ownName) {
|
|
2322
|
+
const names = [];
|
|
2323
|
+
for (let depth = 1; depth <= $pos.depth; depth += 1) {
|
|
2324
|
+
const node = $pos.node(depth);
|
|
2325
|
+
if (node.type.name === "apiParam") names.push(String(node.attrs.name ?? ""));
|
|
2326
|
+
}
|
|
2327
|
+
if (names.length) names[names.length - 1] = ownName;
|
|
2328
|
+
else names.push(ownName);
|
|
2329
|
+
return names.filter(Boolean).join(".");
|
|
2330
|
+
}
|
|
2331
|
+
function normalizeField(raw, nullable) {
|
|
2332
|
+
const trimmed = raw.trim();
|
|
2333
|
+
if (nullable) return trimmed || null;
|
|
2334
|
+
return trimmed;
|
|
2335
|
+
}
|
|
2336
|
+
function useCollapsible(props, options = {}) {
|
|
2337
|
+
const { editor, node, getPos } = props;
|
|
2338
|
+
const [open, setOpen] = useState(options.defaultOpen ?? false);
|
|
2339
|
+
const openRef = useRef(open);
|
|
2340
|
+
openRef.current = open;
|
|
2341
|
+
useEffect(() => {
|
|
2342
|
+
const follow = () => {
|
|
2343
|
+
if (openRef.current) return;
|
|
2344
|
+
const pos = getPos();
|
|
2345
|
+
if (typeof pos !== "number") return;
|
|
2346
|
+
const current = editor.state.doc.nodeAt(pos);
|
|
2347
|
+
if (!current || current.type !== node.type) return;
|
|
2348
|
+
if (isSelectionInside(current, pos, editor.state.selection.from)) setOpen(true);
|
|
2349
|
+
};
|
|
2350
|
+
editor.on("selectionUpdate", follow);
|
|
2351
|
+
return () => {
|
|
2352
|
+
editor.off("selectionUpdate", follow);
|
|
2353
|
+
};
|
|
2354
|
+
}, [editor, getPos, node.type]);
|
|
2355
|
+
const toggle = useCallback(() => {
|
|
2356
|
+
const next = !openRef.current;
|
|
2357
|
+
if (!next) {
|
|
2358
|
+
const pos = getPos();
|
|
2359
|
+
if (typeof pos === "number") {
|
|
2360
|
+
const current = editor.state.doc.nodeAt(pos);
|
|
2361
|
+
if (current && isSelectionInside(current, pos, editor.state.selection.from)) {
|
|
2362
|
+
const target = Math.min(positionAfter(current, pos), editor.state.doc.content.size);
|
|
2363
|
+
editor.view.dispatch(
|
|
2364
|
+
editor.state.tr.setSelection(TextSelection.near(editor.state.doc.resolve(target), 1))
|
|
2365
|
+
);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
setOpen(next);
|
|
2370
|
+
}, [editor, getPos]);
|
|
2371
|
+
return { open, setOpen, toggle };
|
|
2372
|
+
}
|
|
2373
|
+
function CollapsibleShell({
|
|
2374
|
+
props,
|
|
2375
|
+
kind,
|
|
2376
|
+
open,
|
|
2377
|
+
onToggle,
|
|
2378
|
+
collapsible = true,
|
|
2379
|
+
header,
|
|
2380
|
+
actions,
|
|
2381
|
+
contentLead,
|
|
2382
|
+
className
|
|
2383
|
+
}) {
|
|
2384
|
+
const isEditable = props.editor.isEditable;
|
|
2385
|
+
return /* @__PURE__ */ jsxs(
|
|
2386
|
+
NodeViewWrapper,
|
|
2387
|
+
{
|
|
2388
|
+
className: ["tt-collapsible", `tt-collapsible--${kind}`, className].filter(Boolean).join(" "),
|
|
2389
|
+
"data-collapsible": kind,
|
|
2390
|
+
"data-open": open ? "true" : "false",
|
|
2391
|
+
children: [
|
|
2392
|
+
/* @__PURE__ */ jsxs("div", { className: "tt-collapsible__header", contentEditable: false, children: [
|
|
2393
|
+
collapsible ? /* @__PURE__ */ jsx(
|
|
2394
|
+
"button",
|
|
2395
|
+
{
|
|
2396
|
+
type: "button",
|
|
2397
|
+
className: "tt-collapsible__toggle",
|
|
2398
|
+
"aria-expanded": open,
|
|
2399
|
+
"aria-label": open ? "收起" : "展开",
|
|
2400
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2401
|
+
onClick: onToggle,
|
|
2402
|
+
onKeyDown: (e) => {
|
|
2403
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
2404
|
+
e.preventDefault();
|
|
2405
|
+
onToggle();
|
|
2406
|
+
}
|
|
2407
|
+
},
|
|
2408
|
+
children: /* @__PURE__ */ jsx("span", { className: "tt-collapsible__chevron", "aria-hidden": "true" })
|
|
2409
|
+
}
|
|
2410
|
+
) : /* @__PURE__ */ jsx(
|
|
2411
|
+
"span",
|
|
2412
|
+
{
|
|
2413
|
+
className: "tt-collapsible__toggle tt-collapsible__toggle--placeholder",
|
|
2414
|
+
"aria-hidden": "true"
|
|
2415
|
+
}
|
|
2416
|
+
),
|
|
2417
|
+
/* @__PURE__ */ jsx("div", { className: "tt-collapsible__fields", children: header }),
|
|
2418
|
+
isEditable && actions ? /* @__PURE__ */ jsx("div", { className: "tt-collapsible__actions", children: actions }) : null
|
|
2419
|
+
] }),
|
|
2420
|
+
contentLead && open ? /* @__PURE__ */ jsx("div", { className: "tt-collapsible__lead", contentEditable: false, children: contentLead }) : null,
|
|
2421
|
+
/* @__PURE__ */ jsx(NodeViewContent, { as: "div", className: "tt-collapsible__content", hidden: !open })
|
|
2422
|
+
]
|
|
2423
|
+
}
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
function textColumns(text) {
|
|
2427
|
+
let cols = 0;
|
|
2428
|
+
for (const ch of text) {
|
|
2429
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
2430
|
+
const wide = code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 126976;
|
|
2431
|
+
cols += wide ? 2 : 1;
|
|
2432
|
+
}
|
|
2433
|
+
return cols;
|
|
2434
|
+
}
|
|
2435
|
+
function InlineField({
|
|
2436
|
+
field,
|
|
2437
|
+
value,
|
|
2438
|
+
placeholder,
|
|
2439
|
+
ariaLabel,
|
|
2440
|
+
nullable,
|
|
2441
|
+
readOnly,
|
|
2442
|
+
onCommit,
|
|
2443
|
+
onEnter,
|
|
2444
|
+
className
|
|
2445
|
+
}) {
|
|
2446
|
+
const [draft, setDraft] = useState(value ?? "");
|
|
2447
|
+
const committed = useRef(value ?? "");
|
|
2448
|
+
const reverting = useRef(false);
|
|
2449
|
+
useEffect(() => {
|
|
2450
|
+
setDraft(value ?? "");
|
|
2451
|
+
committed.current = value ?? "";
|
|
2452
|
+
}, [value]);
|
|
2453
|
+
if (readOnly) {
|
|
2454
|
+
if (!value) return null;
|
|
2455
|
+
return /* @__PURE__ */ jsx(
|
|
2456
|
+
"span",
|
|
2457
|
+
{
|
|
2458
|
+
className: ["tt-field", `tt-field--${field}`, className].filter(Boolean).join(" "),
|
|
2459
|
+
"data-field": field,
|
|
2460
|
+
children: value
|
|
2461
|
+
}
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
const commit = () => {
|
|
2465
|
+
if (reverting.current) {
|
|
2466
|
+
reverting.current = false;
|
|
2467
|
+
return;
|
|
2468
|
+
}
|
|
2469
|
+
const next = normalizeField(draft, nullable);
|
|
2470
|
+
if ((next ?? "") !== committed.current) {
|
|
2471
|
+
committed.current = next ?? "";
|
|
2472
|
+
onCommit(next);
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
return /* @__PURE__ */ jsx(
|
|
2476
|
+
"input",
|
|
2477
|
+
{
|
|
2478
|
+
type: "text",
|
|
2479
|
+
"data-field": field,
|
|
2480
|
+
className: ["tt-field", `tt-field--${field}`, className].filter(Boolean).join(" "),
|
|
2481
|
+
value: draft,
|
|
2482
|
+
placeholder,
|
|
2483
|
+
"aria-label": ariaLabel,
|
|
2484
|
+
style: { width: `calc(${Math.max(textColumns(draft || placeholder), 2)}ch + 2px)` },
|
|
2485
|
+
onChange: (e) => setDraft(e.target.value),
|
|
2486
|
+
onBlur: commit,
|
|
2487
|
+
onKeyDown: (e) => {
|
|
2488
|
+
if (e.key === "Enter") {
|
|
2489
|
+
e.preventDefault();
|
|
2490
|
+
commit();
|
|
2491
|
+
if (onEnter) onEnter();
|
|
2492
|
+
else e.currentTarget.blur();
|
|
2493
|
+
} else if (e.key === "Escape") {
|
|
2494
|
+
e.preventDefault();
|
|
2495
|
+
setDraft(committed.current);
|
|
2496
|
+
reverting.current = true;
|
|
2497
|
+
e.currentTarget.blur();
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/components/tiptap-node/collapsible-node/collapsible-types.ts
|
|
2505
|
+
var CALLOUT_TYPES = ["info", "tip", "warning", "danger"];
|
|
2506
|
+
var CALLOUT_DEFAULT_TITLES = {
|
|
2507
|
+
info: "信息",
|
|
2508
|
+
tip: "提示",
|
|
2509
|
+
warning: "注意",
|
|
2510
|
+
danger: "危险"
|
|
2511
|
+
};
|
|
2512
|
+
function isCalloutType(value) {
|
|
2513
|
+
return typeof value === "string" && CALLOUT_TYPES.includes(value);
|
|
2514
|
+
}
|
|
2515
|
+
var CONTAINER_NODE_NAMES = ["callout", "details", "apiParam"];
|
|
2516
|
+
var CONTAINER_MARKDOWN_NAMES = {
|
|
2517
|
+
details: "details",
|
|
2518
|
+
info: "callout",
|
|
2519
|
+
tip: "callout",
|
|
2520
|
+
warning: "callout",
|
|
2521
|
+
danger: "callout",
|
|
2522
|
+
param: "apiParam"
|
|
2523
|
+
};
|
|
2524
|
+
function CalloutNodeView(props) {
|
|
2525
|
+
const { node, editor, updateAttributes } = props;
|
|
2526
|
+
const attrs = node.attrs;
|
|
2527
|
+
const { open, toggle } = useCollapsible(props, { defaultOpen: true });
|
|
2528
|
+
const isEditable = editor.isEditable;
|
|
2529
|
+
const shownTitle = attrs.title ?? CALLOUT_DEFAULT_TITLES[attrs.type];
|
|
2530
|
+
return /* @__PURE__ */ jsx(
|
|
2531
|
+
CollapsibleShell,
|
|
2532
|
+
{
|
|
2533
|
+
props,
|
|
2534
|
+
kind: "callout",
|
|
2535
|
+
open,
|
|
2536
|
+
onToggle: toggle,
|
|
2537
|
+
className: `tt-callout tt-callout--${attrs.type}`,
|
|
2538
|
+
header: isEditable ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2539
|
+
/* @__PURE__ */ jsx("fieldset", { className: "tt-callout__types", "aria-label": "提示框类型", children: CALLOUT_TYPES.map((type) => /* @__PURE__ */ jsx(
|
|
2540
|
+
"button",
|
|
2541
|
+
{
|
|
2542
|
+
type: "button",
|
|
2543
|
+
className: `tt-callout__type tt-callout__type--${type}`,
|
|
2544
|
+
"data-callout-type": type,
|
|
2545
|
+
"aria-pressed": type === attrs.type,
|
|
2546
|
+
title: CALLOUT_DEFAULT_TITLES[type],
|
|
2547
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2548
|
+
onClick: () => updateAttributes({ type })
|
|
2549
|
+
},
|
|
2550
|
+
type
|
|
2551
|
+
)) }),
|
|
2552
|
+
/* @__PURE__ */ jsx(
|
|
2553
|
+
InlineField,
|
|
2554
|
+
{
|
|
2555
|
+
field: "title",
|
|
2556
|
+
value: attrs.title,
|
|
2557
|
+
placeholder: CALLOUT_DEFAULT_TITLES[attrs.type],
|
|
2558
|
+
ariaLabel: "提示框标题",
|
|
2559
|
+
nullable: true,
|
|
2560
|
+
readOnly: false,
|
|
2561
|
+
onCommit: (value) => updateAttributes({ title: value }),
|
|
2562
|
+
className: "tt-callout__title"
|
|
2563
|
+
}
|
|
2564
|
+
)
|
|
2565
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "tt-callout__title tt-field", "data-field": "title", children: shownTitle })
|
|
2566
|
+
}
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
function parseContainerBody(token, helpers) {
|
|
2570
|
+
const tokens = token.tokens ?? [];
|
|
2571
|
+
const children = tokens.length ? helpers.parseChildren(tokens) : [];
|
|
2572
|
+
return children.length ? children : [helpers.createNode("paragraph")];
|
|
2573
|
+
}
|
|
2574
|
+
var CalloutSchema = Node$1.create({
|
|
2575
|
+
name: "callout",
|
|
2576
|
+
group: "block",
|
|
2577
|
+
content: "block+",
|
|
2578
|
+
isolating: true,
|
|
2579
|
+
defining: true,
|
|
2580
|
+
addAttributes() {
|
|
2581
|
+
return {
|
|
2582
|
+
type: {
|
|
2583
|
+
default: "info",
|
|
2584
|
+
parseHTML: (el) => {
|
|
2585
|
+
const raw = el.getAttribute("data-callout");
|
|
2586
|
+
return isCalloutType(raw) ? raw : "info";
|
|
2587
|
+
},
|
|
2588
|
+
rendered: false
|
|
2589
|
+
},
|
|
2590
|
+
title: {
|
|
2591
|
+
default: null,
|
|
2592
|
+
parseHTML: (el) => el.getAttribute("data-title") || null,
|
|
2593
|
+
rendered: false
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
},
|
|
2597
|
+
parseHTML() {
|
|
2598
|
+
return [{ tag: "div[data-callout]" }];
|
|
2599
|
+
},
|
|
2600
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
2601
|
+
const { type, title } = node.attrs;
|
|
2602
|
+
return [
|
|
2603
|
+
"div",
|
|
2604
|
+
mergeAttributes$1(HTMLAttributes, { "data-callout": type, "data-title": title || null }),
|
|
2605
|
+
0
|
|
2606
|
+
];
|
|
2607
|
+
},
|
|
2608
|
+
markdownTokenName: "callout",
|
|
2609
|
+
markdownTokenizer: createContainerTokenizer("callout", CALLOUT_TYPES),
|
|
2610
|
+
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
2611
|
+
"callout",
|
|
2612
|
+
{
|
|
2613
|
+
type: isCalloutType(token.name) ? token.name : "info",
|
|
2614
|
+
title: token.info || null
|
|
2615
|
+
},
|
|
2616
|
+
parseContainerBody(token, helpers)
|
|
2617
|
+
),
|
|
2618
|
+
renderMarkdown: (node, h) => {
|
|
2619
|
+
const { type, title } = node.attrs ?? {};
|
|
2620
|
+
return renderContainerMarkdown(
|
|
2621
|
+
isCalloutType(type) ? type : "info",
|
|
2622
|
+
title ?? "",
|
|
2623
|
+
h.renderChildren(node.content ?? [], "\n\n")
|
|
2624
|
+
);
|
|
2625
|
+
}
|
|
2626
|
+
});
|
|
2627
|
+
|
|
2628
|
+
// src/components/tiptap-node/collapsible-node/callout-node-extension.ts
|
|
2629
|
+
var CalloutExtension = CalloutSchema.extend({
|
|
2630
|
+
addNodeView() {
|
|
2631
|
+
return ReactNodeViewRenderer(CalloutNodeView);
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
function DetailsNodeView(props) {
|
|
2635
|
+
const { node, editor, updateAttributes } = props;
|
|
2636
|
+
const attrs = node.attrs;
|
|
2637
|
+
const { open, toggle } = useCollapsible(props);
|
|
2638
|
+
const isEditable = editor.isEditable;
|
|
2639
|
+
return /* @__PURE__ */ jsx(
|
|
2640
|
+
CollapsibleShell,
|
|
2641
|
+
{
|
|
2642
|
+
props,
|
|
2643
|
+
kind: "details",
|
|
2644
|
+
open,
|
|
2645
|
+
onToggle: toggle,
|
|
2646
|
+
header: isEditable ? /* @__PURE__ */ jsx(
|
|
2647
|
+
InlineField,
|
|
2648
|
+
{
|
|
2649
|
+
field: "title",
|
|
2650
|
+
value: attrs.title,
|
|
2651
|
+
placeholder: "标题",
|
|
2652
|
+
ariaLabel: "折叠块标题",
|
|
2653
|
+
nullable: true,
|
|
2654
|
+
readOnly: false,
|
|
2655
|
+
onCommit: (value) => updateAttributes({ title: value }),
|
|
2656
|
+
className: "tt-details__title"
|
|
2657
|
+
}
|
|
2658
|
+
) : /* @__PURE__ */ jsx("span", { className: "tt-details__title tt-field", "data-field": "title", children: attrs.title ?? "详情" })
|
|
2659
|
+
}
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
var DEFAULT_SUMMARY = "详情";
|
|
2663
|
+
function childElements(el) {
|
|
2664
|
+
const out = [];
|
|
2665
|
+
for (let i = 0; i < el.childNodes.length; i += 1) {
|
|
2666
|
+
const n = el.childNodes.item(i);
|
|
2667
|
+
if (n && n.nodeType === 1) out.push(n);
|
|
2668
|
+
}
|
|
2669
|
+
return out;
|
|
2670
|
+
}
|
|
2671
|
+
function readTitle(el) {
|
|
2672
|
+
const direct = el.getAttribute("data-title");
|
|
2673
|
+
if (direct) return direct;
|
|
2674
|
+
const summary = childElements(el).find((c) => c.tagName.toLowerCase() === "summary");
|
|
2675
|
+
const text = summary?.textContent?.trim();
|
|
2676
|
+
return text || null;
|
|
2677
|
+
}
|
|
2678
|
+
function readContent(el) {
|
|
2679
|
+
const wrapper = childElements(el).find((c) => c.hasAttribute("data-details-content"));
|
|
2680
|
+
if (wrapper) return wrapper;
|
|
2681
|
+
const tmp = el.ownerDocument.createElement("div");
|
|
2682
|
+
for (const child of Array.from(el.childNodes)) {
|
|
2683
|
+
if (child.nodeName.toLowerCase() === "summary") continue;
|
|
2684
|
+
tmp.appendChild(child.cloneNode(true));
|
|
2685
|
+
}
|
|
2686
|
+
return tmp;
|
|
2687
|
+
}
|
|
2688
|
+
var DetailsSchema = Node$1.create({
|
|
2689
|
+
name: "details",
|
|
2690
|
+
group: "block",
|
|
2691
|
+
content: "block+",
|
|
2692
|
+
isolating: true,
|
|
2693
|
+
defining: true,
|
|
2694
|
+
addAttributes() {
|
|
2695
|
+
return {
|
|
2696
|
+
title: { default: null, parseHTML: (el) => readTitle(el), rendered: false }
|
|
2697
|
+
};
|
|
2698
|
+
},
|
|
2699
|
+
parseHTML() {
|
|
2700
|
+
return [
|
|
2701
|
+
{
|
|
2702
|
+
tag: "details",
|
|
2703
|
+
// 参数行也用 <details>,由 apiParam 的规则(优先级更高)处理
|
|
2704
|
+
getAttrs: (el) => el.hasAttribute("data-param") ? false : null,
|
|
2705
|
+
contentElement: (el) => readContent(el)
|
|
2706
|
+
}
|
|
2707
|
+
];
|
|
2708
|
+
},
|
|
2709
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
2710
|
+
const { title } = node.attrs;
|
|
2711
|
+
return [
|
|
2712
|
+
"details",
|
|
2713
|
+
mergeAttributes$1(HTMLAttributes, { "data-details": "", "data-title": title || null }),
|
|
2714
|
+
["summary", {}, title || DEFAULT_SUMMARY],
|
|
2715
|
+
["div", { "data-details-content": "" }, 0]
|
|
2716
|
+
];
|
|
2717
|
+
},
|
|
2718
|
+
markdownTokenName: "details",
|
|
2719
|
+
markdownTokenizer: createContainerTokenizer("details", ["details"]),
|
|
2720
|
+
parseMarkdown: (token, helpers) => helpers.createNode(
|
|
2721
|
+
"details",
|
|
2722
|
+
{ title: token.info || null },
|
|
2723
|
+
parseContainerBody(token, helpers)
|
|
2724
|
+
),
|
|
2725
|
+
renderMarkdown: (node, h) => {
|
|
2726
|
+
const { title } = node.attrs ?? {};
|
|
2727
|
+
return renderContainerMarkdown(
|
|
2728
|
+
"details",
|
|
2729
|
+
title ?? "",
|
|
2730
|
+
h.renderChildren(node.content ?? [], "\n\n")
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
});
|
|
2734
|
+
|
|
2735
|
+
// src/components/tiptap-node/collapsible-node/details-node-extension.ts
|
|
2736
|
+
var DetailsExtension = DetailsSchema.extend({
|
|
2737
|
+
addNodeView() {
|
|
2738
|
+
return ReactNodeViewRenderer(DetailsNodeView);
|
|
2739
|
+
}
|
|
2740
|
+
});
|
|
2741
|
+
function ApiParamNodeView(props) {
|
|
2742
|
+
const { node, editor, getPos, updateAttributes, deleteNode: deleteNode2 } = props;
|
|
2743
|
+
const attrs = node.attrs;
|
|
2744
|
+
const { open, setOpen, toggle } = useCollapsible(props);
|
|
2745
|
+
const isEditable = editor.isEditable;
|
|
2746
|
+
const hasBody = node.content.size > 0;
|
|
2747
|
+
const path = useMemo(() => {
|
|
2748
|
+
const pos = getPos();
|
|
2749
|
+
if (typeof pos !== "number") return null;
|
|
2750
|
+
const $inside = editor.state.doc.resolve(pos + 1);
|
|
2751
|
+
const full = apiParamPath($inside, attrs.name);
|
|
2752
|
+
return full.includes(".") ? full : null;
|
|
2753
|
+
}, [editor.state.doc, getPos, attrs.name]);
|
|
2754
|
+
const focusBody = () => {
|
|
2755
|
+
const pos = getPos();
|
|
2756
|
+
if (typeof pos !== "number") return;
|
|
2757
|
+
const current = editor.state.doc.nodeAt(pos);
|
|
2758
|
+
if (!current) return;
|
|
2759
|
+
let tr = editor.state.tr;
|
|
2760
|
+
if (current.content.size === 0) tr = tr.insert(pos + 1, editor.schema.nodes.paragraph.create());
|
|
2761
|
+
tr = tr.setSelection(TextSelection.create(tr.doc, pos + 2));
|
|
2762
|
+
editor.view.dispatch(tr);
|
|
2763
|
+
editor.view.focus();
|
|
2764
|
+
setOpen(true);
|
|
2765
|
+
};
|
|
2766
|
+
const addChild = () => {
|
|
2767
|
+
const pos = getPos();
|
|
2768
|
+
if (typeof pos !== "number") return;
|
|
2769
|
+
const current = editor.state.doc.nodeAt(pos);
|
|
2770
|
+
if (!current) return;
|
|
2771
|
+
const child = editor.schema.nodes.apiParam.create({
|
|
2772
|
+
name: "",
|
|
2773
|
+
type: null,
|
|
2774
|
+
required: false,
|
|
2775
|
+
default: null,
|
|
2776
|
+
summary: null
|
|
2777
|
+
});
|
|
2778
|
+
const at = pos + 1 + current.content.size;
|
|
2779
|
+
editor.view.dispatch(editor.state.tr.insert(at, child));
|
|
2780
|
+
setOpen(true);
|
|
2781
|
+
queueMicrotask(() => {
|
|
2782
|
+
const dom = editor.view.nodeDOM(at);
|
|
2783
|
+
dom?.querySelector('input[data-field="name"]')?.focus();
|
|
2784
|
+
});
|
|
2785
|
+
};
|
|
2786
|
+
return /* @__PURE__ */ jsx(
|
|
2787
|
+
CollapsibleShell,
|
|
2788
|
+
{
|
|
2789
|
+
props,
|
|
2790
|
+
kind: "apiParam",
|
|
2791
|
+
open,
|
|
2792
|
+
onToggle: toggle,
|
|
2793
|
+
collapsible: hasBody,
|
|
2794
|
+
contentLead: path ? /* @__PURE__ */ jsx("span", { className: "tt-api-param__path", children: path }) : null,
|
|
2795
|
+
header: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2796
|
+
/* @__PURE__ */ jsx(
|
|
2797
|
+
InlineField,
|
|
2798
|
+
{
|
|
2799
|
+
field: "name",
|
|
2800
|
+
value: attrs.name,
|
|
2801
|
+
placeholder: "参数名",
|
|
2802
|
+
ariaLabel: "参数名",
|
|
2803
|
+
nullable: false,
|
|
2804
|
+
readOnly: !isEditable,
|
|
2805
|
+
onCommit: (v) => updateAttributes({ name: v ?? "" }),
|
|
2806
|
+
className: "tt-api-param__name"
|
|
2807
|
+
}
|
|
2808
|
+
),
|
|
2809
|
+
/* @__PURE__ */ jsx(
|
|
2810
|
+
InlineField,
|
|
2811
|
+
{
|
|
2812
|
+
field: "type",
|
|
2813
|
+
value: attrs.type,
|
|
2814
|
+
placeholder: "类型",
|
|
2815
|
+
ariaLabel: "类型",
|
|
2816
|
+
nullable: true,
|
|
2817
|
+
readOnly: !isEditable,
|
|
2818
|
+
onCommit: (v) => updateAttributes({ type: v }),
|
|
2819
|
+
className: "tt-badge tt-badge--type"
|
|
2820
|
+
}
|
|
2821
|
+
),
|
|
2822
|
+
isEditable ? /* @__PURE__ */ jsx(
|
|
2823
|
+
"button",
|
|
2824
|
+
{
|
|
2825
|
+
type: "button",
|
|
2826
|
+
"data-field": "required",
|
|
2827
|
+
className: "tt-badge tt-badge--required",
|
|
2828
|
+
"aria-pressed": attrs.required,
|
|
2829
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2830
|
+
onClick: () => updateAttributes({ required: !attrs.required }),
|
|
2831
|
+
children: attrs.required ? "必选" : "可选"
|
|
2832
|
+
}
|
|
2833
|
+
) : attrs.required ? /* @__PURE__ */ jsx("span", { className: "tt-badge tt-badge--required", "data-field": "required", children: "必选" }) : null,
|
|
2834
|
+
isEditable || attrs.default !== null ? /* @__PURE__ */ jsxs("span", { className: "tt-badge tt-badge--default", children: [
|
|
2835
|
+
/* @__PURE__ */ jsx("span", { className: "tt-badge__label", children: "默认值" }),
|
|
2836
|
+
/* @__PURE__ */ jsx(
|
|
2837
|
+
InlineField,
|
|
2838
|
+
{
|
|
2839
|
+
field: "default",
|
|
2840
|
+
value: attrs.default,
|
|
2841
|
+
placeholder: "无",
|
|
2842
|
+
ariaLabel: "默认值",
|
|
2843
|
+
nullable: true,
|
|
2844
|
+
readOnly: !isEditable,
|
|
2845
|
+
onCommit: (v) => updateAttributes({ default: v })
|
|
2846
|
+
}
|
|
2847
|
+
)
|
|
2848
|
+
] }) : null,
|
|
2849
|
+
/* @__PURE__ */ jsx("span", { className: "tt-api-param__sep", "aria-hidden": "true", children: "|" }),
|
|
2850
|
+
/* @__PURE__ */ jsx(
|
|
2851
|
+
InlineField,
|
|
2852
|
+
{
|
|
2853
|
+
field: "summary",
|
|
2854
|
+
value: attrs.summary,
|
|
2855
|
+
placeholder: "一句话描述",
|
|
2856
|
+
ariaLabel: "短描述",
|
|
2857
|
+
nullable: true,
|
|
2858
|
+
readOnly: !isEditable,
|
|
2859
|
+
onCommit: (v) => updateAttributes({ summary: v }),
|
|
2860
|
+
onEnter: focusBody,
|
|
2861
|
+
className: "tt-api-param__summary"
|
|
2862
|
+
}
|
|
2863
|
+
)
|
|
2864
|
+
] }),
|
|
2865
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2866
|
+
!hasBody ? /* @__PURE__ */ jsx(
|
|
2867
|
+
"button",
|
|
2868
|
+
{
|
|
2869
|
+
type: "button",
|
|
2870
|
+
"data-action": "add-body",
|
|
2871
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2872
|
+
onClick: focusBody,
|
|
2873
|
+
children: "添加说明"
|
|
2874
|
+
}
|
|
2875
|
+
) : null,
|
|
2876
|
+
/* @__PURE__ */ jsx(
|
|
2877
|
+
"button",
|
|
2878
|
+
{
|
|
2879
|
+
type: "button",
|
|
2880
|
+
"data-action": "add-child",
|
|
2881
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2882
|
+
onClick: addChild,
|
|
2883
|
+
children: "添加子参数"
|
|
2884
|
+
}
|
|
2885
|
+
),
|
|
2886
|
+
/* @__PURE__ */ jsx(
|
|
2887
|
+
"button",
|
|
2888
|
+
{
|
|
2889
|
+
type: "button",
|
|
2890
|
+
"data-action": "remove",
|
|
2891
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
2892
|
+
onClick: () => deleteNode2(),
|
|
2893
|
+
children: "删除"
|
|
2894
|
+
}
|
|
2895
|
+
)
|
|
2896
|
+
] })
|
|
2897
|
+
}
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2900
|
+
function apiParamSummaryText(attrs) {
|
|
2901
|
+
const head = [
|
|
2902
|
+
attrs.name,
|
|
2903
|
+
attrs.type,
|
|
2904
|
+
attrs.required ? "必选" : null,
|
|
2905
|
+
attrs.default != null ? `默认值 ${attrs.default}` : null
|
|
2906
|
+
].filter((x) => Boolean(x)).join(" ");
|
|
2907
|
+
return attrs.summary ? `${head} | ${attrs.summary}`.trim() : head;
|
|
2908
|
+
}
|
|
2909
|
+
function childElements2(el) {
|
|
2910
|
+
const out = [];
|
|
2911
|
+
for (let i = 0; i < el.childNodes.length; i += 1) {
|
|
2912
|
+
const n = el.childNodes.item(i);
|
|
2913
|
+
if (n && n.nodeType === 1) out.push(n);
|
|
2914
|
+
}
|
|
2915
|
+
return out;
|
|
2916
|
+
}
|
|
2917
|
+
function splitParamBody(token, helpers) {
|
|
2918
|
+
const tokens = [...token.tokens ?? []];
|
|
2919
|
+
let summary = null;
|
|
2920
|
+
while (tokens.length && tokens[0].type === "space") tokens.shift();
|
|
2921
|
+
if (tokens[0]?.type === "paragraph") {
|
|
2922
|
+
const first = tokens.shift();
|
|
2923
|
+
summary = String(first.text ?? "").replace(/\s*\n\s*/g, " ").trim() || null;
|
|
2924
|
+
}
|
|
2925
|
+
const content = tokens.length ? helpers.parseChildren(tokens) : [];
|
|
2926
|
+
return { summary, content };
|
|
2927
|
+
}
|
|
2928
|
+
var ApiParamSchema = Node$1.create({
|
|
2929
|
+
name: "apiParam",
|
|
2930
|
+
group: "block",
|
|
2931
|
+
content: "block*",
|
|
2932
|
+
isolating: true,
|
|
2933
|
+
defining: true,
|
|
2934
|
+
// 解析规则要排在 details 的裸 <details> 规则前面
|
|
2935
|
+
priority: 110,
|
|
2936
|
+
addAttributes() {
|
|
2937
|
+
const text = (attr) => ({
|
|
2938
|
+
default: null,
|
|
2939
|
+
parseHTML: (el) => el.getAttribute(attr) || null,
|
|
2940
|
+
rendered: false
|
|
2941
|
+
});
|
|
2942
|
+
return {
|
|
2943
|
+
name: {
|
|
2944
|
+
default: "",
|
|
2945
|
+
parseHTML: (el) => el.getAttribute("data-name") ?? "",
|
|
2946
|
+
rendered: false
|
|
2947
|
+
},
|
|
2948
|
+
type: text("data-type"),
|
|
2949
|
+
required: {
|
|
2950
|
+
default: false,
|
|
2951
|
+
parseHTML: (el) => {
|
|
2952
|
+
const v = el.getAttribute("data-required");
|
|
2953
|
+
return v === "true" || v === "";
|
|
2954
|
+
},
|
|
2955
|
+
rendered: false
|
|
2956
|
+
},
|
|
2957
|
+
default: text("data-default"),
|
|
2958
|
+
summary: text("data-summary")
|
|
2959
|
+
};
|
|
2960
|
+
},
|
|
2961
|
+
parseHTML() {
|
|
2962
|
+
return [
|
|
2963
|
+
{
|
|
2964
|
+
tag: "details[data-param]",
|
|
2965
|
+
contentElement: (el) => {
|
|
2966
|
+
const wrapper = childElements2(el).find(
|
|
2967
|
+
(c) => c.hasAttribute("data-param-content")
|
|
2968
|
+
);
|
|
2969
|
+
return wrapper ? wrapper : el.ownerDocument.createElement("div");
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
];
|
|
2973
|
+
},
|
|
2974
|
+
renderHTML({ node, HTMLAttributes }) {
|
|
2975
|
+
const attrs = node.attrs;
|
|
2976
|
+
return [
|
|
2977
|
+
"details",
|
|
2978
|
+
mergeAttributes$1(HTMLAttributes, {
|
|
2979
|
+
"data-param": "",
|
|
2980
|
+
"data-name": attrs.name || null,
|
|
2981
|
+
"data-type": attrs.type || null,
|
|
2982
|
+
"data-required": attrs.required ? "true" : null,
|
|
2983
|
+
"data-default": attrs.default ?? null,
|
|
2984
|
+
"data-summary": attrs.summary || null
|
|
2985
|
+
}),
|
|
2986
|
+
["summary", {}, apiParamSummaryText(attrs)],
|
|
2987
|
+
["div", { "data-param-content": "" }, 0]
|
|
2988
|
+
];
|
|
2989
|
+
},
|
|
2990
|
+
markdownTokenName: "apiParam",
|
|
2991
|
+
markdownTokenizer: createContainerTokenizer("apiParam", ["param"]),
|
|
2992
|
+
parseMarkdown: (token, helpers) => {
|
|
2993
|
+
const info = parseParamInfo(String(token.info ?? ""));
|
|
2994
|
+
const { summary, content } = splitParamBody(token, helpers);
|
|
2995
|
+
return helpers.createNode("apiParam", { ...info, summary }, content);
|
|
2996
|
+
},
|
|
2997
|
+
renderMarkdown: (node, h) => {
|
|
2998
|
+
const attrs = node.attrs ?? {};
|
|
2999
|
+
const info = formatParamInfo({
|
|
3000
|
+
name: attrs.name ?? "",
|
|
3001
|
+
type: attrs.type ?? null,
|
|
3002
|
+
required: Boolean(attrs.required),
|
|
3003
|
+
default: attrs.default ?? null
|
|
3004
|
+
});
|
|
3005
|
+
const body = h.renderChildren(node.content ?? [], "\n\n");
|
|
3006
|
+
const parts = [attrs.summary ?? "", body].filter(Boolean);
|
|
3007
|
+
return renderContainerMarkdown("param", info, parts.join("\n\n"));
|
|
3008
|
+
}
|
|
3009
|
+
});
|
|
3010
|
+
|
|
3011
|
+
// src/components/tiptap-node/collapsible-node/api-param-node-extension.ts
|
|
3012
|
+
var ApiParamExtension = ApiParamSchema.extend({
|
|
3013
|
+
addNodeView() {
|
|
3014
|
+
return ReactNodeViewRenderer(ApiParamNodeView);
|
|
3015
|
+
}
|
|
3016
|
+
});
|
|
1350
3017
|
var useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
|
|
1351
3018
|
function useEventListener(eventName, handler, element, options) {
|
|
1352
3019
|
const savedHandler = useRef(handler);
|
|
@@ -4199,6 +5866,484 @@ var ImageUploadNode2 = Node.create({
|
|
|
4199
5866
|
};
|
|
4200
5867
|
}
|
|
4201
5868
|
});
|
|
5869
|
+
var codeHighlightPluginKey = new PluginKey("codeHighlight");
|
|
5870
|
+
var CODE_TOKEN_CLASS = "code-token";
|
|
5871
|
+
var SEP = "\0";
|
|
5872
|
+
function codeBlockSignature(node, theme) {
|
|
5873
|
+
return `${theme}${SEP}${node.attrs.language ?? ""}${SEP}${node.textContent}`;
|
|
5874
|
+
}
|
|
5875
|
+
function tokenStyle(token) {
|
|
5876
|
+
const parts = [];
|
|
5877
|
+
if (token.color) parts.push(`--code-token-color:${token.color}`);
|
|
5878
|
+
const fs = token.fontStyle ?? 0;
|
|
5879
|
+
if (fs & 1) parts.push("--code-token-font-style:italic");
|
|
5880
|
+
if (fs & 2) parts.push("--code-token-font-weight:bold");
|
|
5881
|
+
const decorations = [];
|
|
5882
|
+
if (fs & 4) decorations.push("underline");
|
|
5883
|
+
if (fs & 8) decorations.push("line-through");
|
|
5884
|
+
if (decorations.length) parts.push(`--code-token-text-decoration:${decorations.join(" ")}`);
|
|
5885
|
+
return parts.join(";");
|
|
5886
|
+
}
|
|
5887
|
+
function buildBlockDecorations(pos, node, sig, tokens) {
|
|
5888
|
+
const out = [Decoration.node(pos, pos + node.nodeSize, {}, { codeSig: sig })];
|
|
5889
|
+
const contentStart = pos + 1;
|
|
5890
|
+
const max = node.content.size;
|
|
5891
|
+
for (const token of tokens) {
|
|
5892
|
+
const from = Math.max(0, Math.min(token.start, max));
|
|
5893
|
+
const to = Math.max(from, Math.min(token.end, max));
|
|
5894
|
+
if (to <= from) continue;
|
|
5895
|
+
out.push(
|
|
5896
|
+
Decoration.inline(
|
|
5897
|
+
contentStart + from,
|
|
5898
|
+
contentStart + to,
|
|
5899
|
+
{ class: CODE_TOKEN_CLASS, style: tokenStyle(token) },
|
|
5900
|
+
{ codeSig: sig }
|
|
5901
|
+
)
|
|
5902
|
+
);
|
|
5903
|
+
}
|
|
5904
|
+
return out;
|
|
5905
|
+
}
|
|
5906
|
+
function reconcile(doc, decorations, theme, nodeName) {
|
|
5907
|
+
const needs = /* @__PURE__ */ new Map();
|
|
5908
|
+
let next = decorations;
|
|
5909
|
+
doc.descendants((node, pos) => {
|
|
5910
|
+
if (node.type.name !== nodeName) return !node.isTextblock;
|
|
5911
|
+
const sig = codeBlockSignature(node, theme);
|
|
5912
|
+
const inside = next.find(pos, pos + node.nodeSize);
|
|
5913
|
+
const marker = inside.find((d) => d.spec.codeSig !== void 0 && d.from === pos);
|
|
5914
|
+
if (marker?.spec.codeSig === sig) return false;
|
|
5915
|
+
if (inside.length) next = next.remove(inside);
|
|
5916
|
+
if (!needs.has(sig)) {
|
|
5917
|
+
needs.set(sig, { source: node.textContent, language: node.attrs.language ?? null, theme });
|
|
5918
|
+
}
|
|
5919
|
+
return false;
|
|
5920
|
+
});
|
|
5921
|
+
return { decorations: next, needs };
|
|
5922
|
+
}
|
|
5923
|
+
function applyResult(doc, decorations, theme, nodeName, sig, tokens) {
|
|
5924
|
+
const additions = [];
|
|
5925
|
+
doc.descendants((node, pos) => {
|
|
5926
|
+
if (node.type.name !== nodeName) return !node.isTextblock;
|
|
5927
|
+
if (codeBlockSignature(node, theme) !== sig) return false;
|
|
5928
|
+
const inside = decorations.find(pos, pos + node.nodeSize);
|
|
5929
|
+
if (inside.some((d) => d.spec.codeSig === sig && d.from === pos)) return false;
|
|
5930
|
+
additions.push(...buildBlockDecorations(pos, node, sig, tokens));
|
|
5931
|
+
return false;
|
|
5932
|
+
});
|
|
5933
|
+
return additions.length ? decorations.add(doc, additions) : decorations;
|
|
5934
|
+
}
|
|
5935
|
+
function defaultResolveTheme(options) {
|
|
5936
|
+
return () => {
|
|
5937
|
+
const root = typeof document === "undefined" ? null : document.documentElement;
|
|
5938
|
+
const isDark = Boolean(root?.classList.contains("dark"));
|
|
5939
|
+
return resolveShikiTheme(options.shikiTheme, isDark);
|
|
5940
|
+
};
|
|
5941
|
+
}
|
|
5942
|
+
function createCodeHighlightPlugin(options) {
|
|
5943
|
+
const { highlighter, resolveTheme, debounceMs, nodeName } = options;
|
|
5944
|
+
return new Plugin({
|
|
5945
|
+
key: codeHighlightPluginKey,
|
|
5946
|
+
state: {
|
|
5947
|
+
init(_config, state) {
|
|
5948
|
+
const theme = resolveTheme();
|
|
5949
|
+
return { theme, ...reconcile(state.doc, DecorationSet.empty, theme, nodeName) };
|
|
5950
|
+
},
|
|
5951
|
+
apply(tr, prev, _old, next) {
|
|
5952
|
+
const meta = tr.getMeta(codeHighlightPluginKey);
|
|
5953
|
+
if (meta?.type === "theme") {
|
|
5954
|
+
if (meta.theme === prev.theme) return prev;
|
|
5955
|
+
return {
|
|
5956
|
+
theme: meta.theme,
|
|
5957
|
+
...reconcile(next.doc, DecorationSet.empty, meta.theme, nodeName)
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
let decorations = tr.docChanged ? prev.decorations.map(tr.mapping, tr.doc) : prev.decorations;
|
|
5961
|
+
if (meta?.type === "result") {
|
|
5962
|
+
decorations = applyResult(
|
|
5963
|
+
next.doc,
|
|
5964
|
+
decorations,
|
|
5965
|
+
prev.theme,
|
|
5966
|
+
nodeName,
|
|
5967
|
+
meta.sig,
|
|
5968
|
+
meta.tokens
|
|
5969
|
+
);
|
|
5970
|
+
const needs = new Map(prev.needs);
|
|
5971
|
+
needs.delete(meta.sig);
|
|
5972
|
+
if (!tr.docChanged) return { theme: prev.theme, decorations, needs };
|
|
5973
|
+
}
|
|
5974
|
+
if (!tr.docChanged) return prev;
|
|
5975
|
+
return { theme: prev.theme, ...reconcile(next.doc, decorations, prev.theme, nodeName) };
|
|
5976
|
+
}
|
|
5977
|
+
},
|
|
5978
|
+
props: {
|
|
5979
|
+
decorations(state) {
|
|
5980
|
+
return codeHighlightPluginKey.getState(state)?.decorations ?? null;
|
|
5981
|
+
}
|
|
5982
|
+
},
|
|
5983
|
+
view(view) {
|
|
5984
|
+
let destroyed = false;
|
|
5985
|
+
let timer = null;
|
|
5986
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
5987
|
+
const dispatchMeta = (meta) => {
|
|
5988
|
+
if (destroyed) return;
|
|
5989
|
+
view.dispatch(
|
|
5990
|
+
view.state.tr.setMeta(codeHighlightPluginKey, meta).setMeta("addToHistory", false)
|
|
5991
|
+
);
|
|
5992
|
+
};
|
|
5993
|
+
const flush = () => {
|
|
5994
|
+
timer = null;
|
|
5995
|
+
if (destroyed) return;
|
|
5996
|
+
const state = codeHighlightPluginKey.getState(view.state);
|
|
5997
|
+
if (!state) return;
|
|
5998
|
+
for (const [sig, request] of state.needs) {
|
|
5999
|
+
if (inflight.has(sig)) continue;
|
|
6000
|
+
inflight.add(sig);
|
|
6001
|
+
highlighter.highlight(request.source, request.language, request.theme).then((result) => {
|
|
6002
|
+
inflight.delete(sig);
|
|
6003
|
+
dispatchMeta({ type: "result", sig, tokens: result.tokens });
|
|
6004
|
+
}).catch(() => {
|
|
6005
|
+
inflight.delete(sig);
|
|
6006
|
+
});
|
|
6007
|
+
}
|
|
6008
|
+
};
|
|
6009
|
+
const schedule = () => {
|
|
6010
|
+
if (destroyed) return;
|
|
6011
|
+
const state = codeHighlightPluginKey.getState(view.state);
|
|
6012
|
+
if (!state || state.needs.size === 0) return;
|
|
6013
|
+
if (timer !== null) clearTimeout(timer);
|
|
6014
|
+
timer = setTimeout(flush, debounceMs);
|
|
6015
|
+
};
|
|
6016
|
+
let observer = null;
|
|
6017
|
+
const root = typeof document === "undefined" ? null : document.documentElement;
|
|
6018
|
+
if (root && typeof MutationObserver !== "undefined") {
|
|
6019
|
+
observer = new MutationObserver(() => {
|
|
6020
|
+
const theme = resolveTheme();
|
|
6021
|
+
if (theme !== codeHighlightPluginKey.getState(view.state)?.theme) {
|
|
6022
|
+
dispatchMeta({ type: "theme", theme });
|
|
6023
|
+
}
|
|
6024
|
+
});
|
|
6025
|
+
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
|
|
6026
|
+
}
|
|
6027
|
+
schedule();
|
|
6028
|
+
return {
|
|
6029
|
+
update() {
|
|
6030
|
+
schedule();
|
|
6031
|
+
},
|
|
6032
|
+
destroy() {
|
|
6033
|
+
destroyed = true;
|
|
6034
|
+
if (timer !== null) clearTimeout(timer);
|
|
6035
|
+
observer?.disconnect();
|
|
6036
|
+
}
|
|
6037
|
+
};
|
|
6038
|
+
}
|
|
6039
|
+
});
|
|
6040
|
+
}
|
|
6041
|
+
var CodeHighlight = Extension.create({
|
|
6042
|
+
name: "codeHighlight",
|
|
6043
|
+
addOptions() {
|
|
6044
|
+
return {};
|
|
6045
|
+
},
|
|
6046
|
+
addStorage() {
|
|
6047
|
+
return { highlighter: null };
|
|
6048
|
+
},
|
|
6049
|
+
onBeforeCreate() {
|
|
6050
|
+
this.storage.highlighter = this.options.highlighter ?? createCodeHighlighter(this.options);
|
|
6051
|
+
},
|
|
6052
|
+
addProseMirrorPlugins() {
|
|
6053
|
+
return [
|
|
6054
|
+
createCodeHighlightPlugin({
|
|
6055
|
+
highlighter: this.storage.highlighter,
|
|
6056
|
+
resolveTheme: this.options.resolveTheme ?? defaultResolveTheme(this.options),
|
|
6057
|
+
debounceMs: this.options.debounceMs ?? 30,
|
|
6058
|
+
nodeName: this.options.nodeName ?? "codeBlock"
|
|
6059
|
+
})
|
|
6060
|
+
];
|
|
6061
|
+
}
|
|
6062
|
+
});
|
|
6063
|
+
var codeGroupInputPluginKey = new PluginKey("codeGroupInput");
|
|
6064
|
+
var OPENER_RE3 = /^ {0,3}:::[ \t]*code-group[ \t]*$/;
|
|
6065
|
+
var CLOSER_RE2 = /^ {0,3}:::[ \t]*$/;
|
|
6066
|
+
var MAX_LOOKBACK = 2e3;
|
|
6067
|
+
function paragraphLines(node) {
|
|
6068
|
+
return node.textBetween(0, node.content.size, void 0, "\n").split("\n");
|
|
6069
|
+
}
|
|
6070
|
+
function findCodeGroupDraft(state) {
|
|
6071
|
+
const { $from, empty } = state.selection;
|
|
6072
|
+
if (!empty) return null;
|
|
6073
|
+
return findDraftAt($from);
|
|
6074
|
+
}
|
|
6075
|
+
function findDraftAt($from) {
|
|
6076
|
+
if ($from.depth === 0 || $from.parent.type.name !== "paragraph") return null;
|
|
6077
|
+
const parent = $from.node(-1);
|
|
6078
|
+
const cursorIndex = $from.index(-1);
|
|
6079
|
+
const openers = [];
|
|
6080
|
+
const linesByIndex = /* @__PURE__ */ new Map();
|
|
6081
|
+
for (let i = cursorIndex; i >= 0 && cursorIndex - i < MAX_LOOKBACK; i -= 1) {
|
|
6082
|
+
const child = parent.child(i);
|
|
6083
|
+
if (child.type.name !== "paragraph") break;
|
|
6084
|
+
const lines = paragraphLines(child);
|
|
6085
|
+
linesByIndex.set(i, lines);
|
|
6086
|
+
if (lines.length === 1 && OPENER_RE3.test(lines[0])) openers.push(i);
|
|
6087
|
+
}
|
|
6088
|
+
if (openers.length === 0) return null;
|
|
6089
|
+
for (let k = openers.length - 1; k >= 0; k -= 1) {
|
|
6090
|
+
const startIndex = openers[k];
|
|
6091
|
+
const lines = [];
|
|
6092
|
+
for (let i = startIndex; i <= cursorIndex; i += 1) lines.push(...linesByIndex.get(i) ?? []);
|
|
6093
|
+
const result = scanCodeGroupLines(lines);
|
|
6094
|
+
if (result.status === "invalid") continue;
|
|
6095
|
+
if (result.status === "partial")
|
|
6096
|
+
return { startIndex, cursorIndex, status: "partial", blocks: result.blocks };
|
|
6097
|
+
const cursorLines = linesByIndex.get(cursorIndex) ?? [];
|
|
6098
|
+
if (result.lineCount === lines.length && cursorLines.length === 1 && CLOSER_RE2.test(cursorLines[0])) {
|
|
6099
|
+
return { startIndex, cursorIndex, status: "complete", blocks: result.blocks };
|
|
6100
|
+
}
|
|
6101
|
+
return null;
|
|
6102
|
+
}
|
|
6103
|
+
return null;
|
|
6104
|
+
}
|
|
6105
|
+
function stopYjsCapturing(state) {
|
|
6106
|
+
for (const plugin of state.plugins) {
|
|
6107
|
+
const pluginState = plugin.getState(state);
|
|
6108
|
+
pluginState?.undoManager?.stopCapturing?.();
|
|
6109
|
+
}
|
|
6110
|
+
}
|
|
6111
|
+
function dispatchAsOwnUndoStep(view, tr) {
|
|
6112
|
+
stopYjsCapturing(view.state);
|
|
6113
|
+
view.dispatch(closeHistory(tr));
|
|
6114
|
+
stopYjsCapturing(view.state);
|
|
6115
|
+
view.dispatch(closeHistory(view.state.tr));
|
|
6116
|
+
}
|
|
6117
|
+
function convertCodeGroupDraft(view, draft) {
|
|
6118
|
+
const { state } = view;
|
|
6119
|
+
const { schema } = state;
|
|
6120
|
+
const groupType = schema.nodes[CODE_GROUP_NAME];
|
|
6121
|
+
const codeBlockType = schema.nodes.codeBlock;
|
|
6122
|
+
const paragraphType = schema.nodes.paragraph;
|
|
6123
|
+
if (!groupType || !codeBlockType || !paragraphType) return false;
|
|
6124
|
+
const $from = state.selection.$from;
|
|
6125
|
+
const parentDepth = $from.depth - 1;
|
|
6126
|
+
const parent = $from.node(parentDepth);
|
|
6127
|
+
if (!parent.canReplaceWith(draft.startIndex, draft.cursorIndex + 1, groupType)) return false;
|
|
6128
|
+
let from = $from.start(parentDepth);
|
|
6129
|
+
for (let i = 0; i < draft.startIndex; i += 1) from += parent.child(i).nodeSize;
|
|
6130
|
+
let to = from;
|
|
6131
|
+
for (let i = draft.startIndex; i <= draft.cursorIndex; i += 1) to += parent.child(i).nodeSize;
|
|
6132
|
+
const children = draft.blocks.map(
|
|
6133
|
+
(b) => codeBlockType.create(
|
|
6134
|
+
{ language: b.language, label: b.label },
|
|
6135
|
+
b.source ? schema.text(b.source) : null
|
|
6136
|
+
)
|
|
6137
|
+
);
|
|
6138
|
+
let group;
|
|
6139
|
+
try {
|
|
6140
|
+
group = groupType.createChecked(null, children);
|
|
6141
|
+
} catch {
|
|
6142
|
+
return false;
|
|
6143
|
+
}
|
|
6144
|
+
const trailing = paragraphType.create();
|
|
6145
|
+
const tr = state.tr.replaceWith(from, to, [group, trailing]);
|
|
6146
|
+
tr.setSelection(TextSelection.create(tr.doc, from + group.nodeSize + 1));
|
|
6147
|
+
dispatchAsOwnUndoStep(view, tr.scrollIntoView());
|
|
6148
|
+
return true;
|
|
6149
|
+
}
|
|
6150
|
+
function isComposing(view, event) {
|
|
6151
|
+
if (event?.isComposing) return true;
|
|
6152
|
+
return Boolean(view.composing);
|
|
6153
|
+
}
|
|
6154
|
+
function createCodeGroupInputPlugin(editor) {
|
|
6155
|
+
return new Plugin({
|
|
6156
|
+
key: codeGroupInputPluginKey,
|
|
6157
|
+
props: {
|
|
6158
|
+
// 草稿内原文插入:绕过所有输入规则
|
|
6159
|
+
handleTextInput(view, from, to, text) {
|
|
6160
|
+
if (!findCodeGroupDraft(view.state)) return false;
|
|
6161
|
+
view.dispatch(view.state.tr.insertText(text, from, to));
|
|
6162
|
+
return true;
|
|
6163
|
+
},
|
|
6164
|
+
handleKeyDown(view, event) {
|
|
6165
|
+
if (event.key !== "Enter" || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey)
|
|
6166
|
+
return false;
|
|
6167
|
+
if (isComposing(view, event)) return false;
|
|
6168
|
+
const draft = findCodeGroupDraft(view.state);
|
|
6169
|
+
if (!draft) return false;
|
|
6170
|
+
const { $from } = view.state.selection;
|
|
6171
|
+
if (draft.status === "complete" && $from.parentOffset === $from.parent.content.size) {
|
|
6172
|
+
if (convertCodeGroupDraft(view, draft)) return true;
|
|
6173
|
+
}
|
|
6174
|
+
return editor.commands.splitBlock();
|
|
6175
|
+
},
|
|
6176
|
+
handlePaste(view, event) {
|
|
6177
|
+
const { state } = view;
|
|
6178
|
+
if (state.selection.$from.parent.type.name === "codeBlock") return false;
|
|
6179
|
+
const data = event.clipboardData;
|
|
6180
|
+
if (!data) return false;
|
|
6181
|
+
if (view.input?.shiftKey) return false;
|
|
6182
|
+
if (data.getData("text/html")) return false;
|
|
6183
|
+
const text = data.getData("text/plain");
|
|
6184
|
+
if (!text || !containsCompleteContainer(text)) return false;
|
|
6185
|
+
const manager = editor.markdown;
|
|
6186
|
+
if (!manager) return false;
|
|
6187
|
+
let json;
|
|
6188
|
+
try {
|
|
6189
|
+
json = manager.parse(text.replace(/\r\n?/g, "\n"));
|
|
6190
|
+
} catch {
|
|
6191
|
+
return false;
|
|
6192
|
+
}
|
|
6193
|
+
if (!json.content?.length) return false;
|
|
6194
|
+
stopYjsCapturing(view.state);
|
|
6195
|
+
view.dispatch(closeHistory(view.state.tr));
|
|
6196
|
+
const inserted = editor.commands.insertContentAt(
|
|
6197
|
+
{ from: state.selection.from, to: state.selection.to },
|
|
6198
|
+
json.content,
|
|
6199
|
+
{ updateSelection: true }
|
|
6200
|
+
);
|
|
6201
|
+
stopYjsCapturing(view.state);
|
|
6202
|
+
view.dispatch(closeHistory(view.state.tr));
|
|
6203
|
+
return inserted;
|
|
6204
|
+
}
|
|
6205
|
+
}
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
var CodeGroupInput = Extension.create({
|
|
6209
|
+
name: "codeGroupInput",
|
|
6210
|
+
priority: 2e3,
|
|
6211
|
+
addProseMirrorPlugins() {
|
|
6212
|
+
return [createCodeGroupInputPlugin(this.editor)];
|
|
6213
|
+
}
|
|
6214
|
+
});
|
|
6215
|
+
var containerInputPluginKey = new PluginKey("containerInput");
|
|
6216
|
+
var isContainer = (node) => CONTAINER_NODE_NAMES.includes(node.type.name);
|
|
6217
|
+
function buildContainerNode(schema, opener) {
|
|
6218
|
+
const nodeName = CONTAINER_MARKDOWN_NAMES[opener.name];
|
|
6219
|
+
if (!nodeName || !schema.nodes[nodeName]) return null;
|
|
6220
|
+
const paragraph = schema.nodes.paragraph.create();
|
|
6221
|
+
if (nodeName === "callout") {
|
|
6222
|
+
return schema.nodes.callout.create(
|
|
6223
|
+
{ type: isCalloutType(opener.name) ? opener.name : "info", title: opener.info || null },
|
|
6224
|
+
paragraph
|
|
6225
|
+
);
|
|
6226
|
+
}
|
|
6227
|
+
if (nodeName === "details")
|
|
6228
|
+
return schema.nodes.details.create({ title: opener.info || null }, paragraph);
|
|
6229
|
+
const info = parseParamInfo(opener.info);
|
|
6230
|
+
return schema.nodes.apiParam.create({ ...info, summary: null });
|
|
6231
|
+
}
|
|
6232
|
+
function paragraphSource(paragraph) {
|
|
6233
|
+
let out = "";
|
|
6234
|
+
paragraph.forEach((child) => {
|
|
6235
|
+
if (!child.isText) return;
|
|
6236
|
+
const text = child.text ?? "";
|
|
6237
|
+
out += child.marks.some((m) => m.type.name === "code") ? `\`${text}\`` : text;
|
|
6238
|
+
});
|
|
6239
|
+
return out;
|
|
6240
|
+
}
|
|
6241
|
+
function convertOpenerParagraph(view) {
|
|
6242
|
+
const { state } = view;
|
|
6243
|
+
const { $from, empty } = state.selection;
|
|
6244
|
+
if (!empty || $from.parent.type.name !== "paragraph") return false;
|
|
6245
|
+
if ($from.parentOffset !== $from.parent.content.size) return false;
|
|
6246
|
+
if (findCodeGroupDraft(state)) return false;
|
|
6247
|
+
const opener = parseContainerOpener(paragraphSource($from.parent));
|
|
6248
|
+
if (!opener) return false;
|
|
6249
|
+
const node = buildContainerNode(state.schema, opener);
|
|
6250
|
+
if (!node) return false;
|
|
6251
|
+
const parent = $from.node(-1);
|
|
6252
|
+
const index = $from.index(-1);
|
|
6253
|
+
if (!parent.canReplaceWith(index, index + 1, node.type)) return false;
|
|
6254
|
+
const from = $from.before();
|
|
6255
|
+
const to = $from.after();
|
|
6256
|
+
const trailing = state.schema.nodes.paragraph.create();
|
|
6257
|
+
const tr = state.tr.replaceWith(
|
|
6258
|
+
from,
|
|
6259
|
+
to,
|
|
6260
|
+
node.type.name === "apiParam" ? [node, trailing] : [node]
|
|
6261
|
+
);
|
|
6262
|
+
const cursor = node.type.name === "apiParam" ? from + node.nodeSize + 1 : from + 2;
|
|
6263
|
+
tr.setSelection(TextSelection.create(tr.doc, cursor));
|
|
6264
|
+
view.dispatch(closeHistory(tr).scrollIntoView());
|
|
6265
|
+
if (node.type.name === "apiParam") {
|
|
6266
|
+
queueMicrotask(() => {
|
|
6267
|
+
if (view.isDestroyed) return;
|
|
6268
|
+
const dom = view.nodeDOM(from);
|
|
6269
|
+
dom?.querySelector('input[data-field="summary"]')?.focus();
|
|
6270
|
+
});
|
|
6271
|
+
}
|
|
6272
|
+
return true;
|
|
6273
|
+
}
|
|
6274
|
+
function exitContainerOnEnter(view) {
|
|
6275
|
+
const { state } = view;
|
|
6276
|
+
const { $from, empty } = state.selection;
|
|
6277
|
+
if (!empty || $from.depth < 2) return false;
|
|
6278
|
+
const paragraph = $from.parent;
|
|
6279
|
+
const container = $from.node(-1);
|
|
6280
|
+
if (paragraph.type.name !== "paragraph" || !isContainer(container)) return false;
|
|
6281
|
+
if (paragraph.content.size !== 0) return false;
|
|
6282
|
+
if ($from.index(-1) !== container.childCount - 1) return false;
|
|
6283
|
+
const containerPos = $from.before(-1);
|
|
6284
|
+
const after = containerPos + container.nodeSize;
|
|
6285
|
+
let tr = state.tr;
|
|
6286
|
+
if (container.childCount > 1) tr = tr.delete($from.before(), $from.after());
|
|
6287
|
+
const insertAt = tr.mapping.map(after);
|
|
6288
|
+
tr = tr.insert(insertAt, state.schema.nodes.paragraph.create());
|
|
6289
|
+
tr = tr.setSelection(TextSelection.create(tr.doc, insertAt + 1));
|
|
6290
|
+
view.dispatch(tr.scrollIntoView());
|
|
6291
|
+
return true;
|
|
6292
|
+
}
|
|
6293
|
+
function unwrapContainerOnBackspace(view) {
|
|
6294
|
+
const { state } = view;
|
|
6295
|
+
const { $from, empty } = state.selection;
|
|
6296
|
+
if (!empty || $from.depth < 2 || $from.parentOffset !== 0) return false;
|
|
6297
|
+
const paragraph = $from.parent;
|
|
6298
|
+
const container = $from.node(-1);
|
|
6299
|
+
if (paragraph.type.name !== "paragraph" || !isContainer(container)) return false;
|
|
6300
|
+
if (container.childCount !== 1 || paragraph.content.size !== 0) return false;
|
|
6301
|
+
const pos = $from.before(-1);
|
|
6302
|
+
const tr = state.tr.replaceWith(pos, pos + container.nodeSize, container.content);
|
|
6303
|
+
tr.setSelection(TextSelection.create(tr.doc, pos + 1));
|
|
6304
|
+
view.dispatch(tr);
|
|
6305
|
+
return true;
|
|
6306
|
+
}
|
|
6307
|
+
function isComposing2(view, event) {
|
|
6308
|
+
return Boolean(event.isComposing) || Boolean(view.composing);
|
|
6309
|
+
}
|
|
6310
|
+
var ContainerInput = Extension.create({
|
|
6311
|
+
name: "containerInput",
|
|
6312
|
+
priority: 1500,
|
|
6313
|
+
addProseMirrorPlugins() {
|
|
6314
|
+
return [
|
|
6315
|
+
new Plugin({
|
|
6316
|
+
key: containerInputPluginKey,
|
|
6317
|
+
props: {
|
|
6318
|
+
handleKeyDown(view, event) {
|
|
6319
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return false;
|
|
6320
|
+
if (isComposing2(view, event)) return false;
|
|
6321
|
+
if (event.key === "Enter")
|
|
6322
|
+
return convertOpenerParagraph(view) || exitContainerOnEnter(view);
|
|
6323
|
+
if (event.key === "Backspace") return unwrapContainerOnBackspace(view);
|
|
6324
|
+
return false;
|
|
6325
|
+
}
|
|
6326
|
+
}
|
|
6327
|
+
})
|
|
6328
|
+
];
|
|
6329
|
+
}
|
|
6330
|
+
});
|
|
6331
|
+
|
|
6332
|
+
// src/components/tiptap-templates/notion-like/editor-content-config.ts
|
|
6333
|
+
function normalizeEditorContent(content, contentFormat, extensions, options = {}) {
|
|
6334
|
+
const result = parseEditorContent(content, contentFormat, extensions);
|
|
6335
|
+
if (!result.ok) {
|
|
6336
|
+
(options.onError ?? defaultOnError)(result.error);
|
|
6337
|
+
return null;
|
|
6338
|
+
}
|
|
6339
|
+
return result.json;
|
|
6340
|
+
}
|
|
6341
|
+
function defaultOnError(message) {
|
|
6342
|
+
console.error(`[editor] 内容未载入:${message}`);
|
|
6343
|
+
}
|
|
6344
|
+
function isSameDocument(current, next) {
|
|
6345
|
+
return JSON.stringify(current) === JSON.stringify(next);
|
|
6346
|
+
}
|
|
4202
6347
|
var RESIZE_MIN_WIDTH = 35;
|
|
4203
6348
|
var EMPTY_CELL_WIDTH = 120;
|
|
4204
6349
|
var EMPTY_CELL_HEIGHT = 40;
|
|
@@ -12480,6 +14625,30 @@ var CodeBlockIcon = memo(({ className, ...props }) => {
|
|
|
12480
14625
|
);
|
|
12481
14626
|
});
|
|
12482
14627
|
CodeBlockIcon.displayName = "CodeBlockIcon";
|
|
14628
|
+
var MessageSquareIcon = memo(({ className, ...props }) => {
|
|
14629
|
+
return /* @__PURE__ */ jsx(
|
|
14630
|
+
"svg",
|
|
14631
|
+
{
|
|
14632
|
+
width: "24",
|
|
14633
|
+
height: "24",
|
|
14634
|
+
className,
|
|
14635
|
+
viewBox: "0 0 24 24",
|
|
14636
|
+
fill: "currentColor",
|
|
14637
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
14638
|
+
...props,
|
|
14639
|
+
children: /* @__PURE__ */ jsx(
|
|
14640
|
+
"path",
|
|
14641
|
+
{
|
|
14642
|
+
fillRule: "evenodd",
|
|
14643
|
+
clipRule: "evenodd",
|
|
14644
|
+
d: "M5 5C4.73478 5 4.48043 5.10536 4.29289 5.29289C4.10536 5.48043 4 5.73478 4 6V19.5858L6.29289 17.2929C6.48043 17.1054 6.73478 17 7 17H19C19.2652 17 19.5196 16.8946 19.7071 16.7071C19.8946 16.5196 20 16.2652 20 16V6C20 5.73478 19.8946 5.48043 19.7071 5.29289C19.5196 5.10536 19.2652 5 19 5H5ZM2.87868 3.87868C3.44129 3.31607 4.20435 3 5 3H19C19.7957 3 20.5587 3.31607 21.1213 3.87868C21.6839 4.44129 22 5.20435 22 6V16C22 16.7957 21.6839 17.5587 21.1213 18.1213C20.5587 18.6839 19.7957 19 19 19H7.41421L3.70711 22.7071C3.42111 22.9931 2.99099 23.0787 2.61732 22.9239C2.24364 22.7691 2 22.4045 2 22V6C2 5.20435 2.31607 4.44129 2.87868 3.87868Z",
|
|
14645
|
+
fill: "currentColor"
|
|
14646
|
+
}
|
|
14647
|
+
)
|
|
14648
|
+
}
|
|
14649
|
+
);
|
|
14650
|
+
});
|
|
14651
|
+
MessageSquareIcon.displayName = "MessageSquareIcon";
|
|
12483
14652
|
var HeadingOneIcon = memo(({ className, ...props }) => {
|
|
12484
14653
|
return /* @__PURE__ */ jsxs(
|
|
12485
14654
|
"svg",
|
|
@@ -13272,6 +15441,27 @@ var texts = {
|
|
|
13272
15441
|
badge: CodeBlockIcon,
|
|
13273
15442
|
group: "样式"
|
|
13274
15443
|
},
|
|
15444
|
+
callout: {
|
|
15445
|
+
title: "提示框",
|
|
15446
|
+
subtext: "信息 / 提示 / 注意 / 危险",
|
|
15447
|
+
keywords: ["callout", "note", "tip", "warning", "提示", "注意", "警告"],
|
|
15448
|
+
badge: MessageSquareIcon,
|
|
15449
|
+
group: "样式"
|
|
15450
|
+
},
|
|
15451
|
+
details: {
|
|
15452
|
+
title: "折叠块",
|
|
15453
|
+
subtext: "带标题的可折叠内容",
|
|
15454
|
+
keywords: ["details", "toggle", "collapse", "折叠"],
|
|
15455
|
+
badge: ChevronRightIcon,
|
|
15456
|
+
group: "样式"
|
|
15457
|
+
},
|
|
15458
|
+
api_param: {
|
|
15459
|
+
title: "参数",
|
|
15460
|
+
subtext: "API 参数行(名字 / 类型 / 必选 / 默认值)",
|
|
15461
|
+
keywords: ["param", "parameter", "api", "参数"],
|
|
15462
|
+
badge: ListIndentedIcon,
|
|
15463
|
+
group: "样式"
|
|
15464
|
+
},
|
|
13275
15465
|
// Insert
|
|
13276
15466
|
mention: {
|
|
13277
15467
|
title: "提及",
|
|
@@ -13433,6 +15623,27 @@ var getItemImplementations = () => {
|
|
|
13433
15623
|
editor.chain().focus().toggleNode("codeBlock", "paragraph").run();
|
|
13434
15624
|
}
|
|
13435
15625
|
},
|
|
15626
|
+
callout: {
|
|
15627
|
+
check: (editor) => isNodeInSchema("callout", editor),
|
|
15628
|
+
action: ({ editor }) => {
|
|
15629
|
+
editor.chain().focus().insertContent({ type: "callout", attrs: { type: "info", title: null }, content: [{ type: "paragraph" }] }).run();
|
|
15630
|
+
}
|
|
15631
|
+
},
|
|
15632
|
+
details: {
|
|
15633
|
+
check: (editor) => isNodeInSchema("details", editor),
|
|
15634
|
+
action: ({ editor }) => {
|
|
15635
|
+
editor.chain().focus().insertContent({ type: "details", attrs: { title: null }, content: [{ type: "paragraph" }] }).run();
|
|
15636
|
+
}
|
|
15637
|
+
},
|
|
15638
|
+
api_param: {
|
|
15639
|
+
check: (editor) => isNodeInSchema("apiParam", editor),
|
|
15640
|
+
action: ({ editor }) => {
|
|
15641
|
+
editor.chain().focus().insertContent([
|
|
15642
|
+
{ type: "apiParam", attrs: { name: "", type: null, required: false, default: null, summary: null } },
|
|
15643
|
+
{ type: "paragraph" }
|
|
15644
|
+
]).run();
|
|
15645
|
+
}
|
|
15646
|
+
},
|
|
13436
15647
|
// Insert
|
|
13437
15648
|
mention: {
|
|
13438
15649
|
check: (editor) => isExtensionAvailable(editor, ["mention", "mentionAdvanced"]),
|
|
@@ -20892,7 +23103,6 @@ var Indent = Extension.create({
|
|
|
20892
23103
|
};
|
|
20893
23104
|
}
|
|
20894
23105
|
});
|
|
20895
|
-
var lowlight = createLowlight(common);
|
|
20896
23106
|
function LoadingSpinner({ text = "Connecting..." }) {
|
|
20897
23107
|
return /* @__PURE__ */ jsx("div", { className: "spinner-container", children: /* @__PURE__ */ jsxs("div", { className: "spinner-content", children: [
|
|
20898
23108
|
/* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [
|
|
@@ -20936,6 +23146,8 @@ function EditorProvider(props) {
|
|
|
20936
23146
|
ydoc,
|
|
20937
23147
|
placeholder = "开始书写...",
|
|
20938
23148
|
content,
|
|
23149
|
+
contentFormat,
|
|
23150
|
+
codeBlock: codeBlockOptions,
|
|
20939
23151
|
readOnly = false,
|
|
20940
23152
|
hideHeader = false,
|
|
20941
23153
|
embedded = false,
|
|
@@ -20962,178 +23174,196 @@ function EditorProvider(props) {
|
|
|
20962
23174
|
const videoMaxSize = resolveMaxSize("video", editorConfig?.upload);
|
|
20963
23175
|
const audioMaxSize = resolveMaxSize("audio", editorConfig?.upload);
|
|
20964
23176
|
const fileMaxSize = resolveMaxSize("file", editorConfig?.upload);
|
|
23177
|
+
const extensions = [
|
|
23178
|
+
StarterKit.configure({
|
|
23179
|
+
undoRedo: false,
|
|
23180
|
+
horizontalRule: false,
|
|
23181
|
+
// 关掉 StarterKit 自带的纯 codeBlock,换成 Shiki 高亮 + 分组的版本
|
|
23182
|
+
codeBlock: false,
|
|
23183
|
+
dropcursor: {
|
|
23184
|
+
width: 2
|
|
23185
|
+
},
|
|
23186
|
+
link: { openOnClick: false }
|
|
23187
|
+
}),
|
|
23188
|
+
// 代码块 / 分组 / 高亮 / Markdown / 直接输入:顺序固定,且在协同与 UniqueID 之前
|
|
23189
|
+
CodeBlockExtension,
|
|
23190
|
+
CodeGroupExtension,
|
|
23191
|
+
CodeHighlight.configure(codeBlockOptions ?? {}),
|
|
23192
|
+
createMarkdownExtension(),
|
|
23193
|
+
CodeGroupInput,
|
|
23194
|
+
// 提示框 / 折叠块 / API 参数:与代码分组同一套容器语法与折叠机制
|
|
23195
|
+
CalloutExtension,
|
|
23196
|
+
DetailsExtension,
|
|
23197
|
+
ApiParamExtension,
|
|
23198
|
+
ContainerInput,
|
|
23199
|
+
HorizontalRule,
|
|
23200
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
23201
|
+
// Collaboration 可以无 provider 运行(仅操作本地 yjs doc),CollaborationCaret
|
|
23202
|
+
// 强依赖 provider.awareness,没 provider 时不挂以避免 runtime 崩。
|
|
23203
|
+
Collaboration.configure({ document: ydoc }),
|
|
23204
|
+
...provider ? [
|
|
23205
|
+
CollaborationCaret.configure({
|
|
23206
|
+
provider,
|
|
23207
|
+
user: { id: user.id, name: user.name, color: user.color }
|
|
23208
|
+
})
|
|
23209
|
+
] : [],
|
|
23210
|
+
Placeholder.configure({
|
|
23211
|
+
placeholder,
|
|
23212
|
+
emptyNodeClass: "is-empty with-slash"
|
|
23213
|
+
}),
|
|
23214
|
+
Mention,
|
|
23215
|
+
Emoji.configure({
|
|
23216
|
+
emojis: gitHubEmojis.filter(
|
|
23217
|
+
(emoji) => !emoji.name.includes("regional")
|
|
23218
|
+
),
|
|
23219
|
+
forceFallbackImages: true
|
|
23220
|
+
}),
|
|
23221
|
+
TableKit.configure({
|
|
23222
|
+
table: {
|
|
23223
|
+
resizable: true,
|
|
23224
|
+
cellMinWidth: 120
|
|
23225
|
+
}
|
|
23226
|
+
}),
|
|
23227
|
+
NodeBackground.configure({
|
|
23228
|
+
types: [
|
|
23229
|
+
"paragraph",
|
|
23230
|
+
"heading",
|
|
23231
|
+
"blockquote",
|
|
23232
|
+
"taskList",
|
|
23233
|
+
"bulletList",
|
|
23234
|
+
"orderedList",
|
|
23235
|
+
"tableCell",
|
|
23236
|
+
"tableHeader",
|
|
23237
|
+
"tocNode"
|
|
23238
|
+
]
|
|
23239
|
+
}),
|
|
23240
|
+
NodeAlignment,
|
|
23241
|
+
// 行距 / 段前段后距:Word 把它们挂在每个段落上,导入时没有对应属性就会
|
|
23242
|
+
// 整篇塌回编辑器的统一边距,原文的疏密节奏全丢。
|
|
23243
|
+
BlockSpacing.configure({
|
|
23244
|
+
types: ["paragraph", "heading", "blockquote"]
|
|
23245
|
+
}),
|
|
23246
|
+
TextStyle,
|
|
23247
|
+
// 字号与字体是 textStyle 上的属性。不注册这两个扩展,schema 里就没有
|
|
23248
|
+
// fontSize / fontFamily,ProseMirror 会**静默丢弃**这两个属性 ——
|
|
23249
|
+
// 表现是 docx 导入后全文一个字号(见 docx/parse.ts 的 fmtToMarks)。
|
|
23250
|
+
FontSize,
|
|
23251
|
+
FontFamily,
|
|
23252
|
+
Mathematics,
|
|
23253
|
+
Superscript,
|
|
23254
|
+
Subscript,
|
|
23255
|
+
Indent,
|
|
23256
|
+
Color,
|
|
23257
|
+
TaskList,
|
|
23258
|
+
TaskItem.configure({ nested: true }),
|
|
23259
|
+
Highlight.configure({ multicolor: true }),
|
|
23260
|
+
Selection$1,
|
|
23261
|
+
Image,
|
|
23262
|
+
TableOfContents.configure({
|
|
23263
|
+
getIndex: getHierarchicalIndexes,
|
|
23264
|
+
// 见 resolveScrollParent 的说明:默认的 window 在「内层 div 滚动」的宿主里收不到事件
|
|
23265
|
+
scrollParent: resolveScrollParent,
|
|
23266
|
+
onUpdate(content2) {
|
|
23267
|
+
setTocContent(content2);
|
|
23268
|
+
}
|
|
23269
|
+
}),
|
|
23270
|
+
TableHandleExtension,
|
|
23271
|
+
ListNormalizationExtension,
|
|
23272
|
+
ImageUploadNode2.configure({
|
|
23273
|
+
accept: "image/*",
|
|
23274
|
+
maxSize: imageMaxSize,
|
|
23275
|
+
limit: 3,
|
|
23276
|
+
upload: onUpload,
|
|
23277
|
+
onError: onUploadError
|
|
23278
|
+
}),
|
|
23279
|
+
Video,
|
|
23280
|
+
Audio,
|
|
23281
|
+
FileAttachment,
|
|
23282
|
+
MediaUploadPlaceholder,
|
|
23283
|
+
Columns,
|
|
23284
|
+
Column,
|
|
23285
|
+
// 编辑器整面:粘贴/拖拽 文件 → 自动按 mime 分发到 image/video/audio/fileAttachment 节点;
|
|
23286
|
+
// 拖拽外链图片 → XHR rehost 到自家 CDN
|
|
23287
|
+
PasteDropMedia.configure({
|
|
23288
|
+
upload: onUpload,
|
|
23289
|
+
maxFileSizes: {
|
|
23290
|
+
image: imageMaxSize,
|
|
23291
|
+
video: videoMaxSize,
|
|
23292
|
+
audio: audioMaxSize,
|
|
23293
|
+
file: fileMaxSize
|
|
23294
|
+
},
|
|
23295
|
+
rehostExternalUrls: true,
|
|
23296
|
+
onError: onUploadError,
|
|
23297
|
+
onRehostProgress
|
|
23298
|
+
}),
|
|
23299
|
+
UniqueID.configure({
|
|
23300
|
+
/**
|
|
23301
|
+
* 带稳定 id 的节点类型 —— 也就是 AI 能直接寻址的那些(见 doc-runtime)。
|
|
23302
|
+
*
|
|
23303
|
+
* 原本只配了上面 9 种块级类型,于是 listItem / taskItem / tableCell /
|
|
23304
|
+
* columns / column / image 这些**结构**节点全都没 id:AI 能读到它们,
|
|
23305
|
+
* 却没法指着其中一个说「改这个」。doc_modify_nodes 的 modify 要求
|
|
23306
|
+
* litexml 带 id,所以「把第 3 个列表项加粗」「把这张图换掉」
|
|
23307
|
+
* 「改这个单元格的底色」在旧配置下根本表达不出来。
|
|
23308
|
+
*
|
|
23309
|
+
* 代价是每个此类节点在文档里多一个 id 属性(大表格尤其明显)。
|
|
23310
|
+
* 拿可寻址性换体积,这笔是划算的 —— 寻址不了的能力等于没有。
|
|
23311
|
+
*/
|
|
23312
|
+
types: [
|
|
23313
|
+
// 块级容器
|
|
23314
|
+
"paragraph",
|
|
23315
|
+
"heading",
|
|
23316
|
+
"blockquote",
|
|
23317
|
+
"codeBlock",
|
|
23318
|
+
"codeGroup",
|
|
23319
|
+
"callout",
|
|
23320
|
+
"details",
|
|
23321
|
+
"apiParam",
|
|
23322
|
+
"tocNode",
|
|
23323
|
+
// 列表:容器和条目都要,否则改不了单条
|
|
23324
|
+
"bulletList",
|
|
23325
|
+
"orderedList",
|
|
23326
|
+
"listItem",
|
|
23327
|
+
"taskList",
|
|
23328
|
+
"taskItem",
|
|
23329
|
+
// 表格:结构性改动走 (tableId,row,col),但改单元格属性只能靠 id
|
|
23330
|
+
"table",
|
|
23331
|
+
"tableCell",
|
|
23332
|
+
"tableHeader",
|
|
23333
|
+
// 分栏
|
|
23334
|
+
"columns",
|
|
23335
|
+
"column",
|
|
23336
|
+
// 媒体
|
|
23337
|
+
"image",
|
|
23338
|
+
"video",
|
|
23339
|
+
"audio",
|
|
23340
|
+
"fileAttachment"
|
|
23341
|
+
],
|
|
23342
|
+
filterTransaction: (transaction) => !isChangeOrigin(transaction)
|
|
23343
|
+
}),
|
|
23344
|
+
Typography,
|
|
23345
|
+
UiState,
|
|
23346
|
+
TocNode.configure({
|
|
23347
|
+
topOffset: 48
|
|
23348
|
+
}),
|
|
23349
|
+
// 业务方注入的扩展放在最后,可覆盖同名配置
|
|
23350
|
+
...extraExtensions ?? []
|
|
23351
|
+
];
|
|
23352
|
+
const [initialContent] = useState(
|
|
23353
|
+
() => provider ? void 0 : normalizeEditorContent(content, contentFormat, extensions) ?? void 0
|
|
23354
|
+
);
|
|
20965
23355
|
const editor = useEditor({
|
|
20966
23356
|
immediatelyRender: false,
|
|
20967
23357
|
// 只读模式:禁用编辑。扩展集保持不变,自定义节点仍走各自 NodeView 渲染
|
|
20968
23358
|
editable: !readOnly,
|
|
20969
23359
|
// 仅本地编辑模式(无 provider)下用 content 作初始值;协同模式让 yjs sync 接管
|
|
20970
|
-
content:
|
|
23360
|
+
content: initialContent,
|
|
20971
23361
|
editorProps: {
|
|
20972
23362
|
attributes: {
|
|
20973
23363
|
class: "notion-like-editor"
|
|
20974
23364
|
}
|
|
20975
23365
|
},
|
|
20976
|
-
extensions
|
|
20977
|
-
StarterKit.configure({
|
|
20978
|
-
undoRedo: false,
|
|
20979
|
-
horizontalRule: false,
|
|
20980
|
-
// 关掉 StarterKit 自带的纯 codeBlock,换成带 lowlight 语法高亮的版本
|
|
20981
|
-
codeBlock: false,
|
|
20982
|
-
dropcursor: {
|
|
20983
|
-
width: 2
|
|
20984
|
-
},
|
|
20985
|
-
link: { openOnClick: false }
|
|
20986
|
-
}),
|
|
20987
|
-
CodeBlockLowlight.configure({ lowlight }),
|
|
20988
|
-
HorizontalRule,
|
|
20989
|
-
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
20990
|
-
// Collaboration 可以无 provider 运行(仅操作本地 yjs doc),CollaborationCaret
|
|
20991
|
-
// 强依赖 provider.awareness,没 provider 时不挂以避免 runtime 崩。
|
|
20992
|
-
Collaboration.configure({ document: ydoc }),
|
|
20993
|
-
...provider ? [
|
|
20994
|
-
CollaborationCaret.configure({
|
|
20995
|
-
provider,
|
|
20996
|
-
user: { id: user.id, name: user.name, color: user.color }
|
|
20997
|
-
})
|
|
20998
|
-
] : [],
|
|
20999
|
-
Placeholder.configure({
|
|
21000
|
-
placeholder,
|
|
21001
|
-
emptyNodeClass: "is-empty with-slash"
|
|
21002
|
-
}),
|
|
21003
|
-
Mention,
|
|
21004
|
-
Emoji.configure({
|
|
21005
|
-
emojis: gitHubEmojis.filter(
|
|
21006
|
-
(emoji) => !emoji.name.includes("regional")
|
|
21007
|
-
),
|
|
21008
|
-
forceFallbackImages: true
|
|
21009
|
-
}),
|
|
21010
|
-
TableKit.configure({
|
|
21011
|
-
table: {
|
|
21012
|
-
resizable: true,
|
|
21013
|
-
cellMinWidth: 120
|
|
21014
|
-
}
|
|
21015
|
-
}),
|
|
21016
|
-
NodeBackground.configure({
|
|
21017
|
-
types: [
|
|
21018
|
-
"paragraph",
|
|
21019
|
-
"heading",
|
|
21020
|
-
"blockquote",
|
|
21021
|
-
"taskList",
|
|
21022
|
-
"bulletList",
|
|
21023
|
-
"orderedList",
|
|
21024
|
-
"tableCell",
|
|
21025
|
-
"tableHeader",
|
|
21026
|
-
"tocNode"
|
|
21027
|
-
]
|
|
21028
|
-
}),
|
|
21029
|
-
NodeAlignment,
|
|
21030
|
-
// 行距 / 段前段后距:Word 把它们挂在每个段落上,导入时没有对应属性就会
|
|
21031
|
-
// 整篇塌回编辑器的统一边距,原文的疏密节奏全丢。
|
|
21032
|
-
BlockSpacing.configure({
|
|
21033
|
-
types: ["paragraph", "heading", "blockquote"]
|
|
21034
|
-
}),
|
|
21035
|
-
TextStyle,
|
|
21036
|
-
// 字号与字体是 textStyle 上的属性。不注册这两个扩展,schema 里就没有
|
|
21037
|
-
// fontSize / fontFamily,ProseMirror 会**静默丢弃**这两个属性 ——
|
|
21038
|
-
// 表现是 docx 导入后全文一个字号(见 docx/parse.ts 的 fmtToMarks)。
|
|
21039
|
-
FontSize,
|
|
21040
|
-
FontFamily,
|
|
21041
|
-
Mathematics,
|
|
21042
|
-
Superscript,
|
|
21043
|
-
Subscript,
|
|
21044
|
-
Indent,
|
|
21045
|
-
Color,
|
|
21046
|
-
TaskList,
|
|
21047
|
-
TaskItem.configure({ nested: true }),
|
|
21048
|
-
Highlight.configure({ multicolor: true }),
|
|
21049
|
-
Selection$1,
|
|
21050
|
-
Image,
|
|
21051
|
-
TableOfContents.configure({
|
|
21052
|
-
getIndex: getHierarchicalIndexes,
|
|
21053
|
-
// 见 resolveScrollParent 的说明:默认的 window 在「内层 div 滚动」的宿主里收不到事件
|
|
21054
|
-
scrollParent: resolveScrollParent,
|
|
21055
|
-
onUpdate(content2) {
|
|
21056
|
-
setTocContent(content2);
|
|
21057
|
-
}
|
|
21058
|
-
}),
|
|
21059
|
-
TableHandleExtension,
|
|
21060
|
-
ListNormalizationExtension,
|
|
21061
|
-
ImageUploadNode2.configure({
|
|
21062
|
-
accept: "image/*",
|
|
21063
|
-
maxSize: imageMaxSize,
|
|
21064
|
-
limit: 3,
|
|
21065
|
-
upload: onUpload,
|
|
21066
|
-
onError: onUploadError
|
|
21067
|
-
}),
|
|
21068
|
-
Video,
|
|
21069
|
-
Audio,
|
|
21070
|
-
FileAttachment,
|
|
21071
|
-
MediaUploadPlaceholder,
|
|
21072
|
-
Columns,
|
|
21073
|
-
Column,
|
|
21074
|
-
// 编辑器整面:粘贴/拖拽 文件 → 自动按 mime 分发到 image/video/audio/fileAttachment 节点;
|
|
21075
|
-
// 拖拽外链图片 → XHR rehost 到自家 CDN
|
|
21076
|
-
PasteDropMedia.configure({
|
|
21077
|
-
upload: onUpload,
|
|
21078
|
-
maxFileSizes: {
|
|
21079
|
-
image: imageMaxSize,
|
|
21080
|
-
video: videoMaxSize,
|
|
21081
|
-
audio: audioMaxSize,
|
|
21082
|
-
file: fileMaxSize
|
|
21083
|
-
},
|
|
21084
|
-
rehostExternalUrls: true,
|
|
21085
|
-
onError: onUploadError,
|
|
21086
|
-
onRehostProgress
|
|
21087
|
-
}),
|
|
21088
|
-
UniqueID.configure({
|
|
21089
|
-
/**
|
|
21090
|
-
* 带稳定 id 的节点类型 —— 也就是 AI 能直接寻址的那些(见 doc-runtime)。
|
|
21091
|
-
*
|
|
21092
|
-
* 原本只配了上面 9 种块级类型,于是 listItem / taskItem / tableCell /
|
|
21093
|
-
* columns / column / image 这些**结构**节点全都没 id:AI 能读到它们,
|
|
21094
|
-
* 却没法指着其中一个说「改这个」。doc_modify_nodes 的 modify 要求
|
|
21095
|
-
* litexml 带 id,所以「把第 3 个列表项加粗」「把这张图换掉」
|
|
21096
|
-
* 「改这个单元格的底色」在旧配置下根本表达不出来。
|
|
21097
|
-
*
|
|
21098
|
-
* 代价是每个此类节点在文档里多一个 id 属性(大表格尤其明显)。
|
|
21099
|
-
* 拿可寻址性换体积,这笔是划算的 —— 寻址不了的能力等于没有。
|
|
21100
|
-
*/
|
|
21101
|
-
types: [
|
|
21102
|
-
// 块级容器
|
|
21103
|
-
"paragraph",
|
|
21104
|
-
"heading",
|
|
21105
|
-
"blockquote",
|
|
21106
|
-
"codeBlock",
|
|
21107
|
-
"tocNode",
|
|
21108
|
-
// 列表:容器和条目都要,否则改不了单条
|
|
21109
|
-
"bulletList",
|
|
21110
|
-
"orderedList",
|
|
21111
|
-
"listItem",
|
|
21112
|
-
"taskList",
|
|
21113
|
-
"taskItem",
|
|
21114
|
-
// 表格:结构性改动走 (tableId,row,col),但改单元格属性只能靠 id
|
|
21115
|
-
"table",
|
|
21116
|
-
"tableCell",
|
|
21117
|
-
"tableHeader",
|
|
21118
|
-
// 分栏
|
|
21119
|
-
"columns",
|
|
21120
|
-
"column",
|
|
21121
|
-
// 媒体
|
|
21122
|
-
"image",
|
|
21123
|
-
"video",
|
|
21124
|
-
"audio",
|
|
21125
|
-
"fileAttachment"
|
|
21126
|
-
],
|
|
21127
|
-
filterTransaction: (transaction) => !isChangeOrigin(transaction)
|
|
21128
|
-
}),
|
|
21129
|
-
Typography,
|
|
21130
|
-
UiState,
|
|
21131
|
-
TocNode.configure({
|
|
21132
|
-
topOffset: 48
|
|
21133
|
-
}),
|
|
21134
|
-
// 业务方注入的扩展放在最后,可覆盖同名配置
|
|
21135
|
-
...extraExtensions ?? []
|
|
21136
|
-
]
|
|
23366
|
+
extensions
|
|
21137
23367
|
});
|
|
21138
23368
|
useEffect(() => {
|
|
21139
23369
|
if (!editor) return;
|
|
@@ -21157,18 +23387,18 @@ function EditorProvider(props) {
|
|
|
21157
23387
|
if (provider) return;
|
|
21158
23388
|
if (!editor) return;
|
|
21159
23389
|
if (content === void 0 || content === null || content === "") return;
|
|
21160
|
-
const
|
|
21161
|
-
|
|
21162
|
-
if (
|
|
23390
|
+
const next = normalizeEditorContent(content, contentFormat, editor.extensionManager.extensions);
|
|
23391
|
+
if (!next) return;
|
|
23392
|
+
if (isSameDocument(editor.getJSON(), next)) return;
|
|
21163
23393
|
let cancelled = false;
|
|
21164
23394
|
queueMicrotask(() => {
|
|
21165
23395
|
if (cancelled || editor.isDestroyed) return;
|
|
21166
|
-
editor.commands.setContent(
|
|
23396
|
+
editor.commands.setContent(next, { emitUpdate: false });
|
|
21167
23397
|
});
|
|
21168
23398
|
return () => {
|
|
21169
23399
|
cancelled = true;
|
|
21170
23400
|
};
|
|
21171
|
-
}, [content, editor, provider]);
|
|
23401
|
+
}, [content, contentFormat, editor, provider]);
|
|
21172
23402
|
useEffect(() => {
|
|
21173
23403
|
if (!editor) return;
|
|
21174
23404
|
onEditorReady?.(editor);
|
|
@@ -21211,6 +23441,8 @@ function NotionEditor({
|
|
|
21211
23441
|
room,
|
|
21212
23442
|
placeholder = "开始书写...",
|
|
21213
23443
|
content,
|
|
23444
|
+
contentFormat,
|
|
23445
|
+
codeBlock,
|
|
21214
23446
|
readOnly = false,
|
|
21215
23447
|
hideHeader = false,
|
|
21216
23448
|
onEditorReady,
|
|
@@ -21221,6 +23453,8 @@ function NotionEditor({
|
|
|
21221
23453
|
{
|
|
21222
23454
|
placeholder,
|
|
21223
23455
|
content,
|
|
23456
|
+
contentFormat,
|
|
23457
|
+
codeBlock,
|
|
21224
23458
|
readOnly,
|
|
21225
23459
|
hideHeader,
|
|
21226
23460
|
onEditorReady,
|
|
@@ -21231,6 +23465,8 @@ function NotionEditor({
|
|
|
21231
23465
|
function NotionEditorContent({
|
|
21232
23466
|
placeholder,
|
|
21233
23467
|
content,
|
|
23468
|
+
contentFormat,
|
|
23469
|
+
codeBlock,
|
|
21234
23470
|
readOnly = false,
|
|
21235
23471
|
hideHeader = false,
|
|
21236
23472
|
onEditorReady,
|
|
@@ -21250,6 +23486,8 @@ function NotionEditorContent({
|
|
|
21250
23486
|
ydoc,
|
|
21251
23487
|
placeholder,
|
|
21252
23488
|
content,
|
|
23489
|
+
contentFormat,
|
|
23490
|
+
codeBlock,
|
|
21253
23491
|
readOnly,
|
|
21254
23492
|
hideHeader,
|
|
21255
23493
|
onEditorReady,
|
|
@@ -21257,7 +23495,7 @@ function NotionEditorContent({
|
|
|
21257
23495
|
}
|
|
21258
23496
|
);
|
|
21259
23497
|
}
|
|
21260
|
-
function ArticleViewer({ content, placeholder = "" }) {
|
|
23498
|
+
function ArticleViewer({ content, contentFormat, codeBlock, placeholder = "" }) {
|
|
21261
23499
|
const ydoc = useMemo(() => new Doc(), []);
|
|
21262
23500
|
return /* @__PURE__ */ jsx(UserProvider, { children: /* @__PURE__ */ jsx(TocProvider, { children: /* @__PURE__ */ jsx(
|
|
21263
23501
|
EditorProvider,
|
|
@@ -21266,12 +23504,14 @@ function ArticleViewer({ content, placeholder = "" }) {
|
|
|
21266
23504
|
ydoc,
|
|
21267
23505
|
placeholder,
|
|
21268
23506
|
content,
|
|
23507
|
+
contentFormat,
|
|
23508
|
+
codeBlock,
|
|
21269
23509
|
readOnly: true,
|
|
21270
23510
|
embedded: true
|
|
21271
23511
|
}
|
|
21272
23512
|
) }) });
|
|
21273
23513
|
}
|
|
21274
23514
|
|
|
21275
|
-
export { ArticleViewer, Audio, CollabContext, CollabProvider, Column, Columns, DEFAULT_MAX_FILE_SIZES, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EditorConfigProvider, EditorContentArea, EditorProvider, FileAttachment, FloatingElement, HIDE_FLOATING_META, LoadingSpinner, MediaUploadButton, MediaUploadPlaceholder, NotionEditor, NotionEditorContent, PasteDropMedia, Separator, Spacer, Toolbar, ToolbarGroup, ToolbarSeparator, UiState, UserContext, UserProvider, Video, defaultUiState, detectMediaKind, findSelectionPosition, hasContentAbove, markHideFloatingOnNext, pickFile, resolveMaxSize, runMediaUpload, selectNodeAndHideFloating, useCollab, useCollaboration, useComposedRef, useCursorVisibility, useEditorConfig, useElementRect, useFloatingElement, useFloatingToolbarVisibility, useIsBreakpoint, useIsomorphicLayoutEffect, useMenuNavigation, useOnClickOutside, useThrottledCallback, useUiEditorState, useUnmount, useUser, useWindowSize, use_ui_editor_state_default };
|
|
21276
|
-
//# sourceMappingURL=chunk-
|
|
21277
|
-
//# sourceMappingURL=chunk-
|
|
23515
|
+
export { ApiParamExtension, ArticleViewer, Audio, CalloutExtension, CodeBlockExtension, CodeGroupExtension, CollabContext, CollabProvider, Column, Columns, DEFAULT_MAX_FILE_SIZES, DetailsExtension, DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EditorConfigProvider, EditorContentArea, EditorProvider, FileAttachment, FloatingElement, HIDE_FLOATING_META, LoadingSpinner, MediaUploadButton, MediaUploadPlaceholder, NotionEditor, NotionEditorContent, PasteDropMedia, Separator, Spacer, Toolbar, ToolbarGroup, ToolbarSeparator, UiState, UserContext, UserProvider, Video, defaultUiState, detectMediaKind, findSelectionPosition, getEditorMarkdown, hasContentAbove, markHideFloatingOnNext, parseEditorContent, pickFile, renderHighlightedHTML, resolveMaxSize, runMediaUpload, selectNodeAndHideFloating, serializeEditorMarkdown, useCollab, useCollaboration, useComposedRef, useCursorVisibility, useEditorConfig, useElementRect, useFloatingElement, useFloatingToolbarVisibility, useIsBreakpoint, useIsomorphicLayoutEffect, useMenuNavigation, useOnClickOutside, useThrottledCallback, useUiEditorState, useUnmount, useUser, useWindowSize, use_ui_editor_state_default };
|
|
23516
|
+
//# sourceMappingURL=chunk-SGLNT6Z2.js.map
|
|
23517
|
+
//# sourceMappingURL=chunk-SGLNT6Z2.js.map
|