@sonordev/contracts 2.8.0 → 2.9.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,124 @@
1
+ 'use strict';
2
+
3
+ // src/contracts/seo-meta.ts
4
+ var TITLE_SEPARATOR = " | ";
5
+ function stripTitleSuffix(title) {
6
+ const t = title.trim();
7
+ const i = t.lastIndexOf(TITLE_SEPARATOR);
8
+ return i > 0 ? t.slice(0, i).trim() : t;
9
+ }
10
+
11
+ // src/contracts/page-titles.ts
12
+ var PIPE_RUN = /\|(?:\s*\|)+/g;
13
+ var EDGE_PIPES = /^[\s|]+|[\s|]+$/g;
14
+ function titleHead(title) {
15
+ if (typeof title !== "string") return "";
16
+ const tidy = title.replace(/\s+/g, " ").replace(PIPE_RUN, " | ").replace(EDGE_PIPES, "").replace(/\s+/g, " ");
17
+ return tidy ? stripTitleSuffix(tidy) : "";
18
+ }
19
+ var ACRONYMS = /* @__PURE__ */ new Map([
20
+ ["ac", "AC"],
21
+ ["ai", "AI"],
22
+ ["aeo", "AEO"],
23
+ ["api", "API"],
24
+ ["cms", "CMS"],
25
+ ["crm", "CRM"],
26
+ ["faq", "FAQ"],
27
+ ["faqs", "FAQs"],
28
+ ["fsbo", "FSBO"],
29
+ ["hvac", "HVAC"],
30
+ ["jl", "JL"],
31
+ ["llc", "LLC"],
32
+ ["llp", "LLP"],
33
+ ["mls", "MLS"],
34
+ ["pdf", "PDF"],
35
+ ["pllc", "PLLC"],
36
+ ["ppc", "PPC"],
37
+ ["roi", "ROI"],
38
+ ["seo", "SEO"],
39
+ ["ui", "UI"],
40
+ ["usa", "USA"],
41
+ ["ux", "UX"]
42
+ ]);
43
+ var STATE_CODES = new Set(
44
+ "al ak az ar ca co ct de dc fl ga hi id il in ia ks ky la me md ma mi mn ms mo mt ne nv nh nj nm ny nc nd oh ok or pa ri sc sd tn tx ut vt va wa wv wi wy".split(" ")
45
+ );
46
+ var WORD_STATE_CODES = /* @__PURE__ */ new Set([
47
+ "in",
48
+ "or",
49
+ "me",
50
+ "hi",
51
+ "id",
52
+ "la",
53
+ "ma",
54
+ "pa",
55
+ "al",
56
+ "de",
57
+ "co"
58
+ ]);
59
+ var TRAILING_STATE_CODES = /* @__PURE__ */ new Set(["mt"]);
60
+ function isStateCode(lower, isLast) {
61
+ if (!STATE_CODES.has(lower) || WORD_STATE_CODES.has(lower)) return false;
62
+ return TRAILING_STATE_CODES.has(lower) ? isLast : true;
63
+ }
64
+ var SMALL_WORDS = /* @__PURE__ */ new Set([
65
+ "a",
66
+ "an",
67
+ "and",
68
+ "at",
69
+ "by",
70
+ "for",
71
+ "in",
72
+ "of",
73
+ "on",
74
+ "or",
75
+ "the",
76
+ "to",
77
+ "vs",
78
+ "with"
79
+ ]);
80
+ var PAGE_EXTENSION = /\.(?:html?|php|aspx)$/i;
81
+ var URL_ORIGIN = /^[a-z][a-z0-9+.-]*:\/\/[^/\\]*/i;
82
+ var PERCENT_RUN = /(?:%[0-9a-f]{2})+/gi;
83
+ function decodeEscapes(value) {
84
+ return value.replace(PERCENT_RUN, (run) => {
85
+ try {
86
+ return decodeURIComponent(run);
87
+ } catch {
88
+ return run;
89
+ }
90
+ });
91
+ }
92
+ function capitalizeFirst(word) {
93
+ const code = word.codePointAt(0);
94
+ if (code === void 0) return word;
95
+ const first = String.fromCodePoint(code);
96
+ return first.toUpperCase() + word.slice(first.length);
97
+ }
98
+ function labelWord(token, isFirst, isLast) {
99
+ const lower = token.toLowerCase();
100
+ const acronym = ACRONYMS.get(lower);
101
+ if (acronym) return acronym;
102
+ if (isStateCode(lower, isLast)) return lower.toUpperCase();
103
+ if (/\p{N}/u.test(token)) return /^\p{L}/u.test(token) ? capitalizeFirst(token) : token;
104
+ if (!isFirst && SMALL_WORDS.has(lower)) return lower;
105
+ return capitalizeFirst(lower);
106
+ }
107
+ function slugLabel(input) {
108
+ if (typeof input !== "string") return "";
109
+ const path = input.split(/[?#]/, 1)[0].replace(URL_ORIGIN, "");
110
+ const segment = path.split(/[/\\]/).filter((s) => s.trim() !== "").pop();
111
+ if (!segment) return "";
112
+ const name = decodeEscapes(segment).replace(PAGE_EXTENSION, "");
113
+ const tokens = name.split(/[-_+\s]+/).filter(Boolean);
114
+ return tokens.map((token, i) => labelWord(token, i === 0, i === tokens.length - 1)).join(" ");
115
+ }
116
+ var PLACEHOLDER_TOKEN = /^(?::[A-Za-z_][\w-]*[?*+]?|\[\[\.\.\.[A-Za-z_][\w-]*\]\]|\[(?:\.\.\.)?[A-Za-z_][\w-]*\])$/;
117
+ function isRoutePlaceholder(value) {
118
+ if (typeof value !== "string") return false;
119
+ return value.split("/").some((segment) => PLACEHOLDER_TOKEN.test(segment.trim()));
120
+ }
121
+
122
+ exports.isRoutePlaceholder = isRoutePlaceholder;
123
+ exports.slugLabel = slugLabel;
124
+ exports.titleHead = titleHead;
@@ -0,0 +1,120 @@
1
+ // src/contracts/seo-meta.ts
2
+ var TITLE_SEPARATOR = " | ";
3
+ function stripTitleSuffix(title) {
4
+ const t = title.trim();
5
+ const i = t.lastIndexOf(TITLE_SEPARATOR);
6
+ return i > 0 ? t.slice(0, i).trim() : t;
7
+ }
8
+
9
+ // src/contracts/page-titles.ts
10
+ var PIPE_RUN = /\|(?:\s*\|)+/g;
11
+ var EDGE_PIPES = /^[\s|]+|[\s|]+$/g;
12
+ function titleHead(title) {
13
+ if (typeof title !== "string") return "";
14
+ const tidy = title.replace(/\s+/g, " ").replace(PIPE_RUN, " | ").replace(EDGE_PIPES, "").replace(/\s+/g, " ");
15
+ return tidy ? stripTitleSuffix(tidy) : "";
16
+ }
17
+ var ACRONYMS = /* @__PURE__ */ new Map([
18
+ ["ac", "AC"],
19
+ ["ai", "AI"],
20
+ ["aeo", "AEO"],
21
+ ["api", "API"],
22
+ ["cms", "CMS"],
23
+ ["crm", "CRM"],
24
+ ["faq", "FAQ"],
25
+ ["faqs", "FAQs"],
26
+ ["fsbo", "FSBO"],
27
+ ["hvac", "HVAC"],
28
+ ["jl", "JL"],
29
+ ["llc", "LLC"],
30
+ ["llp", "LLP"],
31
+ ["mls", "MLS"],
32
+ ["pdf", "PDF"],
33
+ ["pllc", "PLLC"],
34
+ ["ppc", "PPC"],
35
+ ["roi", "ROI"],
36
+ ["seo", "SEO"],
37
+ ["ui", "UI"],
38
+ ["usa", "USA"],
39
+ ["ux", "UX"]
40
+ ]);
41
+ var STATE_CODES = new Set(
42
+ "al ak az ar ca co ct de dc fl ga hi id il in ia ks ky la me md ma mi mn ms mo mt ne nv nh nj nm ny nc nd oh ok or pa ri sc sd tn tx ut vt va wa wv wi wy".split(" ")
43
+ );
44
+ var WORD_STATE_CODES = /* @__PURE__ */ new Set([
45
+ "in",
46
+ "or",
47
+ "me",
48
+ "hi",
49
+ "id",
50
+ "la",
51
+ "ma",
52
+ "pa",
53
+ "al",
54
+ "de",
55
+ "co"
56
+ ]);
57
+ var TRAILING_STATE_CODES = /* @__PURE__ */ new Set(["mt"]);
58
+ function isStateCode(lower, isLast) {
59
+ if (!STATE_CODES.has(lower) || WORD_STATE_CODES.has(lower)) return false;
60
+ return TRAILING_STATE_CODES.has(lower) ? isLast : true;
61
+ }
62
+ var SMALL_WORDS = /* @__PURE__ */ new Set([
63
+ "a",
64
+ "an",
65
+ "and",
66
+ "at",
67
+ "by",
68
+ "for",
69
+ "in",
70
+ "of",
71
+ "on",
72
+ "or",
73
+ "the",
74
+ "to",
75
+ "vs",
76
+ "with"
77
+ ]);
78
+ var PAGE_EXTENSION = /\.(?:html?|php|aspx)$/i;
79
+ var URL_ORIGIN = /^[a-z][a-z0-9+.-]*:\/\/[^/\\]*/i;
80
+ var PERCENT_RUN = /(?:%[0-9a-f]{2})+/gi;
81
+ function decodeEscapes(value) {
82
+ return value.replace(PERCENT_RUN, (run) => {
83
+ try {
84
+ return decodeURIComponent(run);
85
+ } catch {
86
+ return run;
87
+ }
88
+ });
89
+ }
90
+ function capitalizeFirst(word) {
91
+ const code = word.codePointAt(0);
92
+ if (code === void 0) return word;
93
+ const first = String.fromCodePoint(code);
94
+ return first.toUpperCase() + word.slice(first.length);
95
+ }
96
+ function labelWord(token, isFirst, isLast) {
97
+ const lower = token.toLowerCase();
98
+ const acronym = ACRONYMS.get(lower);
99
+ if (acronym) return acronym;
100
+ if (isStateCode(lower, isLast)) return lower.toUpperCase();
101
+ if (/\p{N}/u.test(token)) return /^\p{L}/u.test(token) ? capitalizeFirst(token) : token;
102
+ if (!isFirst && SMALL_WORDS.has(lower)) return lower;
103
+ return capitalizeFirst(lower);
104
+ }
105
+ function slugLabel(input) {
106
+ if (typeof input !== "string") return "";
107
+ const path = input.split(/[?#]/, 1)[0].replace(URL_ORIGIN, "");
108
+ const segment = path.split(/[/\\]/).filter((s) => s.trim() !== "").pop();
109
+ if (!segment) return "";
110
+ const name = decodeEscapes(segment).replace(PAGE_EXTENSION, "");
111
+ const tokens = name.split(/[-_+\s]+/).filter(Boolean);
112
+ return tokens.map((token, i) => labelWord(token, i === 0, i === tokens.length - 1)).join(" ");
113
+ }
114
+ var PLACEHOLDER_TOKEN = /^(?::[A-Za-z_][\w-]*[?*+]?|\[\[\.\.\.[A-Za-z_][\w-]*\]\]|\[(?:\.\.\.)?[A-Za-z_][\w-]*\])$/;
115
+ function isRoutePlaceholder(value) {
116
+ if (typeof value !== "string") return false;
117
+ return value.split("/").some((segment) => PLACEHOLDER_TOKEN.test(segment.trim()));
118
+ }
119
+
120
+ export { isRoutePlaceholder, slugLabel, titleHead };
@@ -0,0 +1,143 @@
1
+ import { type BroadcastStyleId, type PublicationCardLayout } from './broadcast-layouts';
2
+ import { type PublicationCharter } from './publication-charter';
3
+ export { PUBLICATION_CHARTER_FIELDS, parsePublicationCharter, parseSiteCharters, publicationCharterContext, publicationCharterFor, type PublicationCharter, } from './publication-charter';
4
+ /** The stored profile's version. A breaking change to the shape bumps it. */
5
+ export declare const PUBLICATION_BRANDING_VERSION: 1;
6
+ /** Where a publication appears. `social` overrides every social network; each network can override again. */
7
+ export declare const PUBLICATION_PLATFORMS: readonly ['publication', 'email', 'social', 'instagram', 'facebook', 'linkedin', 'gbp'];
8
+ export type PublicationPlatform = (typeof PUBLICATION_PLATFORMS)[number];
9
+ /** The sections of a profile and of each platform override, in the order they're read. */
10
+ export declare const PUBLICATION_BRANDING_SECTIONS: readonly ['identity', 'colors', 'typography', 'editorial', 'newsletter'];
11
+ export type PublicationBrandingSection = (typeof PUBLICATION_BRANDING_SECTIONS)[number];
12
+ export declare const PUBLICATION_IDENTITY_FIELDS: readonly ['publicationName', 'wordmark', 'tagline', 'publisherName', 'publisherUrl', 'publisherLogoUrl', 'logos'];
13
+ export declare const PUBLICATION_LOGO_VARIANTS: readonly ['default', 'light', 'dark'];
14
+ export declare const PUBLICATION_COLOR_FIELDS: readonly ['primary', 'secondary', 'accent', 'background', 'surface', 'text', 'muted', 'border'];
15
+ export declare const PUBLICATION_TYPOGRAPHY_FIELDS: readonly ['heading', 'body', 'mono', 'emailHeadingFallback', 'emailBodyFallback'];
16
+ export declare const PUBLICATION_EDITORIAL_FIELDS: readonly ['articleStyle', 'cardLayout', 'charter', 'siteCharters'];
17
+ export declare const PUBLICATION_NEWSLETTER_FIELDS: readonly ['name', 'tagline', 'layout', 'maxWidth'];
18
+ /** Fonts every email client has, the only families an email fallback may name. Lowercase. */
19
+ export declare const EMAIL_SAFE_FONTS: ReadonlySet<string>;
20
+ /** CSS generic families: written bare and lowercase in a font stack, never quoted. */
21
+ export declare const GENERIC_FONT_FAMILIES: ReadonlySet<string>;
22
+ export interface PublicationIdentity {
23
+ publicationName?: string;
24
+ wordmark?: string;
25
+ tagline?: string;
26
+ publisherName?: string;
27
+ publisherUrl?: string;
28
+ /** The publisher's attribution mark, separate from the publication masthead. */
29
+ publisherLogoUrl?: string;
30
+ logos?: {
31
+ default?: string;
32
+ light?: string;
33
+ dark?: string;
34
+ };
35
+ }
36
+ export interface PublicationColors {
37
+ primary?: string;
38
+ secondary?: string;
39
+ accent?: string;
40
+ background?: string;
41
+ surface?: string;
42
+ text?: string;
43
+ muted?: string;
44
+ border?: string;
45
+ }
46
+ export interface PublicationTypography {
47
+ heading?: string;
48
+ body?: string;
49
+ mono?: string;
50
+ emailHeadingFallback?: string;
51
+ emailBodyFallback?: string;
52
+ }
53
+ export interface PublicationEditorial {
54
+ charter?: PublicationCharter;
55
+ siteCharters?: Record<string, PublicationCharter>;
56
+ /** Shared Broadcast style for articles, reviews and generated posts; portfolio is excluded. Legacy wire name. */
57
+ articleStyle?: BroadcastStyleId;
58
+ cardLayout?: PublicationCardLayout;
59
+ }
60
+ export interface PublicationNewsletter {
61
+ name?: string;
62
+ tagline?: string;
63
+ layout?: 'editorial';
64
+ maxWidth?: 600 | 640;
65
+ }
66
+ export interface PublicationBrandingSections {
67
+ identity?: PublicationIdentity;
68
+ colors?: PublicationColors;
69
+ typography?: PublicationTypography;
70
+ editorial?: PublicationEditorial;
71
+ newsletter?: PublicationNewsletter;
72
+ }
73
+ /** Exactly the object stored in projects.settings.publication_branding. */
74
+ export interface PublicationBranding extends PublicationBrandingSections {
75
+ version: typeof PUBLICATION_BRANDING_VERSION;
76
+ platforms?: Partial<Record<PublicationPlatform, PublicationBrandingSections>>;
77
+ }
78
+ export type PublicationBrandingProfile = PublicationBranding;
79
+ type LeafPatch<T> = {
80
+ [K in keyof T]?: NonNullable<T[K]> extends object ? LeafPatch<NonNullable<T[K]>> : T[K] | null;
81
+ };
82
+ export type PublicationBrandingSectionsPatch = LeafPatch<PublicationBrandingSections>;
83
+ /** A change: any leaf set to a value, or to null to clear that override. */
84
+ export interface PublicationBrandingPatch extends PublicationBrandingSectionsPatch {
85
+ version?: typeof PUBLICATION_BRANDING_VERSION;
86
+ platforms?: Partial<Record<PublicationPlatform, PublicationBrandingSectionsPatch>>;
87
+ }
88
+ /**
89
+ * A refusal, naming the field: `publication_branding.colors.primary: use a
90
+ * valid opaque color`. One case throws a plain Error instead: a charter set on
91
+ * a channel override, which belongs to the publication or a site.
92
+ */
93
+ export declare class PublicationBrandingValidationError extends Error {
94
+ readonly path: string;
95
+ constructor(path: string, reason: string);
96
+ }
97
+ /** True for a control character (U+0000-U+001F, U+007F) anywhere in the text. */
98
+ export declare function containsControlCharacters(value: string): boolean;
99
+ /**
100
+ * A CSS font stack as data, never a stylesheet fragment: quoted family names
101
+ * and lowercase generic families joined by commas ("'Brand Display',serif"),
102
+ * or undefined when the value carries rules, variables, url() or anything
103
+ * that isn't a family name. The theme aliases heading, body and mono drop out.
104
+ */
105
+ export declare function safeFontStack(value: unknown): string | undefined;
106
+ export interface PublicationBrandingValidatorOptions {
107
+ /**
108
+ * True when a URL's hostname must never be stored as a logo or publisher
109
+ * URL: loopback, private and link-local ranges, `localhost` and the like.
110
+ * Each API passes its own server-side SSRF host guard. It receives
111
+ * `new URL(value).hostname`, so IPv6 keeps its brackets.
112
+ */
113
+ isBlockedHost: (hostname: string) => boolean;
114
+ }
115
+ export interface PublicationBrandingValidator {
116
+ /** A whole saved profile. Retired card layouts still read. */
117
+ validatePublicationBranding(value: unknown): PublicationBranding;
118
+ /** A change. Leaves may be null (clear that override); a card layout must be one drawn today. */
119
+ validatePublicationBrandingPatch(value: unknown): PublicationBrandingPatch;
120
+ /** The sections of a profile or a platform override, at `path`. */
121
+ validatePublicationBrandingSections(input: unknown, path: string, patch: boolean): PublicationBrandingSections | PublicationBrandingSectionsPatch;
122
+ /**
123
+ * The saved profile with the change applied: a null leaf clears just that
124
+ * override and empty containers are pruned. No profile yet reads as
125
+ * `{ version: 1 }`; a change that sets nothing returns the profile as it was.
126
+ */
127
+ mergePublicationBrandingPatch(current: unknown, patch: unknown): PublicationBranding;
128
+ /** One text leaf: trimmed, 1 to max characters (0 when empty is allowed), no control characters. */
129
+ text(value: unknown, path: string, max: number, empty?: boolean): string;
130
+ /** One URL leaf: a public HTTPS URL without credentials, normalized. */
131
+ url(value: unknown, path: string): string;
132
+ /** One font leaf, normalized by safeFontStack; email fallbacks may name only EMAIL_SAFE_FONTS. */
133
+ font(value: unknown, path: string, emailSafe?: boolean): string;
134
+ }
135
+ /**
136
+ * Leaf-wise merge of a validated change into a profile layer: a null leaf
137
+ * deletes that key, an object merges recursively, and a container left empty
138
+ * is removed. The inputs are never mutated. The Sonor API layers resolved
139
+ * branding with the same merge.
140
+ */
141
+ export declare function mergePublicationBrandingLayers(current: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown>;
142
+ /** Single source of truth for publication-branding validation, bound to the caller's host guard. */
143
+ export declare function createPublicationBrandingValidator(options: PublicationBrandingValidatorOptions): PublicationBrandingValidator;