@nuucognition/ncm-ui 0.1.0-dev.1 → 0.1.1-beta.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.
Files changed (67) hide show
  1. package/README.md +90 -0
  2. package/dist/blocks/admonition/index.d.ts +2 -3
  3. package/dist/blocks/admonition/index.js +63 -12
  4. package/dist/blocks/aside/index.d.ts +2 -3
  5. package/dist/blocks/aside/index.js +63 -12
  6. package/dist/blocks/attribution/index.d.ts +2 -3
  7. package/dist/blocks/blockquote/index.d.ts +5 -4
  8. package/dist/blocks/blockquote/index.js +52 -7
  9. package/dist/blocks/chart/index.d.ts +2 -3
  10. package/dist/blocks/code/index.d.ts +2 -3
  11. package/dist/blocks/code/index.js +2 -2
  12. package/dist/blocks/container/index.d.ts +2 -3
  13. package/dist/blocks/container/index.js +50 -5
  14. package/dist/blocks/custom-code/index.d.ts +2 -3
  15. package/dist/blocks/dataview/index.d.ts +2 -3
  16. package/dist/blocks/definition/index.d.ts +2 -3
  17. package/dist/blocks/definition/index.js +63 -12
  18. package/dist/blocks/embed/index.d.ts +2 -3
  19. package/dist/blocks/embedding/index.d.ts +2 -3
  20. package/dist/blocks/embedding/index.js +50 -5
  21. package/dist/blocks/figure/index.d.ts +2 -3
  22. package/dist/blocks/figure/index.js +50 -5
  23. package/dist/blocks/footnote/index.d.ts +2 -3
  24. package/dist/blocks/footnote/index.js +63 -12
  25. package/dist/blocks/frontmatter/index.d.ts +2 -3
  26. package/dist/blocks/header/index.d.ts +2 -3
  27. package/dist/blocks/header/index.js +50 -5
  28. package/dist/blocks/horizontal-rule/index.d.ts +2 -3
  29. package/dist/blocks/html/index.d.ts +3 -4
  30. package/dist/blocks/index.d.ts +1 -2
  31. package/dist/blocks/index.js +63 -12
  32. package/dist/blocks/latex/index.d.ts +2 -3
  33. package/dist/blocks/link/index.d.ts +2 -3
  34. package/dist/blocks/list/index.d.ts +2 -3
  35. package/dist/blocks/list/index.js +57 -7
  36. package/dist/blocks/math/index.d.ts +2 -3
  37. package/dist/blocks/mermaid/index.d.ts +2 -3
  38. package/dist/blocks/page-breaker/index.d.ts +2 -3
  39. package/dist/blocks/paragraph/index.d.ts +2 -3
  40. package/dist/blocks/paragraph/index.js +50 -5
  41. package/dist/blocks/spoiler/index.d.ts +2 -3
  42. package/dist/blocks/spoiler/index.js +63 -12
  43. package/dist/blocks/table/index.d.ts +2 -3
  44. package/dist/blocks/table/index.js +50 -5
  45. package/dist/blocks/task-list/index.d.ts +2 -3
  46. package/dist/blocks/task-list/index.js +52 -6
  47. package/dist/components/index.d.ts +34 -4
  48. package/dist/components/index.js +101 -14
  49. package/dist/components/ncm-block.d.ts +2 -3
  50. package/dist/components/ncm-block.js +63 -12
  51. package/dist/components/ncm-document.d.ts +4 -4
  52. package/dist/components/ncm-document.js +63 -12
  53. package/dist/context/index.d.ts +2 -3
  54. package/dist/hooks/index.d.ts +1 -2
  55. package/dist/hooks/index.js +63 -12
  56. package/dist/hooks/use-block-renderer.js +63 -12
  57. package/dist/hooks/use-ncm-config.d.ts +1 -2
  58. package/dist/index.d.ts +3 -4
  59. package/dist/index.js +102 -15
  60. package/dist/inline/index.d.ts +4 -4
  61. package/dist/inline/index.js +51 -6
  62. package/dist/lib/defaults.js +63 -12
  63. package/dist/{references-Bs5wJybr.d.ts → references-BYW5pnj_.d.ts} +4 -4
  64. package/dist/styles/nuu.css +304 -42
  65. package/dist/styles/paper.css +62 -1
  66. package/dist/{use-ncm-config-D0doMYTH.d.ts → use-ncm-config-CnntcShj.d.ts} +55 -4
  67. package/package.json +5 -4
