@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,9 +1,9 @@
1
1
  "use client";
2
- import { createContext, useState, createElement, useMemo, useContext, useCallback, useEffect, useRef, Component, useId } from 'react';
2
+ import { createContext, useMemo, useState, createElement, useContext, useCallback, useEffect, useRef, Component, 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 } from '@nuucognition/ncm';
6
+ import { serializeInlineContent, isTypedNestedList, parseNcm } from '@nuucognition/ncm';
7
7
  import { extractExtension, embedTypeFromExtension } from '@nuucognition/mesh-core';
8
8
 
9
9
  // src/lib/defaults.ts
@@ -71,6 +71,52 @@ function tryParseInlineRef(input) {
71
71
  }
72
72
  return { token: { type: "inline_ref", kind: "footnote", content: inner.trim() }, length };
73
73
  }
74
+ function runLength(input, index, char) {
75
+ let end = index;
76
+ while (end < input.length && input[end] === char) end++;
77
+ return end - index;
78
+ }
79
+ function tryParseEmphasis(input) {
80
+ const char = input[0];
81
+ if (char !== "*" && char !== "_") return null;
82
+ const open = Math.min(3, runLength(input, 0, char));
83
+ const need = open;
84
+ let i = open;
85
+ while (i < input.length) {
86
+ const ch = input[i];
87
+ if (ch === "`") {
88
+ const close = input.indexOf("`", i + 1);
89
+ if (close === -1) break;
90
+ i = close + 1;
91
+ continue;
92
+ }
93
+ if (ch !== char) {
94
+ i++;
95
+ continue;
96
+ }
97
+ const run = runLength(input, i, char);
98
+ const closes = need === 1 ? run % 2 === 1 : run >= need;
99
+ if (!closes) {
100
+ i += run;
101
+ continue;
102
+ }
103
+ const extra = run - need;
104
+ const inner = input.slice(open, i + extra);
105
+ if (inner.length === 0) return null;
106
+ const length = i + run;
107
+ const children = parseInline(inner);
108
+ let token;
109
+ if (need === 3) {
110
+ token = { type: "bold", children: [{ type: "italic", children }] };
111
+ } else if (need === 2) {
112
+ token = { type: "bold", children };
113
+ } else {
114
+ token = { type: "italic", children };
115
+ }
116
+ return { token, length };
117
+ }
118
+ return null;
119
+ }
74
120
  function parseInline(content) {
75
121
  if (!content) return [];
76
122
  const tokens = [];
@@ -142,17 +188,11 @@ function parseInline(content) {
142
188
  remaining = remaining.slice(linkMatch[0].length);
143
189
  continue;
144
190
  }
145
- const boldMatch = remaining.match(/^\*\*(.+?)\*\*|^__(.+?)__/);
146
- if (boldMatch) {
147
- const innerContent = boldMatch[1] ?? boldMatch[2];
148
- if (innerContent !== void 0) {
149
- tokens.push({
150
- type: "bold",
151
- children: parseInline(innerContent)
152
- });
153
- remaining = remaining.slice(boldMatch[0].length);
154
- continue;
155
- }
191
+ const emphasis = tryParseEmphasis(remaining);
192
+ if (emphasis) {
193
+ tokens.push(emphasis.token);
194
+ remaining = remaining.slice(emphasis.length);
195
+ continue;
156
196
  }
157
197
  const strikeMatch = remaining.match(/^~~(.+?)~~/);
158
198
  if (strikeMatch && strikeMatch[1] !== void 0) {
@@ -163,18 +203,6 @@ function parseInline(content) {
163
203
  remaining = remaining.slice(strikeMatch[0].length);
164
204
  continue;
165
205
  }
166
- const italicMatch = remaining.match(/^\*([^*]+)\*|^_([^_]+)_/);
167
- if (italicMatch) {
168
- const innerContent = italicMatch[1] ?? italicMatch[2];
169
- if (innerContent !== void 0) {
170
- tokens.push({
171
- type: "italic",
172
- children: parseInline(innerContent)
173
- });
174
- remaining = remaining.slice(italicMatch[0].length);
175
- continue;
176
- }
177
- }
178
206
  const nextSpecial = findNextSpecialChar(remaining);
179
207
  if (nextSpecial > 0) {
180
208
  tokens.push({ type: "text", content: remaining.slice(0, nextSpecial) });
@@ -209,17 +237,30 @@ function mergeAdjacentText(tokens) {
209
237
  }
210
238
  return result;
211
239
  }
212
- var ReferenceBasesContext = createContext(null);
240
+ function normalizeReferenceContent(content) {
241
+ return content.replace(/\s+/g, " ").trim().toLowerCase();
242
+ }
243
+ var ReferenceNumberingContext = createContext(null);
213
244
  var BlockReferenceScopeContext = createContext(null);
214
- function BlockReferenceScope({ blockId, children }) {
215
- const bases = useContext(ReferenceBasesContext);
216
- const base = (blockId ? bases?.get(blockId) : void 0) ?? { ref: 0, footnote: 0 };
245
+ function BlockReferenceScope({ children }) {
246
+ const numbering = useContext(ReferenceNumberingContext);
247
+ const local = useRef({ ref: /* @__PURE__ */ new Map(), footnote: /* @__PURE__ */ new Map() });
217
248
  const counter = useRef({ ref: 0, footnote: 0 });
218
- counter.current = { ref: 0, footnote: 0 };
219
249
  const value = {
250
+ numberFor(kind, content) {
251
+ const key = normalizeReferenceContent(content);
252
+ const fromDocument = numbering?.[kind].get(key);
253
+ if (fromDocument !== void 0) return fromDocument;
254
+ const map = local.current[kind];
255
+ const existing = map.get(key);
256
+ if (existing !== void 0) return existing;
257
+ counter.current[kind] += 1;
258
+ map.set(key, counter.current[kind]);
259
+ return counter.current[kind];
260
+ },
220
261
  next(kind) {
221
262
  counter.current[kind] += 1;
222
- return base[kind] + counter.current[kind];
263
+ return counter.current[kind];
223
264
  }
224
265
  };
225
266
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value, children });
@@ -227,7 +268,7 @@ function BlockReferenceScope({ blockId, children }) {
227
268
  function useBlockReferenceScope() {
228
269
  return useContext(BlockReferenceScopeContext);
229
270
  }
230
- var INERT_SCOPE = { next: () => 0 };
271
+ var INERT_SCOPE = { numberFor: () => 0, next: () => 0 };
231
272
  function InertReferenceScope({ children }) {
232
273
  return /* @__PURE__ */ jsx(BlockReferenceScopeContext.Provider, { value: INERT_SCOPE, children });
233
274
  }
@@ -751,7 +792,7 @@ function TokenRenderer({ token, disableBionic, disableBeeline }) {
751
792
  );
752
793
  }
753
794
  case "inline_ref": {
754
- const number = refScope?.next(token.kind) ?? 0;
795
+ const number = refScope?.numberFor(token.kind, token.content) ?? 0;
755
796
  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 });
756
797
  }
757
798
  default:
@@ -817,8 +858,32 @@ function renderReadingText(content, opts) {
817
858
  if (cursor < content.length) pieces.push(content.slice(cursor));
818
859
  return /* @__PURE__ */ jsx(Fragment, { children: pieces });
819
860
  }
861
+ function quoteBlocks(block) {
862
+ try {
863
+ const doc = parseNcm(block.content, {
864
+ preset: "ncm-full",
865
+ allowMissingFrontmatter: true,
866
+ // Attribution off, as the NUU Editor's bridge keeps it: the promoter's
867
+ // trigger is any last quote line starting with `-`, so a nested quote
868
+ // ending in a list item would lose that item to a `— Author` line.
869
+ blockOptions: { extensions: { attribution: false } }
870
+ });
871
+ const blocks = doc.blocks ?? [];
872
+ if (blocks.length === 0 || blocks.some((child) => child.type === "frontmatter")) {
873
+ return null;
874
+ }
875
+ return blocks.map((child, index) => ({ ...child, id: `${block.id}.${index + 1}`, stableId: void 0 }));
876
+ } catch {
877
+ return null;
878
+ }
879
+ }
880
+ function QuotedLines({ content }) {
881
+ const lines = content.split("\n");
882
+ 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)) });
883
+ }
820
884
  function BlockquoteBlock({ block, className }) {
821
- const lines = block.content.split("\n");
885
+ const multiline = block.content.includes("\n");
886
+ const blocks = useMemo(() => multiline ? quoteBlocks(block) : null, [block, multiline]);
822
887
  return /* @__PURE__ */ jsx(
823
888
  "blockquote",
824
889
  {
@@ -828,7 +893,9 @@ function BlockquoteBlock({ block, className }) {
828
893
  "data-block-id": block.id,
829
894
  "data-ncm-blockquote": "",
830
895
  className,
831
- 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))
896
+ children: !multiline ? /* @__PURE__ */ jsx(InlineContent, { content: block.content }) : blocks === null ? /* @__PURE__ */ jsx(QuotedLines, { content: block.content }) : blocks.map(
897
+ (child) => child.type === "paragraph" ? /* @__PURE__ */ jsx(QuotedLines, { content: child.content ?? "" }, blockKey(child)) : /* @__PURE__ */ jsx(NcmBlock, { block: child }, blockKey(child))
898
+ )
832
899
  }
833
900
  );
