[UTIL][HTML] HTML abstraction class was extended with a more specialised Heading class

This little abstraction layer made it a bit easier to add a different title to a Note or Actor Feed Collection template, from whichever controller that uses it. Please, bear in mind, that abstract templates such as those found in Components\Collection, may act in a very 'declarative' way upon using them. This makes it difficult to dynamically choose what type of header is used without undergoing a mining operation in the likes of a pyramid of doom. Hence, this _little_ change.
This commit is contained in:
Eliseu Amaro 2022-02-16 03:01:25 +00:00 committed by Diogo Peralta Cordeiro
parent f66e178dfc
commit e70acd5c3b
Signed by: diogo
GPG Key ID: 18D2D35001FBFAB0
19 changed files with 227 additions and 132 deletions

View File

@ -4,7 +4,11 @@
{% block body %}
<section class="frame-section frame-section-padding">
<h1 class="frame-section-title">{{ title }}</h1>
<header class="feed-header">
{% if actors_feed_title is defined %}
{{ actors_feed_title.getHtml() }}
{% endif %}
</header>
{% set prepend_actors_collection = handle_event('PrependActorsCollection', request) %}
{% for widget in prepend_actors_collection %}

View File

@ -14,18 +14,10 @@
{% endfor %}
{% if notes is defined %}
<header class="feed-header" title="{{ 'Current page main header' | trans }}">
<header class="feed-header">
{% set current_path = app.request.get('_route') %}
{% if page_title is defined %}
{% if current_path starts with 'feed_' or 'conversation' in current_path %}
<h1 class="section-title" role="heading">{{ page_title | trans }}</h1>
{% endif %}
{% else %}
{% if current_path starts with 'search' %}
<h3 class="heading-no-margin">{{ 'Notes found' | trans }}</h3>
{% else %}
<h1 class="section-title">{{ 'Notes' | trans }}</h1>
{% endif %}
{% if notes_feed_title is defined %}
{{ notes_feed_title.getHtml() }}
{% endif %}
<nav class="feed-actions" title="{{ 'Actions that change how the feed behaves' | trans }}">
<details class="feed-actions-details" role="group">

View File

@ -40,6 +40,7 @@ use App\Util\Exception\NoLoggedInUser;
use App\Util\Exception\NoSuchNoteException;
use App\Util\Exception\RedirectException;
use App\Util\Exception\ServerException;
use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Component\Conversation\Entity\ConversationMute;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
@ -58,11 +59,14 @@ class Conversation extends FeedController
*/
public function showConversation(Request $request, int $conversation_id): array
{
$page_title = _m('Conversation');
return [
'_template' => 'collection/notes.html.twig',
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
'should_format' => false,
'page_title' => _m('Conversation'),
'_template' => 'collection/notes.html.twig',
'notes' => $this->query(query: "note-conversation:{$conversation_id}")['notes'] ?? [],
'should_format' => false,
'page_title' => $page_title,
'notes_feed_title' => (new Heading(1, [], $page_title)),
];
}

View File

@ -37,6 +37,7 @@ namespace Component\Feed\Controller;
use function App\Core\I18n\_m;
use App\Util\Common;
use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request;
@ -47,11 +48,13 @@ class Feeds extends FeedController
*/
public function public(Request $request): array
{
$data = $this->query('note-local:true');
$data = $this->query('note-local:true');
$page_title = _m(\is_null(Common::user()) ? 'Feed' : 'Planet');
return [
'_template' => 'collection/notes.html.twig',
'page_title' => _m(\is_null(Common::user()) ? 'Feed' : 'Planet'),
'notes' => $data['notes'],
'_template' => 'collection/notes.html.twig',
'page_title' => $page_title,
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: $page_title)),
'notes' => $data['notes'],
];
}
@ -63,9 +66,10 @@ class Feeds extends FeedController
Common::ensureLoggedIn();
$data = $this->query('note-from:subscribed-person,subscribed-group,subscribed-organisation');
return [
'_template' => 'collection/notes.html.twig',
'page_title' => _m('Home'),
'notes' => $data['notes'],
'_template' => 'collection/notes.html.twig',
'page_title' => _m('Home'),
'notes_feed_title' => (new Heading(level: 1, classes: ['feed-title'], text: 'Home')),
'notes' => $data['notes'],
];
}
}

