/** * Substack's document format. * * `draft_body` is not HTML. It is a JSON ProseMirror document, and this is the * single most common way an integration gets Substack wrong: send HTML and the * API accepts it with a 200, then the post renders with the tags visible as * literal text. There is no error to catch. You find out by looking at the * published post. * * The schema below was read back off live drafts rather than guessed, including * the embed nodes, which are the reason a pasted YouTube link becomes a player * instead of a blue link. */ export type PMMark = { type: "strong" | "em" | "code" | "strikethrough" | "link"; attrs?: Record; }; export type PMNode = { type: string; attrs?: Record; content?: PMNode[]; text?: string; marks?: PMMark[]; }; export type PMDoc = { type: "doc"; content: PMNode[]; }; /** * Published to the model in tool descriptions so it can write structured bodies * directly instead of guessing node names. Kept in sync with the builders here. */ export declare const NODE_VOCABULARY: readonly ["paragraph", "heading (attrs.level 1-6, rendered by Substack as its H1-H3)", "blockquote", "bullet_list / ordered_list (attrs.order, attrs.tight) with list_item children", "code_block (attrs.language)", "horizontal_rule", "image2 (attrs.src, attrs.alt, attrs.caption)", "youtube2 (attrs.videoId)", "twitter2 (attrs.url)", "spotify2 (attrs.url)", "vimeo (attrs.url)", "paywall (everything after it is for paying subscribers)", "button (attrs.url, attrs.text)", "subscribeWidget (attrs.text)", "footnote (attrs.number) with footnoteAnchor"]; export declare const MARK_VOCABULARY: readonly ["strong", "em", "code", "strikethrough", "link (attrs.href)"]; export declare function doc(content: PMNode[]): PMDoc; export declare function text(value: string, marks?: PMMark[]): PMNode; export declare function paragraph(content: PMNode[]): PMNode; /** * Substack renders heading level 1 as its largest in-body heading. Markdown `#` * maps to level 1 here and Substack styles it down, which is why a post title * should be the `title` field rather than an H1 in the body. */ export declare function heading(level: number, content: PMNode[]): PMNode; export declare function blockquote(content: PMNode[]): PMNode; export declare function codeBlock(code: string, language?: string): PMNode; export declare function listItem(content: PMNode[]): PMNode; export declare function bulletList(items: PMNode[], tight?: boolean): PMNode; export declare function orderedList(items: PMNode[], start?: number, tight?: boolean): PMNode; export declare function horizontalRule(): PMNode; export declare function image(src: string, alt?: string, caption?: string): PMNode; /** * The paywall divider. Everything below it is visible only to paying * subscribers. Substack allows exactly one per post. */ export declare function paywall(): PMNode; export declare function button(url: string, label: string): PMNode; export declare function subscribeWidget(label?: string): PMNode; /** Serialise for the `draft_body` field, which takes a JSON string. */ export declare function serialize(document: PMDoc): string; /** * Read a `draft_body` back. Substack returns it as a JSON string, but older * drafts and templates can hold a raw object, and a body written by a broken * integration can hold HTML. Handle all three rather than throwing. */ export declare function parseBody(raw: unknown): PMDoc | null; export declare function validate(document: PMDoc): string[];