@nuucognition/ncm-ui 0.1.1-beta.1 → 0.1.1-beta.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.
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { createContext, createElement, useContext, useRef, Component, useState, useEffect, useMemo, useCallback, useId } from 'react';
2
+ import { createContext, createElement, useContext, useMemo, useRef, Component, useState, useEffect, useCallback, useId } from 'react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
+ import { serializeInlineContent, isTypedNestedList, parseNcm } from '@nuucognition/ncm';
6
7
  import { extractExtension, embedTypeFromExtension } from '@nuucognition/mesh-core';
7
- import { serializeInlineContent, isTypedNestedList } from '@nuucognition/ncm';
8
8
 
9
9
  // src/components/block-shell.tsx
10
10
  function cn(...inputs) {
@@ -84,6 +84,52 @@ function tryParseInlineRef(input) {
84
84
  }
85
85
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
86
86
  }
87
+ function runLength(input, index, char) {
88
+ let end = index;
89
+ while (end < input.length && input[end] === char) end++;
90
+ return end - index;
91
+ }
92
+ function tryParseEmphasis(input) {
93
+ const char = input[0];
94
+ if (char !== "*" && char !== "_") return null;
95
+ const open = Math.min(3, runLength(input, 0, char));
96
+ const need = open;
97
+ let i = open;
98
+ while (i < input.length) {
99
+ const ch = input[i];
100
+ if (ch === "`") {
101
+ const close = input.indexOf("`", i + 1);
102
+ if (close === -1) break;
103
+ i = close + 1;
104
+ continue;
105
+ }
106
+ if (ch !== char) {
107
+ i++;
108
+ continue;
109
+ }
110
+ const run = runLength(input, i, char);
111
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
112
+ if (!closes) {
113
+ i += run;
114
+ continue;
115
+ }
116
+ const extra = run - need;
117
+ const inner = input.slice(open, i + extra);
118
+ if (inner.length === 0) return null;
119
+ const length = i + run;
120
+ const children = parseInline(inner);
121
+ let token;
122
+ if (need === 3) {
123
+ token = { type: "bold", children: [{ type: "italic", children }] };
124
+ } else if (need === 2) {
125
+ token = { type: "bold", children };
126
+ } else {
127
+ token = { type: "italic", children };
128
+ }
129
+ return { token, length };
130
+ }
131
+ return null;
132
+ }
87
133
  function parseInline(content) {
88
134
  if (!content) return [];
89
135
  const tokens = [];
@@ -155,17 +201,11 @@ function parseInline(content) {
155
201
  remaining = remaining.slice(linkMatch[0].length);
156
202
  continue;
157
203
  }
158
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
159
- if (boldMatch) {
160
- const innerContent = boldMatch[1] ?? boldMatch[2];
161
- if (innerContent !== void 0) {
162
- tokens.push({
163
- type: "bold",
164
- children: parseInline(innerContent)
165
- });
166
- remaining = remaining.slice(boldMatch[0].length);
167
- continue;
168
- }
204
+ const emphasis = tryParseEmphasis(remaining);
205
+ if (emphasis) {
206
+ tokens.push(emphasis.token);
207
+ remaining = remaining.slice(emphasis.length);
208
+ continue;
169
209
  }
170
210
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
171
211
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -176,18 +216,6 @@ function parseInline(content) {
176
216
  remaining = remaining.slice(strikeMatch[0].length);
177
217
  continue;
178
218
  }
179
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
180
- if (italicMatch) {
181
- const innerContent = italicMatch[1] ?? italicMatch[2];
182
- if (innerContent !== void 0) {
183
- tokens.push({
184
- type: "italic",
185
- children: parseInline(innerContent)
186
- });
187
- remaining = remaining.slice(italicMatch[0].length);
188
- continue;
189
- }
190
- }
191
219
  const nextSpecial = findNextSpecialChar(remaining);
192
220
  if (nextSpecial > 0) {
193
221
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -298,17 +326,30 @@ function ReferenceMarker({ kind, number, children }) {
298
326
  }
299
327
  );
300
328
  }
301
- var ReferenceBasesContext = createContext(null);
329
+ function normalizeReferenceContent(content) {
330
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
331
+ }
332
+ var ReferenceNumberingContext = createContext(null);
302
333
  var BlockReferenceScopeContext = createContext(null);
303
- function BlockReferenceScope({ blockId, children }) {
304
- const bases = useContext(ReferenceBasesContext);
305
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
334
+ function BlockReferenceScope({ children }) {
335
+ const numbering = useContext(ReferenceNumberingContext);
336
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
306
337
  const counter = useRef({ ref: 0, footnote: 0 });
307
- counter.current = { ref: 0, footnote: 0 };
308
338
  const value = {
339
+ numberFor(kind, content) {
340
+ const key = normalizeReferenceContent(content);
341
+ const fromDocument = numbering?.[kind].get(key);
342
+ if (fromDocument !== void 0) return fromDocument;
343
+ const map = local.current[kind];
344
+ const existing = map.get(key);
345
+ if (existing !== void 0) return existing;
346
+ counter.current[kind] += 1;
347
+ map.set(key, counter.current[kind]);
348
+ return counter.current[kind];
349
+ },
309
350
  next(kind) {
310
351
  counter.current[kind] += 1;
311
- return base[kind] + counter.current[kind];
352
+ return counter.current[kind];
312
353
  }
313
354
  };
314
355
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -316,7 +357,7 @@ function BlockReferenceScope({ blockId, children }) {
316
357
  function useBlockReferenceScope() {
317
358
  return useContext(BlockReferenceScopeContext);
318
359
  }
319
- var INERT_SCOPE = { next: () => 0 };
360
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
320
361
  function InertReferenceScope({ children }) {
321
362
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
322
363
  }
@@ -607,7 +648,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
607
648
  );
608
649
  }
609
650
  case "inline_ref": {
610
- const number = refScope?.next(token.kind) ?? 0;
651
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
611
652
  return /* @__PURE__ */ jsx(ReferenceMarker, { kind: token.kind, number, children: token.content ? /* @__PURE__ */ jsx(InertReferenceScope, { children: /* @__PURE__ */ jsx(InlineContent, { content: token.content, disableBionic: true, disableBeeline: true }) }) : null });
612
653
  }
613
654
  default:
@@ -673,8 +714,32 @@ function renderReadingText(content, opts) {
673
714
  if (cursor < content.length) pieces.push(content.slice(cursor));
674
715
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
675
716
  }
717
+ function quoteBlocks(block) {
718
+ try {
719
+ const doc = parseNcm(block.content, {
720
+ preset: "ncm-full",
721
+ allowMissingFrontmatter: true,
722
+ // Attribution off, as the NUU Editor's bridge keeps it: the promoter's
723
+ // trigger is any last quote line starting with `-`, so a nested quote
724
+ // ending in a list item would lose that item to a `— Author` line.
725
+ blockOptions: { extensions: { attribution: false } }
726
+ });
727
+ const blocks = doc.blocks ?? [];
728
+ if (blocks.length === 0 || blocks.some((child) => child.type === "frontmatter")) {
729
+ return null;
730
+ }
731
+ return blocks.map((child, index) => ({ ...child, id: `${block.id}.${index + 1}`, stableId: void 0 }));
732
+ } catch {
733
+ return null;
734
+ }
735
+ }
736
+ function QuotedLines({ content }) {
737
+ const lines = content.split("\n");
738
+ return /* @__PURE__ */ jsx(Fragment, { children: 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)) });
739
+ }
676
740
  function BlockquoteBlock({ block, className }) {
677
- const lines = block.content.split("\n");
741
+ const multiline = block.content.includes("\n");
742
+ const blocks = useMemo(() => multiline ? quoteBlocks(block) : null, [block, multiline]);
678
743
  return /* @__PURE__ */ jsx(
679
744
  "blockquote",
680
745
  {
@@ -684,7 +749,9 @@ function BlockquoteBlock({ block, className }) {
684
749
  "data-block-id": block.id,
685
750
  "data-ncm-blockquote": "",
686
751
  className,
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))
752
+ children: !multiline ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : blocks === null ? /* @__PURE__ */ jsx(QuotedLines, { content: block.content }) : blocks.map(
753
+ (child) => child.type === "paragraph" ? /* @__PURE__ */ jsx(QuotedLines, { content: child.content ?? "" }, blockKey(child)) : /* @__PURE__ */ jsx(NcmBlock, { block: child }, blockKey(child))
754
+ )
688
755
  }
689
756
  );
