@avocadostudio-ai/shared 0.3.2 → 0.4.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.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  export { EDITOR_PROTOCOL_VERSION } from "./protocol.ts";
2
2
  export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret, type DraftSecretValidationResult } from "./draft-mode.ts";
3
3
  export type { FieldDiffKind, FieldDiff, BlockDiffStatus, BlockDiff, PageDiffStatus, PageDiff, PublishDiff, SiteConfigFieldDiff, SiteConfigDiff, } from "./publish-diff.ts";
4
- export { isImagePath, toAltPath, setPropAtPath } from "./editable-path.ts";
4
+ export { isImagePath, toAltPath, isAltPath, toImagePath, setPropAtPath } from "./editable-path.ts";
5
+ export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "./editable-coverage.ts";
6
+ export type { MarkedBlock, BlockCoverageGap, EditableCoverage } from "./editable-coverage.ts";
7
+ export { parseLink, resolveLink, normalizeLinkPath, isKnownRoute, internalPathForUrl, rankLinkTargets, rankFileTargets, suggestLinkTargets, scoreLinkCandidate, suggestLinkTarget, newTabKeyFor, linkAttrs, isFilePath, knownFileExtensions, linksInRichText, type LinkKind, type ParsedLink, type ResolvedLink, type LinkPageOption, type LinkFileOption, type LinkSuggestions, } from "./links.ts";
5
8
  export { parseInline, parseRichText, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel, clampMarkdownHeadings, unescapeMarkdownText, isRichTextDoc, fromMarkdown, toMarkdown, mergeRichTextDoc, NODE, MARK, type InlineToken, type RichTextBlock, type RichTextList, type RichTextListItem, type RichTextDoc, type RichTextNode, type RichTextMark } from "@avocadostudio-ai/richtext";
6
9
  export { blockDefinitionSchema, blockManifestSchema, buildBlockManifest, jsonSchemaLikeSchema, validateByJsonSchemaLike, findManifestSchemaIssue, type ManifestSchemaIssue, validateManifestDefaultProps, deriveFieldMetaFromSchema, resolveManifestFieldMeta, isProseMirrorDocSchema, type BlockDefinition, type BlockManifest } from "./block-manifest.ts";
7
10
  export { z } from "zod";
8
- export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, getImageFields, getListImageFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, declareBlockCatalogue, getBlockCatalogue, isInBlockCatalogue, catalogueBlockTypes, undeclaredBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
11
+ export { type FieldKind, type ImageSpec, type FieldMeta, type ListFieldMeta, type BlockMeta, type BlockType, type BlockInstance, type BlockRegistration, IMAGE_PLACEHOLDER, isImagePlaceholder, registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes, blockSchemas, allowedBlockTypes, declareBlockCatalogue, getBlockCatalogue, isInBlockCatalogue, catalogueBlockTypes, undeclaredBlockTypes, getPropDisplayName, defaultListItemForBlock, blockInstanceSchema, blockInstanceSchemaLenient, validateBlockProps, getBlockJsonSchema, } from "./blocks/_registry.ts";
9
12
  export { defaultPropsForType, declaredDefaultPropsForType, resolveHeadingTag, resolveItemHeadingTag, DEFAULT_HEADING_LEVELS, } from "./blocks/index.ts";
10
13
  export { blockTypeToCamel, camelToBlockType, blockTypeToLower, lowerToBlockType, } from "./block-names.ts";
11
14
  export { makeAddBlock, generateBlockId, makeAddItem, generateItemId, ensureItemIds, type AddBlockOp, type MakeAddBlockOptions, type AddItemOp, type MakeAddItemOptions, } from "./ops/builders.ts";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  export { EDITOR_PROTOCOL_VERSION } from "./protocol.js";
2
2
  export { getConfiguredDraftSecret, getSafeInternalRedirectPath, validateDraftSecret } from "./draft-mode.js";
