@nuucognition/ncm-ui 0.1.1-beta.1 → 0.1.1-beta.2

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.
@@ -51,6 +51,52 @@ function tryParseInlineRef(input) {
51
51
  }
52
52
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
53
53
  }
54
+ function runLength(input, index, char) {
55
+ let end = index;
56
+ while (end < input.length && input[end] === char) end++;
57
+ return end - index;
58
+ }
59
+ function tryParseEmphasis(input) {
60
+ const char = input[0];
61
+ if (char !== "*" && char !== "_") return null;
62
+ const open = Math.min(3, runLength(input, 0, char));
63
+ const need = open;
64
+ let i = open;
65
+ while (i < input.length) {
66
+ const ch = input[i];
67
+ if (ch === "`") {
68
+ const close = input.indexOf("`", i + 1);
69
+ if (close === -1) break;
70
+ i = close + 1;
71
+ continue;
72
+ }
73
+ if (ch !== char) {
74
+ i++;
75
+ continue;
76
+ }
77
+ const run = runLength(input, i, char);
78
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
79
+ if (!closes) {
80
+ i += run;
81
+ continue;
82
+ }
83
+ const extra = run - need;
84
+ const inner = input.slice(open, i + extra);
85
+ if (inner.length === 0) return null;
86
+ const length = i + run;
87
+ const children = parseInline(inner);
88
+ let token;
89
+ if (need === 3) {
90
+ token = { type: "bold", children: [{ type: "italic", children }] };
91
+ } else if (need === 2) {
92
+ token = { type: "bold", children };
93
+ } else {
94
+ token = { type: "italic", children };
95
+ }
96
+ return { token, length };
97
+ }
98
+ return null;
99
+ }
54
100
  function parseInline(content) {
55
101
  if (!content) return [];
56
102
  const tokens = [];
@@ -122,17 +168,11 @@ function parseInline(content) {
122
168
  remaining = remaining.slice(linkMatch[0].length);
123
169
  continue;
124
170
  }
125
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
126
- if (boldMatch) {
127
- const innerContent = boldMatch[1] ?? boldMatch[2];
128
- if (innerContent !== void 0) {
129
- tokens.push({
130
- type: "bold",
131
- children: parseInline(innerContent)
132
- });
133
- remaining = remaining.slice(boldMatch[0].length);
134
- continue;
135
- }
171
+ const emphasis = tryParseEmphasis(remaining);
172
+ if (emphasis) {
173
+ tokens.push(emphasis.token);
174
+ remaining = remaining.slice(emphasis.length);
175
+ continue;
136
176
  }
137
177
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
138
178
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -143,18 +183,6 @@ function parseInline(content) {
143
183
  remaining = remaining.slice(strikeMatch[0].length);
144
184
  continue;
145
185
  }
146
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
147
- if (italicMatch) {
148
- const innerContent = italicMatch[1] ?? italicMatch[2];
149
- if (innerContent !== void 0) {
150
- tokens.push({
151
- type: "italic",
152
- children: parseInline(innerContent)
153
- });
154
- remaining = remaining.slice(italicMatch[0].length);
155
- continue;
156
- }
157
- }
158
186
  const nextSpecial = findNextSpecialChar(remaining);
159
187
  if (nextSpecial > 0) {
160
188
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -265,17 +293,30 @@ function ReferenceMarker({ kind, number, children }) {
265
293
  }
266
294
  );
267
295
  }
268
- var ReferenceBasesContext = createContext(null);
296
+ function normalizeReferenceContent(content) {
297
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
298
+ }
299
+ var ReferenceNumberingContext = createContext(null);
269
300
  var BlockReferenceScopeContext = createContext(null);
270
- function BlockReferenceScope({ blockId, children }) {
271
- const bases = useContext(ReferenceBasesContext);
272
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
301
+ function BlockReferenceScope({ children }) {
302
+ const numbering = useContext(ReferenceNumberingContext);
303
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
273
304
  const counter = useRef({ ref: 0, footnote: 0 });
274
- counter.current = { ref: 0, footnote: 0 };
275
305
  const value = {
306
+ numberFor(kind, content) {
307
+ const key = normalizeReferenceContent(content);
308
+ const fromDocument = numbering?.[kind].get(key);
309
+ if (fromDocument !== void 0) return fromDocument;
310
+ const map = local.current[kind];
311
+ const existing = map.get(key);
312
+ if (existing !== void 0) return existing;
313
+ counter.current[kind] += 1;
314
+ map.set(key, counter.current[kind]);
315
+ return counter.current[kind];
316
+ },
276
317
  next(kind) {
277
318
  counter.current[kind] += 1;
278
- return base[kind] + counter.current[kind];
319
+ return counter.current[kind];
279
320
  }
280
321
  };
281
322
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -283,7 +324,7 @@ function BlockReferenceScope({ blockId, children }) {
283
324
  function useBlockReferenceScope() {
284
325
  return useContext(BlockReferenceScopeContext);
285
326
  }
286
- var INERT_SCOPE = { next: () => 0 };
327
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
287
328
  function InertReferenceScope({ children }) {
288
329
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
289
330
  }
@@ -574,7 +615,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
574
615
  );
575
616
  }
576
617
  case "inline_ref": {
577
- const number = refScope?.next(token.kind) ?? 0;
618
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
578
619
  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 });
579
620
  }