690
757
  }
@@ -903,6 +970,48 @@ function DefinitionBlock({ block, className }) {
903
970
  }
904
971
  );
905
972
  }
973
+ function fileNameOf(src) {
974
+ let path = src.split(/[?#]/)[0] ?? "";
975
+ if (/^https?:\/\//i.test(src)) {
976
+ try {
977
+ path = new URL(src).pathname;
978
+ } catch {
979
+ return "";
980
+ }
981
+ }
982
+ return path.slice(path.lastIndexOf("/") + 1);
983
+ }
984
+ function isExtensionless(src) {
985
+ const name = fileNameOf(src);
986
+ const dot = name.lastIndexOf(".");
987
+ return dot <= 0 || dot === name.length - 1;
988
+ }
989
+ function EmbedPictureOrLink({
990
+ src,
991
+ alt,
992
+ title,
993
+ imgClassName,
994
+ linkAttrs
995
+ }) {
996
+ const [failedSrc, setFailedSrc] = useState(null);
997
+ useEffect(() => {
998
+ setFailedSrc(null);
999
+ }, [src]);
1000
+ if (failedSrc === src) {
1001
+ return /* @__PURE__ */ jsx("a", { href: src, title, ...linkAttrs, children: alt || src });
1002
+ }
1003
+ return /* @__PURE__ */ jsx(
1004
+ "img",
1005
+ {
1006
+ "data-ncm-embed-picture": "",
1007
+ src,
1008
+ alt,
1009
+ title,
1010
+ className: imgClassName,
1011
+ onError: () => setFailedSrc(src)
1012
+ }
1013
+ );
1014
+ }
906
1015
  function toEmbeddingConfigType(embedType) {
907
1016
  switch (embedType) {
908
1017
  case "image":
@@ -931,6 +1040,9 @@ function EmbedBlock({ block, className }) {
931
1040
  if (block.embedType === "image") {
932
1041
  return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className: cn(config.embeddings?.imageClassName, className), children: /* @__PURE__ */ jsx("img", { src: resolvedUrl, alt: block.alt, title: block.title ?? void 0 }) });
933
1042
  }
1043
+ if (block.embedType === "file" && isExtensionless(block.src)) {
1044
+ return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className: cn(config.embeddings?.imageClassName, className), children: /* @__PURE__ */ jsx(EmbedPictureOrLink, { src: resolvedUrl, alt: block.alt, title: block.title ?? void 0 }) });
1045
+ }
934
1046
  return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className, children: /* @__PURE__ */ jsx("a", { href: resolvedUrl, title: block.title ?? void 0, children: block.alt || block.src }) });
