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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
44
packages/safe-content/src/plain.ts
Normal file
44
packages/safe-content/src/plain.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { RichTextDocument } from "./ast.js";
|
||||
import { DocBuilder, type ParseOptions } from "./builder.js";
|
||||
|
||||
// Single-pass, backtracking-safe token scan: URL | @mention | #hashtag.
|
||||
const TOKENS =
|
||||
/(https?:\/\/[^\s<>"'`\u0000-\u001F]{1,2048})|(?<![\p{L}\p{N}_@])@([\p{L}\p{N}_.-]{1,64}(?:@[\p{L}\p{N}-]{1,63}(?:\.[\p{L}\p{N}-]{1,63}){0,8})?)|(?<![\p{L}\p{N}_#&])#([\p{L}\p{N}\p{M}_]{1,100})/gu;
|
||||
const TRAILING_PUNCT = /[.,;:!?)\]}'"»”’]+$/;
|
||||
|
||||
function lineToNodes(builder: DocBuilder, line: string): void {
|
||||
let last = 0;
|
||||
for (const match of line.matchAll(TOKENS)) {
|
||||
const [whole, url, handle, tag] = match;
|
||||
let consumed = whole;
|
||||
if (match.index > last) builder.text(line.slice(last, match.index));
|
||||
if (url !== undefined) {
|
||||
const trimmed = url.replace(TRAILING_PUNCT, "");
|
||||
consumed = trimmed;
|
||||
builder.link(trimmed, builder.inlineText(trimmed.replace(/^https?:\/\//i, "")));
|
||||
} else if (handle !== undefined) {
|
||||
const clean = handle.replace(/[.-]+$/, "");
|
||||
consumed = "@" + clean;
|
||||
builder.mention(clean);
|
||||
} else if (tag !== undefined) {
|
||||
builder.hashtag(tag);
|
||||
}
|
||||
last = match.index + consumed.length;
|
||||
}
|
||||
if (last < line.length) builder.text(line.slice(last));
|
||||
}
|
||||
|
||||
/** Plain provider text (Bluesky, Telegram, bios, …): newlines, URLs, mentions, hashtags. */
|
||||
export function parsePlainText(input: unknown, options: ParseOptions = {}): RichTextDocument {
|
||||
const builder = new DocBuilder(options);
|
||||
if (typeof input !== "string") return builder.finish();
|
||||
const text = builder.clip(input).replace(/\r\n?/g, "\n");
|
||||
for (const paragraph of text.split(/\n{2,}/)) {
|
||||
paragraph.split("\n").forEach((line, i) => {
|
||||
if (i > 0) builder.lineBreak();
|
||||
lineToNodes(builder, line);
|
||||
});
|
||||
builder.endParagraph();
|
||||
}
|
||||
return builder.finish();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue