import { linkify } from "./linkify/linkify.js"; import { getIdentifierColorNumber, avatarInitials } from "../../../avatar.js"; /** * Parse text into parts such as newline, links and text. * @param {string} body A string to parse into parts * @returns {MessageBody} Parsed result */ export function parsePlainBody(body) { const parts = []; const lines = body.split("\n"); // create callback outside of loop const linkifyCallback = (text, isLink) => { if (isLink) { parts.push(new LinkPart(text, [new TextPart(text)])); } else { parts.push(new TextPart(text)); } }; for (let i = 0; i < lines.length; i += 1) { const line = lines[i]; if (line.length) { linkify(line, linkifyCallback); } const isLastLine = i >= (lines.length - 1); if (!isLastLine) { parts.push(new NewLinePart()); } } return new MessageBody(body, parts); } export function stringAsBody(body) { return new MessageBody(body, [new TextPart(body)]); } export class HeaderBlock { constructor(level, inlines) { this.level = level; this.inlines = inlines; } get type() { return "header"; } } export class CodeBlock { constructor(language, text) { this.language = language; this.text = text; } get type() { return "codeblock"; } } export class ListBlock { constructor(startOffset, items) { this.items = items; this.startOffset = startOffset; } get type() { return "list"; } } export class TableBlock { constructor(head, body) { this.head = head; this.body = body; } get type() { return "table"; } } export class RulePart { get type() { return "rule"; } } export class NewLinePart { get type() { return "newline"; } } export class FormatPart { constructor(format, children) { this.format = format.toLowerCase(); this.children = children; } get type() { return "format"; } } export class ImagePart { constructor(src, width, height, alt, title) { this.src = src; this.width = width; this.height = height; this.alt = alt; this.title = title; } get type() { return "image"; } } export class PillPart { constructor(id, href, children) { this.id = id; this.href = href; this.children = children; } get type() { return "pill"; } get avatarColorNumber() { return getIdentifierColorNumber(this.id); } get avatarInitials() { return avatarInitials(this.id); } } export class LinkPart { constructor(url, inlines) { this.url = url; this.inlines = inlines; } get type() { return "link"; } } export class TextPart { constructor(text) { this.text = text; } get type() { return "text"; } } function isBlockquote(part){ return part.type === "format" && part.format === "blockquote"; } export class MessageBody { constructor(sourceString, parts) { this.sourceString = sourceString; this.parts = parts; } insertEmote(string) { // We want to skip quotes introduced by replies when emoting. // We assume that such quotes are not TextParts, because replies // must have a formatted body. let i = 0; for (; i < this.parts.length && isBlockquote(this.parts[i]); i++); this.parts.splice(i, 0, new TextPart(string)); } } export function tests() { function test(assert, input, output) { assert.deepEqual(parsePlainBody(input), new MessageBody(input, output)); } return { // Tests for text "Text only": assert => { const input = "This is a sentence"; const output = [new TextPart(input)]; test(assert, input, output); }, "Text with newline": assert => { const input = "This is a sentence.\nThis is another sentence."; const output = [ new TextPart("This is a sentence."), new NewLinePart(), new TextPart("This is another sentence.") ]; test(assert, input, output); }, "Text with newline & trailing newline": assert => { const input = "This is a sentence.\nThis is another sentence.\n"; const output = [ new TextPart("This is a sentence."), new NewLinePart(), new TextPart("This is another sentence."), new NewLinePart() ]; test(assert, input, output); } }; }