580
621
  default:
@@ -3,8 +3,8 @@ import { createContext, createElement, useMemo, useContext, useRef, Component, u
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 { extractExtension, embedTypeFromExtension } from '@nuucognition/mesh-core';
7
6
  import { parseNcm, serializeInlineContent, isTypedNestedList } from '@nuucognition/ncm';
7
+ import { extractExtension, embedTypeFromExtension } from '@nuucognition/mesh-core';
8
8
 
9
9
  // src/components/block-shell.tsx
10
10
  function cn(...inputs) {
@@ -121,6 +121,52 @@ function tryParseInlineRef(input) {
121
121
  }
122
122
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
123
123
  }
124
+ function runLength(input, index, char) {
125
+ let end = index;
126
+ while (end < input.length && input[end] === char) end++;
127
+ return end - index;
128
+ }
129
+ function tryParseEmphasis(input) {
130
+ const char = input[0];
131
+ if (char !== "*" && char !== "_") return null;
132
+ const open = Math.min(3, runLength(input, 0, char));
133
+ const need = open;
134
+ let i = open;
135
+ while (i < input.length) {
136
+ const ch = input[i];
137
+ if (ch === "`") {
138
+ const close = input.indexOf("`", i + 1);
139
+ if (close === -1) break;
140
+ i = close + 1;
141
+ continue;
142
+ }
143
+ if (ch !== char) {
144
+ i++;
145
+ continue;
146
+ }
147
+ const run = runLength(input, i, char);
148
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
149
+ if (!closes) {
150
+ i += run;
151
+ continue;
152
+ }
153
+ const extra = run - need;
154
+ const inner = input.slice(open, i + extra);
155
+ if (inner.length === 0) return null;
156
+ const length = i + run;
157
+ const children = parseInline(inner);
158
+ let token;
159
+ if (need === 3) {
160
+ token = { type: "bold", children: [{ type: "italic", children }] };
161
+ } else if (need === 2) {
162
+ token = { type: "bold", children };
163
+ } else {
164
+ token = { type: "italic", children };
165
+ }
166
+ return { token, length };
167
+ }
168
+ return null;
169
+ }
124
170
  function parseInline(content) {
125
171
  if (!content) return [];
126
172
  const tokens = [];
@@ -192,17 +238,11 @@ function parseInline(content) {
192
238
  remaining = remaining.slice(linkMatch[0].length);
193
239
  continue;
194
240
  }
195
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
196
- if (boldMatch) {
197
- const innerContent = boldMatch[1] ?? boldMatch[2];
198
- if (innerContent !== void 0) {
199
- tokens.push({
200
- type: "bold",
201
- children: parseInline(innerContent)
202
- });
203
- remaining = remaining.slice(boldMatch[0].length);
204
- continue;
205
- }
241
+ const emphasis = tryParseEmphasis(remaining);
242
+ if (emphasis) {
243
+ tokens.push(emphasis.token);
244
+ remaining = remaining.slice(emphasis.length);
245
+ continue;
206
246
  }
207
247
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
208
248
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -213,18 +253,6 @@ function parseInline(content) {
213
253
  remaining = remaining.slice(strikeMatch[0].length);
214
254
  continue;
215
255
  }
216
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
217
- if (italicMatch) {
218
- const innerContent = italicMatch[1] ?? italicMatch[2];
219
- if (innerContent !== void 0) {
220
- tokens.push({
221
- type: "italic",
222
- children: parseInline(innerContent)
223
- });
224
- remaining = remaining.slice(italicMatch[0].length);
225
- continue;
226
- }
227
- }
228
256
  const nextSpecial = findNextSpecialChar(remaining);
229
257
  if (nextSpecial > 0) {
230
258
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -375,48 +403,81 @@ function collectListStrings(items) {
375
403
  }
376
404
  return out;
377
405
  }
378
- function countInlineRefs(tokens, acc) {
406
+ function normalizeReferenceContent(content) {
407
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
408
+ }
409
+ var BIBLIOGRAPHY_ENTRY_RE = /^\s*\[(\d+)\]\s+([\s\S]+?)\s*$/;
410
+ function collectBibliography(blocks) {
411
+ const entries = /* @__PURE__ */ new Map();
412
+ for (const block of blocks) {
413
+ if (block.type !== "paragraph") continue;
414
+ const content = block.content ?? "";
415
+ const match = BIBLIOGRAPHY_ENTRY_RE.exec(content);
416
+ if (!match || !match[1] || !match[2]) continue;
417
+ const key = normalizeReferenceContent(match[2]);
418
+ if (key && !entries.has(key)) entries.set(key, Number(match[1]));
419
+ }
420
+ return entries;
421
+ }
422
+ function walkInlineRefs(tokens, visit) {
379
423
  for (const token of tokens) {
380
424
  if (token.type === "inline_ref") {
381
- acc[token.kind] += 1;
425
+ visit(token);
382
426
  } else if ("children" in token) {
383
- countInlineRefs(token.children, acc);
427
+ walkInlineRefs(token.children, visit);
384
428
  }
385
429
  }
386
430
  }
387
- function countBlock(block) {
388
- const acc = { ref: 0, footnote: 0 };
389
- for (const str of collectInlineStrings(block)) {
390
- if (str) countInlineRefs(parseInline(str), acc);
391
- }
392
- return acc;
393
- }
394
- function computeReferenceBases(blocks) {
395
- const bases = /* @__PURE__ */ new Map();
396
- const acc = { ref: 0, footnote: 0 };
431
+ function computeReferenceNumbering(blocks) {
432
+ const bibliography = collectBibliography(blocks);
433
+ let maxBibliography = 0;
434
+ for (const n of bibliography.values()) maxBibliography = Math.max(maxBibliography, n);
435
+ const numbering = { ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() };
436
+ let nextRef = maxBibliography;
437
+ let nextFootnote = 0;
397
438
  for (const block of blocks) {
398
- bases.set(block.id, { ...acc });
399
- const counts = countBlock(block);
400
- acc.ref += counts.ref;
401
- acc.footnote += counts.footnote;
439
+ for (const str of collectInlineStrings(block)) {
440
+ if (!str) continue;
441
+ walkInlineRefs(parseInline(str), (token) => {
442
+ const key = normalizeReferenceContent(token.content);
443
+ const map = numbering[token.kind];
444
+ if (map.has(key)) return;
445
+ if (token.kind === "ref") {
446
+ const fromBibliography = bibliography.get(key);
447
+ map.set(key, fromBibliography ?? ++nextRef);
448
+ } else {
449
+ map.set(key, ++nextFootnote);
450
+ }
451
+ });
452
+ }
402
453
  }
403
- return bases;
454
+ return numbering;
404
455
  }
405
- var ReferenceBasesContext = createContext(null);
456
+ var ReferenceNumberingContext = createContext(null);
406
457
  var BlockReferenceScopeContext = createContext(null);
407
458
  function NcmReferencesProvider({ blocks, children }) {
408
- const bases = useMemo(() => computeReferenceBases(blocks), [blocks]);
409
- return /* @__PURE__ */ jsx(ReferenceBasesContext.Provider, { value: bases, children });
459
+ const numbering = useMemo(() => computeReferenceNumbering(blocks), [blocks]);
460
+ return /* @__PURE__ */ jsx(ReferenceNumberingContext.Provider, { value: numbering, children });
410
461
  }
411
- function BlockReferenceScope({ blockId, children }) {
412
- const bases = useContext(ReferenceBasesContext);
413
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
462
+ function BlockReferenceScope({ children }) {
463
+ const numbering = useContext(ReferenceNumberingContext);
464
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
414
465
  const counter = useRef({ ref: 0, footnote: 0 });
415
- counter.current = { ref: 0, footnote: 0 };
416
466
  const value = {
467
+ numberFor(kind, content) {
468
+ const key = normalizeReferenceContent(content);
469
+ const fromDocument = numbering?.[kind].get(key);
470
+ if (fromDocument !== void 0) return fromDocument;
471
+ const map = local.current[kind];
472
+ const existing = map.get(key);
473
+ if (existing !== void 0) return existing;
474
+ counter.current[kind] += 1;
475
+ map.set(key, counter.current[kind]);
476
+ return counter.current[kind];
477
+ },
417
478
  next(kind) {
418
479
  counter.current[kind] += 1;
419
- return base[kind] + counter.current[kind];
480
+ return counter.current[kind];
420
481
  }
421
482
  };
422
483
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -424,7 +485,7 @@ function BlockReferenceScope({ blockId, children }) {
424
485
  function useBlockReferenceScope() {
425
486
  return useContext(BlockReferenceScopeContext);
426
487
  }
427
- var INERT_SCOPE = { next: () => 0 };
488
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
428
489
  function InertReferenceScope({ children }) {
429
490
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
430
491
  }
@@ -715,7 +776,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
715
776
  );
716
777
  }
717
778
  case "inline_ref": {
718
- const number = refScope?.next(token.kind) ?? 0;
779
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
719
780
  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 });
720
781
  }
721
782
  default:
@@ -781,8 +842,32 @@ function renderReadingText(content, opts) {
781
842
  if (cursor < content.length) pieces.push(content.slice(cursor));
782
843
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
783
844
  }
845
+ function quoteBlocks(block) {
846
+ try {
847
+ const doc = parseNcm(block.content, {
848
+ preset: "ncm-full",
849
+ allowMissingFrontmatter: true,
850
+ // Attribution off, as the NUU Editor's bridge keeps it: the promoter's
851
+ // trigger is any last quote line starting with `-`, so a nested quote
852
+ // ending in a list item would lose that item to a `— Author` line.
853
+ blockOptions: { extensions: { attribution: false } }
854
+ });
855
+ const blocks = doc.blocks ?? [];
856
+ if (blocks.length === 0 || blocks.some((child) => child.type === "frontmatter")) {
857
+ return null;
858
+ }
859
+ return blocks.map((child, index) => ({ ...child, id: `${block.id}.${index + 1}`, stableId: void 0 }));
860
+ } catch {
861
+ return null;
862
+ }
863
+ }
864
+ function QuotedLines({ content }) {
865
+ const lines = content.split("\n");
866
+ 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)) });
867
+ }
784
868
  function BlockquoteBlock({ block, className }) {
785
- const lines = block.content.split("\n");
869
+ const multiline = block.content.includes("\n");
870
+ const blocks = useMemo(() => multiline ? quoteBlocks(block) : null, [block, multiline]);
786
871
  return /* @__PURE__ */ jsx(
787
872
  "blockquote",
788
873
  {
@@ -792,7 +877,9 @@ function BlockquoteBlock({ block, className }) {
792
877
  "data-block-id": block.id,
793
878
  "data-ncm-blockquote": "",
794
879
  className,
795
- 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))
880
+ children: !multiline ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : blocks === null ? /* @__PURE__ */ jsx(QuotedLines, { content: block.content }) : blocks.map(
881
+ (child) => child.type === "paragraph" ? /* @__PURE__ */ jsx(QuotedLines, { content: child.content ?? "" }, blockKey(child)) : /* @__PURE__ */ jsx(NcmBlock, { block: child }, blockKey(child))
882
+ )
796
883
  }
797
884
  );
