@ai-react-markdown/core 1.4.2 → 1.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { JSX, ComponentType, PropsWithChildren, CSSProperties, FC } from 'react';
3
3
  import { Element, ElementContent } from 'hast';
4
+ import { defaultSchema } from 'rehype-sanitize';
4
5
 
5
6
  /**
6
7
  * Public types for the local Markdown wrapper. Ported 1:1 from react-markdown
@@ -17,6 +18,24 @@ interface ExtraProps {
17
18
  type Components = {
18
19
  [Key in keyof JSX.IntrinsicElements]?: ComponentType<JSX.IntrinsicElements[Key] & ExtraProps> | keyof JSX.IntrinsicElements;
19
20
  };
21
+ /** Transform every URL on every element attribute. Return null/empty to strip. */
22
+ type UrlTransform = (url: string, key: string, node: Readonly<Element>) => string | null | undefined;
23
+
24
+ /**
25
+ * Default URL transform — same allowlist as react-markdown / GitHub.
26
+ *
27
+ * @module components/markdown/urlTransform
28
+ */
29
+
30
+ /**
31
+ * Make a URL safe.
32
+ *
33
+ * Allows `http`, `https`, `irc`, `ircs`, `mailto`, and `xmpp` protocols, plus
34
+ * URLs relative to the current protocol (e.g. `/foo`). Other protocols are
35
+ * stripped to the empty string. Mirrors GitHub's behaviour and matches
36
+ * `micromark-util-sanitize-uri` minus the URL-encoding pass.
37
+ */
38
+ declare const defaultUrlTransform: UrlTransform;
20
39
 
21
40
  /**
22
41
  * Core type definitions, enums, and default configuration for ai-react-markdown.
@@ -299,6 +318,16 @@ type IsNever<T> = [T] extends [never] ? true : false;
299
318
  // https://github.com/sindresorhus/type-fest
300
319
  // SPDX-License-Identifier: (MIT OR CC0-1.0)
301
320
  // Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
321
+ //
322
+ // Vendored verbatim from upstream. The `any[]` spellings in
323
+ // `HasMultipleCallSignatures` and the function-fallback branch are
324
+ // load-bearing for type-fest's variadic-arity matching — replacing them
325
+ // with `unknown[]` would either narrow or widen the inference in a
326
+ // non-equivalent way, and the test suite for those exact helpers lives in
327
+ // type-fest's own repo. Disable the rule at the file scope to mirror
328
+ // upstream while keeping the rest of the package's `no-explicit-any`
329
+ // contract strict.
330
+ /* eslint-disable @typescript-eslint/no-explicit-any */
302
331
 
303
332
 
304
333
 
@@ -645,7 +674,7 @@ declare function useAIMarkdownRenderState<TConfig extends AIMarkdownRenderConfig
645
674
  *
646
675
  * Metadata lives in a separate React context so that changes to metadata
647
676
  * do not cause re-renders in components that only consume render state
648
- * (e.g. {@link MarkdownContent}).
677
+ * (e.g. the internal `MarkdownContent` renderer).
649
678
  *
650
679
  * ### `TMetadata` is a caller-asserted type
651
680
  *
@@ -722,6 +751,97 @@ interface AIMarkdownMetadataProviderProps<TMetadata extends AIMarkdownMetadata =
722
751
  */
723
752
  type AIMDContentPreprocessor = (content: string) => string;
724
753
 