package/README.md CHANGED
@@ -102,6 +102,81 @@ import { NcmDocument, SanitizedHtmlBlock } from "@nuucognition/ncm-ui"
102
102
  markup. Keep `HtmlBlock` as the default for untrusted documents. Use
103
103
  `SanitizedHtmlBlock` only when the surface intentionally supports HTML.
104
104
 
105
+ ## Embeds
106
+
107
+ `![[target]]` and `![alt](url)` render through `EmbeddingsConfig` on `NcmConfig`.
108
+ Everything below is optional; a config that omits a key gets the behaviour the
109
+ package has always had.
110
+
111
+ | Key | Effect |
112
+ |---|---|
113
+ | `resolveUrl(target, embedType)` | Turn an authored target into a fetchable URL. |
114
+ | `renderers` | Per-embed-type block renderers. |
115
+ | `imageClassName` / `videoClassName` / `audioClassName` | Class names on the media elements. |
116
+ | `renderImageError` | Draw a failed image instead of leaving the browser's broken-image box. |
117
+ | `onEmbedClick` | Make a non-media embed a real control instead of an `<a href>`. |
118
+
119
+ ### Failed images
120
+
121
+ Whether an `<img>` actually loads is only learned by trying, and what to say
122
+ about the failure is the host's vocabulary — a mesh embedder can retry against
123
+ its own blob cache; a static-site embedder has nothing to retry.
124
+
125
+ ```tsx
126
+ <NcmProvider config={{
127
+ embeddings: {
128
+ renderImageError: ({ target, retry }) => (
129
+ <button type="button" onClick={() => { myCache.refresh(target); retry() }}>
130
+ {target} didn’t load — retry
131
+ </button>
132
+ ),
133
+ },
134
+ }}>
135
+ ```
136
+
137
+ **Opt-in, strictly.** With no renderer supplied the `<img>` carries no `onError`
138
+ handler at all, so the emitted DOM is identical to what it has always been — not
139
+ merely visually identical. `retry` re-keys the element, so it is a real second
140
+ request rather than the browser replaying its cached failure.
141
+
142
+ ### Non-media embeds
143
+
144
+ An `![[target]]` with no player — an embedded document, or any unrecognised type
145
+ — renders as `<a href={resolveUrl(...)}>`. That is only honest when the target
146
+ really has a URL. In a title-addressed mesh a document does not: the host
147
+ navigates in its own state, so the href ends up being the raw target.
148
+
149
+ Supply `onEmbedClick` and the same content renders as a `<button>` instead,
150
+ claiming nothing about URLs — the shape `inline.onWikiLinkClick` already
151
+ produces for a wikilink with no href.
152
+
153
+ ```tsx
154
+ <NcmProvider config={{
155
+ embeddings: {
156
+ onEmbedClick: (target, result) => openDocument(result?.resolved?.title ?? target),
157
+ },
158
+ }}>
159
+ ```
160
+
161
+ ### Reference states in the shipped themes
162
+
163
+ `nuu.css` and `paper.css` paint three states of a wikilink or embed, and they
164
+ mean the same thing in both themes:
165
+
166
+ | State | Treatment | Why |
167
+ |---|---|---|
168
+ | resolved | link colour, solid underline | it goes somewhere |
169
+ | `missing` | muted foreground, **dashed** | not an error — a link to a document that is not in this mesh is very often a plan. Matches Obsidian, where an unresolved link is the link colour faded, never red. |
170
+ | `ambiguous` | warning amber, **dotted** | several targets match; something to decide, nothing has failed |
171
+
172
+ Red is reserved for genuine failure — an embed whose bytes did not arrive.
173
+
174
+ > **Changed in the 513 batch:** `ambiguous` used to be painted with
175
+ > `--fire-text`/`--destructive`, i.e. red, in both themes. That is a deliberate
176
+ > visual change for every embedder. It was made because the editor surface in
177
+ > the same product paints red for a *missing* link, so one hue carried two
178
+ > different facts depending on which mode a reader was in.
179
+
105
180
  ## Math And Code Rendering
106
181
 
107
182
  KaTeX is lazy-loaded and memoized, so documents without math do not pay the
@@ -116,8 +191,23 @@ pnpm --filter @nuucognition/ncm-ui lint
116
191
  pnpm --filter @nuucognition/ncm-ui typecheck
117
192
  pnpm --filter @nuucognition/ncm-ui build
118
193
  pnpm --filter @nuucognition/ncm-ui test