798
885
  }
@@ -1,10 +1,10 @@
1
1
  "use client";
2
- import { createContext, useContext, createElement, useRef, Component, useState, useEffect, useMemo, useCallback, useId } from 'react';
2
+ import { createContext, useContext, createElement, useMemo, useRef, Component, useState, useEffect, useCallback, useId } from 'react';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
  import { clsx } from 'clsx';
5
5
  import { twMerge } from 'tailwind-merge';
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
  function blockKey(block) {
10
10
  return block.stableId ? `${block.id}:${block.stableId}` : block.id;
@@ -153,6 +153,52 @@ function tryParseInlineRef(input) {
153
153
  }
154
154
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
155
155
  }
156
+ function runLength(input, index, char) {
157
+ let end = index;
158
+ while (end < input.length && input[end] === char) end++;
159
+ return end - index;
160
+ }
161
+ function tryParseEmphasis(input) {
162
+ const char = input[0];
163
+ if (char !== "*" && char !== "_") return null;
164
+ const open = Math.min(3, runLength(input, 0, char));
165
+ const need = open;
166
+ let i = open;
167
+ while (i < input.length) {
168
+ const ch = input[i];
169
+ if (ch === "`") {
170
+ const close = input.indexOf("`", i + 1);
171
+ if (close === -1) break;
172
+ i = close + 1;
173
+ continue;
174
+ }
175
+ if (ch !== char) {
176
+ i++;
177
+ continue;
178
+ }
179
+ const run = runLength(input, i, char);
180
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
181
+ if (!closes) {
182
+ i += run;
183
+ continue;
184
+ }
185
+ const extra = run - need;
186
+ const inner = input.slice(open, i + extra);
187
+ if (inner.length === 0) return null;
188
+ const length = i + run;
189
+ const children = parseInline(inner);
190
+ let token;
191
+ if (need === 3) {
192
+ token = { type: "bold", children: [{ type: "italic", children }] };
193
+ } else if (need === 2) {
194
+ token = { type: "bold", children };
195
+ } else {
196
+ token = { type: "italic", children };
197
+ }
198
+ return { token, length };
199
+ }
200
+ return null;
201
+ }
156
202
  function parseInline(content) {
157
203
  if (!content) return [];
158
204
  const tokens = [];
@@ -224,17 +270,11 @@ function parseInline(content) {
224
270
  remaining = remaining.slice(linkMatch[0].length);
225
271
  continue;
226
272
  }
227
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
228
- if (boldMatch) {
229
- const innerContent = boldMatch[1] ?? boldMatch[2];
230
- if (innerContent !== void 0) {
231
- tokens.push({
232
- type: "bold",
233
- children: parseInline(innerContent)
234
- });
235
- remaining = remaining.slice(boldMatch[0].length);
236
- continue;
237
- }
273
+ const emphasis = tryParseEmphasis(remaining);
274
+ if (emphasis) {
275
+ tokens.push(emphasis.token);
276
+ remaining = remaining.slice(emphasis.length);
277
+ continue;
238
278
  }
239
279
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
240
280
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -245,18 +285,6 @@ function parseInline(content) {
245
285
  remaining = remaining.slice(strikeMatch[0].length);
246
286
  continue;
247
287
  }
248
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
249
- if (italicMatch) {
250
- const innerContent = italicMatch[1] ?? italicMatch[2];
251
- if (innerContent !== void 0) {
252
- tokens.push({
253
- type: "italic",
254
- children: parseInline(innerContent)
255
- });
256
- remaining = remaining.slice(italicMatch[0].length);
257
- continue;
258
- }
259
- }
260
288
  const nextSpecial = findNextSpecialChar(remaining);
261
289
  if (nextSpecial > 0) {
262
290
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -367,17 +395,30 @@ function ReferenceMarker({ kind, number, children }) {
367
395
  }
368
396
  );
369
397
  }
370
- var ReferenceBasesContext = createContext(null);
398
+ function normalizeReferenceContent(content) {
399
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
400
+ }
401
+ var ReferenceNumberingContext = createContext(null);
371
402
  var BlockReferenceScopeContext = createContext(null);
372
- function BlockReferenceScope({ blockId, children }) {
373
- const bases = useContext(ReferenceBasesContext);
374
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
403
+ function BlockReferenceScope({ children }) {
404
+ const numbering = useContext(ReferenceNumberingContext);
405
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
375
406
  const counter = useRef({ ref: 0, footnote: 0 });
376
- counter.current = { ref: 0, footnote: 0 };
377
407
  const value = {
408
+ numberFor(kind, content) {
409
+ const key = normalizeReferenceContent(content);
410
+ const fromDocument = numbering?.[kind].get(key);
411
+ if (fromDocument !== void 0) return fromDocument;
412
+ const map = local.current[kind];
413
+ const existing = map.get(key);
414
+ if (existing !== void 0) return existing;
415
+ counter.current[kind] += 1;
416
+ map.set(key, counter.current[kind]);
417
+ return counter.current[kind];
418
+ },
378
419
  next(kind) {
379
420
  counter.current[kind] += 1;
380
- return base[kind] + counter.current[kind];
421
+ return counter.current[kind];
381
422
  }
382
423
  };
383
424
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -385,7 +426,7 @@ function BlockReferenceScope({ blockId, children }) {
385
426
  function useBlockReferenceScope() {
386
427
  return useContext(BlockReferenceScopeContext);
387
428
  }
388
- var INERT_SCOPE = { next: () => 0 };
429
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
389
430
  function InertReferenceScope({ children }) {
390
431
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
391
432
  }
@@ -676,7 +717,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
676
717
  );
677
718
  }
678
719
  case "inline_ref": {
679
- const number = refScope?.next(token.kind) ?? 0;
720
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
680
721
  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 });
681
722
  }