834
901
  }
@@ -1047,6 +1114,48 @@ function DefinitionBlock({ block, className }) {
1047
1114
  }
1048
1115
  );
1049
1116
  }
1117
+ function fileNameOf(src) {
1118
+ let path = src.split(/[?#]/)[0] ?? "";
1119
+ if (/^https?:\/\//i.test(src)) {
1120
+ try {
1121
+ path = new URL(src).pathname;
1122
+ } catch {
1123
+ return "";
1124
+ }
1125
+ }
1126
+ return path.slice(path.lastIndexOf("/") + 1);
1127
+ }
1128
+ function isExtensionless(src) {
1129
+ const name = fileNameOf(src);
1130
+ const dot = name.lastIndexOf(".");
1131
+ return dot <= 0 || dot === name.length - 1;
1132
+ }
1133
+ function EmbedPictureOrLink({
1134
+ src,
1135
+ alt,
1136
+ title,
1137
+ imgClassName,
1138
+ linkAttrs
1139
+ }) {
1140
+ const [failedSrc, setFailedSrc] = useState(null);
1141
+ useEffect(() => {
1142
+ setFailedSrc(null);
1143
+ }, [src]);
1144
+ if (failedSrc === src) {
1145
+ return /* @__PURE__ */ jsx("a", { href: src, title, ...linkAttrs, children: alt || src });
1146
+ }
1147
+ return /* @__PURE__ */ jsx(
1148
+ "img",
1149
+ {
1150
+ "data-ncm-embed-picture": "",
1151
+ src,
1152
+ alt,
1153
+ title,
1154
+ className: imgClassName,
1155
+ onError: () => setFailedSrc(src)
1156
+ }
1157
+ );
1158
+ }
1050
1159
  function toEmbeddingConfigType(embedType) {
1051
1160
  switch (embedType) {
1052
1161
  case "image":
@@ -1075,6 +1184,9 @@ function EmbedBlock({ block, className }) {
1075
1184
  if (block.embedType === "image") {
1076
1185
  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 }) });
1077
1186
  }
1187
+ if (block.embedType === "file" && isExtensionless(block.src)) {
1188
+ 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 }) });
1189
+ }
1078
1190
  return /* @__PURE__ */ jsx("figure", { ...dataAttrs(block), className, children: /* @__PURE__ */ jsx("a", { href: resolvedUrl, title: block.title ?? void 0, children: block.alt || block.src }) });
