@thomasfosterau/prosekit-utilities 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ import { a as DocumentViolation, c as checkAllowList, d as MarkJSON, f as NodeJSON, i as AllowList, l as defineAllowList, n as DocumentShape, o as allowListFilter, r as defineDocumentShape, s as allowListViolation, t as DocumentGrammarGuard, u as DocumentJSON } from "./document-shape-CrKjPIEf.js";
2
+ /**
3
+ * Extracts plain text from a document, joining the `text` leaves with
4
+ * `options.separator` (default `' '`).
5
+ *
6
+ * Tolerant by design: `null`, `undefined`, a document with no `content`, a
7
+ * stray string or `null` inside `content`, and a `text`-typed node whose
8
+ * `text` is not a string all contribute nothing rather than throwing. The
9
+ * callers are database columns and queue payloads, where a malformed row
10
+ * must degrade to empty text rather than fail a batch.
11
+ */
12
+ export declare function extractDocumentText(doc: NodeJSON | DocumentJSON | null | undefined, options?: {
13
+ readonly separator?: string | undefined;
14
+ }): string;
15
+ /**
16
+ * Extracts plain text from an inline document, joining the `text` leaves
17
+ * with nothing at all.
18
+ *
19
+ * This is the form a title or other flat field wants: `extractDocumentText`
20
+ * would turn `Rosa` + `canina` (two runs, one styled) into `Rosa canina`
21
+ * with a space that is already in the first run, or into `Rosacanina` with
22
+ * one missing — the `''` join keeps whatever whitespace the author wrote.
23
+ * Recurses into any node that carries `content`, so a wrapping inline node
24
+ * (a sidenote, a mention) still contributes its text.
25
+ */
26
+ export declare function inlineDocumentText(doc: NodeJSON | DocumentJSON): string;
27
+ /**
28
+ * Rewrites a document's root `type` to `doc`, ready for a ProseMirror
29
+ * schema.
30
+ *
31
+ * Anything that is not already `doc` is treated as an envelope name — the
32
+ * remap does not need to know which one, which is what lets one component
33
+ * serve every shape an application declares.
34
+ */
35
+ export declare const toEditorDocument: <N extends string>(doc: DocumentJSON<N> | NodeJSON) => NodeJSON;
36
+ /**
37
+ * Rewrites an editor document's root `type` back to the envelope `name`,
38
+ * ready to persist.
39
+ *
40
+ * Curried on the name so a component can bind it once
41
+ * (`const toStored = fromEditorDocument(shape.name)`). A document with no
42
+ * `content` at all becomes an empty one rather than an envelope missing its
43
+ * required field.
44
+ */
45
+ export declare const fromEditorDocument: <N extends string>(name: N) => (doc: NodeJSON) => DocumentJSON<N>;
46
+ /**
47
+ * What a node serialiser is handed alongside its node: the two recursive
48
+ * passes and the escaping function, so an application's serialiser renders
49
+ * its children exactly as the stock ones do.
50
+ */
51
+ interface MarkdownContext {
52
+ /** Serialise a run of inline nodes (text with marks, hard breaks, atoms). */
53
+ readonly serialiseInline: (nodes: ReadonlyArray<NodeJSON>) => string;
54
+ /** Serialise a run of block nodes, grouping consecutive list siblings. */
55
+ readonly serialiseBlocks: (nodes: ReadonlyArray<NodeJSON>) => string;
56
+ /** Backslash-escape CommonMark metacharacters in literal text. */
57
+ readonly escape: (text: string) => string;
58
+ }
59
+ /** Renders one block node. Returns `''` to drop the block entirely. */
60
+ type BlockSerialiser = (node: NodeJSON, context: MarkdownContext) => string;
61
+ /** Renders one non-text inline node. Returns `''` to contribute nothing. */
62
+ type InlineSerialiser = (node: NodeJSON, context: MarkdownContext) => string;
63
+ /** Wraps already-rendered text in one mark's Markdown syntax. */
64
+ type MarkSerialiser = (text: string, mark: MarkJSON) => string;
65
+ /** A configured serialiser: one entry point per document shape. */
66
+ interface MarkdownSerialiser {
67
+ /** A block document → multi-line Markdown, trimmed. */
68
+ readonly document: (doc: NodeJSON | DocumentJSON) => string;
69
+ /** An inline document → a single line of Markdown, trimmed. */
70
+ readonly inline: (doc: NodeJSON | DocumentJSON) => string;
71
+ }
72
+ /**
73
+ * Builds a Markdown serialiser.
74
+ *
75
+ * The stock cases — paragraph, heading, blockquote, code block, horizontal
76
+ * rule, image, table, hard break and the five marks with a Markdown form —
77
+ * are built in, along with the flat-list run model and the fallback that
78
+ * emits an unknown node's inline text rather than dropping it. `blocks`,
79
+ * `inlines` and `marks` are merged **over** the stock maps, so an
80
+ * application adds its own nodes (and may replace a stock one) without
81
+ * restating any of this:
82
+ *
83
+ * ```ts
84
+ * const toMarkdown = createMarkdownSerialiser({
85
+ * inlines: { mention: mentionMarkdown },
86
+ * blocks: { figure: figureMarkdown },
87
+ * });
88
+ * ```
89
+ *
90
+ * `list` is the one type a `blocks` entry cannot override: consecutive
91
+ * `list` siblings are one list, so the run — not the node — is the unit of
92
+ * serialisation, and that grouping lives here.
93
+ */
94
+ export declare function createMarkdownSerialiser(options?: {
95
+ readonly blocks?: Record<string, BlockSerialiser> | undefined;
96
+ readonly inlines?: Record<string, InlineSerialiser> | undefined;
97
+ readonly marks?: Record<string, MarkSerialiser> | undefined;
98
+ }): MarkdownSerialiser;
99
+ /**
100
+ * CommonMark escaping for literal text.
101
+ *
102
+ * @module
103
+ */
104
+ /**
105
+ * Backslash-escapes the CommonMark inline metacharacters in literal text, so
106
+ * an author's `*` stays an asterisk instead of opening emphasis.
107
+ *
108
+ * Exported because an application's own node serialiser emits literal text
109
+ * too — a mention's `@label`, a caption — and must escape it the same way
110
+ * the stock serialisers do.
111
+ */
112
+ export declare function escapeMarkdown(text: string): string;
113
+ /**
114
+ * A mention as its `@label`, escaped.
115
+ *
116
+ * The referenced account's id has no Markdown form and is dropped: a
117
+ * mention is a link to something only the application can resolve, so the
118
+ * text is all that survives. The label is escaped like any other literal
119
+ * text, so a display name containing `*` cannot open emphasis.
120
+ */
121
+ export declare const mentionMarkdown: InlineSerialiser;
122
+ /**
123
+ * A figure as its image line followed by the caption as an italic
124
+ * paragraph — Markdown has no figure element, and this is the form a reader
125
+ * renders closest to one.
126
+ *
127
+ * A figure whose image is dropped (an unsafe `src`) still emits its caption;
128
+ * a figure with no caption emits the image line alone.
129
+ */
130
+ export declare const figureMarkdown: BlockSerialiser;
131
+ /**
132
+ * A sidenote as a Pandoc inline footnote (`^[note]`).
133
+ *
134
+ * This is the established Markdown form for a reference aside with no
135
+ * separate definition block: it keeps the note where it sits in the prose
136
+ * and renders as a footnote in Pandoc-flavoured pipelines, the closest
137
+ * faithful analogue to a margin note. `^[…]` is a Pandoc extension, not core
138
+ * CommonMark; a plain CommonMark reader shows the bracketed content inline,
139
+ * which still reads as a parenthetical aside.
140
+ *
141
+ * The note holds rich inline content, so its children go through the same
142
+ * inline pass body prose does — `^[…]` legitimately contains inline Markdown
143
+ * (`[t](u)`, `*em*`, `` `code` ``), and literal `[` / `]` in a text run is
144
+ * already backslash-escaped so it cannot close the note early. Any newline
145
+ * (a hard break renders as `\␤`) is collapsed to a space afterwards: a
146
+ * literal newline would terminate the inline footnote and split the
147
+ * surrounding paragraph.
148
+ *
149
+ * A nested sidenote is flattened to its body first. A grammar restricting a
150
+ * sidenote to `(text | hardBreak)*` rules one out, but a persistence guard
151
+ * checks the node registry rather than content expressions, so a
152
+ * hand-crafted or migrated document can still carry one — and `^[…^[…]…]`
153
+ * has no valid Pandoc rendering, since the inner `]` closes the outer note.
154
+ */
155
+ export declare const sidenoteMarkdown: InlineSerialiser;
156
+ /**
157
+ * The default serialiser: the shared base grammar and nothing else.
158
+ *
159
+ * An application whose grammar includes an opt-in node builds its own with
160
+ * `createMarkdownSerialiser` rather than reaching for this one, because a
161
+ * node with no serialiser falls back to its inline text — readable, but not
162
+ * a faithful rendering of a figure or a sidenote.
163
+ */
164
+ export declare const toMarkdown: MarkdownSerialiser;
165
+ /**
166
+ * The safe-URL allow-list shared by every serialiser in this package.
167
+ *
168
+ * Link `href`s and image `src`s are passed through this allow-list before
169
+ * being embedded, so a serialiser never emits a `javascript:` / `data:` /
170
+ * `vbscript:` URI. Single-sourced here so the Markdown serialiser, the HTML
171
+ * serialiser (the ProseKit entry point) and the allow-list write-seam check can never
172
+ * drift apart — the SEC-XSS-001 defence-in-depth both apps this package was
173
+ * extracted from rely on.
174
+ *
175
+ * @module
176
+ */
177
+ /**
178
+ * The schemes {@link isSafeHref} accepts when the caller names none.
179
+ *
180
+ * Deliberately short: these are the three a rich-text document has a reason
181
+ * to link to. An application that needs `tel:` or a custom scheme passes it
182
+ * in rather than widening the default for everyone.
183
+ */
184
+ export declare const DEFAULT_SAFE_SCHEMES: ReadonlyArray<string>;
185
+ /**
186
+ * Returns true when `href` is safe to embed as a link target or image
187
+ * source.
188
+ *
189
+ * Allowed: any of `options.schemes` (default `http`, `https`, `mailto`),
190
+ * root-relative paths (`/foo`, but not `//evil.example` or `/\evil.example`,
191
+ * both of which resolve off-origin), and fragment links (`#bar`).
192
+ *
193
+ * The scheme is read off `new URL()` rather than compared as a string
194
+ * prefix, which is what makes the obfuscated forms fail: the WHATWG parser
195
+ * lowercases the scheme and strips leading and inner ASCII whitespace and
196
+ * control characters before reporting it, so `JaVaScRiPt:`, ` javascript:`
197
+ * and `java\tscript:` all normalise to `javascript:` and are rejected. A
198
+ * `startsWith('javascript:')` test would pass all three.
199
+ */
200
+ export declare function isSafeHref(href: unknown, options?: {
201
+ readonly schemes?: ReadonlyArray<string> | undefined;
202
+ }): href is string;
203
+ export { type AllowList, type BlockSerialiser, type DocumentGrammarGuard, type DocumentJSON, type DocumentShape, DocumentViolation, type InlineSerialiser, type MarkJSON, type MarkSerialiser, type MarkdownContext, type MarkdownSerialiser, type NodeJSON, allowListFilter, allowListViolation, checkAllowList, defineAllowList, defineDocumentShape };