View File

@ -22,12 +22,11 @@ declare(strict_types = 1);
namespace Component\Group;
use App\Core\Event;
use App\Entity\Activity;
use Component\Notification\Notification;
use function App\Core\I18n\_m;
use App\Core\Modules\Component;
use App\Core\Router\RouteLoader;
use App\Core\Router\Router;
use App\Entity\Activity;
use App\Entity\Actor;
use App\Util\Common;
use App\Util\HTML;
@ -35,6 +34,7 @@ use App\Util\Nickname;
use Component\Circle\Controller\SelfTagsSettings;
use Component\Group\Controller as C;
use Component\Group\Entity\LocalGroup;
use Component\Notification\Notification;
use Symfony\Component\HttpFoundation\Request;
class Group extends Component
@ -51,15 +51,10 @@ class Group extends Component
/**
* Enqueues a notification for an Actor (such as person or group) which means
* it shows up in their home feed and such.
* @param Actor $sender
* @param Activity $activity
* @param array $targets
* @param string|null $reason
* @return bool
*/
public function onNewNotificationWithTargets(Actor $sender, Activity $activity, array $targets = [], ?string $reason = null): bool
{
foreach($targets as $target) {
foreach ($targets as $target) {
if ($target->isGroup()) {
// The Group announces to its subscribers
Notification::notify($target, $activity, $target->getSubscribers(), $reason);
@ -78,11 +73,10 @@ class Group extends Component
$group = $vars['actor'];
if (!\is_null($actor) && $group->isGroup()) {
if ($actor->canModerate($group)) {
$url = Router::url('group_settings', ['id' => $group->getId()]);
$url = Router::url('group_settings', ['id' => $group->getId()]);
$res[] = HTML::html(['a' => ['attrs' => ['href' => $url, 'title' => _m('Edit group settings'), 'class' => 'profile-extra-actions'], _m('Group settings')]]);
}
$res[] = HTML::html(['a' => ['attrs' => ['href' => Router::url('blog_post', ['in' => $group->getId()]), 'title' => _m('Make a new blog post'), 'class' => 'profile-extra-actions'], _m('Post in blog')]]);
}
return Event::next;
}
@ -90,7 +84,7 @@ class Group extends Component
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs): bool
{
if ($section === 'profile' && $request->get('_route') === 'group_settings') {
$group_id = (int)$request->get('id');
$group_id = (int) $request->get('id');
$group = Actor::getById($group_id);
$tabs[] = [
'title' => 'Self tags',

View File

@ -46,7 +46,7 @@ class Link extends Component
preg_match_all($this->getURLRegex(), $content, $matched_urls);
$matched_urls = array_unique($matched_urls[1]);
foreach ($matched_urls as $match) {
if (in_array($match, $ignore)) {
if (\in_array($match, $ignore)) {
continue;
}
try {

View File

@ -30,6 +30,7 @@ use App\Entity as E;
use App\Entity\LocalUser;
use App\Util\Exception\ClientException;
use App\Util\Exception\ServerException;
use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Symfony\Component\HttpFoundation\Request;
@ -75,10 +76,12 @@ class PersonFeed extends FeedController
public function personView(Request $request, Actor $person): array
{
return [
'_template' => 'actor/view.html.twig',
'actor' => $person,
'nickname' => $person->getNickname(),
'notes' => E\Note::getAllNotesByActor($person),
'_template' => 'actor/view.html.twig',
'actor' => $person,
'nickname' => $person->getNickname(),
'notes' => E\Note::getAllNotesByActor($person),
'page_title' => _m($person->getNickname() . '\'s profile'),
'notes_feed_title' => (new Heading(level: 2, classes: ['section-title'], text: 'Notes by ' . $person->getNickname())),
];
}
}

View File

@ -30,6 +30,7 @@ use App\Util\Exception\BugFoundException;
use App\Util\Exception\RedirectException;
use App\Util\Form\FormFields;
use App\Util\Formatting;
use App\Util\HTML\Heading;
use Component\Collection\Util\Controller\FeedController;
use Component\Search as Comp;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@ -135,6 +136,8 @@ class Search extends FeedController
'search_form' => Comp\Search::searchForm($request, query: $q, add_subscribe: !\is_null($actor)),
'search_builder_form' => $search_builder_form->createView(),
'notes' => $notes ?? [],
'notes_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Notes found')),
'actors_feed_title' => (new Heading(level: 3, classes: ['section-title'], text: 'Actors found')),
'actors' => $actors ?? [],
'page' => 1, // TODO paginate
];

View File

@ -39,8 +39,6 @@ use App\Core\DB\DB;
use App\Core\Event;
use App\Core\GSFile;
use App\Core\HTTPClient;
use App\Util\HTML;
use Plugin\ActivityPub\Util\Explorer;
use function App\Core\I18n\_m;
use App\Core\Log;
use App\Core\Router\Router;
@ -52,6 +50,7 @@ use App\Util\Exception\DuplicateFoundException;
use App\Util\Exception\NoSuchActorException;
use App\Util\Exception\ServerException;
use App\Util\Formatting;
use App\Util\HTML;
use App\Util\TemporaryFile;
use Component\Attachment\Entity\ActorToAttachment;
use Component\Attachment\Entity\AttachmentToNote;
@ -66,6 +65,7 @@ use Exception;
use InvalidArgumentException;
use Plugin\ActivityPub\ActivityPub;
use Plugin\ActivityPub\Entity\ActivitypubObject;
use Plugin\ActivityPub\Util\Explorer;
use Plugin\ActivityPub\Util\Model;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
@ -118,8 +118,8 @@ class Note extends Model
$type_note = \is_string($json) ? self::jsonToType($json) : $json;
$actor_id = null;
$actor = null;
$to = $type_note->has('to') ? (is_string($type_note->get('to')) ? [$type_note->get('to')] : $type_note->get('to')) : [];
$cc = $type_note->has('cc') ? (is_string($type_note->get('cc')) ? [$type_note->get('cc')] : $type_note->get('cc')) : [];
$to = $type_note->has('to') ? (\is_string($type_note->get('to')) ? [$type_note->get('to')] : $type_note->get('to')) : [];
$cc = $type_note->has('cc') ? (\is_string($type_note->get('cc')) ? [$type_note->get('cc')] : $type_note->get('cc')) : [];
if ($json instanceof AbstractObject
&& \array_key_exists('test_authority', $options)
&& $options['test_authority']
@ -184,7 +184,7 @@ class Note extends Model
continue;
}
try {
$actor = ActivityPub::getActorByUri($target);
$actor = ActivityPub::getActorByUri($target);
$object_mentions_ids[$actor->getId()] = $target;
// If $to is a group, set note's scope as Group
if ($actor->isGroup()) {
@ -199,7 +199,7 @@ class Note extends Model
continue;
}
try {
$actor = ActivityPub::getActorByUri($target);
$actor = ActivityPub::getActorByUri($target);
$object_mentions_ids[$actor->getId()] = $target;
} catch (Exception $e) {
Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]);
@ -248,7 +248,7 @@ class Note extends Model
case 'Mention':
case 'Group':
try {
$actor = ActivityPub::getActorByUri($ap_tag->get('href'));
$actor = ActivityPub::getActorByUri($ap_tag->get('href'));
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
} catch (Exception $e) {
Log::debug('ActivityPub->Model->Note->fromJson->getActorByUri', [$e]);
@ -258,7 +258,7 @@ class Note extends Model
$explorer = new Explorer();
try {
$actors = $explorer->lookup($ap_tag->get('href'));
foreach($actors as $actor) {
foreach ($actors as $actor) {
$object_mentions_ids[$actor->getId()] = $ap_tag->get('href');
}
} catch (Exception $e) {
@ -356,7 +356,6 @@ class Note extends Model
break;
case VisibilityScope::GROUP:
// Will have the group in the To
// no break
case VisibilityScope::COLLECTION:
// Since we don't support sending unlisted/followers-only
// notices, arriving here means we're instead answering to that type

View File

@ -87,6 +87,15 @@
padding: unset;
}
.feed-header > h1,
.feed-header > h2,
.feed-header > h3,
.feed-header > h4,
.feed-header > h5,
.feed-header > h6 {
margin-bottom: unset;
}
.feed-actions-details summary,
.note-actions-extra-details summary {
display: block;

View File

@ -241,7 +241,7 @@
display: block;
}
.profile-info-url-nickname {
.profile-info-url strong {
font-size: 1.215rem;
font-weight: 900;
}

View File

@ -74,12 +74,12 @@ use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Security as SSecurity;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
use Symfony\Component\Yaml;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
use Twig\Environment;
use Symfony\Component\Yaml;
/**
* @codeCoverageIgnore
@ -230,8 +230,8 @@ class GNUsocial implements EventSubscriberInterface
$local_file = INSTALLDIR . '/social.local.yaml';
if (!file_exists($local_file)) {
$node_name = $_ENV['CONFIG_NODE_NAME'];
$domain = $_ENV['CONFIG_DOMAIN'];
$yaml = (new Yaml\Dumper(indentation: 2))->dump(['parameters' => ['locals' => ['gnusocial' => ['site' => ['server' => $domain, 'name' => $node_name]]]]], Yaml\Yaml::DUMP_OBJECT_AS_MAP);
$domain = $_ENV['CONFIG_DOMAIN'];
$yaml = (new Yaml\Dumper(indentation: 2))->dump(['parameters' => ['locals' => ['gnusocial' => ['site' => ['server' => $domain, 'name' => $node_name]]]]], Yaml\Yaml::DUMP_OBJECT_AS_MAP);
file_put_contents($local_file, $yaml);
}

View File

@ -36,7 +36,6 @@ use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Log;
use App\Entity\Actor;
use App\Entity\Note;
use App\Util\Exception\NicknameException;
use App\Util\Exception\ServerException;
use Component\Circle\Circle;

View File

@ -30,16 +30,31 @@ declare(strict_types = 1);
namespace App\Util;
use BadMethodCallException;
use const ENT_QUOTES;
use const ENT_SUBSTITUTE;
use Functional as F;
use HtmlSanitizer\SanitizerInterface;
use InvalidArgumentException;
use function str_starts_with;
/**
* @mixin SanitizerInterface
*
* @method static string sanitize(string $html)
*/
abstract class HTML
{
/**
* Tags whose content is sensitive to indentation, so we shouldn't indent them
*/
public const NO_INDENT_TAGS = ['a', 'b', 'em', 'i', 'q', 's', 'p', 'sub', 'sup', 'u'];
public const ALLOWED_TAGS = ['p', 'b', 'br', 'a', 'span', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
public const FORBIDDEN_ATTRIBUTES = [
'onerror', 'form', 'onforminput', 'onbeforescriptexecute', 'formaction', 'onfocus', 'onload',
'data', 'event', 'autofocus', 'onactivate', 'onanimationstart', 'onwebkittransitionend', 'onblur', 'poster',
'onratechange', 'ontoggle', 'onscroll', 'actiontype', 'dirname', 'srcdoc',
];
public const SELF_CLOSING_TAG = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
private static ?SanitizerInterface $sanitizer;
public static function setSanitizer($sanitizer): void
@ -47,21 +62,6 @@ abstract class HTML
self::$sanitizer = $sanitizer;
}
/**
* Tags whose content is sensitive to indentation, so we shouldn't indent them
*/
public const NO_INDENT_TAGS = ['a', 'b', 'em', 'i', 'q', 's', 'p', 'sub', 'sup', 'u'];
public const ALLOWED_TAGS = ['p', 'b', 'br', 'a', 'span', 'div', 'hr'];
public const FORBIDDEN_ATTRIBUTES = [
'onerror', 'form', 'onforminput', 'onbeforescriptexecute', 'formaction', 'onfocus', 'onload',
'data', 'event', 'autofocus', 'onactivate', 'onanimationstart', 'onwebkittransitionend', 'onblur', 'poster',
'onratechange', 'ontoggle', 'onscroll', 'actiontype', 'dirname', 'srcdoc',
];
public const SELF_CLOSING_TAG = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
/**
* Creates an HTML tag without attributes
*/
@ -78,13 +78,11 @@ abstract class HTML
$html = '<' . $tag . (\is_string($attrs) ? ($attrs ? ' ' : '') . $attrs : self::attr($attrs, $options));
if (\in_array($tag, self::SELF_CLOSING_TAG)) {
$html .= '>';
} elseif (($options['indent'] ?? true) && !\in_array($tag, self::NO_INDENT_TAGS)) {
$inner = Formatting::indent($content);
$html .= ">\n" . ($inner === '' ? '' : $inner . "\n") . "</{$tag}>";
} else {
if (!\in_array($tag, self::NO_INDENT_TAGS) && ($options['indent'] ?? true)) {
$inner = Formatting::indent($content);
$html .= ">\n" . ($inner == '' ? '' : $inner . "\n") . "</{$tag}>";
} else {
$html .= ">{$content}</{$tag}>";
}
$html .= ">{$content}</{$tag}>";
}
return $html;
}
@ -102,12 +100,11 @@ abstract class HTML
*/
private static function process_attribute(string $val, string $key, array $options): string
{
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES))
|| str_starts_with($val, 'javascript:')) {
if (\in_array($key, array_merge($options['forbidden_attributes'] ?? [], self::FORBIDDEN_ATTRIBUTES)) || str_starts_with($val, 'javascript:')) {
throw new InvalidArgumentException("HTML::html: Attribute {$key} is not allowed");
}
if (!($options['raw'] ?? false)) {
$val = htmlspecialchars($val, flags: \ENT_QUOTES | \ENT_SUBSTITUTE, double_encode: false);
$val = htmlspecialchars($val, flags: ENT_QUOTES | ENT_SUBSTITUTE, double_encode: false);
}
return "{$key}=\"{$val}\"";
}
@ -122,7 +119,7 @@ abstract class HTML
if ($options['raw'] ?? false) {
return $html;
} else {
return htmlspecialchars($html, flags: \ENT_QUOTES | \ENT_SUBSTITUTE, double_encode: false);
return htmlspecialchars($html, flags: ENT_QUOTES | ENT_SUBSTITUTE, double_encode: false);
}
} else {
$out = '';
@ -134,10 +131,10 @@ abstract class HTML
$is_tag = \is_string($tag) && preg_match('/[A-Za-z][A-Za-z0-9]*/', $tag);
$inner = self::html($contents, $options);
if ($is_tag) {
if (!\in_array($tag, array_merge($options['allowed_tags'] ?? [], self::ALLOWED_TAGS))) {
if (!\in_array($tag, array_merge($options['allowed_tags'] ?? [], self::ALLOWED_TAGS), true)) {
throw new InvalidArgumentException("HTML::html: Tag {$tag} is not allowed");
}
if (!empty($inner) && !\in_array($tag, self::NO_INDENT_TAGS) && ($options['indent'] ?? true)) {
if (!empty($inner) && !\in_array($tag, self::NO_INDENT_TAGS, true) && ($options['indent'] ?? true)) {
$inner = "\n" . Formatting::indent($inner) . "\n";
}
$out .= "<{$tag}{$attrs}>{$inner}</{$tag}>";
@ -154,8 +151,8 @@ abstract class HTML
{
if (method_exists(self::$sanitizer, $name)) {
return self::$sanitizer->{$name}(...$args);
} else {
throw new BadMethodCallException("Method Security::{$name} doesn't exist");
}
throw new BadMethodCallException("Method Security::{$name} doesn't exist");
}
}

90
src/Util/HTML/Heading.php Normal file
View File

@ -0,0 +1,90 @@
<?php
declare(strict_types = 1);
// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social. If not, see <http://www.gnu.org/licenses/>.
// }}}
namespace App\Util\HTML;
use function App\Core\I18n\_m;
use App\Util\HTML;
use Twig\Markup;
class Heading extends HTML
{
private string $heading_type = 'h1';
private string $heading_text;
private array $classes = [];
public function __construct(int $level, array $classes, string $text)
{
$this->setHeadingText($text);
foreach ($classes as $class) {
$this->addClass($class);
}
if ($level >= 1 && $level <= 6) {
$this->heading_type = 'h' . $level;
}
}
public function addClass(string $c): self
{
if (!\in_array($c, $this->classes, true)) {
$this->classes[] = $c;
}
return $this;
}
public function getHtml(): Markup
{
return new Markup($this->__toString(), 'UTF-8');
}
public function __toString()
{
return $this::html([$this->getHeadingType() => ['attrs' => ['class' => !empty($this->getClasses()) ? implode(' ', $this->getClasses()) : ''], _m($this->getHeadingText())]]);
}
public function getHeadingType(): string
{
return $this->heading_type;
}
public function setHeadingType(string $value): static
{
$this->heading_type = $value;
return $this;
}
public function getClasses(): array
{
return $this->classes;
}
public function getHeadingText(): string
{
return $this->heading_text;
}
public function setHeadingText(string $value): static
{
$this->heading_text = $value;
return $this;
}
}

View File

@ -1,11 +1,13 @@
{% extends '/collection/notes.html.twig' %}
{% block title %}{% trans %}%nickname%'s profile{% endtrans %}{% endblock %}
{% block title %}
{% if page_title is defined %}
{{ page_title }}
{% endif %}
{% endblock %}
{% block body %}
{% block profile_view %}
{% include '/cards/blocks/profile.html.twig' %}
{% endblock profile_view %}
{% include 'cards/blocks/profile.html.twig' with { profile_card_type: 'main' } %}
<hr>
{{ parent() }}
{% endblock body %}

View File

@ -73,21 +73,9 @@
<div class="note-text" tabindex="0"
title="{{ 'Main note content' | trans }}">
{% set paragraph_array = note.getRenderedSplit() %}
{% if 'conversation' not in app.request.get('_route') and paragraph_array | length > 3 %}
<p>{{ paragraph_array[0] | raw }}</p>
<details class="note-text-details">
<summary class="note-text-details-summary">
<small>{% trans %}Expand to see all content{% endtrans %}</small>
</summary>
{% for paragraph in paragraph_array | slice(1, paragraph_array | length) %}
<p>{{ paragraph | raw }}</p>
{% endfor %}
</details>
{% else %}
{% for paragraph in paragraph_array %}
<p>{{ paragraph | raw }}</p>
{% endfor %}
{% endif %}
{% for paragraph in paragraph_array %}
<p>{{ paragraph | raw }}</p>
{% endfor %}
</div>
{% endblock note_text %}

View File

@ -1,15 +1,15 @@
{% set actor_fullname = actor.getFullname() %}
{% set actor_nickname = actor.getNickname() %}
{% set actor_avatar = actor.getAvatarUrl() %}
{% set actor_avatar_dimensions = actor.getAvatarDimensions() %}
{% set actor_tags = actor.getSelfTags() %}
{% set actor_has_bio = actor.hasBio() %}
{% set actor_uri = actor.getUri() %}
{% set actor_url = actor.getUrl() %}
{% set actor_is_local = actor.getIsLocal() %}
{% set mention = mention(actor) %}
{% block profile_view %}
{% set actor_fullname = actor.getFullname() %}
{% set actor_nickname = actor.getNickname() %}
{% set actor_avatar = actor.getAvatarUrl() %}
{% set actor_avatar_dimensions = actor.getAvatarDimensions() %}
{% set actor_tags = actor.getSelfTags() %}
{% set actor_has_bio = actor.hasBio() %}
{% set actor_uri = actor.getUri() %}
{% set actor_url = actor.getUrl() %}
{% set actor_is_local = actor.getIsLocal() %}
{% set mention = mention(actor) %}
<section id='profile-{{ actor.id }}' class='profile'
title="{% trans %} %actor_nickname%'s profile information{% endtrans %}">
<header>
@ -19,12 +19,20 @@
title="{% trans %} %actor_nickname%'s avatar{% endtrans %}"
width="{{ actor_avatar_dimensions['width'] }}"
height="{{ actor_avatar_dimensions['height'] }}">
<section>
<div>
<a class="profile-info-url" href="{{ actor_url }}">
<strong class="profile-info-url-nickname"
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
</strong>
{% if profile_card_type is defined and profile_card_type starts with 'main' %}
<h1 class="profile-info-url-nickname"
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
</h1>
{% else %}
<strong class="profile-info-url-nickname"
title="{% trans %} %actor_nickname%'s profile {% endtrans %}">
{% if actor_fullname is not null %}{{ actor_fullname }}{% else %}{{ actor_nickname }}{% endif %}
</strong>
{% endif %}
{% if not actor_is_local %}
<span class="profile-info-url-remote">
<a href="{{ actor_uri }}" class="u-url" title="{% trans %} %actor_nickname%'s permalink {% endtrans %}">{{ mention }}</a>
@ -40,7 +48,7 @@
</li>
{% endfor %}
</ul>
</section>
</div>
</div>
<div class="profile-stats">
<span class="profile-stats-subscriptions"

View File

@ -21,7 +21,6 @@ declare(strict_types = 1);
namespace App\Tests\Util;
use App\Util\HTML;
use Jchook\AssertThrows\AssertThrows;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use TypeError;
@ -32,17 +31,17 @@ class HTMLTest extends WebTestCase
public function testHTML()
{
static::assertSame('', HTML::html(''));
static::assertSame('<a></a>', HTML::html(['a' => '']));
static::assertSame("<div>\n <p></p>\n</div>", HTML::html(['div' => ['p' => '']]));
static::assertSame("<div>\n <div>\n <p></p>\n </div>\n</div>", HTML::html(['div' => ['div' => ['p' => '']]]));
static::assertSame("<div>\n <div>\n <div>\n <p></p>\n </div>\n </div>\n</div>", HTML::html(['div' => ['div' => ['div' => ['p' => '']]]]));
static::assertSame('<a href="test"><p></p></a>', HTML::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
static::assertSame('<a><p>foo</p><br></a>', HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']]));
static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
static::assertSame('<div><a><p>foo</p><br></a></div>', HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
static::assertThrows(TypeError::class, fn () => HTML::html(1));
static::assertSame('<a href="test">foo</a>', HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
static::assertSame('<br>', HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
static::assertSame('', HTML\HTML::html(''));
static::assertSame('<a></a>', HTML\HTML::html(['a' => '']));
static::assertSame("<div>\n <p></p>\n</div>", HTML\HTML::html(['div' => ['p' => '']]));
static::assertSame("<div>\n <div>\n <p></p>\n </div>\n</div>", HTML\HTML::html(['div' => ['div' => ['p' => '']]]));
static::assertSame("<div>\n <div>\n <div>\n <p></p>\n </div>\n </div>\n</div>", HTML\HTML::html(['div' => ['div' => ['div' => ['p' => '']]]]));
static::assertSame('<a href="test"><p></p></a>', HTML\HTML::html(['a' => ['attrs' => ['href' => 'test'], 'p' => '']]));
static::assertSame('<a><p>foo</p><br></a>', HTML\HTML::html(['a' => ['p' => 'foo', 'br' => 'empty']]));
static::assertSame("<div>\n <a><p>foo</p><br></a>\n</div>", HTML\HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]]));
static::assertSame('<div><a><p>foo</p><br></a></div>', HTML\HTML::html(['div' => ['a' => ['p' => 'foo', 'br' => 'empty']]], options: ['indent' => false]));
static::assertThrows(TypeError::class, fn () => HTML\HTML::html(1));
static::assertSame('<a href="test">foo</a>', HTML\HTML::tag('a', ['href' => 'test'], content: 'foo', options: ['empty' => false]));
static::assertSame('<br>', HTML\HTML::tag('br', attrs: null, content: null, options: ['empty' => true]));
}
}