682
723
  default:
@@ -742,8 +783,32 @@ function renderReadingText(content, opts) {
742
783
  if (cursor < content.length) pieces.push(content.slice(cursor));
743
784
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
744
785
  }
786
+ function quoteBlocks(block) {
787
+ try {
788
+ const doc = parseNcm(block.content, {
789
+ preset: "ncm-full",
790
+ allowMissingFrontmatter: true,
791
+ // Attribution off, as the NUU Editor's bridge keeps it: the promoter's
792
+ // trigger is any last quote line starting with `-`, so a nested quote
793
+ // ending in a list item would lose that item to a `— Author` line.
794
+ blockOptions: { extensions: { attribution: false } }
795
+ });
796
+ const blocks = doc.blocks ?? [];
797
+ if (blocks.length === 0 || blocks.some((child) => child.type === "frontmatter")) {
798
+ return null;
799
+ }
800
+ return blocks.map((child, index) => ({ ...child, id: `${block.id}.${index + 1}`, stableId: void 0 }));
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+ function QuotedLines({ content }) {
806
+ const lines = content.split("\n");
807
+ 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)) });
808
+ }
745
809
  function BlockquoteBlock({ block, className }) {
746
- const lines = block.content.split("\n");
810
+ const multiline = block.content.includes("\n");
811
+ const blocks = useMemo(() => multiline ? quoteBlocks(block) : null, [block, multiline]);
747
812
  return /* @__PURE__ */ jsx(
748
813
  "blockquote",
749
814
  {
@@ -753,7 +818,9 @@ function BlockquoteBlock({ block, className }) {
753
818
  "data-block-id": block.id,
754
819
  "data-ncm-blockquote": "",
755
820
  className,
756
- 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))
821
+ children: !multiline ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : blocks === null ? /* @__PURE__ */ jsx(QuotedLines, { content: block.content }) : blocks.map(
822
+ (child) => child.type === "paragraph" ? /* @__PURE__ */ jsx(QuotedLines, { content: child.content ?? "" }, blockKey(child)) : /* @__PURE__ */ jsx(NcmBlock, { block: child }, blockKey(child))
823
+ )
757
824
  }
758
825
  );
759
826
  }