Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/text formatter #7

Merged
merged 13 commits into from
Jan 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ It adds similar features to Flarum forums.
- [x] are changed to show the title of target discussion when the text is the same as the href
- [x] have `(comment)` indication if it points to a specific comment
- [x] load the target discussion faster as they use the _FrontEnd Router_
- [ ] are shown in the preview the way they will be seen once posted
- [ ] are automatically created from `#<id>` text (e.g. `#42`)
- [x] are shown in the preview the way they will be seen once posted
- [x] are automatically created from `#<id>` text (e.g. `#42`)
- [ ] are auto-completed with a selection box when `#` is entered
- [x] show the ID of the target (option)
- [ ] show the primary tags of the target (option)
Expand Down
10 changes: 9 additions & 1 deletion extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,25 @@

namespace Club1\CrossReferences;

use Club1\CrossReferences\Formatter\CrossReferencesConfigurator;
use Club1\CrossReferences\Formatter\CrossReferencesRenderer;
use Club1\CrossReferences\Post\DiscussionReferencedPost;
use Flarum\Api\Controller\ShowDiscussionController;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
use Flarum\Extend;
use Flarum\Post;
use Flarum\Settings\Event\Saved as SettingsSaved;

return [
(new Extend\Formatter)
->configure(CrossReferencesConfigurator::class)
->render(CrossReferencesRenderer::class),

(new Extend\Event())
->listen(Post\Event\Posted::class, Listener\PostEventListener::class)
->listen(Post\Event\Revised::class, Listener\PostEventListener::class),
->listen(Post\Event\Revised::class, Listener\PostEventListener::class)
->listen(SettingsSaved::class, Listener\SettingsSavedListener::class),

(new Extend\Post())
->type(DiscussionReferencedPost::class),
Expand Down
648 changes: 624 additions & 24 deletions js/dist/forum.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/dist/forum.js.map

Large diffs are not rendered by default.

94 changes: 94 additions & 0 deletions js/src/forum/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* This file is part of club-1/flarum-ext-cross-references.
*
* Copyright (c) 2023 Nicolas Peugnet <nicolas@club1.fr>.
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import Model from 'flarum/common/Model';
import Discussion from 'flarum/common/models/Discussion';
import app from 'flarum/forum/app';

export interface Cache<T> {
get(key: string): T | undefined
set(key: string, value: T): this
}

type FIFOCacheEntry = {key: string, next?: FIFOCacheEntry};

export class FIFOCache<T> implements Cache<T> {
protected head?: FIFOCacheEntry;
protected tail?: FIFOCacheEntry;
protected data: Map<string, T> = new Map();
protected capacity: number;

constructor(capacity: number) {
this.capacity = capacity;
}

get(key: string): T | undefined {
return this.data.get(key);
}

set(key: string, value: T): this {
if (this.data.size == this.capacity) {
const evicted = this.head!;
this.data.delete(evicted.key);
this.head = evicted.next;
}
const entry: FIFOCacheEntry = {key}
if (!this.head) {
this.head = entry;
}
if (!this.tail) {
this.tail = entry;
} else {
this.tail.next = entry;
this.tail = entry;
}
this.data.set(key, value);
return this;
}
}

function key(name: string, id: string): string {
return name + id;
}

const ModelMap: {[name: string]: string} = {}
ModelMap[Discussion.name] = 'discussions';


/** Empty function used to disable callbacks */
function noop() {};

export abstract class ResponseCache {
private static responseErrors: Cache<boolean> = new FIFOCache(128);

public static async find<T extends Model>(m: new () => T, id: string, options = {}): Promise<T | null> {
if (this.responseErrors.get(key(m.name, id)) == true) {
return null;
}
const res = await app.store.find<T>(ModelMap[m.name] , id, options, {errorHandler: noop})
.catch(noop);
if (res) {
return res;
}
this.responseErrors.set(key(m.name, id), true);
return null;
}
}
32 changes: 32 additions & 0 deletions js/src/forum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import app from 'flarum/forum/app';
import CommentPost from 'flarum/forum/components/CommentPost';
import DiscussionHero from 'flarum/forum/components/DiscussionHero';
import DiscussionListItem from 'flarum/forum/components/DiscussionListItem';
import { ResponseCache } from './cache';
import DiscussionId from './components/DiscussionId';
import DiscussionLink from './components/DiscussionLink';
import DiscussionReferencedPost from './components/DiscussionReferencedPost';
Expand Down Expand Up @@ -104,3 +105,34 @@ function addDiscussionListId() {
items.add('id', m(DiscussionId, {discussionId}), 90);
});
}

/**
* Extremely dirty hack to trigger a refresh of the composer preview
* by inserting a ZeroWidthSpace at the beginning of the message and
* then removing it 50ms later after the render pass.
*
* TODO: Replace this workaround once this issue is fixed:
* <https://github.com/flarum/framework/issues/3720>
*/
function refreshComposerPreview() {
const content = app.composer.fields?.content;
if (content) {
content('​' + content());
setTimeout(() => content(content().slice(1)), 50);
}
}