754
+ /**
755
+ * Builds a `rehype-sanitize` schema by handing the caller a deep clone of
756
+ * the library's internal default schema to mutate or replace.
757
+ *
758
+ * The mutate-and-return pattern matches the ergonomics of Next.js's
759
+ * `webpack(config)` and Express middleware: a callback receives a draft,
760
+ * either modifies it in place (returning nothing) or returns a fresh object
761
+ * to replace it. The library guarantees the draft is a deep clone, so direct
762
+ * mutation never leaks into the singleton — and the singleton itself is not
763
+ * exported, so consumers cannot accidentally hand-roll a schema that drops
764
+ * the library's cross-chunk tag allowlist or KaTeX className additions.
765
+ *
766
+ * @module components/extendSanitizeSchema
767
+ */
768
+
769
+ /**
770
+ * The full `rehype-sanitize` schema type. Re-exported as the canonical
771
+ * library-internal alias so other modules don't each redeclare
772
+ * `typeof defaultSchema`.
773
+ *
774
+ * The shape is owned by `rehype-sanitize`; consumers should treat it as
775
+ * tracking that upstream type — it may evolve across rehype-sanitize major
776
+ * versions.
777
+ */
778
+ type SanitizeSchema = typeof defaultSchema;
779
+ /**
780
+ * Build a sanitize schema by mutating (or replacing) a deep clone of the
781
+ * library default.
782
+ *
783
+ * Designed to be called ONCE at module scope so the returned object has a
784
+ * stable identity across renders — passing it directly into
785
+ * `<AIMarkdown sanitizeSchema={…}>` keeps the block-memo cache warm.
786
+ *
787
+ * @example Append a custom URL protocol via mutation:
788
+ * ```ts
789
+ * const SCHEMA = extendSanitizeSchema((s) => {
790
+ * s.protocols!.href!.push('myapp');
791
+ * s.protocols!.src!.push('myapp');
792
+ * });
793
+ *
794
+ * function App() {
795
+ * return <AIMarkdown content={…} sanitizeSchema={SCHEMA} />;
796
+ * }
797
+ * ```
798
+ *
799
+ * @example Return-style replacement for wider edits:
800
+ * ```ts
801
+ * const SCHEMA = extendSanitizeSchema((s) => ({
802
+ * ...s,
803
+ * tagNames: [...(s.tagNames ?? []), 'my-widget'],
804
+ * }));
805
+ * ```
806
+ *
807
+ * @example Inspect the library default (e.g. to learn what's already allowed):
808
+ * ```ts
809
+ * // The draft handed to the modifier IS the library default, deep-cloned.
810
+ * // Logging it once at module load surfaces every default field — protocols,
811
+ * // attributes, tagNames, etc. — without ever exposing the singleton itself.
812
+ * extendSanitizeSchema((s) => {
813
+ * console.log('default sanitize schema:', s);
814
+ * });
815
+ * ```
816
+ *
817
+ * @remarks Allowing a protocol on `protocols.href` lets the URL through the
818
+ * SECOND sanitize gate. The FIRST gate (`urlTransform`) must permit the
819
+ * same protocol independently — see the `urlTransform` prop on
820
+ * `<AIMarkdown>` and the exported {@link defaultUrlTransform} for
821
+ * composition. Keep the two protocol lists in sync.
822
+ *
823
+ * ### Footguns
824
+ *
825
+ * - **Reassigning the local parameter** (`(s) => { s = { …new schema… }; }`)
826
+ * does NOT replace the draft — JS only rebinds the local variable. Either
827
+ * mutate the original draft or `return` the new object explicitly.
828
+ * - **Returning `null`** is treated the same as returning nothing (the
829
+ * modified draft is used). The TypeScript signature does not permit
830
+ * `null`, but JS callers or `as`-casted code paths could silently hit
831
+ * this. Prefer `return` with no value, or an explicit `return draft;`.
832
+ * - **Throwing inside the modifier** propagates to the call site
833
+ * uncaught — there is no try/catch. Callers usually invoke this once at
834
+ * module load, where a thrown error surfaces as a startup-time crash and
835
+ * is the correct failure mode.
836
+ *
837
+ * @param modifier - Receives a deep clone of the library default. Mutate it
838
+ * freely; either return the (possibly different) result, or return
839
+ * nothing to use the mutated draft. Returning `undefined` (and, by
840
+ * convention, `null`) is treated the same as a mutate-only call.
841
+ * @returns A new `Schema` object — never the library default singleton.
842
+ */
843
+ declare function extendSanitizeSchema(modifier: (draft: SanitizeSchema) => SanitizeSchema | void): SanitizeSchema;
844
+
725
845
  /**
726
846
  * Hook for referential stability of deep-equal values.
727
847
  *
@@ -805,45 +925,62 @@ interface ChunkData {
805
925
  ownFootnoteLabels: Set<string>;
806
926
  ownLinkLabels: Set<string>;
807
927
  }
928
+ /**
929
+ * Public, read-only view of the cross-chunk registry. This is the type
930
+ * surfaced by `useDocumentRegistry` and re-exported from the package
931
+ * barrel. Consumers can:
932
+ *
933
+ * - read the registry's current state (`chunkOrder`, `chunkData`,
934
+ * `labelSet`, `version`)
935
+ * - observe changes (`subscribe`)
936
+ * - run selectors (`globalNumber`, `resolveLinkDef`, …)
937
+ *
938
+ * Mutators — `registerChunk`, `allocateSymbol`, `releaseSymbol`,
939
+ * `contributeLabels`, `contributeChunkData` — are intentionally off this
940
+ * interface. Driving the registry directly is reserved for internal
941
+ * coordinators (the package's own `MarkdownContent` renderer) and tests,
942
+ * which import the wider `RegistryInternal` type from this module.
943
+ * `RegistryInternal` is exported here but NOT re-exported from the package
944
+ * barrel — keeping mutators off the public surface prevents a misbehaving
945
+ * consumer-component from corrupting refcounts, skipping version bumps, or
946
+ * otherwise breaking the invariants the renderer relies on.
947
+ */
808
948
  interface Registry {
809
- /** Chunk mount-order Symbol list. **Read-only from outside the registry.**
810
- * Direct mutation (`.push`, `.splice`, index assignment) corrupts
811
- * footnote numbering, "last chunk" detection, and eviction. Use the
812
- * `allocateSymbol` / `releaseSymbol` / `registerChunk` API instead. */
949
+ /** Chunk mount-order Symbol list. **Read-only.** Direct mutation
950
+ * (`.push`, `.splice`, index assignment) corrupts footnote numbering,
951
+ * "last chunk" detection, and eviction. */
813
952
  readonly chunkOrder: readonly symbol[];
814
- /** Chunk Symbol → contribution payload. **Read-only from outside.**
815
- * Use `contributeChunkData` / `contributeLabels` / `registerChunk` to
816
- * publish; direct `.set` / `.delete` bypasses version bumps and
817
- * subscriber wake-ups. */
953
+ /** Chunk Symbol → contribution payload. **Read-only.** Direct `.set` /
954
+ * `.delete` bypasses version bumps and subscriber wake-ups. */
818
955
  readonly chunkData: ReadonlyMap<symbol, ChunkData>;
819
956
  /** Union of own-def labels across all chunks. PASS 0.5 phantom-injection
820
- * driver. **Read-only from outside.** The registry derives this from
821
- * per-chunk contributions; direct mutation breaks the derivation. */
957
+ * driver. **Read-only.** The registry derives this from per-chunk
958
+ * contributions; direct mutation breaks the derivation. */
822
959
  readonly labelSet: {
823
960
  readonly footnoteLabels: ReadonlySet<string>;
824
961
  readonly linkLabels: ReadonlySet<string>;
825
962
  };
826
- /** Monotonic version counter bumped by every mutation. **Read-only from
827
- * outside** — consumers should observe via `subscribe`, not by writing. */
963
+ /** Monotonic version counter bumped by every mutation. **Read-only**
964
+ * consumers should observe via `subscribe`, not by writing. */
828
965
  readonly version: number;
829
- /** Allocate (or reuse, for Strict Mode remount) the chunk Symbol for
830
- * `reactId` AND publish this chunk's own def labels (footnotes + links)
831
- * in one call. Canonical pair API used by `MarkdownContent`'s allocate
832
- * effect — combining the two reduces the pair to a single registry
833
- * version step, which downstream consumers see as one wake-up rather
834
- * than two (the second was already coalesced by microtask, but this
835
- * keeps the version monotonic-by-1-per-mount which makes debugging
836
- * easier). The granular `allocateSymbol` / `contributeLabels` methods
837
- * remain available for tests that need to exercise each step. */
838
- registerChunk(reactId: string, footnotes: Set<string>, links: Set<string>): symbol;
839
- allocateSymbol(reactId: string): symbol;
840
- releaseSymbol(reactId: string): void;
841
- contributeLabels(symbol: symbol, footnotes: Set<string>, links: Set<string>): void;
842
- contributeChunkData(symbol: symbol, data: ChunkData): void;
843
966
  subscribe(cb: () => void): () => void;
844
967
  canonicalFootnoteFor(label: string): symbol | null;
845
968
  canonicalLinkFor(label: string): symbol | null;
846
969
  globalNumber(label: string): number | null;
970
+ /**
971
+ * Resolve a cross-chunk link definition by label. The returned `url` is the
972
+ * value the contributing chunk's `urlTransform` produced — cross-chunk
973
+ * link/image references run a second, per-attribute sanitization pass
974
+ * (`urlTransform` + `sanitizeSchema.protocols`) at render time, so the
975
+ * placeholder components themselves never trust this value blindly.
976
+ *
977
+ * Consumers reading `def.url` directly (custom backlink panels, analytics,
978
+ * dev tooling) receive a defense-in-depth-filtered string but should still
979
+ * pipe it through their own `urlTransform` if they intend to render it as
980
+ * an `href`/`src` — the contribute-time pass uses the `'href'` key and a
981
+ * synthetic `<a>` node, so a key-aware policy may treat the value
982
+ * differently when used as an `<img src>`.
983
+ */
847
984
  resolveLinkDef(label: string): LinkDef | null;
848
985
  getRefsForLabel(label: string): number;
849
986
  /** Map a chunk-local footnote-ref occurrence index (1-based, as emitted by
@@ -957,12 +1094,98 @@ interface AIMarkdownProps<TConfig extends AIMarkdownRenderConfig = AIMarkdownRen
957
1094
  * characters like `:`, `/`, or spaces.
958
1095
  */
959
1096
  documentId?: string;
1097
+ /**
1098
+ * Override the function that decides which URL protocols are allowed
1099
+ * through the FIRST sanitization gate. The default allowlist mirrors
1100
+ * `react-markdown` / GitHub: `http`, `https`, `irc`, `ircs`, `mailto`,
1101
+ * `xmpp`. Anything else is rewritten to `''`.
1102
+ *
1103
+ * **Recommended pattern**: compose with the exported
1104
+ * {@link defaultUrlTransform} so the built-in XSS protections survive,
1105
+ * and define the result at module scope so its identity is stable across
1106
+ * renders:
1107
+ *
1108
+ * ```ts
1109
+ * import AIMarkdown, { defaultUrlTransform } from '@ai-react-markdown/core';
1110
+ *
1111
+ * const ALLOWED = /^(myapp|tel):/i;
1112
+ * const URL_TRANSFORM = (url, key, node) =>
1113
+ * ALLOWED.test(url) ? url : defaultUrlTransform(url, key, node);
1114
+ *
1115
+ * function App() {
1116
+ * return <AIMarkdown urlTransform={URL_TRANSFORM} ... />;
1117
+ * }
1118
+ * ```
1119
+ *
1120
+ * **Regex-escaping**: scheme names per RFC 3986 may contain `+`, `-`, and
1121
+ * `.` (e.g. `web+app`, `coap+tcp`). All three are regex metacharacters,
1122
+ * so write `/^web\+app:/i` rather than `/^web+app:/i`. The latter would
1123
+ * match URLs starting with `we`, `wee`, `weee`, … and silently broaden
1124
+ * the allowlist.
1125
+ *
1126
+ * **Reference stability matters.** The block-memo cache treats this prop
1127
+ * as a dependency. Defining the function inline (`urlTransform={(url) =>
1128
+ * …}`) creates a new closure on every parent render, discards the cache
1129
+ * for the entire markdown document on each render, and effectively
1130
+ * disables Phase 5 memoization. In development the library will
1131
+ * `console.warn` if it detects this pattern.
1132
+ *
1133
+ * Allowing a protocol here is necessary but **not sufficient** to render
1134
+ * a link — the second gate (`rehype-sanitize`) also enforces its own
1135
+ * protocol allowlist. See the `sanitizeSchema` prop on this component
1136
+ * and the {@link extendSanitizeSchema} helper for the second gate.
1137
+ *
1138
+ * **API stability**: the `UrlTransform` type tracks the upstream
1139
+ * `react-markdown` shape and may change with its major versions.
1140
+ */
1141
+ urlTransform?: UrlTransform | null;
1142
+ /**
1143
+ * Override the `rehype-sanitize` schema applied to the rendered output.
1144
+ * The library default extends `rehype-sanitize`'s own `defaultSchema`
1145
+ * with the `<mark>` tag, KaTeX class names, and the cross-chunk
1146
+ * coordination tags (`cross-chunk-link`, `cross-chunk-image`,
1147
+ * `footnote-sup`). The default is not exported as a value — see
1148
+ * {@link extendSanitizeSchema} for how to inspect or extend it safely.
1149
+ *
1150
+ * **Recommended pattern**: build the schema with {@link extendSanitizeSchema}
1151
+ * (mutate-and-return form) so those library additions stay intact, and
1152
+ * define the result at module scope:
1153
+ *
1154
+ * ```ts
1155
+ * import AIMarkdown, { extendSanitizeSchema } from '@ai-react-markdown/core';
1156
+ *
1157
+ * const SCHEMA = extendSanitizeSchema((s) => {
1158
+ * s.protocols.href.push('myapp');
1159
+ * s.protocols.src.push('myapp');
1160
+ * });
1161
+ *
1162
+ * function App() {
1163
+ * return <AIMarkdown sanitizeSchema={SCHEMA} ... />;
1164
+ * }
1165
+ * ```
1166
+ *
1167
+ * **Footgun**: hand-rolling a schema (e.g. spreading from
1168
+ * `rehype-sanitize`'s `defaultSchema` directly) silently drops the
1169
+ * cross-chunk tag allowlist — coordinated multi-chunk rendering will then
1170
+ * lose its placeholders. Prefer the helper unless you have a specific
1171
+ * reason to opt out.
1172
+ *
1173
+ * **Reference stability matters.** An inline call
1174
+ * (`sanitizeSchema={extendSanitizeSchema((s) => { … })}`) is mitigated by
1175
+ * an internal `useStableValue` deep-equal pass, but the safer pattern is
1176
+ * still module-scope. Development builds will `console.warn` on identity
1177
+ * flips.
1178
+ *
1179
+ * **API stability**: the `SanitizeSchema` type tracks the upstream
1180
+ * `rehype-sanitize` shape and may change with its major versions.
1181
+ */
1182
+ sanitizeSchema?: SanitizeSchema;
960
1183
  }