1079
1191
  }
1080
1192
  var embedFrameStyle = {
@@ -1251,7 +1363,19 @@ function FigureBlock({ block, className }) {
1251
1363
  "data-ncm-figure": "",
1252
1364
  className: cn("space-y-2", className),
1253
1365
  children: [
1254
- embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : /* @__PURE__ */ jsx(
1366
+ embed.embedType === "image" ? /* @__PURE__ */ jsx("img", { src: embed.src, alt: embed.alt, title: embed.title ?? void 0 }) : embed.embedType === "file" && isExtensionless(embed.src) ? (
1367
+ // Same rule as the standalone embed block: an image spelling with an
1368
+ // extension-less destination is a picture until it fails to load.
1369
+ /* @__PURE__ */ jsx(
1370
+ EmbedPictureOrLink,
1371
+ {
1372
+ src: embed.src,
1373
+ alt: embed.alt,
1374
+ title: embed.title ?? void 0,
1375
+ linkAttrs: { "data-ncm-embed-type": embed.embedType }
1376
+ }
1377
+ )
1378
+ ) : /* @__PURE__ */ jsx(
1255
1379
  "a",
1256
1380
  {
1257
1381
  href: embed.src,
@@ -0,0 +1,99 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { Block } from '@nuucognition/ncm';
4
+
5
+ /**
6
+ * Numbering for inline references (`^[ref: ...]` / `^[footnote: ...]`).
7
+ *
8
+ * The two kinds are counted **independently**: refs produce `[1] [2] …` and
9
+ * footnotes produce `¹ ² …`.
10
+ *
11
+ * Numbers are assigned by a pure pre-pass over the `blocks` array and looked up
12
+ * by **token content**, never by render order. That makes the marker number a
13
+ * function of the document alone: it is stable under React re-renders,
14
+ * StrictMode double rendering, virtualised or out-of-order block rendering,
15
+ * and it gives every citation of the same source the same number.
16
+ *
17
+ * Refs additionally honour the document's own bibliography. When a paragraph
18
+ * reads `[16] Google Gemini Team. Gemini: …` and a `^[ref: Google Gemini
19
+ * Team. Gemini: …]` token carries that entry, the marker shows `[16]` — the
20
+ * number the source document uses — so the inline citation, the reference
21
+ * list, and the original paper all agree. Refs with no bibliography match are
22
+ * numbered sequentially in first-appearance order, continuing after the
23
+ * highest bibliography number so the two ranges never collide.
24
+ *
25
+ * Footnotes are numbered sequentially in first-appearance order (identical
26
+ * footnote text shares a number).
27
+ */
28
+ type ReferenceKind = "ref" | "footnote";
29
+ interface ReferenceCounts {
30
+ ref: number;
31
+ footnote: number;
32
+ }
33
+ /** Document-wide numbering: normalised token content → marker number. */
34
+ interface ReferenceNumbering {
35
+ ref: Map<string, number>;
36
+ footnote: Map<string, number>;
37
+ }
38
+ /** Collapse whitespace and case so a token and its bibliography entry compare equal. */
39
+ declare function normalizeReferenceContent(content: string): string;
40
+ /**
41
+ * Map every `[n] entry` paragraph in the document to its number. The first
42
+ * paragraph carrying a given entry text wins.
43
+ */
44
+ declare function collectBibliography(blocks: readonly Block[]): Map<string, number>;
45
+ /**
46
+ * Compute the document-wide numbering (see the module comment for the rules).
47
+ */
48
+ declare function computeReferenceNumbering(blocks: readonly Block[]): ReferenceNumbering;
49
+ /**
50
+ * Per-block count of inline references, as the *previous* numbering scheme
51
+ * exposed it. Kept for API compatibility; the marker numbers no longer derive
52
+ * from these offsets.
53
+ */
54
+ declare function computeReferenceBases(blocks: readonly Block[]): Map<string, ReferenceCounts>;
55
+ interface BlockReferenceScopeValue {
56
+ /**
57
+ * The marker number for a token with this `kind` and `content`. Pure with
58
+ * respect to the document: the same content always gets the same number.
59
+ */
60
+ numberFor(kind: ReferenceKind, content: string): number;
61
+ /**
62
+ * Allocate the next number for `kind` in render order. Legacy API — prefer
63
+ * `numberFor`, which is stable under re-renders.
64
+ */
65
+ next(kind: ReferenceKind): number;
66
+ }
67
+ interface NcmReferencesProviderProps {
68
+ blocks: readonly Block[];
69
+ children: ReactNode;
70
+ }
71
+ /** Document-root provider — runs the numbering pre-pass over `blocks`. */
72
+ declare function NcmReferencesProvider({ blocks, children }: NcmReferencesProviderProps): react.JSX.Element;
73
+ interface BlockReferenceScopeProps {
74
+ /** Block id. Accepted for API compatibility; numbering is document-wide. */
75
+ blockId?: string;
76
+ children: ReactNode;
77
+ }
78
+ /**
79
+ * Reference scope for one block (or one standalone `InlineContent` fragment).
80
+ *
81
+ * Inside an `NcmDocument` it resolves numbers from the document pre-pass.
82
+ * Standalone — no `NcmReferencesProvider` above it — it numbers the fragment's
83
+ * own tokens by content in first-request order, which is still idempotent
84
+ * under StrictMode double rendering because a repeated request for the same
85
+ * content returns the same number.
86
+ */
87
+ declare function BlockReferenceScope({ children }: BlockReferenceScopeProps): react.JSX.Element;
88
+ /** Read the active per-block reference scope, if any. */
89
+ declare function useBlockReferenceScope(): BlockReferenceScopeValue | null;
90
+ /**
91
+ * Wrap a subtree so any inline references inside it do NOT take document
92
+ * numbers — used for a reference's own popover content, which the numbering
93
+ * pre-pass deliberately does not descend into.
94
+ */
95
+ declare function InertReferenceScope({ children }: {
96
+ children: ReactNode;
97
+ }): react.JSX.Element;
98
+
99
+ export { BlockReferenceScope as B, InertReferenceScope as I, NcmReferencesProvider as N, type ReferenceCounts as R, type BlockReferenceScopeProps as a, type BlockReferenceScopeValue as b, type NcmReferencesProviderProps as c, type ReferenceKind as d, type ReferenceNumbering as e, collectBibliography as f, computeReferenceBases as g, computeReferenceNumbering as h, normalizeReferenceContent as n, useBlockReferenceScope as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nuucognition/ncm-ui",
3
- "version": "0.1.1-beta.1",
3
+ "version": "0.1.1-beta.3",
4
4
  "description": "React renderers for NUU Cognition Markdown (NCM) blocks — a component library that renders parsed NCM Blocks to accessible, themeable markup.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,63 +0,0 @@
1
- import * as react from 'react';
2
- import { ReactNode } from 'react';
3
- import { Block } from '@nuucognition/ncm';
4
-
5
- /**
6
- * Document-order numbering for inline references (`^[ref: ...]` /
7
- * `^[footnote: ...]`).
8
- *
9
- * The two kinds are counted **independently**: refs produce `[1] [2] …` and
10
- * footnotes produce `¹ ² …`, each with its own running counter across the
11
- * whole document.
12
- *
13
- * Numbering is computed from the `blocks` array — a pure pre-pass that does not
14
- * depend on React render order — so markers stay stable even if a host
15
- * virtualises blocks or renders them out of order. The pre-pass yields, per
16
- * block, the count of each kind in all *preceding* blocks (the "base"); within
17
- * a block, a `BlockReferenceScope` hands out sequential numbers in render
18
- * order. Only the within-block step relies on the block's inline content
19
- * rendering as a synchronous subtree (it always does).
20
- */
21
- type ReferenceKind = "ref" | "footnote";
22
- interface ReferenceCounts {
23
- ref: number;
24
- footnote: number;
25
- }
26
- /**
27
- * Map each block id to the number of refs/footnotes that precede it in
28
- * document order — the starting offset for that block's own markers.
29
- */
30
- declare function computeReferenceBases(blocks: readonly Block[]): Map<string, ReferenceCounts>;
31
- interface BlockReferenceScopeValue {
32
- /** Allocate the next document-order number for `kind` within this block. */
33
- next(kind: ReferenceKind): number;
34
- }
35
- interface NcmReferencesProviderProps {
36
- blocks: readonly Block[];
37
- children: ReactNode;
38
- }
39
- /** Document-root provider — runs the numbering pre-pass over `blocks`. */
40
- declare function NcmReferencesProvider({ blocks, children }: NcmReferencesProviderProps): react.JSX.Element;
41
- interface BlockReferenceScopeProps {
42
- /** Block id; resolves the base offset from the document pre-pass. */
43
- blockId?: string;
44
- children: ReactNode;
45
- }
46
- /**
47
- * Per-block numbering scope. Seeds a fresh counter from the block's base offset
48
- * on every render and hands out sequential numbers as markers render. Resetting
49
- * each render keeps numbering idempotent under React re-renders / StrictMode.
50
- */
51
- declare function BlockReferenceScope({ blockId, children }: BlockReferenceScopeProps): react.JSX.Element;
52
- /** Read the active per-block reference scope, if any. */
53
- declare function useBlockReferenceScope(): BlockReferenceScopeValue | null;
54
- /**
55
- * Wrap a subtree so any inline references inside it do NOT allocate
56
- * document-order numbers — used for a reference's own popover content, which
57
- * the numbering pre-pass deliberately does not descend into.
58
- */
59
- declare function InertReferenceScope({ children }: {
60
- children: ReactNode;
61
- }): react.JSX.Element;
62
-
63
- export { BlockReferenceScope as B, InertReferenceScope as I, NcmReferencesProvider as N, type ReferenceCounts as R, type BlockReferenceScopeProps as a, type BlockReferenceScopeValue as b, type NcmReferencesProviderProps as c, type ReferenceKind as d, computeReferenceBases as e, useBlockReferenceScope as u };