ronantakizawa/codereview-bench huggingface dataset:
Repository: cloudflare/wildebeest
File: backend/src/activitypub/activities/handle.ts (TypeScript)

Code before:
	activity: Activity,
	db: D1Database,
	userKEK: string,
	adminEmail: string,
	vapidKeys: JWK
) {
	// The `object` field of the activity is required to be an object, with an
	// `id` and a `type` field.
	const requireComplexObject = () => {
		if (typeof activity.object !== 'object') {
			throw new Error('`activity.object` must be of type object')
		}
	}

	const getObjectAsId = makeGetObjectAsId(activity)
	const getActorAsId = makeGetActorAsId(activity)

	console.log(activity)
	switch (activity.type) {
		case 'Update': {
			requireComplexObject()
			const actorId = getActorAsId()
			const objectId = getObjectAsId()

			// check current object
			const object = await objects.getObjectBy(db, 'original_object_id', (objectId as object).toString())
			if (object === null) {
				throw new Error(`object ${objectId} does not exist`)
			}

			if ((actorId as object).toString() !== object.originalActorId) {
				throw new Error('actorid mismatch when updating object')
			}

			const updated = await updateObject(db, activity.object, object.id)
			if (!updated) {
				throw new Error('could not update object in database')
			}
			break
		}

		// https://www.w3.org/TR/activitypub/#create-activity-inbox
		case 'Create': {
			requireComplexObject()
			const actorId = getActorAsId()

			// FIXME: download any attachment Objects

			let recipients: Array<string> = []
			let target = PUBLIC_GROUP


Review comment (suggestion):
I think we could/should avoid these casts by making the `makeGetObjectAsId()` and `makeActorAsId()` functions return better types.

Code after:
	activity: Activity,
	db: D1Database,
	userKEK: string,
	adminEmail: string,
	vapidKeys: JWK
) {
	// The `object` field of the activity is required to be an object, with an
	// `id` and a `type` field.
	const requireComplexObject = () => {
		if (typeof activity.object !== 'object') {
			throw new Error('`activity.object` must be of type object')
		}
	}

	const getObjectAsId = makeGetObjectAsId(activity)
	const getActorAsId = makeGetActorAsId(activity)

	console.log(activity)
	switch (activity.type) {
		case 'Update': {
			requireComplexObject()
			const actorId = getActorAsId()
			const objectId = getObjectAsId()

			// check current object
			const object = await objects.getObjectBy(db, 'original_object_id', objectId.toString())
			if (object === null) {
				throw new Error(`object ${objectId} does not exist`)
			}

			if (actorId.toString() !== object.originalActorId) {
				throw new Error('actorid mismatch when updating object')
			}

			const updated = await updateObject(db, activity.object, object.id)
			if (!updated) {
				throw new Error('could not update object in database')
			}
			break
		}

		// https://www.w3.org/TR/activitypub/#create-activity-inbox
		case 'Create': {
			requireComplexObject()
			const actorId = getActorAsId()

			// FIXME: download any attachment Objects

			let recipients: Array<string> = []
			let target = PUBLIC_GROUP
Repository: cloudflare/wildebeest
File: backend/src/activitypub/objects/index.ts (TypeScript)

Code before:
	// Extension
	preferredUsername?: string
	// Internal
	originalActorId?: string
	originalObjectId?: string
	mastodonId?: UUID
}

// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
export interface Document extends Object {}

export function uri(domain: string, id: string): URL {
	return new URL('/ap/o/' + id, 'https://' + domain)
}

export async function createObject<Type extends Object>(
	domain: string,
	db: D1Database,
	type: string,
	properties: any,
	originalActorId: URL,
	local: boolean
): Promise<Type> {
	const uuid = crypto.randomUUID()
	const apId = uri(domain, uuid).toString()
	const sanitizedObject = await sanitizeObjectProperties(properties)

	const row: any = await db
		.prepare(
			'INSERT INTO objects(id, type, properties, original_actor_id, local, mastodon_id) VALUES(?, ?, ?, ?, ?, ?) RETURNING *'
		)
		.bind(apId, type, JSON.stringify(sanitizedObject), originalActorId.toString(), local ? 1 : 0, uuid)
		.first()

	return {
		...sanitizedObject,
		type,
		id: new URL(row.id),
		mastodonId: row.mastodon_id,
		published: new Date(row.cdate).toISOString(),
		originalActorId: row.original_actor_id,
	} as Type
}

export async function get<T>(url: URL): Promise<T> {
	const headers = {
		accept: 'application/activity+json',
	}
	const res = await fetch(url, { headers })
	if (!res.ok) {
		throw new Error(`${url} returned: ${res.status}`)

Review comment (security):
```suggestion
	const sanitizedProperties = await sanitizeObjectProperties(properties)
```

Code after:
	// Extension
	preferredUsername?: string
	// Internal
	originalActorId?: string
	originalObjectId?: string
	mastodonId?: UUID
}

// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-document
export interface Document extends Object {}

export function uri(domain: string, id: string): URL {
	return new URL('/ap/o/' + id, 'https://' + domain)
}

export async function createObject<Type extends Object>(
	domain: string,
	db: D1Database,
	type: string,
	properties: any,
	originalActorId: URL,
	local: boolean
): Promise<Type> {
	const uuid = crypto.randomUUID()
	const apId = uri(domain, uuid).toString()
	const sanitizedProperties = await sanitizeObjectProperties(properties)

	const row: any = await db
		.prepare(
			'INSERT INTO objects(id, type, properties, original_actor_id, local, mastodon_id) VALUES(?, ?, ?, ?, ?, ?) RETURNING *'
		)
		.bind(apId, type, JSON.stringify(sanitizedProperties), originalActorId.toString(), local ? 1 : 0, uuid)
		.first()

	return {
		...sanitizedProperties,
		type,
		id: new URL(row.id),
		mastodonId: row.mastodon_id,
		published: new Date(row.cdate).toISOString(),
		originalActorId: row.original_actor_id,
	} as Type
}

export async function get<T>(url: URL): Promise<T> {
	const headers = {
		accept: 'application/activity+json',
	}
	const res = await fetch(url, { headers })
	if (!res.ok) {
		throw new Error(`${url} returned: ${res.status}`)
Repository: cloudflare/wildebeest
File: backend/src/activitypub/objects/index.ts (TypeScript)

Code before:
export async function get<T>(url: URL): Promise<T> {
	const headers = {
		accept: 'application/activity+json',
	}
	const res = await fetch(url, { headers })
	if (!res.ok) {
		throw new Error(`${url} returned: ${res.status}`)
	}

	return res.json<T>()
}

type CacheObjectRes = {
	created: boolean
	object: Object
}

export async function cacheObject(
	domain: string,
	db: D1Database,
	properties: unknown,
	originalActorId: URL,
	originalObjectId: URL,
	local: boolean
): Promise<CacheObjectRes> {
	const sanitizedObject = await sanitizeObjectProperties(properties)

	const cachedObject = await getObjectBy(db, 'original_object_id', originalObjectId.toString())
	if (cachedObject !== null) {
		return {
			created: false,
			object: cachedObject,
		}
	}

	const uuid = crypto.randomUUID()
	const apId = uri(domain, uuid).toString()

	const row: any = await db
		.prepare(
			'INSERT INTO objects(id, type, properties, original_actor_id, original_object_id, local, mastodon_id) VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING *'
		)
		.bind(
			apId,
			sanitizedObject.type,
			JSON.stringify(sanitizedObject),
			originalActorId.toString(),
			originalObjectId.toString(),
			local ? 1 : 0,
			uuid
		)

Review comment (security):
```suggestion
	const sanitizedProperties = await sanitizeObjectProperties(properties)
```

Code after:
export async function get<T>(url: URL): Promise<T> {
	const headers = {
		accept: 'application/activity+json',
	}
	const res = await fetch(url, { headers })
	if (!res.ok) {
		throw new Error(`${url} returned: ${res.status}`)
	}

	return res.json<T>()
}

type CacheObjectRes = {
	created: boolean
	object: Object
}

export async function cacheObject(
	domain: string,
	db: D1Database,
	properties: unknown,
	originalActorId: URL,
	originalObjectId: URL,
	local: boolean
): Promise<CacheObjectRes> {
	const sanitizedProperties = await sanitizeObjectProperties(properties)

	const cachedObject = await getObjectBy(db, 'original_object_id', originalObjectId.toString())
	if (cachedObject !== null) {
		return {
			created: false,
			object: cachedObject,
		}
	}

	const uuid = crypto.randomUUID()
	const apId = uri(domain, uuid).toString()

	const row: any = await db
		.prepare(
			'INSERT INTO objects(id, type, properties, original_actor_id, original_object_id, local, mastodon_id) VALUES(?, ?, ?, ?, ?, ?, ?) RETURNING *'
		)
		.bind(
			apId,
			sanitizedProperties.type,
			JSON.stringify(sanitizedProperties),
			originalActorId.toString(),
			originalObjectId.toString(),
			local ? 1 : 0,
			uuid
		)
Repository: cloudflare/wildebeest
File: backend/src/media/index.ts (TypeScript)

Code before:
import type { MediaAttachment } from 'wildebeest/backend/src/types/media'
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
import { IMAGE } from 'wildebeest/backend/src/activitypub/objects/image'
import type { Object } from 'wildebeest/backend/src/activitypub/objects'

export function fromObject(obj: Object): MediaAttachment {
	if (IMAGE.toLowerCase() === obj.type.toLowerCase()) {
		return fromObjectImage(obj)
	} else if (obj.type.toLowerCase() === 'video') {
		return fromObjectVideo(obj)
	} else if (obj.type === 'Document') {
		return fromObjectDocument(obj as Document)
	} else {
		throw new Error(`unsupported media type ${obj.type}: ${JSON.stringify(obj)}`)
	}
}

export function fromObjectDocument(obj: Document): MediaAttachment {
	if (obj.mediaType === 'image/jpeg' || obj.mediaType === 'image/png') {
		return fromObjectImage(obj)
	} else if (obj.mediaType === 'video/mp4') {
		return fromObjectVideo(obj)
	} else {
		throw new Error(`unsupported media Document type: ${JSON.stringify(obj)}`)
	}
}

function fromObjectImage(obj: Object): MediaAttachment {
	return {
		url: new URL(obj.url),
		id: obj.mastodonId || obj.url.toString(),
		preview_url: new URL(obj.url),

Review comment (suggestion):
In our dummy data and from mastodon the type is `image` but here we were only handling `Image`... not sure what's up with this 😕 🤷 

Code after:
import type { MediaAttachment } from 'wildebeest/backend/src/types/media'
import type { Document } from 'wildebeest/backend/src/activitypub/objects'
import { IMAGE } from 'wildebeest/backend/src/activitypub/objects/image'
import type { APObject } from 'wildebeest/backend/src/activitypub/objects'

export function fromObject(obj: APObject): MediaAttachment {
	if (obj.type === IMAGE) {
		return fromObjectImage(obj)
	} else if (obj.type === 'Video') {
		return fromObjectVideo(obj)
	} else if (obj.type === 'Document') {
		return fromObjectDocument(obj as Document)
	} else {
		throw new Error(`unsupported media type ${obj.type}: ${JSON.stringify(obj)}`)
	}
}

export function fromObjectDocument(obj: Document): MediaAttachment {
	if (obj.mediaType === 'image/jpeg' || obj.mediaType === 'image/png') {
		return fromObjectImage(obj)
	} else if (obj.mediaType === 'video/mp4') {
		return fromObjectVideo(obj)
	} else {
		throw new Error(`unsupported media Document type: ${JSON.stringify(obj)}`)
	}
}

function fromObjectImage(obj: APObject): MediaAttachment {
	return {
		url: new URL(obj.url),
		id: obj.mastodonId || obj.url.toString(),
		preview_url: new URL(obj.url),
Repository: cloudflare/wildebeest
File: backend/src/activitypub/objects/note.ts (TypeScript)

Code before:
	tag?: Array<string>
}

export async function createPublicNote(
	domain: string,
	db: D1Database,
	content: string,
	actor: Actor,
	attachments: Array<objects.Object> = [],
	extraProperties: any = {}
): Promise<Note> {
	const actorId = new URL(actor.id)

	const properties = {
		attributedTo: actorId,
		content,
		to: [PUBLIC_GROUP],
		cc: [followersURL(actorId)],

		// FIXME: stub values
		replies: null,
		sensitive: false,
		summary: null,
		tag: [],

		attachment: attachments,
		inReplyTo: null,
		...extraProperties,
	}

	return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
}

export async function createPrivateNote(
	domain: string,
	db: D1Database,
	content: string,
	actor: Actor,
	targetActor: Actor,
	attachment: Array<objects.Object> = [],
	extraProperties: any = {}
): Promise<Note> {
	const actorId = new URL(actor.id)

	const properties = {
		attributedTo: actorId,
		content,
		to: [targetActor.id.toString()],
		cc: [],

		// FIXME: stub values

Review comment (suggestion):
@xtuc in your comment: https://github.com/cloudflare/wildebeest/pull/152/files#r1085391616
are you saying that here we should take the attachments and replace all types `image` and `video` with `Image` and `Video`? (`audio` and `Audio` too I guess?)

Code after:
	tag?: Array<string>
}

export async function createPublicNote(
	domain: string,
	db: D1Database,
	content: string,
	actor: Actor,
	attachments: Array<objects.APObject> = [],
	extraProperties: any = {}
): Promise<Note> {
	const actorId = new URL(actor.id)

	const properties = {
		attributedTo: actorId,
		content,
		to: [PUBLIC_GROUP],
		cc: [followersURL(actorId)],

		// FIXME: stub values
		replies: null,
		sensitive: false,
		summary: null,
		tag: [],

		attachment: attachments,
		inReplyTo: null,
		...extraProperties,
	}

	return (await objects.createObject(domain, db, NOTE, properties, actorId, true)) as Note
}

export async function createPrivateNote(
	domain: string,
	db: D1Database,
	content: string,
	actor: Actor,
	targetActor: Actor,
	attachment: Array<objects.APObject> = [],
	extraProperties: any = {}
): Promise<Note> {
	const actorId = new URL(actor.id)

	const properties = {
		attributedTo: actorId,
		content,
		to: [targetActor.id.toString()],
		cc: [],

		// FIXME: stub values
Repository: cloudflare/wildebeest
File: frontend/src/components/MediaGallery.tsx/Image.tsx (TypeScript)

Code before:
import { component$, useStore, $ } from '@builder.io/qwik'
import { MediaAttachment } from '~/types'

type Props = {
	mediaAttachment: MediaAttachment
}

export default component$<Props>(({ mediaAttachment }) => {
	const store = useStore({
		isModalOpen: false,
	})

	const onPreviewClick = $(() => {
		document.body.style.overflowY = 'hidden'
		store.isModalOpen = true
	})

	const onModalClose = $(() => {
		document.body.style.overflowY = 'scroll'
		store.isModalOpen = false
	})

	return (
		<>
			<div class={`${store.isModalOpen ? '' : 'cursor-zoom-in'} w-full h-full`}>
				<img
					class={'object-cover w-full h-full rounded'}
					src={mediaAttachment.preview_url || mediaAttachment.url}
					onClick$={onPreviewClick}
				/>
				{store.isModalOpen && (
					<div class="relative pointer-events-auto z-50">
						<div class="overlay inset-0 fixed z-60 bg-black opacity-70"></div>

Review comment (suggestion):
This bit actually does something important -- given a user provided focal point, it will zoom the preview image to that location. 

Visually, with this focus logic:
![image](https://user-images.githubusercontent.com/204386/215136433-e8fde60b-23c0-4ea2-afde-8807a7c9ccd4.png)

without:
![image](https://user-images.githubusercontent.com/204386/215136499-3849d238-64a7-4057-849a-3ca40c77c634.png)




Code after:
import { component$, useStore, $ } from '@builder.io/qwik'
import { MediaAttachment } from '~/types'

type Props = {
	mediaAttachment: MediaAttachment
}

export const focusToObjectFit = (focus: { x: number; y: number }) => {
	const xShift = ((1 - Math.abs(focus.x)) / 2) * 100
	const yShift = ((1 - Math.abs(focus.y)) / 2) * 100

	const x2 = focus.x < 0 ? xShift : 100 - xShift
	const y2 = focus.y > 0 ? yShift : 100 - yShift

	return { x: Math.floor(x2 * 100) / 100, y: Math.floor(y2 * 100) / 100 }
}

export default component$<Props>(({ mediaAttachment }) => {
	const store = useStore({
		isModalOpen: false,
	})

	let objectFit: { x: number; y: number } | undefined
	if (mediaAttachment.meta.focus) {
		objectFit = focusToObjectFit(mediaAttachment.meta.focus)
	}

	const onPreviewClick = $(() => {
		document.body.style.overflowY = 'hidden'
		store.isModalOpen = true
	})

	const onModalClose = $(() => {
Repository: cloudflare/wildebeest
File: backend/src/mastodon/microformats.ts (TypeScript)

Code before:
	let htmlAttrs = ''
	for (const [key, value] of Object.entries(attrs)) {
		htmlAttrs += ` ${key}="${value}"`
	}

	return `<${name}${htmlAttrs}>${content}</${name}>`
}

/// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string): string {
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === ':'
		) {
			state = 'link'
		}
		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === 's' &&
			status[i + 5] === ':'
		) {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

Review comment (suggestion):
I think this can be simplified?
```suggestion
                        status.slice(i, i+5) === 'http:'
```


Code after:
	let htmlAttrs = ''
	for (const [key, value] of Object.entries(attrs)) {
		htmlAttrs += ` ${key}="${value}"`
	}

	return `<${name}${htmlAttrs}>${content}</${name}>`
}

/// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string): string {
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (status.slice(i, i + 5) === 'http:' || status.slice(i, i + 6) === 'https:') {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

				out += char
				state = 'normal'
			} else {
				buffer += char
			}
			continue
		}

		if (state === 'mention') {
			if (char === ' ') {
				const handle = parseHandle(buffer)
				buffer = ''

				// TODO: the link to the profile is a guess, we could rely on
				// the cached Actors to find the right link.
Repository: cloudflare/wildebeest
File: backend/src/mastodon/microformats.ts (TypeScript)

Code before:
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === ':'
		) {
			state = 'link'
		}
		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === 's' &&
			status[i + 5] === ':'
		) {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

				out += char
				state = 'normal'
			} else {
				buffer += char
			}
			continue
		}

		if (state === 'mention') {

Review comment (suggestion):
```suggestion
			status.slice(i, i+6) === 'https:'
```

Code after:
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (status.slice(i, i + 5) === 'http:' || status.slice(i, i + 6) === 'https:') {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

				out += char
				state = 'normal'
			} else {
				buffer += char
			}
			continue
		}

		if (state === 'mention') {
			if (char === ' ') {
				const handle = parseHandle(buffer)
				buffer = ''

				// TODO: the link to the profile is a guess, we could rely on
				// the cached Actors to find the right link.
				const linkToProfile = `https://${handle.domain}/@${handle.localPart}`

				const mention = '@' + tag('span', handle.localPart)
				out += tag('span', tag('a', mention, { href: linkToProfile, class: 'u-url mention' }), { class: 'h-card' })
				out += char
				state = 'normal'
			} else {
				buffer += char
			}
			continue
Repository: cloudflare/wildebeest
File: backend/src/mastodon/microformats.ts (TypeScript)

Code before:

	return `<${name}${htmlAttrs}>${content}</${name}>`
}

/// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string): string {
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === ':'
		) {
			state = 'link'
		}
		if (
			char === 'h' &&
			status[i + 1] === 't' &&
			status[i + 2] === 't' &&
			status[i + 3] === 'p' &&
			status[i + 4] === 's' &&
			status[i + 5] === ':'
		) {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

				out += char
				state = 'normal'
			} else {

Review comment (suggestion):
```suggestion
		else if (
```
(just so that we don't run extra unnecessary code?)

Code after:

	return `<${name}${htmlAttrs}>${content}</${name}>`
}

/// Transform a text status into a HTML status; enriching it with links / mentions.
export function enrichStatus(status: string): string {
	let out = ''
	let state = 'normal'
	let buffer = ''

	for (let i = 0, len = status.length; i < len; i++) {
		const char = status[i]
		if (char === '@') {
			state = 'mention'
		}

		if (status.slice(i, i + 5) === 'http:' || status.slice(i, i + 6) === 'https:') {
			state = 'link'
		}

		if (state === 'link') {
			if (char === ' ') {
				try {
					const url = new URL(buffer)
					buffer = ''

					out += tag('a', url.hostname + url.pathname, { href: url.href })
				} catch (err: unknown) {
					console.warn('failed to parse link', err)
					out += buffer
				}

				out += char
				state = 'normal'
			} else {
				buffer += char
			}
			continue
		}

		if (state === 'mention') {
			if (char === ' ') {
				const handle = parseHandle(buffer)
				buffer = ''

				// TODO: the link to the profile is a guess, we could rely on
				// the cached Actors to find the right link.
				const linkToProfile = `https://${handle.domain}/@${handle.localPart}`

				const mention = '@' + tag('span', handle.localPart)
				out += tag('span', tag('a', mention, { href: linkToProfile, class: 'u-url mention' }), { class: 'h-card' })