935
1047
  }
936
1048
  var embedFrameStyle = {
@@ -1107,7 +1219,19 @@ function FigureBlock({ block, className }) {
1107
1219
  "data-ncm-figure": "",
1108
1220
  className: cn("space-y-2", className),
1109
1221
  children: [
1110
- embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : /* @__PURE__ */ jsx(
1222
+ embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : embed.embedType === "file" && isExtensionless(embed.src) ? (
1223
+ // Same rule as the standalone embed block: an image spelling with an
1224
+ // extension-less destination is a picture until it fails to load.
1225
+ /* @__PURE__ */ jsx(
1226
+ EmbedPictureOrLink,
1227
+ {
1228
+ src: embed.src,
1229
+ alt: embed.alt,
1230
+ title: embed.title ?? void 0,
1231
+ linkAttrs: { "data-ncm-embed-type": embed.embedType }
1232
+ }
1233
+ )
1234
+ ) : /* @__PURE__ */ jsx(
1111
1235
  "a",
1112
1236
  {
1113
1237
  href: embed.src,
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { createContext, createElement, useContext, useRef, Component, useState, useEffect, useMemo, useCallback, useId } from 'react';
2
+ import { createContext, createElement, useContext, useMemo, useRef, Component, useState, useEffect, useCallback, useId } from 'react';
3
3
  import { clsx } from 'clsx';
4
4
  import { twMerge } from 'tailwind-merge';
5
5
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+ import { serializeInlineContent, isTypedNestedList, parseNcm } from '@nuucognition/ncm';
6
7
  import { extractExtension, embedTypeFromExtension } from '@nuucognition/mesh-core';
7
- import { serializeInlineContent, isTypedNestedList } from '@nuucognition/ncm';
8
8
 
9
9
  // src/components/block-shell.tsx
10
10
  function cn(...inputs) {
@@ -109,6 +109,52 @@ function tryParseInlineRef(input) {
109
109
  }
110
110
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
111
111
  }
112
+ function runLength(input, index, char) {
113
+ let end = index;
114
+ while (end < input.length && input[end] === char) end++;
115
+ return end - index;
116
+ }
117
+ function tryParseEmphasis(input) {
118
+ const char = input[0];
119
+ if (char !== "*" && char !== "_") return null;
120
+ const open = Math.min(3, runLength(input, 0, char));
121
+ const need = open;
122
+ let i = open;
123
+ while (i < input.length) {
124
+ const ch = input[i];
125
+ if (ch === "`") {
126
+ const close = input.indexOf("`", i + 1);
127
+ if (close === -1) break;
128
+ i = close + 1;
129
+ continue;
130
+ }
131
+ if (ch !== char) {
132
+ i++;
133
+ continue;
134
+ }
135
+ const run = runLength(input, i, char);
136
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
137
+ if (!closes) {
138
+ i += run;
139
+ continue;
140
+ }
141
+ const extra = run - need;
142
+ const inner = input.slice(open, i + extra);
143
+ if (inner.length === 0) return null;
144
+ const length = i + run;
145
+ const children = parseInline(inner);
146
+ let token;
147
+ if (need === 3) {
148
+ token = { type: "bold", children: [{ type: "italic", children }] };
149
+ } else if (need === 2) {
150
+ token = { type: "bold", children };
151
+ } else {
152
+ token = { type: "italic", children };
153
+ }
154
+ return { token, length };
155
+ }
156
+ return null;
157
+ }
112
158
  function parseInline(content) {
113
159
  if (!content) return [];
114
160
  const tokens = [];
@@ -180,17 +226,11 @@ function parseInline(content) {
180
226
  remaining = remaining.slice(linkMatch[0].length);
181
227
  continue;
182
228
  }
183
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
184
- if (boldMatch) {
185
- const innerContent = boldMatch[1] ?? boldMatch[2];
186
- if (innerContent !== void 0) {
187
- tokens.push({
188
- type: "bold",
189
- children: parseInline(innerContent)
190
- });
191
- remaining = remaining.slice(boldMatch[0].length);
192
- continue;
193
- }
229
+ const emphasis = tryParseEmphasis(remaining);
230
+ if (emphasis) {
231
+ tokens.push(emphasis.token);
232
+ remaining = remaining.slice(emphasis.length);
233
+ continue;
194
234
  }
195
235
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
196
236
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -201,18 +241,6 @@ function parseInline(content) {
201
241
  remaining = remaining.slice(strikeMatch[0].length);
202
242
  continue;
203
243
  }
204
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
205
- if (italicMatch) {
206
- const innerContent = italicMatch[1] ?? italicMatch[2];
207
- if (innerContent !== void 0) {
208
- tokens.push({
209
- type: "italic",
210
- children: parseInline(innerContent)
211
- });
212
- remaining = remaining.slice(italicMatch[0].length);
213
- continue;
214
- }
215
- }
216
244
  const nextSpecial = findNextSpecialChar(remaining);
217
245
  if (nextSpecial > 0) {
218
246
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -323,17 +351,30 @@ function ReferenceMarker({ kind, number, children }) {
323
351
  }
324
352
  );
325
353
  }
326
- var ReferenceBasesContext = createContext(null);
354
+ function normalizeReferenceContent(content) {
355
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
356
+ }
357
+ var ReferenceNumberingContext = createContext(null);
327
358
  var BlockReferenceScopeContext = createContext(null);
328
- function BlockReferenceScope({ blockId, children }) {
329
- const bases = useContext(ReferenceBasesContext);
330
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
359
+ function BlockReferenceScope({ children }) {
360
+ const numbering = useContext(ReferenceNumberingContext);
361
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
331
362
  const counter = useRef({ ref: 0, footnote: 0 });
332
- counter.current = { ref: 0, footnote: 0 };
333
363
  const value = {
364
+ numberFor(kind, content) {
365
+ const key = normalizeReferenceContent(content);
366
+ const fromDocument = numbering?.[kind].get(key);
367
+ if (fromDocument !== void 0) return fromDocument;
368
+ const map = local.current[kind];
369
+ const existing = map.get(key);
370
+ if (existing !== void 0) return existing;
371
+ counter.current[kind] += 1;
372
+ map.set(key, counter.current[kind]);
373
+ return counter.current[kind];
374
+ },
334
375
  next(kind) {
335
376
  counter.current[kind] += 1;
336
- return base[kind] + counter.current[kind];
377
+ return counter.current[kind];
337
378
  }
338
379
  };
339
380
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -341,7 +382,7 @@ function BlockReferenceScope({ blockId, children }) {
341
382
  function useBlockReferenceScope() {
342
383
  return useContext(BlockReferenceScopeContext);
343
384
  }
344
- var INERT_SCOPE = { next: () => 0 };
385
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
345
386
  function InertReferenceScope({ children }) {
346
387
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
347
388
  }
@@ -632,7 +673,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
632
673
  );
633
674
  }
634
675
  case "inline_ref": {
635
- const number = refScope?.next(token.kind) ?? 0;
676
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
636
677
  return /* @__PURE__ */ jsx(ReferenceMarker, { kind: token.kind, number, children: token.content ? /* @__PURE__ */ jsx(InertReferenceScope, { children: /* @__PURE__ */ jsx(InlineContent, { content: token.content, disableBionic: true, disableBeeline: true }) }) : null });
637
678
  }
638
679
  default:
@@ -698,8 +739,32 @@ function renderReadingText(content, opts) {
698
739
  if (cursor < content.length) pieces.push(content.slice(cursor));
699
740
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
700
741
  }
742
+ function quoteBlocks(block) {
743
+ try {
744
+ const doc = parseNcm(block.content, {
745
+ preset: "ncm-full",
746
+ allowMissingFrontmatter: true,
747
+ // Attribution off, as the NUU Editor's bridge keeps it: the promoter's
748
+ // trigger is any last quote line starting with `-`, so a nested quote
749
+ // ending in a list item would lose that item to a `— Author` line.
750
+ blockOptions: { extensions: { attribution: false } }
751
+ });
752
+ const blocks = doc.blocks ?? [];
753
+ if (blocks.length === 0 || blocks.some((child) => child.type === "frontmatter")) {
754
+ return null;
755
+ }
756
+ return blocks.map((child, index) => ({ ...child, id: `${block.id}.${index + 1}`, stableId: void 0 }));
757
+ } catch {
758
+ return null;
759
+ }
760
+ }
761
+ function QuotedLines({ content }) {
762
+ const lines = content.split("\n");
763
+ return /* @__PURE__ */ jsx(Fragment, { children: 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)) });
764
+ }
701
765
  function BlockquoteBlock({ block, className }) {
702
- const lines = block.content.split("\n");
766
+ const multiline = block.content.includes("\n");
767
+ const blocks = useMemo(() => multiline ? quoteBlocks(block) : null, [block, multiline]);
703
768
  return /* @__PURE__ */ jsx(
704
769
  "blockquote",
705
770
  {
@@ -709,7 +774,9 @@ function BlockquoteBlock({ block, className }) {
709
774
  "data-block-id": block.id,
710
775
  "data-ncm-blockquote": "",
711
776
  className,
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))
777
+ children: !multiline ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : blocks === null ? /* @__PURE__ */ jsx(QuotedLines, { content: block.content }) : blocks.map(
778
+ (child) => child.type === "paragraph" ? /* @__PURE__ */ jsx(QuotedLines, { content: child.content ?? "" }, blockKey(child)) : /* @__PURE__ */ jsx(NcmBlock, { block: child }, blockKey(child))
779
+ )
713
780
  }
714
781
  );
715
782
  }
@@ -928,6 +995,48 @@ function DefinitionBlock({ block, className }) {
928
995
  }
929
996
  );
930
997
  }
998
+ function fileNameOf(src) {
999
+ let path = src.split(/[?#]/)[0] ?? "";
1000
+ if (/^https?:\/\//i.test(src)) {
1001
+ try {
1002
+ path = new URL(src).pathname;
1003
+ } catch {
1004
+ return "";
1005
+ }
1006
+ }
1007
+ return path.slice(path.lastIndexOf("/") + 1);
1008
+ }
1009
+ function isExtensionless(src) {
1010
+ const name = fileNameOf(src);
1011
+ const dot = name.lastIndexOf(".");
1012
+ return dot <= 0 || dot === name.length - 1;
1013
+ }
1014
+ function EmbedPictureOrLink({
1015
+ src,
1016
+ alt,
1017
+ title,
1018
+ imgClassName,
1019
+ linkAttrs
1020
+ }) {
1021
+ const [failedSrc, setFailedSrc] = useState(null);
1022
+ useEffect(() => {
1023
+ setFailedSrc(null);
1024
+ }, [src]);
1025
+ if (failedSrc === src) {
1026
+ return /* @__PURE__ */ jsx("a", { href: src, title, ...linkAttrs, children: alt || src });
1027
+ }
1028
+ return /* @__PURE__ */ jsx(
1029
+ "img",
1030
+ {
1031
+ "data-ncm-embed-picture": "",
1032
+ src,
1033
+ alt,
1034
+ title,
1035
+ className: imgClassName,
1036
+ onError: () => setFailedSrc(src)
1037
+ }
1038
+ );
1039
+ }
931
1040
  function toEmbeddingConfigType(embedType) {
932
1041
  switch (embedType) {
933
1042
  case "image":
@@ -956,6 +1065,9 @@ function EmbedBlock({ block, className }) {
956
1065
  if (block.embedType === "image") {
957
1066
  return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className: cn(config.embeddings?.imageClassName, className), children: /* @__PURE__ */ jsx("img", { src: resolvedUrl, alt: block.alt, title: block.title ?? void 0 }) });
958
1067
  }
1068
+ if (block.embedType === "file" && isExtensionless(block.src)) {
1069
+ return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className: cn(config.embeddings?.imageClassName, className), children: /* @__PURE__ */ jsx(EmbedPictureOrLink, { src: resolvedUrl, alt: block.alt, title: block.title ?? void 0 }) });
1070
+ }
959
1071
  return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className, children: /* @__PURE__ */ jsx("a", { href: resolvedUrl, title: block.title ?? void 0, children: block.alt || block.src }) });
960
1072
  }
961
1073
  var embedFrameStyle = {
@@ -1132,7 +1244,19 @@ function FigureBlock({ block, className }) {
1132
1244
  "data-ncm-figure": "",
1133
1245
  className: cn("space-y-2", className),
1134
1246
  children: [
1135
- embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : /* @__PURE__ */ jsx(
1247
+ embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : embed.embedType === "file" && isExtensionless(embed.src) ? (
1248
+ // Same rule as the standalone embed block: an image spelling with an
1249
+ // extension-less destination is a picture until it fails to load.
1250
+ /* @__PURE__ */ jsx(
1251
+ EmbedPictureOrLink,
1252
+ {
1253
+ src: embed.src,
1254
+ alt: embed.alt,
1255
+ title: embed.title ?? void 0,
1256
+ linkAttrs: { "data-ncm-embed-type": embed.embedType }
1257
+ }
1258
+ )
1259
+ ) : /* @__PURE__ */ jsx(
1136
1260
  "a",
1137
1261
  {
1138
1262
  href: embed.src,
@@ -4,12 +4,14 @@ import { a as BlockRendererProps } from '../../renderers-C5f41FCy.js';
4
4
 
5
5
  type BlockquoteBlockProps = BlockRendererProps<BlockquoteBlock$1>;
6
6
  /**
7
- * Blockquote block renderer with multiline support.
7
+ * Blockquote block renderer with nested-block support.
8
8
  *
9
- * Splits content by newlines and renders each line as a separate paragraph
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.
9
+ * A single-line quote renders its inline content directly. A multi-line quote
10
+ * renders the blocks its content parses to: a quoted paragraph keeps one `<p>`
11
+ * per source line (an empty line renders one non-breaking space, so it keeps a
12
+ * full text line of height — the same guard the list renderers use), and every
13
+ * other block — a nested quote, a list, a code fence, a heading, a table —
14
+ * renders through `NcmBlock` exactly as it would at the top level.
13
15
  */
14
16
  declare function BlockquoteBlock({ block, className }: BlockquoteBlockProps): react.JSX.Element;
15
17