961
1184
  /**
962
1185
  * Root component that preprocesses markdown content and renders it through
963
1186
  * a configurable remark/rehype pipeline wrapped in typography and style layers.
964
1187
  */
965
- declare const AIMarkdownComponent: <TConfig extends AIMarkdownRenderConfig = AIMarkdownRenderConfig, TRenderData extends AIMarkdownMetadata = AIMarkdownMetadata>({ streaming, content, fontSize, contentPreprocessors, customComponents, defaultConfig, config, metadata, Typography, ExtraStyles, variant, colorScheme, documentId, }: AIMarkdownProps<TConfig, TRenderData>) => react_jsx_runtime.JSX.Element;
1188
+ declare const AIMarkdownComponent: <TConfig extends AIMarkdownRenderConfig = AIMarkdownRenderConfig, TRenderData extends AIMarkdownMetadata = AIMarkdownMetadata>({ streaming, content, fontSize, contentPreprocessors, customComponents, defaultConfig, config, metadata, Typography, ExtraStyles, variant, colorScheme, documentId, urlTransform, sanitizeSchema, }: AIMarkdownProps<TConfig, TRenderData>) => react_jsx_runtime.JSX.Element;
966
1189
  declare const _default: typeof AIMarkdownComponent;
967
1190
 
968
- export { type AIMDContentPreprocessor, type AIMarkdownColorScheme, type AIMarkdownCustomComponents, AIMarkdownDocuments, type AIMarkdownDocumentsProps, type AIMarkdownExtraStylesComponent, type AIMarkdownExtraStylesProps, type AIMarkdownMetadata, type AIMarkdownProps, type AIMarkdownRenderConfig, AIMarkdownRenderDisplayOptimizeAbility, AIMarkdownRenderExtraSyntax, type AIMarkdownRenderState, type AIMarkdownTypographyComponent, type AIMarkdownTypographyProps, type AIMarkdownVariant, type ChunkData, type FootnoteDef, type LinkDef, type PartialDeep, type RefKind, type RefRecord, type Registry, _default as default, defaultAIMarkdownRenderConfig, useAIMarkdownMetadata, useAIMarkdownRenderState, useDocumentRegistry, useStableValue };
1191
+ export { type AIMDContentPreprocessor, type AIMarkdownColorScheme, type AIMarkdownCustomComponents, AIMarkdownDocuments, type AIMarkdownDocumentsProps, type AIMarkdownExtraStylesComponent, type AIMarkdownExtraStylesProps, type AIMarkdownMetadata, type AIMarkdownProps, type AIMarkdownRenderConfig, AIMarkdownRenderDisplayOptimizeAbility, AIMarkdownRenderExtraSyntax, type AIMarkdownRenderState, type AIMarkdownTypographyComponent, type AIMarkdownTypographyProps, type AIMarkdownVariant, type ChunkData, type FootnoteDef, type LinkDef, type PartialDeep, type RefKind, type RefRecord, type Registry, type SanitizeSchema, type UrlTransform, _default as default, defaultAIMarkdownRenderConfig, defaultUrlTransform, extendSanitizeSchema, useAIMarkdownMetadata, useAIMarkdownRenderState, useDocumentRegistry, useStableValue };