| 
					
				 | 
			
			
				@@ -0,0 +1,262 @@ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+import { Tweet } from './twitter.ts'; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+class SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  constructor(readonly raw: string) {} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  get length(): number { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return this.raw.length; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  toString(): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return this.raw; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  toJSON(): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return this.raw; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+type StringLike = string | SafeString; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function escapeHTML(unsafe: string): StringLike { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return new SafeString( 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    unsafe 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .replace(/&/g, "&") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .replace(/</g, "<") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .replace(/>/g, ">") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .replace(/"/g, """) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .replace(/'/g, "'") 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  ); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function joinChildren(children: StringLike[]): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return new SafeString(children 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    .map(child => typeof child === 'string' ? escapeHTML(child) : child) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    .join('') 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  ); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function tag(tag: string, attributes: Record<string, string> = {}, children: StringLike | StringLike[] = []): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  const attrs = Object.entries(attributes).map(([ key, value ]) => { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return ` ${key}="${escapeHTML(value)}"`; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  }).join(''); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  if (children.length === 0) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return new SafeString(`<${tag}${attrs} />`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const childrenArray = Array.isArray(children) ? children : [children]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const normalizedChildren = joinChildren(childrenArray); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return new SafeString(`<${tag}${attrs}>${normalizedChildren}</${tag}>`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function buildTwitterUrl(url: string): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return new URL(url, 'https://twitter.com').toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function buildProxyUrl(url: string): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  const search = new URLSearchParams({ target: url }).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return `/__proxy?${search}`; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+function formatPlainText(text: string): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return new SafeString(escapeHTML(text).toString().replace(/\n/g, "<br />")); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+class TextFormatter { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  private splices: { text: StringLike, indices: [number, number] }[]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  private media: { type: 'video' | 'img', url: string, link?: string }[]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  private characters: string[]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  constructor(readonly tweet: Tweet, readonly useProxy: boolean) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    this.characters = [...tweet.full_text]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    this.splices = []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    this.media = []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const { indices, text } of tweet.entities.hashtags) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const url = buildTwitterUrl(`/hashtag/${text}`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      this.splices.push({ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        indices, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        text: tag('a', { href: url }, `#${text}`), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const link of tweet.entities.urls) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const url = new URL(link.expanded_url).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      this.splices.push({ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        indices: link.indices, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        text: tag('a', { href: url }, link.display_url), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const { indices, name, screen_name } of tweet.entities.user_mentions) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const url = buildTwitterUrl(`/${screen_name}`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      this.splices.push({ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        indices: indices, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        text: tag('a', { href: url, title: name }, `@${screen_name}`), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const media = tweet.extended_entities?.media ?? []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const item of media) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      if (item.type === 'photo') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        const url = new URL(item.media_url_https).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        this.media.push({ type: 'img', url }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      } else if (item.video_info !== undefined) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let max = -1; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        let maxUrl: string | undefined = undefined; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        for (const variant of item.video_info.variants) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          if (variant.bitrate === undefined) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            continue; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          if (variant.bitrate > max) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            max = variant.bitrate; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+            maxUrl = variant.url; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        if (maxUrl !== undefined) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          const url = new URL(maxUrl).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          this.media.push({ type: 'video', url }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } else { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          const url = new URL(item.media_url_https).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          this.media.push({ type: 'img', url, link: item.expanded_url }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  getRange(start: number, end?: number): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const max = this.tweet.display_text_range[1]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return this.characters.slice(start, end ?? max).join(''); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  headerHTML(): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const date = new Date(this.tweet.created_at); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const dateOptions = { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      weekday: 'short', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      year: 'numeric', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      month: '2-digit', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      day: '2-digit', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      hour: '2-digit', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      minute: '2-digit', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      timeZoneName: 'short', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } as const; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const imageUrl = new URL(this.tweet.user.profile_image_url_https).toString(); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const imageSrc = this.useProxy ? buildProxyUrl(imageUrl) : imageUrl; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const profileUrl = buildTwitterUrl(`/${this.tweet.user.screen_name}`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const tweetUrl = buildTwitterUrl(`/${this.tweet.user.screen_name}/status/${this.tweet.id_str}`); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const html = [ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      tag('img', { loading: 'lazy', src: imageSrc, height: '24px', width: '24px' }), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      ' ', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      tag('strong', {}, this.tweet.user.name), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      ' ', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      tag('a', { href: profileUrl }, `@${this.tweet.user.screen_name}`), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      tag('br'), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      'Posted ', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      tag('a', { href: tweetUrl }, date.toLocaleString(this.tweet.lang, dateOptions)), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    ]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return joinChildren(html); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  bodyHTML(): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const max = this.tweet.display_text_range[1]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const splices = this.splices 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .filter(({ indices }) => indices[0] < max && indices[1] <= max) 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      .sort((a, b) => a.indices[0] - b.indices[0]); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    let index = 0; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const html: StringLike[] = []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const { text, indices } of splices) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const start = index; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const end = indices[0]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      html.push(formatPlainText(this.getRange(start, end))); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      html.push(text); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      index = indices[1]; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    html.push(formatPlainText(this.getRange(index))); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    for (const { type, url, link } of this.media) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      html.push(tag('br')); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      html.push(tag('br')); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      const src = this.useProxy ? buildProxyUrl(url) : url; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      if (type === 'img') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        html.push(tag('a', { href: link ?? url }, [ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+          tag('img', { loading: 'lazy', src }), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        ])); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      } else if (type === 'video') { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        html.push(tag('video', { controls: '', src })); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return joinChildren(html); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  toHTML(): SafeString { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return joinChildren([ this.headerHTML(), tag('br'), this.bodyHTML() ]); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+const STYLES = ` 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+body > div { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  margin: 10px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  padding: 10px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  border: solid 1px gray; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  border-radius: 10px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+blockquote { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  padding: 10px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  border: solid 1px lightgray; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  border-radius: 10px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+div { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  max-width: 600px; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+img { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  max-width: 100%; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+`; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+export function timelineAsHTML(tweets: Tweet[]): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  const body = tweets.map(tweet => { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const displayTweet = tweet.retweeted_status ?? tweet; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const children: StringLike[] = []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    children.push(new TextFormatter(displayTweet, true).toHTML()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const quoteTweet = displayTweet.quoted_status; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if (quoteTweet !== undefined) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      children.push(tag('blockquote', {}, new TextFormatter(quoteTweet, true).toHTML())); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return tag('div', {}, children); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  }).join('\n'); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return ` 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    <html> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      <head> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        <style>${STYLES}</style> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      </head> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      <body>${body}</body> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    </html> 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  `; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+export function timelineAsJSON(username: string, tweets: Tweet[]): string { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  const items = tweets.map(tweet => { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const displayTweet = tweet.retweeted_status ?? tweet; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const children: StringLike[] = []; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    children.push(new TextFormatter(displayTweet, false).bodyHTML()); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const quoteTweet = displayTweet.quoted_status; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    if (quoteTweet !== undefined) { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      children.push(tag('blockquote', {}, [ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+        new TextFormatter(quoteTweet, false).toHTML(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      ])); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    } 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    const html = joinChildren(children); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    return { 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      id: tweet.id_str, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      url: buildTwitterUrl(`/${tweet.user.screen_name}/status/${tweet.id_str}`), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      content_html: html, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+      date_published: new Date(tweet.created_at).toISOString(), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    }; 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  return JSON.stringify({ 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    version: '1.1', 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    title: `Twitter @${username}`, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    home_page_url: buildTwitterUrl(`/${username}`), 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+    items, 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+  }); 
			 | 
		
	
		
			
				 | 
				 | 
			
			
				+} 
			 |