3
- export { isImagePath, toAltPath, setPropAtPath } from "./editable-path.js";
3
+ export { isImagePath, toAltPath, isAltPath, toImagePath, setPropAtPath } from "./editable-path.js";
4
+ export { extractMarkedBlocks, editableCoverage, formatEditableCoverage } from "./editable-coverage.js";
5
+ export { parseLink, resolveLink, normalizeLinkPath, isKnownRoute, internalPathForUrl, rankLinkTargets, rankFileTargets, suggestLinkTargets, scoreLinkCandidate, suggestLinkTarget, newTabKeyFor, linkAttrs, isFilePath, knownFileExtensions, linksInRichText, } from "./links.js";
4
6
  /*
5
7
  * The rich-text grammar lives in `@avocadostudio-ai/richtext`, which owns the
6
8
  * parser and every CMS converter and has no dependencies of its own. It is
@@ -33,7 +35,7 @@ export {
33
35
  // Constants & helpers
34
36
  IMAGE_PLACEHOLDER, isImagePlaceholder,
35
37
  // Registry functions
36
- registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, getImageFields, getListImageFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes,
38
+ registerBlock, resetListFieldWarnings, getBlockMeta, getAllBlockMeta, blockAcceptsProp, blockListItemAcceptsKey, getImageFields, getListImageFields, getMediaFields, isFieldInlineEditable, getImageSpec, isChrome, getChromeTypes,
37
39
  // Backwards-compatible exports
38
40
  blockSchemas, allowedBlockTypes,
39
41
  // The catalogue a site actually renders — see `declareBlockCatalogue`
@@ -0,0 +1,243 @@
1
+ /**
2
+ * What a link field's string actually means.
3
+ *
4
+ * A link is stored as a plain string — `/pricing`, `https://example.com`,
5
+ * `mailto:hi@example.com` — and three places need to agree on how to read it:
6
+ * the editor's link control (which icon, which label, which warning), the
7
+ * `seo.internal-link-dead` check (is this route real?), and the rename rewriter
8
+ * (is this string a route at all?). Before this module each answered the
9
+ * question with its own inline `startsWith("/")`, and they did not agree: the
10
+ * rename rewriter matched on prop *names* containing "href", so a link stored
11
+ * under `url` or inside Footer's `label|url` text went stale on every rename.
12
+ *
13
+ * Nothing here validates in the blocking sense. A path this module cannot
14
+ * resolve is still a legitimate link — on a CMS-backed site Avocado sees only
15
+ * the pages it was handed, and a route can exist in Next, in the CMS, or behind
16
+ * a rewrite and be invisible from here. `resolveLink` reports what it knows; it
17
+ * never decides what the editor may type.
18
+ */
19
+ /** How a link string addresses its target. */
20
+ export type LinkKind = "empty" | "page" | "file" | "external" | "email" | "phone" | "anchor";
21
+ /** Does this route-shaped path name a document rather than a page? */
22
+ export declare function isFilePath(path: string): boolean;
23
+ /**
24
+ * The file extensions this module recognises, for a caller that has to describe
25
+ * them — an upload control's `accept`, a picker's filter, a docs page.
26
+ */
27
+ export declare function knownFileExtensions(): string[];
28
+ /** A link string, classified. */
29
+ export type ParsedLink = {
30
+ kind: LinkKind;
31
+ /** The original string, untouched. */
32
+ raw: string;
33
+ /**
34
+ * The route, for `kind: "page"` — path only, with `?query` and `#fragment`
35
+ * stripped. This is what `resolveLink` matches against the site's slugs.
36
+ */
37
+ path?: string;
38
+ /** The `?a=b#c` tail of a page link, preserved so a rewrite can restore it. */
39
+ suffix?: string;
40
+ /** Host for `external`, address for `email`, number for `phone`, id for `anchor`. */
41
+ target?: string;
42
+ };
43
+ /** A page the editor can offer as a link target. */
44
+ export type LinkPageOption = {
45
+ slug: string;
46
+ /** The URL, when the site says it differs from the slug (locale prefixes). */
47
+ path?: string;
48
+ title?: string;
49
+ };
50
+ /** Classify a link string. Never throws; anything unrecognised parses as `page`-ish text. */
51
+ export declare function parseLink(value: unknown): ParsedLink;
52
+ /**
53
+ * Give a route its leading slash and drop a trailing one.
54
+ *
55
+ * `pricing`, `/pricing`, `/pricing/` are the same page. The editor accepts all
56
+ * three and stores the middle one. The homepage keeps its lone slash.
57
+ */
58
+ export declare function normalizeLinkPath(path: string): string;
59
+ /**
60
+ * Does this route exist among the site's known pages?
61
+ *
62
+ * Compares on the path alone — a link may legitimately carry a query or a
63
+ * fragment, and neither changes which page it points at — and accepts a
64
+ * trailing slash on either side. This is the matcher `seo.internal-link-dead`
65
+ * uses, extracted so the inline field warning and the checks panel cannot
66
+ * disagree about what "dead" means.
67
+ */
68
+ export declare function isKnownRoute(path: string, knownSlugs: Iterable<string>): boolean;
69
+ /**
70
+ * A document the site can link to, as the editor needs to know it.
71
+ *
72
+ * `path` is what goes in the link. Everything else is for showing it to a
73
+ * person, and every field but `path` is optional because a site that answers
74
+ * this question from a directory listing has only the path.
75
+ */
76
+ export type LinkFileOption = {
77
+ /** The URL to link to — `/downloads/menu-de.pdf`. */
78
+ path: string;
79
+ /** What to call it in a picker. Defaults to the filename. */
80
+ name?: string;
81
+ /** MIME type, when the store knows it. */
82
+ contentType?: string;
83
+ /** Bytes, when the store knows it — a picker shows it, nothing decides on it. */
84
+ size?: number;
85
+ };
86
+ /** A parsed link plus what the site knows about its target. */
87
+ export type ResolvedLink = ParsedLink & {
88
+ /** The matching page, when `kind` is `page` and the route is known. */
89
+ page?: LinkPageOption;
90
+ /** The matching document, when `kind` is `file` and the asset list has it. */
91
+ file?: LinkFileOption;
92
+ /**
93
+ * True for a link whose target matches nothing the site knows about.
94
+ *
95
+ * For a `page` this is decided from the slug list, which is always present.
96
+ * For a `file` it is decided from the asset list, which is *not* — a site
97
+ * that cannot enumerate its documents passes none, and then `missing` stays
98
+ * undefined rather than becoming `true`. "We did not check" and "it is not
99
+ * there" have to stay distinguishable, or every site without an asset store
100
+ * would report all of its own documents as broken.
101
+ */
102
+ missing?: boolean;
103
+ };
104
+ /**
105
+ * Classify a link *and* look up its target. Pages may be keyed by `slug` or by
106
+ * `path` (they differ on locale-prefixed sites — see
107
+ * `docs/ideas/page-identity-punch-list.md`), so both are matched.
108
+ *
109
+ * `files` is optional and its absence is meaningful — see `missing` above.
110
+ */
111
+ export declare function resolveLink(value: unknown, pages?: readonly LinkPageOption[], files?: readonly LinkFileOption[]): ResolvedLink;
112
+ /**
113
+ * How well a page answers what was typed, from 0 (nothing in common) to 1.
114
+ *
115
+ * The balanced mean of precision and recall over shared words. Balanced because
116
+ * both halves are wrong alone: precision by itself ranks a sprawling page that
117
+ * happens to contain the word above the page actually named for it, and recall
118
+ * by itself ranks a one-word slug above every longer match.
119
+ *
120
+ * Pass the site's pages as `corpus` to weight words by how distinctive they are
121
+ * (see `tokenWeights`); without it every word counts the same.
122
+ */
123
+ export declare function scoreLinkCandidate(query: string, page: LinkPageOption, corpus?: readonly LinkPageOption[]): number;
124
+ /**
125
+ * Known pages ranked by how well they answer `query`, best first.
126
+ *
127
+ * Held to the same floor as `suggestLinkTarget`, which it did not used to be:
128
+ * anything sharing a single token came back, so on a tri-lingual site every
129
+ * `/fr/*` page answered every French-flavoured query. Typing a document path
130
+ * into the link picker listed `/fr/`, `/fr/faq/` and `/fr/evenements/` — six
131
+ * rows of pages, each one click away from replacing a working PDF link with a
132
+ * link to the FAQ. A near-miss still ranks well above the floor; what the floor
133
+ * removes is the coincidence.
134
+ */
135
+ export declare function rankLinkTargets(query: string, pages: readonly LinkPageOption[]): LinkPageOption[];
136
+ /**
137
+ * Known documents ranked by how well they answer `query`, best first.
138
+ *
139
+ * The same scorer as pages, and the corpus weighting is what makes it work
140
+ * here: every document on a site shares its directory and its extension, so
141
+ * `downloads` and `pdf` identify nothing and are weighted to nearly nothing,
142
+ * while the part of the filename someone got wrong is what decides the order.
143
+ *
144
+ * This is the half a substring filter cannot do. `AadventureArenaBerm` is a
145
+ * real filename on a real site, typo included; a person typing it from memory
146
+ * gets one character wrong and a substring match returns nothing at all —
147
+ * which reads exactly like "this site has no such document".
148
+ */
149
+ export declare function rankFileTargets(query: string, files: readonly LinkFileOption[]): LinkFileOption[];
150
+ /** What a link picker should offer for what has been typed so far. */
151
+ export type LinkSuggestions = {
152
+ pages: LinkPageOption[];
153
+ files: LinkFileOption[];
154
+ };
155
+ /**
156
+ * The pages and documents to offer for a partly-typed link.
157
+ *
158
+ * Two surfaces ask this question — the property panel's link field and the
159
+ * prose editor's link popover — and they answered it with their own inline
160
+ * copies of "substring, else rank". The copies disagreed, and both made the
161
+ * same mistake: they ranked *pages* for a query the parser had already
162
+ * classified as a document. A screenshot of the result is why this function
163
+ * exists — `/downloads/…-Gruppen-FR.pdf` typed in, six pages offered, not one
164
+ * of the site's fifteen PDFs among them.
165
+ *
166
+ * So the kind decides which list is offered at all:
167
+ *
168
+ * - a document path offers documents, never pages;
169
+ * - a route offers pages, and any document whose path literally contains what
170
+ * was typed (`menu` should still find the menu PDFs);
171
+ * - `mailto:`, `tel:`, `#anchor` and `https://` offer neither — there is
172
+ * nothing on this site they could mean, and the caller's empty state can say
173
+ * so instead.
174
+ *
175
+ * Within a kind, a literal substring is what a person typing into a box
176
+ * expects, and ranking is the fallback for when nothing matches literally.
177
+ */
178
+ export declare function suggestLinkTargets(query: string, options?: {
179
+ pages?: readonly LinkPageOption[];
180
+ files?: readonly LinkFileOption[];
181
+ limit?: number;
182
+ /**
183
+ * With an empty box, list the site's first documents alongside its pages.
184
+ * The prose picker wants that — it is how an editor discovers the site has
185
+ * documents at all. The link *field* does not: a document shelf under every
186
+ * link field would bury the pages, which is what a link usually wants.
187
+ */
188
+ browseFiles?: boolean;
189
+ }): LinkSuggestions;
190
+ /**
191
+ * The page a dead internal link probably meant.
192
+ *
193
+ * `/avocado-sustainability` on a site that has `/sustainability` is a typo, a
194
+ * stale link, or a slug someone renamed by hand — and the site already holds
195
+ * everything needed to say so. Returns undefined when nothing scores well
196
+ * enough, or when the link already resolves; the caller offers what comes back,
197
+ * and never applies it on the editor's behalf.
198
+ */
199
+ export declare function suggestLinkTarget(value: unknown, pages: readonly LinkPageOption[]): LinkPageOption | undefined;
200
+ /**
201
+ * The absolute URL a viewer would copy out of the address bar, turned back into
202
+ * an internal route — when it belongs to this site.
203
+ *
204
+ * Pasting the address bar is how people actually get a URL, and an absolute one
205
+ * survives right up until the site moves domain. Returns undefined when the
206
+ * origin is someone else's, so the caller leaves the link alone.
207
+ */
208
+ export declare function internalPathForUrl(value: string, siteOrigin: string | undefined): string | undefined;
209
+ /**
210
+ * The prop holding the "open in new tab" flag for a link prop, by convention.
211
+ *
212
+ * `ctaHref` → `ctaNewTab`, `secondaryCtaHref` → `secondaryCtaNewTab`,
213
+ * `href` → `newTab`. The same shape as the `image`/`imageAlt` pairing the
214
+ * property panel already resolves by name, so a block declares two props and
215
+ * the editor renders one control.
216
+ */
217
+ export declare function newTabKeyFor(linkKey: string): string;
218
+ /** Anchor attributes for a link, given its stored value and new-tab flag. */
219
+ export declare function linkAttrs(href: unknown, newTab?: unknown): {
220
+ href: string;
221
+ target?: string;
222
+ rel?: string;
223
+ };
224
+ /**
225
+ * Every href inside a richtext value.
226
+ *
227
+ * A link is not only a `link`-kind prop. Most of the links on a real page are
228
+ * written *into* prose — `[Menükarte](/downloads/menu-de.pdf)` — and every
229
+ * link-aware surface we have was walking declared fields only. So the four
230
+ * menu-PDF links on a live site's Bistro section were not checked, not
231
+ * rewritten on rename, and not reported; the one linking to a filename with a
232
+ * typo in it had been wrong since August with nothing able to notice.
233
+ *
234
+ * Three shapes, because a richtext value is three things depending on where it
235
+ * came from: markdown (a site that projects its CMS prose to markdown), a
236
+ * ProseMirror document (the editor's own format), and raw HTML (a block with a
237
+ * loose schema). Walking all three costs one function and means a caller never
238
+ * has to know which it was handed.
239
+ *
240
+ * Returns hrefs in document order, duplicates included — a caller that reports
241
+ * findings wants one per occurrence, and a caller that wants a set can make one.
242
+ */
243
+ export declare function linksInRichText(value: unknown): string[];