export function filterCrossReferences(tag) {
const id = tag.getAttribute('id');
const res = app.store.getById('discussions', id);
if (res) {
const discussion = res as Discussion;
tag.setAttribute('title', discussion.title());
} else {
ResponseCache.find(Discussion, id).then((d) => {
if (d) refreshComposerPreview();
});
return false;
}
tag.setAttribute('comment', app.translator.trans('club-1-cross-references.forum.comment'));
}
1 change: 1 addition & 0 deletions locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ club-1-cross-references:
forum:
post_stream:
discussion_referenced_text: '{username} referenced this discussion from {source}'
unknown_discussion: '<UNKNOWN DISCUSSION>'
comment: 'comment'
1 change: 1 addition & 0 deletions locale/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ club-1-cross-references:
forum:
post_stream:
discussion_referenced_text: '{username} a référencé cette discussion depuis {source}'
unknown_discussion: '<DISCUSSION INCONNUE>'
comment: 'commentaire'
144 changes: 144 additions & 0 deletions src/Formatter/CrossReferencesConfigurator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php

/*
* This file is part of club-1/flarum-ext-cross-references.
*
* Copyright (c) 2022 Nicolas Peugnet <nicolas@club1.fr>.
*
* This program 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.
*
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace Club1\CrossReferences\Formatter;

use Flarum\Discussion\Discussion;
use Flarum\Http\UrlGenerator;
use Flarum\Settings\SettingsRepositoryInterface;
use s9e\TextFormatter\Configurator;
use s9e\TextFormatter\Parser\Tag;

class CrossReferencesConfigurator
{
const PARAM_DISCUSSION_URL = 'DISCUSSION_URL';
const PARAM_SHOW_DISCUSSION_ID = 'SHOW_DISCUSSION_ID';

/** @var SettingsRepositoryInterface */
protected $settings;

/** @var UrlGenerator */
protected $urlGen;

/** @var string */
protected $discussionPath;

/** @var string */
protected $discussionPathEsc;

public function __construct(SettingsRepositoryInterface $settings, UrlGenerator $urlGenerator)
{
$this->settings = $settings;
$this->urlGen = $urlGenerator;
$this->discussionPath = $this->urlGen->to('forum')->route('discussion', ['id' => '']);
$this->discussionPathEsc = addcslashes($this->discussionPath, '/');
}

public function __invoke(Configurator $config)
{
$config->rendering->parameters[self::PARAM_DISCUSSION_URL] = $this->discussionPath;
$config->rendering->parameters[self::PARAM_SHOW_DISCUSSION_ID] = $this->settings->get('club-1-cross-references.show_discussion_id');
$this->configureCrossReferenceShort($config);
$this->configureCrossReferenceURL($config);
$this->configureCrossReferenceURLComment($config);
error_log('configured cross references');
}

public static function filterCrossReferences(Tag $tag)
{
/** @var Discussion|null */
$d = Discussion::find($tag->getAttribute('id'));
if (is_null($d)) {
$tag->invalidate();
return false;
}
// Set placeholder values for TextFormatter to be happy.
// The real values is set during render.
$tag->setAttribute('title', $d->title);
$tag->setAttribute('comment', '');
return true;
}

protected function configureCrossReferenceShort(Configurator $config)
{
$tagName = 'CROSSREFERENCESHORT';

$tag = $config->tags->add($tagName);
$tag->attributes->add('id')->filterChain->append('#uint');
$tag->attributes->add('title');
$tag->template = '
<a href="{$DISCUSSION_URL}{@id}" class="DiscussionLink">
<xsl:value-of select="@title"/> <xsl:if test="$SHOW_DISCUSSION_ID = 1">
<span class="DiscussionId">#<xsl:value-of select="@id"/></span>
</xsl:if>
</a>';

$tag->filterChain
->prepend([static::class, 'filterCrossReferences'])
->setJS('flarum.extensions["club-1-cross-references"].filterCrossReferences');
$config->Preg->match('/\B#(?<id>[0-9]+)\b/i', $tagName);
}

protected function configureCrossReferenceURL(Configurator $config)
{
$tagName = 'CROSSREFERENCEURL';

$tag = $config->tags->add($tagName);
$tag->attributes->add('id')->filterChain->append('#uint');
$tag->attributes->add('url')->filterChain->append('#url');
$tag->attributes->add('title');
$tag->template = '
<a href="{@url}" class="DiscussionLink">
<xsl:value-of select="@title"/> <xsl:if test="$SHOW_DISCUSSION_ID = 1">
<span class="DiscussionId">#<xsl:value-of select="@id"/></span>
</xsl:if>
</a>';

$tag->filterChain
->prepend([static::class, 'filterCrossReferences'])
->setJS('flarum.extensions["club-1-cross-references"].filterCrossReferences');
$config->Preg->match("/(?:^|\b)(?<url>$this->discussionPathEsc(?<id>[0-9]+)[^\s\/]*\/?)(?=\s|$)/i", $tagName);
}

protected function configureCrossReferenceURLComment(Configurator $config)
{
$tagName = 'CROSSREFERENCEURLCOMMENT';

$tag = $config->tags->add($tagName);
$tag->attributes->add('id')->filterChain->append('#uint');
$tag->attributes->add('url')->filterChain->append('#url');
$tag->attributes->add('title');
$tag->attributes->add('comment');
$tag->template = '
<a href="{@url}" class="DiscussionLink">
<xsl:value-of select="@title"/> <xsl:if test="$SHOW_DISCUSSION_ID = 1">
<span class="DiscussionId">#<xsl:value-of select="@id"/></span>
</xsl:if> <span class="DiscussionComment">(<xsl:value-of select="@comment"/>)</span>
</a>';

$tag->filterChain
->prepend([static::class, 'filterCrossReferences'])
->setJS('flarum.extensions["club-1-cross-references"].filterCrossReferences');
$config->Preg->match("/(?:^|\b)(?<url>$this->discussionPathEsc(?<id>[0-9]+)[^\s\/]*\/[0-9]+)(?=\s|$)/i", $tagName);
}
}
Loading