@frontlit/text-editor 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import React5, { createContext, useState, useRef, useMemo, useEffect, useCallback, useContext } from 'react';
3
- import { Node, generateText } from '@tiptap/core';
3
+ import { Node, getSchema, generateText } from '@tiptap/core';
4
4
  import { useEditor, EditorContent } from '@tiptap/react';
5
5
  import StarterKit from '@tiptap/starter-kit';
6
6
  import Placeholder from '@tiptap/extension-placeholder';
7
7
  import Link from '@tiptap/extension-link';
8
- import { TableRow, TableHeader, TableCell, Table } from '@tiptap/extension-table';
8
+ import { TableRow, Table, TableCell, TableHeader } from '@tiptap/extension-table';
9
9
  import Image from '@tiptap/extension-image';
10
10
  import Dropcursor from '@tiptap/extension-dropcursor';
11
11
  import Gapcursor from '@tiptap/extension-gapcursor';
@@ -190,7 +190,8 @@ var LinkEditorPopover = ({
190
190
  "input",
191
191
  {
192
192
  ref: inputRef,
193
- type: "url",
193
+ type: "text",
194
+ inputMode: "url",
194
195
  placeholder: "https://example.com",
195
196
  value: linkValue,
196
197
  onChange: (event) => setLinkValue(event.target.value),
@@ -243,7 +244,8 @@ var LinkEditorPopover = ({
243
244
  "input",
244
245
  {
245
246
  ref: inputRef,
246
- type: "url",
247
+ type: "text",
248
+ inputMode: "url",
247
249
  placeholder: "https://example.com",
248
250
  value: linkValue,
249
251
  onChange: (event) => setLinkValue(event.target.value),
@@ -327,7 +329,8 @@ function Divider() {
327
329
  var Toolbar = ({
328
330
  editor,
329
331
  onRequestImageUpload,
330
- isUploadingImage
332
+ isUploadingImage,
333
+ profile = "default"
331
334
  }) => {
332
335
  const [activePopover, setActivePopover] = useState(null);
333
336
  const [wordCount, setWordCount] = useState(0);
@@ -362,7 +365,13 @@ var Toolbar = ({
362
365
  };
363
366
  }, [editor]);
364
367
  if (!editor) return null;
365
- const activeHeading = headingOptions.find(
368
+ const availableHeadingOptions = profile === "documentation" ? [
369
+ ...headingOptions,
370
+ { label: "Heading 4", value: "h4", level: 4 },
371
+ { label: "Heading 5", value: "h5", level: 5 },
372
+ { label: "Heading 6", value: "h6", level: 6 }
373
+ ] : headingOptions;
374
+ const activeHeading = availableHeadingOptions.find(
366
375
  (option) => option.level ? editor.isActive("heading", { level: option.level }) : editor.isActive("paragraph")
367
376
  ) ?? headingOptions[0];
368
377
  const readTime = wordCount === 0 ? 0 : Math.max(1, Math.ceil(wordCount / 200));
@@ -419,7 +428,7 @@ var Toolbar = ({
419
428
  ]
420
429
  }
421
430
  ),
422
- activePopover === "heading" && /* @__PURE__ */ jsx("div", { className: "te-popover absolute left-0 top-full z-30 mt-1 w-40 rounded-md p-1 shadow-lg", children: headingOptions.map((option) => /* @__PURE__ */ jsx(
431
+ activePopover === "heading" && /* @__PURE__ */ jsx("div", { className: "te-popover absolute left-0 top-full z-30 mt-1 w-40 rounded-md p-1 shadow-lg", children: availableHeadingOptions.map((option) => /* @__PURE__ */ jsx(
423
432
  "button",
424
433
  {
425
434
  type: "button",
@@ -452,7 +461,7 @@ var Toolbar = ({
452
461
  onClick: () => editor.chain().focus().toggleItalic().run()
453
462
  }
454
463
  ),
455
- /* @__PURE__ */ jsx(
464
+ profile !== "documentation" && /* @__PURE__ */ jsx(
456
465
  IconButton,
457
466
  {
458
467
  icon: Underline,
@@ -476,9 +485,7 @@ var Toolbar = ({
476
485
  icon: Link$1,
477
486
  label: "Insert link",
478
487
  active: editor.isActive("link"),
479
- onClick: () => setActivePopover(
480
- (current) => current === "link" ? null : "link"
481
- )
488
+ onClick: () => setActivePopover((current) => current === "link" ? null : "link")
482
489
  }
483
490
  ),
484
491
  /* @__PURE__ */ jsx(Divider, {}),
@@ -523,9 +530,7 @@ var Toolbar = ({
523
530
  className: "flex h-8 items-center gap-1.5 rounded border border-primary px-2.5 text-sm font-semibold text-primary hover:bg-[var(--primary-soft)]",
524
531
  "aria-haspopup": "menu",
525
532
  "aria-expanded": activePopover === "insert",
526
- onClick: () => setActivePopover(
527
- (current) => current === "insert" ? null : "insert"
528
- ),
533
+ onClick: () => setActivePopover((current) => current === "insert" ? null : "insert"),
529
534
  children: [
530
535
  /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
531
536
  "Insert"
@@ -706,15 +711,7 @@ var TipTapBubbleMenu = ({
706
711
  getReferencedVirtualElement,
707
712
  options
708
713
  ]);
709
- return /* @__PURE__ */ jsx(
710
- "div",
711
- {
712
- ref: menuRef,
713
- className,
714
- style: { visibility: "hidden" },
715
- children
716
- }
717
- );
714
+ return /* @__PURE__ */ jsx("div", { ref: menuRef, className, style: { visibility: "hidden" }, children });
718
715
  };
719
716
  function InlineButton({
720
717
  icon: Icon,
@@ -735,7 +732,8 @@ function InlineButton({
735
732
  );
736
733
  }
737
734
  var InlineBubbleMenu = ({
738
- editor
735
+ editor,
736
+ profile = "default"
739
737
  }) => {
740
738
  const [isLinkEditing, setIsLinkEditing] = useState(false);
741
739
  const openLinkEditor = useCallback(() => {
@@ -778,7 +776,7 @@ var InlineBubbleMenu = ({
778
776
  onClick: () => editor.chain().focus().toggleItalic().run()
779
777
  }
780
778
  ),
781
- /* @__PURE__ */ jsx(
779
+ profile !== "documentation" && /* @__PURE__ */ jsx(
782
780
  InlineButton,
783
781
  {
784
782
  icon: Underline,
@@ -831,7 +829,9 @@ var bubble_menu_default = InlineBubbleMenu;
831
829
  function cn(...inputs) {
832
830
  return twMerge(clsx(inputs));
833
831
  }
834
- var ImagePickerContext = createContext(null);
832
+ var ImagePickerContext = createContext(
833
+ null
834
+ );
835
835
  function ImagePickerContextProvider({
836
836
  pickImage,
837
837
  children
@@ -841,10 +841,74 @@ function ImagePickerContextProvider({
841
841
  function useImagePicker() {
842
842
  return useContext(ImagePickerContext);
843
843
  }
844
+
845
+ // src/docs/urls.ts
846
+ function isSafeDocsUrl(value, image = false) {
847
+ if (typeof value !== "string" || !value || value !== value.trim()) return false;
848
+ if (
849
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Browsers ignore controls in dangerous protocols.
850
+ /[\u0000-\u0020\u007f\\]/.test(value) || /%(?:0[0-9a-f]|1[0-9a-f]|7f|5c)/i.test(value)
851
+ )
852
+ return false;
853
+ if (value.startsWith("//")) return false;
854
+ const privatePath = /\/(?:attachments|private|ticket-attachments|conversation-attachments)(?:\/|$)/i;
855
+ const signedKey = /^(?:x-amz-.+|x-goog-.+|signature|sig|token|access_token|expires)$/i;
856
+ if (/^(?:https?:|mailto:)/i.test(value)) {
857
+ try {
858
+ const url = new URL(value);
859
+ if (image && url.protocol !== "https:") return false;
860
+ if (url.username || url.password) return false;
861
+ if (url.protocol === "mailto:") return !image && Boolean(url.pathname);
862
+ if (privatePath.test(decodeURIComponent(url.pathname))) return false;
863
+ for (const key of url.searchParams.keys()) {
864
+ if (signedKey.test(key)) return false;
865
+ }
866
+ return Boolean(url.hostname);
867
+ } catch {
868
+ return false;
869
+ }
870
+ }
871
+ if (image) return false;
872
+ if (/^[^/?#]*:/.test(value) || /%(?:2f|3a)/i.test(value)) return false;
873
+ try {
874
+ const url = new URL(value, "https://docs.invalid/");
875
+ return !privatePath.test(decodeURIComponent(url.pathname)) && ![...url.searchParams.keys()].some((key) => signedKey.test(key));
876
+ } catch {
877
+ return false;
878
+ }
879
+ }
880
+
881
+ // src/docs/extensions.ts
882
+ function createDocsExtensions() {
883
+ return [
884
+ StarterKit.configure({
885
+ document: false,
886
+ underline: false,
887
+ link: false,
888
+ dropcursor: false,
889
+ gapcursor: false,
890
+ trailingNode: false
891
+ }),
892
+ Node.create({ name: "doc", topNode: true, content: "block*" }),
893
+ Link.extend({
894
+ addAttributes() {
895
+ return { ...this.parent?.(), title: { default: null } };
896
+ }
897
+ }).configure({ openOnClick: false, isAllowedUri: (url) => isSafeDocsUrl(url) }),
898
+ Image.configure({ allowBase64: false, resize: false }),
899
+ Table.configure({ resizable: false }),
900
+ TableRow,
901
+ TableCell.extend({ content: "paragraph" }),
902
+ TableHeader.extend({ content: "paragraph" })
903
+ ];
904
+ }
905
+ getSchema(createDocsExtensions());
844
906
  var Editor = Object.assign(
845
907
  ({
846
908
  initialContent,
847
909
  onChange,
910
+ onFlushChangeReady,
911
+ debounceMs = 1e3,
848
912
  placeholder,
849
913
  children,
850
914
  showToolbar = true,
@@ -857,23 +921,28 @@ var Editor = Object.assign(
857
921
  className,
858
922
  editorClassName,
859
923
  contentClassName,
860
- beforeContent
924
+ beforeContent,
925
+ profile = "default"
861
926
  }) => {
862
927
  const [isUploading, setIsUploading] = useState(false);
863
928
  const fileInputRef = useRef(null);
864
929
  const pickImage = useImagePicker();
865
930
  const hasInteractedRef = useRef(false);
866
- const lastSerializedContentRef = useRef(
867
- JSON.stringify(initialContent ?? empty_doc_default)
868
- );
931
+ const lastSerializedContentRef = useRef(JSON.stringify(initialContent ?? empty_doc_default));
869
932
  const refreshRef = useRef(refresh);
870
933
  const extensions = useMemo(
871
- () => createExtensions({ placeholder }),
872
- [placeholder]
873
- );
874
- const changeTimeoutRef = useRef(
875
- null
934
+ () => profile === "documentation" ? [
935
+ ...createDocsExtensions(),
936
+ Placeholder.configure({ placeholder: placeholder || "Write something\u2026" })
937
+ ] : createExtensions({ placeholder }),
938
+ [placeholder, profile]
876
939
  );
940
+ const changeTimeoutRef = useRef(null);
941
+ const pendingJsonRef = useRef(null);
942
+ const onChangeRef = useRef(onChange);
943
+ onChangeRef.current = onChange;
944
+ const debounceMsRef = useRef(debounceMs);
945
+ debounceMsRef.current = debounceMs;
877
946
  const editor = useEditor(
878
947
  {
879
948
  immediatelyRender: false,
@@ -894,10 +963,13 @@ var Editor = Object.assign(
894
963
  if (changeTimeoutRef.current) {
895
964
  clearTimeout(changeTimeoutRef.current);
896
965
  }
966
+ pendingJsonRef.current = instance.getJSON();
897
967
  changeTimeoutRef.current = setTimeout(() => {
898
- onChange(instance.getJSON());
899
968
  changeTimeoutRef.current = null;
900
- }, 1e3);
969
+ const json2 = pendingJsonRef.current;
970
+ pendingJsonRef.current = null;
971
+ if (json2) onChangeRef.current(json2);
972
+ }, debounceMsRef.current);
901
973
  },
902
974
  onSelectionUpdate: ({ editor: instance }) => {
903
975
  hasInteractedRef.current = true;
@@ -905,11 +977,26 @@ var Editor = Object.assign(
905
977
  },
906
978
  [extensions]
907
979
  );
980
+ useEffect(() => {
981
+ onFlushChangeReady?.(() => {
982
+ if (!editor || !changeTimeoutRef.current) return;
983
+ clearTimeout(changeTimeoutRef.current);
984
+ changeTimeoutRef.current = null;
985
+ pendingJsonRef.current = null;
986
+ onChangeRef.current(editor.getJSON());
987
+ });
988
+ return () => onFlushChangeReady?.(() => {
989
+ });
990
+ }, [editor, onFlushChangeReady]);
908
991
  useEffect(() => {
909
992
  return () => {
910
993
  if (changeTimeoutRef.current) {
911
994
  clearTimeout(changeTimeoutRef.current);
995
+ changeTimeoutRef.current = null;
912
996
  }
997
+ const json2 = pendingJsonRef.current;
998
+ pendingJsonRef.current = null;
999
+ if (json2) onChangeRef.current(json2);
913
1000
  };
914
1001
  }, []);
915
1002
  useEffect(() => {
@@ -947,6 +1034,12 @@ var Editor = Object.assign(
947
1034
  }, [editor, initialContent, refresh]);
948
1035
  const handleImageUpload = useCallback(
949
1036
  async (file) => {
1037
+ if (profile === "documentation") {
1038
+ onError?.(
1039
+ "Upload a public image using the media library before inserting it."
1040
+ );
1041
+ return;
1042
+ }
950
1043
  const sizeLimit = imageSizeLimit ?? 2097152;
951
1044
  if (file.size > sizeLimit) {
952
1045
  onError?.("File is larger than the allowed size");
@@ -954,14 +1047,12 @@ var Editor = Object.assign(
954
1047
  }
955
1048
  try {
956
1049
  setIsUploading(true);
957
- const dataUrl = await new Promise(
958
- (resolve, reject) => {
959
- const reader = new FileReader();
960
- reader.onload = () => resolve(reader.result);
961
- reader.onerror = reject;
962
- reader.readAsDataURL(file);
963
- }
964
- );
1050
+ const dataUrl = await new Promise((resolve, reject) => {
1051
+ const reader = new FileReader();
1052
+ reader.onload = () => resolve(reader.result);
1053
+ reader.onerror = reject;
1054
+ reader.readAsDataURL(file);
1055
+ });
965
1056
  editor?.chain().focus().setImage({
966
1057
  src: dataUrl,
967
1058
  alt: file.name,
@@ -973,7 +1064,7 @@ var Editor = Object.assign(
973
1064
  setIsUploading(false);
974
1065
  }
975
1066
  },
976
- [editor]
1067
+ [editor, profile, onError, imageSizeLimit]
977
1068
  );
978
1069
  const handleSelectFile = useCallback(
979
1070
  (event) => {
@@ -989,13 +1080,17 @@ var Editor = Object.assign(
989
1080
  );
990
1081
  const insertPickedImage = useCallback(
991
1082
  (image) => {
1083
+ if (profile === "documentation" && !isSafeDocsUrl(image.src, true)) {
1084
+ onError?.("Documentation images require a stable public HTTPS URL.");
1085
+ return;
1086
+ }
992
1087
  editor?.chain().focus().setImage({
993
1088
  src: image.src,
994
1089
  alt: image.alt,
995
1090
  title: image.alt
996
1091
  }).run();
997
1092
  },
998
- [editor]
1093
+ [editor, profile, onError]
999
1094
  );
1000
1095
  const openFileDialog = useCallback(() => {
1001
1096
  if (pickImage) {
@@ -1012,10 +1107,13 @@ var Editor = Object.assign(
1012
1107
  return /* @__PURE__ */ jsxs(
1013
1108
  "div",
1014
1109
  {
1015
- className: cn(
1016
- "tiptap-editor flex flex-col gap-4 border",
1017
- className
1018
- ),
1110
+ className: cn("tiptap-editor flex flex-col gap-4 border", className),
1111
+ onPointerDownCapture: () => {
1112
+ hasInteractedRef.current = true;
1113
+ },
1114
+ onKeyDownCapture: () => {
1115
+ hasInteractedRef.current = true;
1116
+ },
1019
1117
  children: [
1020
1118
  editable && showToolbar && /* @__PURE__ */ jsx(
1021
1119
  "div",
@@ -1029,42 +1127,34 @@ var Editor = Object.assign(
1029
1127
  {
1030
1128
  editor,
1031
1129
  onRequestImageUpload: openFileDialog,
1032
- isUploadingImage: isUploading
1130
+ isUploadingImage: isUploading,
1131
+ profile
1033
1132
  }
1034
1133
  )
1035
1134
  }
1036
1135
  ),
1037
- /* @__PURE__ */ jsx("div", { className: "flex w-full justify-center", children: /* @__PURE__ */ jsxs(
1038
- "div",
1039
- {
1040
- className: cn(
1041
- "flex w-full max-w-3xl flex-col",
1042
- contentClassName
1043
- ),
1044
- children: [
1045
- beforeContent,
1046
- /* @__PURE__ */ jsx(
1047
- EditorContent,
1048
- {
1049
- editor,
1050
- className: `${editorClassName ?? ""} ${editorClassName?.includes("min-h-") ? "" : "min-h-[200px]"}`
1051
- }
1052
- ),
1053
- pickImage ? null : /* @__PURE__ */ jsx(
1054
- "input",
1055
- {
1056
- ref: fileInputRef,
1057
- type: "file",
1058
- accept: "image/*",
1059
- className: "hidden",
1060
- onChange: handleSelectFile
1061
- }
1062
- ),
1063
- children,
1064
- editor && /* @__PURE__ */ jsx(bubble_menu_default, { editor })
1065
- ]
1066
- }
1067
- ) })
1136
+ /* @__PURE__ */ jsx("div", { className: "flex w-full justify-center", children: /* @__PURE__ */ jsxs("div", { className: cn("flex w-full max-w-3xl flex-col", contentClassName), children: [
1137
+ beforeContent,
1138
+ /* @__PURE__ */ jsx(
1139
+ EditorContent,
1140
+ {
1141
+ editor,
1142
+ className: `${editorClassName ?? ""} ${editorClassName?.includes("min-h-") ? "" : "min-h-[200px]"}`
1143
+ }
1144
+ ),
1145
+ pickImage ? null : /* @__PURE__ */ jsx(
1146
+ "input",
1147
+ {
1148
+ ref: fileInputRef,
1149
+ type: "file",
1150
+ accept: "image/*",
1151
+ className: "hidden",
1152
+ onChange: handleSelectFile
1153
+ }
1154
+ ),
1155
+ children,
1156
+ editor && /* @__PURE__ */ jsx(bubble_menu_default, { editor, profile })
1157
+ ] }) })
1068
1158
  ]
1069
1159
  }
1070
1160
  );
@@ -1140,17 +1230,6 @@ function extractTextFromNode(node) {
1140
1230
  }
1141
1231
  return "";
1142
1232
  }
1143
- function textFromChildren(node) {
1144
- if (node == null || typeof node === "boolean") return "";
1145
- if (typeof node === "string" || typeof node === "number") {
1146
- return String(node);
1147
- }
1148
- if (Array.isArray(node)) return node.map(textFromChildren).join("");
1149
- if (React5.isValidElement(node)) {
1150
- return textFromChildren(node.props.children);
1151
- }
1152
- return "";
1153
- }
1154
1233
  async function writeClipboard(text) {
1155
1234
  if (navigator.clipboard?.writeText) {
1156
1235
  await navigator.clipboard.writeText(text);
@@ -1166,13 +1245,8 @@ async function writeClipboard(text) {
1166
1245
  document.execCommand("copy");
1167
1246
  area.remove();
1168
1247
  }
1169
- function RenderedCodeBlock({
1170
- node,
1171
- children,
1172
- language
1173
- }) {
1248
+ function CopyCodeButton({ source }) {
1174
1249
  const [copied, setCopied] = useState(false);
1175
- const source = extractTextFromNode(node) || textFromChildren(children);
1176
1250
  useEffect(() => {
1177
1251
  if (!copied) return;
1178
1252
  const timer = window.setTimeout(() => setCopied(false), 1500);
@@ -1187,23 +1261,73 @@ function RenderedCodeBlock({
1187
1261
  setCopied(false);
1188
1262
  }
1189
1263
  }
1264
+ return /* @__PURE__ */ jsx(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ className: "te-code-copy",
1269
+ "aria-label": copied ? "Copied" : "Copy code",
1270
+ title: copied ? "Copied" : "Copy",
1271
+ onClick: () => {
1272
+ void handleCopy();
1273
+ },
1274
+ children: copied ? /* @__PURE__ */ jsx(Check, { className: "te-code-copy__icon", "aria-hidden": true }) : /* @__PURE__ */ jsx(Copy, { className: "te-code-copy__icon", "aria-hidden": true })
1275
+ }
1276
+ );
1277
+ }
1278
+ function textFromChildren(node) {
1279
+ if (node == null || typeof node === "boolean") return "";
1280
+ if (typeof node === "string" || typeof node === "number") {
1281
+ return String(node);
1282
+ }
1283
+ if (Array.isArray(node)) return node.map(textFromChildren).join("");
1284
+ if (React5.isValidElement(node)) {
1285
+ return textFromChildren(node.props.children);
1286
+ }
1287
+ return "";
1288
+ }
1289
+ function RenderedCodeBlock({
1290
+ node,
1291
+ children,
1292
+ language
1293
+ }) {
1294
+ const source = extractTextFromNode(node) || textFromChildren(children);
1190
1295
  return /* @__PURE__ */ jsxs("div", { className: "te-code-block", children: [
1191
1296
  /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { className: language ? `language-${language}` : void 0, children }) }),
1192
- /* @__PURE__ */ jsx(
1193
- "button",
1194
- {
1195
- type: "button",
1196
- className: "te-code-copy",
1197
- "aria-label": copied ? "Copied" : "Copy code",
1198
- title: copied ? "Copied" : "Copy",
1199
- onClick: () => {
1200
- void handleCopy();
1201
- },
1202
- children: copied ? /* @__PURE__ */ jsx(Check, { className: "te-code-copy__icon", "aria-hidden": true }) : /* @__PURE__ */ jsx(Copy, { className: "te-code-copy__icon", "aria-hidden": true })
1203
- }
1204
- )
1297
+ /* @__PURE__ */ jsx(CopyCodeButton, { source })
1205
1298
  ] });
1206
1299
  }
1300
+
1301
+ // src/docs/headings.ts
1302
+ function docsNodeText(node) {
1303
+ if (typeof node.text === "string") return node.text;
1304
+ if (node.type === "hardBreak") return "\n";
1305
+ return (node.content ?? []).map(docsNodeText).join("");
1306
+ }
1307
+ function createHeadingIdGenerator() {
1308
+ const used = /* @__PURE__ */ new Set();
1309
+ return (text) => {
1310
+ const base = text.normalize("NFKC").toLowerCase().trim().replace(/[^\p{L}\p{N}\s_-]/gu, "").replace(/\s+/g, "-") || "section";
1311
+ let id = base;
1312
+ let suffix = 2;
1313
+ while (used.has(id)) id = `${base}-${suffix++}`;
1314
+ used.add(id);
1315
+ return id;
1316
+ };
1317
+ }
1318
+ function extractDocsHeadings(json2) {
1319
+ const nextId = createHeadingIdGenerator();
1320
+ const headings = [];
1321
+ function walk(node) {
1322
+ if (node.type === "heading") {
1323
+ const text = docsNodeText(node);
1324
+ headings.push({ level: Number(node.attrs?.level ?? 1), text, id: nextId(text) });
1325
+ }
1326
+ for (const child of node.content ?? []) walk(child);
1327
+ }
1328
+ if (json2) walk(json2);
1329
+ return headings;
1330
+ }
1207
1331
  var typographyClasses = (typography) => [
1208
1332
  typography?.fontFamily,
1209
1333
  typography?.fontSize,
@@ -1230,14 +1354,30 @@ function removeEmptyTextNodes(node) {
1230
1354
  content
1231
1355
  };
1232
1356
  }
1233
- function TextRenderer({ json: json2, className, theme }) {
1357
+ function TextRenderer({
1358
+ json: json2,
1359
+ className,
1360
+ theme,
1361
+ profile = "default"
1362
+ }) {
1234
1363
  const extensions = createExtensions();
1235
1364
  const content = removeEmptyTextNodes(
1236
1365
  json2 ?? empty_doc_default
1237
1366
  );
1367
+ const docsHeadings = profile === "documentation" ? extractDocsHeadings(content) : [];
1368
+ const renderContent = profile === "documentation" ? getSchema(extensions).nodeFromJSON(content) : content;
1369
+ const headingIds = /* @__PURE__ */ new WeakMap();
1370
+ if (profile === "documentation") {
1371
+ let headingIndex = 0;
1372
+ renderContent.descendants((node) => {
1373
+ if (node.type.name === "heading") {
1374
+ headingIds.set(node, docsHeadings[headingIndex++].id);
1375
+ }
1376
+ });
1377
+ }
1238
1378
  const rendered = renderToReactElement({
1239
1379
  extensions,
1240
- content,
1380
+ content: renderContent,
1241
1381
  options: {
1242
1382
  nodeMapping: {
1243
1383
  paragraph: ({ children }) => {
@@ -1251,10 +1391,10 @@ function TextRenderer({ json: json2, className, theme }) {
1251
1391
  heading: ({ node, children }) => {
1252
1392
  const level = node?.attrs?.level ?? 1;
1253
1393
  const textContent = extractTextFromNode(node);
1254
- const id = createId(textContent);
1394
+ const id = profile === "documentation" ? headingIds.get(node) : createId(textContent);
1255
1395
  if (!theme) {
1256
- const Tag = `h${level}`;
1257
- return /* @__PURE__ */ jsx(Tag, { id, children });
1396
+ const Tag2 = `h${level}`;
1397
+ return /* @__PURE__ */ jsx(Tag2, { id, children });
1258
1398
  }
1259
1399
  if (level === 1) {
1260
1400
  const classes2 = cn(
@@ -1272,18 +1412,14 @@ function TextRenderer({ json: json2, className, theme }) {
1272
1412
  }
1273
1413
  const classes = cn(
1274
1414
  "scroll-m-20 text-2xl font-semibold tracking-normal",
1275
- ...typographyClasses(theme?.typography?.header3)
1415
+ ...typographyClasses(
1416
+ profile === "documentation" && level >= 4 ? theme?.typography?.header4 ?? theme?.typography?.header3 : theme?.typography?.header3
1417
+ )
1276
1418
  );
1277
- return /* @__PURE__ */ jsx("h3", { id, className: classes, children });
1419
+ const Tag = profile === "documentation" ? `h${level}` : "h3";
1420
+ return /* @__PURE__ */ jsx(Tag, { id, className: classes, children });
1278
1421
  },
1279
- codeBlock: ({ node, children }) => /* @__PURE__ */ jsx(
1280
- RenderedCodeBlock,
1281
- {
1282
- node,
1283
- language: node?.attrs?.language,
1284
- children
1285
- }
1286
- ),
1422
+ codeBlock: ({ node, children }) => /* @__PURE__ */ jsx(RenderedCodeBlock, { node, language: node?.attrs?.language, children }),
1287
1423
  codeMirror: ({ node, children }) => /* @__PURE__ */ jsx(RenderedCodeBlock, { node, children })
1288
1424
  },
1289
1425
  markMapping: {