T-HUB/packages/safe-content/src/ast.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

59 lines
2.1 KiB
TypeScript

/**
* Closed rich-text AST for provider content (proposal §4.5 "rich content
* contract"). Nothing outside these node types can be rendered: there is no
* node that carries raw HTML, attributes, styles or arbitrary URLs.
*
* URL-bearing fields are only ever produced by `parseSafeHttpUrl` /
* `toProxiedMediaUrl`, and the renderer re-validates them, so an AST that was
* round-tripped through storage or the network cannot smuggle a bad scheme.
*/
export const RICH_TEXT_VERSION = 1 as const;
export type TextNode = { readonly type: "text"; readonly text: string };
export type LineBreakNode = { readonly type: "linebreak" };
/** Custom emoji / emote. `src` is always a media-proxy URL, never the origin URL. */
export type EmojiNode = { readonly type: "emoji"; readonly shortcode: string; readonly src: string };
export type LinkNode = {
readonly type: "link";
/** Absolute http(s) URL, credential-free. */
readonly href: string;
/** ASCII (punycode) host of `href`, shown to the operator next to the label. */
readonly host: string;
readonly children: readonly (TextNode | EmojiNode)[];
};
export type MentionNode = {
readonly type: "mention";
/** Handle without the leading "@", e.g. `alice` or `alice@example.social`. */
readonly handle: string;
readonly href?: string;
};
export type HashtagNode = {
readonly type: "hashtag";
/** Tag without the leading "#". */
readonly tag: string;
readonly href?: string;
};
export type InlineNode = TextNode | LineBreakNode | EmojiNode | LinkNode | MentionNode | HashtagNode;
export type ParagraphNode = { readonly type: "paragraph"; readonly children: readonly InlineNode[] };
export type RichTextDocument = {
readonly type: "doc";
readonly version: typeof RICH_TEXT_VERSION;
readonly children: readonly ParagraphNode[];
/** True when input exceeded a length/node/depth limit and was cut. */
readonly truncated: boolean;
};
export type LinkPreview = {
readonly type: "linkPreview";
readonly href: string;
readonly host: string;
readonly title?: string;
readonly description?: string;
readonly siteName?: string;
/** Media-proxy URL for the preview image. */
readonly image?: string;
};