194
+ pnpm --filter @nuucognition/ncm-ui exec tsx scripts/render-compat.mjs
119
195
  ```
120
196
 
197
+ `render-compat.mjs` is the **published-API compatibility gate**. This package
198
+ has backward-compatibility obligations, so an added optional callback has one
199
+ test that matters: a consumer that does not pass it must get the same *bytes*.
200
+ The script renders fixed fixtures under a bare config, under the shape a real
201
+ host passes today, and under a config with every option supplied, and compares
202
+ against hand-written expected markup. The expectations are literal strings on
203
+ purpose — a snapshot a worker can re-bless is not a gate. It also mounts into
204
+ jsdom and dispatches a real `error` event, because a check written only against
205
+ server markup would report the failure path working while it was unreachable.
206
+
207
+ Note that `vitest.config.ts` pins `NODE_ENV=test`. React's production build does
208
+ not export `act`, so a shell that exports `NODE_ENV=production` makes four tests
209
+ fail for reasons that have nothing to do with this package.
210
+
121
211
  The root NCM gate runs both `@nuucognition/ncm-tests` and `@nuucognition/ncm-ui`:
122
212
 
123
213
  ```bash
@@ -1,13 +1,12 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { AdmonitionBlock as AdmonitionBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type AdmonitionBlockProps = BlockRendererProps<AdmonitionBlock$1>;
7
6
  /**
8
7
  * Admonition (callout) renderer. Collapsible admonitions render as `<details>`;
9
8
  * static ones as a titled region. Child blocks render through `NcmBlock`.
10
9
  */
11
- declare function AdmonitionBlock({ block, className }: AdmonitionBlockProps): react_jsx_runtime.JSX.Element;
10
+ declare function AdmonitionBlock({ block, className }: AdmonitionBlockProps): react.JSX.Element;
12
11
 
13
12
  export { AdmonitionBlock, type AdmonitionBlockProps };
@@ -506,9 +506,10 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
506
506
  case "image": {
507
507
  const src = config.inline?.resolveAssetPath?.(token.src) ?? token.src;
508
508
  return /* @__PURE__ */ jsx(
509
- "img",
509
+ EmbedImage,
510
510
  {
511
- "data-ncm-token": "image",
511
+ attrs: { "data-ncm-token": "image" },
512
+ target: token.src,
512
513
  src,
513
514
  alt: token.alt,
514
515
  title: token.title
@@ -542,10 +543,13 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
542
543
  }
543
544
  if (embedType === "image") {
544
545
  return /* @__PURE__ */ jsx(
545
- "img",
546
+ EmbedImage,
546
547
  {
547
- "data-ncm-token": "wiki_embed",
548
- "data-ncm-wikiembed-type": embedType,
548
+ attrs: {
549
+ "data-ncm-token": "wiki_embed",
550
+ "data-ncm-wikiembed-type": embedType
551
+ },
552
+ target: token.target,
549
553
  src: url,
550
554
  alt: token.display ?? token.target,
551
555
  className: config.embeddings?.imageClassName
@@ -576,6 +580,22 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
576
580
  }
577
581
  );
578
582
  }
583
+ const onEmbedClick = config.embeddings?.onEmbedClick;
584
+ if (onEmbedClick) {
585
+ const handleClick = (e) => {
586
+ onEmbedClick(token.target, result, e);
587
+ };
588
+ return /* @__PURE__ */ jsx(
589
+ "button",
590
+ {
591
+ type: "button",
592
+ "data-ncm-token": "wiki_embed",
593
+ "data-ncm-wikiembed-type": embedType,
594
+ onClick: handleClick,
595
+ children: token.display ?? token.target
596
+ }
597
+ );
598
+ }
579
599
  return /* @__PURE__ */ jsx(
580
600
  "a",
581
601
  {
@@ -594,6 +614,31 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
594
614
  return null;
595
615
  }
596
616
  }
617
+ function EmbedImage({ attrs, target, src, alt, title, className }) {
618
+ const config = useNcmConfig();
619
+ const Fallback = config.embeddings?.renderImageError;
620
+ const [failedSrc, setFailedSrc] = useState(null);
621
+ useEffect(() => {
622
+ setFailedSrc(null);
623
+ }, [src]);
624
+ if (!Fallback) {
625
+ return /* @__PURE__ */ jsx("img", { ...attrs, src, alt, title, className });
626
+ }
627
+ if (failedSrc === src) {
628
+ return /* @__PURE__ */ jsx(Fallback, { target, src, alt, retry: () => setFailedSrc(null) });
629
+ }
630
+ return /* @__PURE__ */ jsx(
631
+ "img",
632
+ {
633
+ ...attrs,
634
+ src,
635
+ alt,
636
+ title,
637
+ className,
638
+ onError: () => setFailedSrc(src)
639
+ }
640
+ );
641
+ }
597
642
  function renderReadingText(content, opts) {
598
643
  if (!content) return /* @__PURE__ */ jsx(Fragment, { children: content });
599
644
  const { bionic = false, beeline = false } = opts;
@@ -629,7 +674,7 @@ function renderReadingText(content, opts) {
629
674
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
630
675
  }
631
676
  function BlockquoteBlock({ block, className }) {
632
- const lines = block.content.split("\n").filter((line) => line.trim() !== "");
677
+ const lines = block.content.split("\n");
633
678
  return /* @__PURE__ */ jsx(
634
679
  "blockquote",
635
680
  {
@@ -639,7 +684,7 @@ function BlockquoteBlock({ block, className }) {
639
684
  "data-block-id": block.id,
640
685
  "data-ncm-blockquote": "",
641
686
  className,
642
- children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(InlineContent, { content: line }) }, index))
687
+ children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: line.trim().length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content: line }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" }) }, index))
643
688
  }
644
689
  );
645
690
  }
@@ -779,7 +824,7 @@ function CodeBlock({ block, className }) {
779
824
  "data-ncm-language": block.language,
780
825
  className: cn("relative", className),
781
826
  children: [
782
- codeConfig.copyButton !== false && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
827
+ codeConfig.copyButton === true && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
783
828
  /* @__PURE__ */ jsx(CustomRenderer, { code: block.content, language: block.language }),
784
829
  languageLabel
785
830
  ]
@@ -798,7 +843,7 @@ function CodeBlock({ block, className }) {
798
843
  "data-ncm-language": block.language,
799
844
  className: cn("relative", className),
800
845
  children: [
801
- codeConfig?.copyButton !== false && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
846
+ codeConfig?.copyButton === true && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
802
847
  /* @__PURE__ */ jsx("pre", { "data-ncm-code-pre": "", children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
803
848
  codeConfig?.lineNumbers && /* @__PURE__ */ jsx(LineNumbers, { count: lineCount }),
804
849
  /* @__PURE__ */ jsx("code", { "data-ncm-code": "", className: "flex-1", children: /* @__PURE__ */ jsx(DefaultShikiRenderer, { code: block.content, language: block.language }) })
@@ -1250,10 +1295,12 @@ function LinkBlock({ block, className }) {
1250
1295
  function ListBlock({ block, className }) {
1251
1296
  const isOrdered = block.type === "list_ordered";
1252
1297
  const Tag = isOrdered ? "ol" : "ul";
1298
+ const start = isOrdered && typeof block.marker === "number" && block.marker !== 1 ? block.marker : void 0;
1253
1299
  return /* @__PURE__ */ jsx(
1254
1300
  Tag,
1255
1301
  {
1256
1302
  role: "list",
1303
+ start,
1257
1304
  "data-ncm-block": block.id,
1258
1305
  "data-ncm-stable": block.stableId,
1259
1306
  "data-ncm-type": block.type,
@@ -1265,6 +1312,9 @@ function ListBlock({ block, className }) {
1265
1312
  }
1266
1313
  );
1267
1314
  }
1315
+ function ItemContent({ content }) {
1316
+ return content.length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" });
1317
+ }
1268
1318
  function nestedItemIdKey(id) {
1269
1319
  if (!id) {
1270
1320
  return void 0;
@@ -1281,7 +1331,7 @@ function ListItems({ items, itemIds, ordered }) {
1281
1331
  return /* @__PURE__ */ jsx(Fragment, { children: items.map((item, index) => {
1282
1332
  const key = nestedItemIdKey(itemIds?.[index]) ?? `item-${index}`;
1283
1333
  if (typeof item === "string") {
1284
- return /* @__PURE__ */ jsx("li", { "data-ncm-list-item": "", children: /* @__PURE__ */ jsx(InlineContent, { content: item }) }, key);
1334
+ return /* @__PURE__ */ jsx("li", { "data-ncm-list-item": "", children: /* @__PURE__ */ jsx(ItemContent, { content: item }) }, key);
1285
1335
  }
1286
1336
  if (isTypedNestedList(item)) {
1287
1337
  return /* @__PURE__ */ jsx(NestedTypedList, { list: item }, key);
@@ -1291,7 +1341,7 @@ function ListItems({ items, itemIds, ordered }) {
1291
1341
  const content = typeof first === "string" ? first : "";
1292
1342
  const nestedContent = processNestedItems(rest, ordered);
1293
1343
  return /* @__PURE__ */ jsxs("li", { "data-ncm-list-item": "", children: [
1294
- /* @__PURE__ */ jsx(InlineContent, { content }),
1344
+ /* @__PURE__ */ jsx(ItemContent, { content }),
1295
1345
  nestedContent
1296
1346
  ] }, key);
1297
1347
  }
@@ -1450,6 +1500,7 @@ function TableBlock({ block, className }) {
1450
1500
  function TaskItems({ items }) {
1451
1501
  return /* @__PURE__ */ jsx(Fragment, { children: items.map((item) => {
1452
1502
  const status = item.checked ? "Completed" : "Incomplete";
1503
+ const text = serializeInlineContent(item.content);
1453
1504
  return /* @__PURE__ */ jsxs(
1454
1505
  "li",
1455
1506
  {
@@ -1476,7 +1527,7 @@ function TaskItems({ items }) {
1476
1527
  ":",
1477
1528
  " "
1478
1529
  ] }),
1479
- /* @__PURE__ */ jsx(InlineContent, { content: serializeInlineContent(item.content) })
1530
+ text.length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content: text }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" })
1480
1531
  ] })
1481
1532
  ] }),
1482
1533
  item.children ? /* @__PURE__ */ jsx("ul", { role: "list", "data-ncm-list-nested": "", className: "ml-6 list-none", children: /* @__PURE__ */ jsx(TaskItems, { items: item.children.items }) }) : null
@@ -1,13 +1,12 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { AsideBlock as AsideBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type AsideBlockProps = BlockRendererProps<AsideBlock$1>;
7
6
  /**
8
7
  * Aside renderer — parenthetical / sidebar content wrapped in `(( ))`. Child
9
8
  * blocks render through `NcmBlock`.
10
9
  */
11
- declare function AsideBlock({ block, className }: AsideBlockProps): react_jsx_runtime.JSX.Element;
10
+ declare function AsideBlock({ block, className }: AsideBlockProps): react.JSX.Element;
12
11
 
13
12
  export { AsideBlock, type AsideBlockProps };
@@ -531,9 +531,10 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
531
531
  case "image": {
532
532
  const src = config.inline?.resolveAssetPath?.(token.src) ?? token.src;
533
533
  return /* @__PURE__ */ jsx(
534
- "img",
534
+ EmbedImage,
535
535
  {
536
- "data-ncm-token": "image",
536
+ attrs: { "data-ncm-token": "image" },
537
+ target: token.src,
537
538
  src,
538
539
  alt: token.alt,
539
540
  title: token.title
@@ -567,10 +568,13 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
567
568
  }
568
569
  if (embedType === "image") {
569
570
  return /* @__PURE__ */ jsx(
570
- "img",
571
+ EmbedImage,
571
572
  {
572
- "data-ncm-token": "wiki_embed",
573
- "data-ncm-wikiembed-type": embedType,
573
+ attrs: {
574
+ "data-ncm-token": "wiki_embed",
575
+ "data-ncm-wikiembed-type": embedType
576
+ },
577
+ target: token.target,
574
578
  src: url,
575
579
  alt: token.display ?? token.target,
576
580
  className: config.embeddings?.imageClassName
@@ -601,6 +605,22 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
601
605
  }
602
606
  );
603
607
  }
608
+ const onEmbedClick = config.embeddings?.onEmbedClick;
609
+ if (onEmbedClick) {
610
+ const handleClick = (e) => {
611
+ onEmbedClick(token.target, result, e);
612
+ };
613
+ return /* @__PURE__ */ jsx(
614
+ "button",
615
+ {
616
+ type: "button",
617
+ "data-ncm-token": "wiki_embed",
618
+ "data-ncm-wikiembed-type": embedType,
619
+ onClick: handleClick,
620
+ children: token.display ?? token.target
621
+ }
622
+ );
623
+ }
604
624
  return /* @__PURE__ */ jsx(
605
625
  "a",
606
626
  {
@@ -619,6 +639,31 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
619
639
  return null;
620
640
  }
621
641
  }
642
+ function EmbedImage({ attrs, target, src, alt, title, className }) {
643
+ const config = useNcmConfig();
644
+ const Fallback = config.embeddings?.renderImageError;
645
+ const [failedSrc, setFailedSrc] = useState(null);
646
+ useEffect(() => {
647
+ setFailedSrc(null);
648
+ }, [src]);
649
+ if (!Fallback) {
650
+ return /* @__PURE__ */ jsx("img", { ...attrs, src, alt, title, className });
651
+ }
652
+ if (failedSrc === src) {
653
+ return /* @__PURE__ */ jsx(Fallback, { target, src, alt, retry: () => setFailedSrc(null) });
654
+ }
655
+ return /* @__PURE__ */ jsx(
656
+ "img",
657
+ {
658
+ ...attrs,
659
+ src,
660
+ alt,
661
+ title,
662
+ className,
663
+ onError: () => setFailedSrc(src)
664
+ }
665
+ );
666
+ }
622
667
  function renderReadingText(content, opts) {
623
668
  if (!content) return /* @__PURE__ */ jsx(Fragment, { children: content });
624
669
  const { bionic = false, beeline = false } = opts;
@@ -654,7 +699,7 @@ function renderReadingText(content, opts) {
654
699
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
655
700
  }
656
701
  function BlockquoteBlock({ block, className }) {
657
- const lines = block.content.split("\n").filter((line) => line.trim() !== "");
702
+ const lines = block.content.split("\n");
658
703
  return /* @__PURE__ */ jsx(
659
704
  "blockquote",
660
705
  {
@@ -664,7 +709,7 @@ function BlockquoteBlock({ block, className }) {
664
709
  "data-block-id": block.id,
665
710
  "data-ncm-blockquote": "",
666
711
  className,
667
- children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(InlineContent, { content: line }) }, index))
712
+ children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: line.trim().length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content: line }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" }) }, index))
668
713
  }
669
714
  );
670
715
  }
@@ -804,7 +849,7 @@ function CodeBlock({ block, className }) {
804
849
  "data-ncm-language": block.language,
805
850
  className: cn("relative", className),
806
851
  children: [
807
- codeConfig.copyButton !== false && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
852
+ codeConfig.copyButton === true && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
808
853
  /* @__PURE__ */ jsx(CustomRenderer, { code: block.content, language: block.language }),
809
854
  languageLabel
810
855
  ]
@@ -823,7 +868,7 @@ function CodeBlock({ block, className }) {
823
868
  "data-ncm-language": block.language,
824
869
  className: cn("relative", className),
825
870
  children: [
826
- codeConfig?.copyButton !== false && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
871
+ codeConfig?.copyButton === true && /* @__PURE__ */ jsx(CopyButton, { content: block.content }),
827
872
  /* @__PURE__ */ jsx("pre", { "data-ncm-code-pre": "", children: /* @__PURE__ */ jsxs("div", { className: "flex", children: [
828
873
  codeConfig?.lineNumbers && /* @__PURE__ */ jsx(LineNumbers, { count: lineCount }),
829
874
  /* @__PURE__ */ jsx("code", { "data-ncm-code": "", className: "flex-1", children: /* @__PURE__ */ jsx(DefaultShikiRenderer, { code: block.content, language: block.language }) })
@@ -1275,10 +1320,12 @@ function LinkBlock({ block, className }) {
1275
1320
  function ListBlock({ block, className }) {
1276
1321
  const isOrdered = block.type === "list_ordered";
1277
1322
  const Tag = isOrdered ? "ol" : "ul";
1323
+ const start = isOrdered && typeof block.marker === "number" && block.marker !== 1 ? block.marker : void 0;
1278
1324
  return /* @__PURE__ */ jsx(
1279
1325
  Tag,
1280
1326
  {
1281
1327
  role: "list",
1328
+ start,
1282
1329
  "data-ncm-block": block.id,
1283
1330
  "data-ncm-stable": block.stableId,
1284
1331
  "data-ncm-type": block.type,
@@ -1290,6 +1337,9 @@ function ListBlock({ block, className }) {
1290
1337
  }
1291
1338
  );
1292
1339
  }
1340
+ function ItemContent({ content }) {
1341
+ return content.length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" });
1342
+ }
1293
1343
  function nestedItemIdKey(id) {
1294
1344
  if (!id) {
1295
1345
  return void 0;
@@ -1306,7 +1356,7 @@ function ListItems({ items, itemIds, ordered }) {
1306
1356
  return /* @__PURE__ */ jsx(Fragment, { children: items.map((item, index) => {
1307
1357
  const key = nestedItemIdKey(itemIds?.[index]) ?? `item-${index}`;
1308
1358
  if (typeof item === "string") {
1309
- return /* @__PURE__ */ jsx("li", { "data-ncm-list-item": "", children: /* @__PURE__ */ jsx(InlineContent, { content: item }) }, key);
1359
+ return /* @__PURE__ */ jsx("li", { "data-ncm-list-item": "", children: /* @__PURE__ */ jsx(ItemContent, { content: item }) }, key);
1310
1360
  }
1311
1361
  if (isTypedNestedList(item)) {
1312
1362
  return /* @__PURE__ */ jsx(NestedTypedList, { list: item }, key);
@@ -1316,7 +1366,7 @@ function ListItems({ items, itemIds, ordered }) {
1316
1366
  const content = typeof first === "string" ? first : "";
1317
1367
  const nestedContent = processNestedItems(rest, ordered);
1318
1368
  return /* @__PURE__ */ jsxs("li", { "data-ncm-list-item": "", children: [
1319
- /* @__PURE__ */ jsx(InlineContent, { content }),
1369
+ /* @__PURE__ */ jsx(ItemContent, { content }),
1320
1370
  nestedContent
1321
1371
  ] }, key);
1322
1372
  }
@@ -1475,6 +1525,7 @@ function TableBlock({ block, className }) {
1475
1525
  function TaskItems({ items }) {
1476
1526
  return /* @__PURE__ */ jsx(Fragment, { children: items.map((item) => {
1477
1527
  const status = item.checked ? "Completed" : "Incomplete";
1528
+ const text = serializeInlineContent(item.content);
1478
1529
  return /* @__PURE__ */ jsxs(
1479
1530
  "li",
1480
1531
  {
@@ -1501,7 +1552,7 @@ function TaskItems({ items }) {
1501
1552
  ":",
1502
1553
  " "
1503
1554
  ] }),
1504
- /* @__PURE__ */ jsx(InlineContent, { content: serializeInlineContent(item.content) })
1555
+ text.length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content: text }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" })
1505
1556
  ] })
1506
1557
  ] }),
1507
1558
  item.children ? /* @__PURE__ */ jsx("ul", { role: "list", "data-ncm-list-nested": "", className: "ml-6 list-none", children: /* @__PURE__ */ jsx(TaskItems, { items: item.children.items }) }) : null
@@ -1,13 +1,12 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { AttributionBlock as AttributionBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type AttributionBlockProps = BlockRendererProps<AttributionBlock$1>;
7
6
  /**
8
7
  * Attribution renderer — a `— Source` line extracted from a blockquote, with
9
8
  * optional date and URL.
10
9
  */
11
- declare function AttributionBlock({ block, className }: AttributionBlockProps): react_jsx_runtime.JSX.Element;
10
+ declare function AttributionBlock({ block, className }: AttributionBlockProps): react.JSX.Element;
12
11
 
13
12
  export { AttributionBlock, type AttributionBlockProps };
@@ -1,15 +1,16 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { BlockquoteBlock as BlockquoteBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type BlockquoteBlockProps = BlockRendererProps<BlockquoteBlock$1>;
7
6
  /**
8
7
  * Blockquote block renderer with multiline support.
9
8
  *
10
9
  * Splits content by newlines and renders each line as a separate paragraph
11
- * to properly display multiline blockquotes.
10
+ * to properly display multiline blockquotes. An empty line (a bare `>` in
11
+ * source) renders one non-breaking space, so it keeps a full text line of
12
+ * height instead of collapsing — the same guard the list renderers use.
12
13
  */
13
- declare function BlockquoteBlock({ block, className }: BlockquoteBlockProps): react_jsx_runtime.JSX.Element;
14
+ declare function BlockquoteBlock({ block, className }: BlockquoteBlockProps): react.JSX.Element;
14
15
 
15
16
  export { BlockquoteBlock, type BlockquoteBlockProps };
@@ -468,9 +468,10 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
468
468
  case "image": {
469
469
  const src = config.inline?.resolveAssetPath?.(token.src) ?? token.src;
470
470
  return /* @__PURE__ */ jsx(
471
- "img",
471
+ EmbedImage,
472
472
  {
473
- "data-ncm-token": "image",
473
+ attrs: { "data-ncm-token": "image" },
474
+ target: token.src,
474
475
  src,
475
476
  alt: token.alt,
476
477
  title: token.title
@@ -504,10 +505,13 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
504
505
  }
505
506
  if (embedType === "image") {
506
507
  return /* @__PURE__ */ jsx(
507
- "img",
508
+ EmbedImage,
508
509
  {
509
- "data-ncm-token": "wiki_embed",
510
- "data-ncm-wikiembed-type": embedType,
510
+ attrs: {
511
+ "data-ncm-token": "wiki_embed",
512
+ "data-ncm-wikiembed-type": embedType
513
+ },
514
+ target: token.target,
511
515
  src: url,
512
516
  alt: token.display ?? token.target,
513
517
  className: config.embeddings?.imageClassName
@@ -538,6 +542,22 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
538
542
  }
539
543
  );
540
544
  }
545
+ const onEmbedClick = config.embeddings?.onEmbedClick;
546
+ if (onEmbedClick) {
547
+ const handleClick = (e) => {
548
+ onEmbedClick(token.target, result, e);
549
+ };
550
+ return /* @__PURE__ */ jsx(
551
+ "button",
552
+ {
553
+ type: "button",
554
+ "data-ncm-token": "wiki_embed",
555
+ "data-ncm-wikiembed-type": embedType,
556
+ onClick: handleClick,
557
+ children: token.display ?? token.target
558
+ }
559
+ );
560
+ }
541
561
  return /* @__PURE__ */ jsx(
542
562
  "a",
543
563
  {
@@ -556,6 +576,31 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
556
576
  return null;
557
577
  }
558
578
  }
579
+ function EmbedImage({ attrs, target, src, alt, title, className }) {
580
+ const config = useNcmConfig();
581
+ const Fallback = config.embeddings?.renderImageError;
582
+ const [failedSrc, setFailedSrc] = useState(null);
583
+ useEffect(() => {
584
+ setFailedSrc(null);
585
+ }, [src]);
586
+ if (!Fallback) {
587
+ return /* @__PURE__ */ jsx("img", { ...attrs, src, alt, title, className });
588
+ }
589
+ if (failedSrc === src) {
590
+ return /* @__PURE__ */ jsx(Fallback, { target, src, alt, retry: () => setFailedSrc(null) });
591
+ }
592
+ return /* @__PURE__ */ jsx(
593
+ "img",
594
+ {
595
+ ...attrs,
596
+ src,
597
+ alt,
598
+ title,
599
+ className,
600
+ onError: () => setFailedSrc(src)
601
+ }
602
+ );
603
+ }
559
604
  function renderReadingText(content, opts) {
560
605
  if (!content) return /* @__PURE__ */ jsx(Fragment, { children: content });
561
606
  const { bionic = false, beeline = false } = opts;
@@ -591,7 +636,7 @@ function renderReadingText(content, opts) {
591
636
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
592
637
  }
593
638
  function BlockquoteBlock({ block, className }) {
594
- const lines = block.content.split("\n").filter((line) => line.trim() !== "");
639
+ const lines = block.content.split("\n");
595
640
  return /* @__PURE__ */ jsx(
596
641
  "blockquote",
597
642
  {
@@ -601,7 +646,7 @@ function BlockquoteBlock({ block, className }) {
601
646
  "data-block-id": block.id,
602
647
  "data-ncm-blockquote": "",
603
648
  className,
604
- children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx(InlineContent, { content: line }) }, index))
649
+ children: lines.length <= 1 ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : lines.map((line, index) => /* @__PURE__ */ jsx("p", { children: line.trim().length > 0 ? /* @__PURE__ */ jsx(InlineContent, { content: line }) : /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xA0" }) }, index))
605
650
  }
606
651
  );
607
652
  }
@@ -1,7 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { ChartBlock as ChartBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type ChartBlockProps = BlockRendererProps<ChartBlock$1>;
7
6
  /**
@@ -9,6 +8,6 @@ type ChartBlockProps = BlockRendererProps<ChartBlock$1>;
9
8
  * charting runtime is bundled here. `data-ncm-chart-type` exposes the detected
10
9
  * chart kind; override the `chart` renderer key to render a live chart.
11
10
  */
12
- declare function ChartBlock({ block, className }: ChartBlockProps): react_jsx_runtime.JSX.Element;
11
+ declare function ChartBlock({ block, className }: ChartBlockProps): react.JSX.Element;
13
12
 
14
13
  export { ChartBlock, type ChartBlockProps };
@@ -1,7 +1,6 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
1
+ import * as react from 'react';
2
2
  import { CodeBlock as CodeBlock$1 } from '@nuucognition/ncm';
3
3
  import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
- import 'react';
5
4
 
6
5
  type CodeBlockProps = BlockRendererProps<CodeBlock$1>;
7
6
  /**
@@ -10,6 +9,6 @@ type CodeBlockProps = BlockRendererProps<CodeBlock$1>;
10
9
  * Highlights code by default using Shiki's web bundle (github-light/github-dark).
11
10
  * Consumers can override with a custom renderer via NcmProvider config.code.renderer.
12
11
  */
13
- declare function CodeBlock({ block, className }: CodeBlockProps): react_jsx_runtime.JSX.Element;
12
+ declare function CodeBlock({ block, className }: CodeBlockProps): react.JSX.Element;
14
13
 
15
14
  export { CodeBlock, type CodeBlockProps };