Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ A wiki for phpBB with version history, side-by-side compare, and a moderation qu
- A wiki at `/wiki`, with an overview page listing its articles.
- Start an article from the overview page's **New article title** box, or by linking to `/wiki/<article-name>` and following the link.
- Link to another article by writing `[[Article name]]`, in articles and in forum posts; links to articles that don't exist yet are shown in red and open the editor. Each article lists the articles that link to it under **What links here**. See [Linking between articles](#linking-between-articles).
- Every edit is saved as a new version; compare any two versions side by side.
- Every edit is saved as a new version; compare any two versions. The compare page opens in a **Readable** view: the newer version as readers see it, with added text in green and underlined, removed text in red and struck through, and the changed words marked inside a line, list item or table row that changed. **Source** shows the BBCode diff. A `Template:` page is compared as written.
- **Restore** in the version history brings back an older version by saving its text, title, description, sources and redirect as a new version, so no version is lost. Members who may edit can restore; the restored version goes live straight away for members who may set versions active, and otherwise waits for approval like any edit.
- Edits can wait for approval: moderators get a **Pending articles** list with Approve and Disapprove. Approving a version older than the live one is blocked, so a stale draft can't overwrite newer content.
- Moderators can take an article offline or delete it, and articles can be made sticky or set to redirect.
Expand Down
6 changes: 0 additions & 6 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,3 @@ header check, and holds no templates yet. Still to build: the templates themselv
button. Admins upload its zip in the ACP or copy the files into a folder in `store/`. Plan,
licence rules and decisions:
[`docs/TODO/todo-wiki-templates-repo.md`](TODO/todo-wiki-templates-repo.md).

## Readable version compare

The compare page shows raw BBCode with changes marked in red and green.
Comparing the rendered text would be easier for editors who don't read
BBCode comfortably.
4 changes: 4 additions & 0 deletions language/en/common.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
'VERSIONS_OF_ARTICLE' => 'Version history',
'VERSION_COMPARE_HEADLINE' => 'Changes from version <a href="%3$s">%1$d</a> to <a href="%4$s">%2$d</a>',
'COMPARE' => 'Compare',
'NO_VERSIONS_SELECTED' => 'Choose two versions of this article to compare.',
'COMPARE_VIEW_READABLE' => 'Readable',
'COMPARE_VIEW_SOURCE' => 'Source',
'COMPARE_READABLE_EXPLAIN' => 'The newer version as readers see it. Added text is green and underlined, removed text is red and struck through, and a part whose words changed shows which ones. A part outlined in blue changed only its formatting; the source view shows how.',
'COMPARE_EXPLAIN' => 'All versions of the article are listed here. Choose two versions to compare them.',
'VERSION' => 'Version',
'TITLE' => 'Title',
Expand Down
10 changes: 9 additions & 1 deletion styles/prosilver/template/article_compare.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,17 @@ <h3>{L_ARTICLE_DESCRIPTION}</h3>
</div>
</div>
<!-- ENDIF -->
<!-- IF S_READABLE_POSSIBLE -->
<p class="wiki-compare-views">
<!-- IF S_READABLE --><strong>{L_COMPARE_VIEW_READABLE}</strong><!-- ELSE --><a href="{U_VIEW_READABLE}">{L_COMPARE_VIEW_READABLE}</a><!-- ENDIF -->
&bull;
<!-- IF S_READABLE --><a href="{U_VIEW_SOURCE}">{L_COMPARE_VIEW_SOURCE}</a><!-- ELSE --><strong>{L_COMPARE_VIEW_SOURCE}</strong><!-- ENDIF -->
<!-- IF S_READABLE and DIFF --><br><span class="wiki-compare-explain">{L_COMPARE_READABLE_EXPLAIN}</span><!-- ENDIF -->
</p>
<!-- ENDIF -->
<div class="panel bg1">
<div class="inner wiki">
<div class="content diff">
<div class="content<!-- IF S_READABLE --> wiki-readable-diff<!-- ELSE --> diff<!-- ENDIF -->">
<!-- IF DIFF -->
{DIFF}
<!-- ELSE -->
Expand Down
22 changes: 22 additions & 0 deletions styles/prosilver/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -174,3 +174,25 @@ form.wiki-start-from p {
padding: 0.5em 0;
overflow-x: auto;
}

/* The readable compare: added, removed and reformatted parts of the newer version */
.wiki-readable-diff ins.wiki-diff-ins,
.wiki-readable-diff .wiki-diff-ins {
background-color: #d4f8d4;
text-decoration: underline;
}
.wiki-readable-diff del.wiki-diff-del,
.wiki-readable-diff .wiki-diff-del {
background-color: #ffd7d5;
text-decoration: line-through;
}
.wiki-readable-diff .wiki-diff-changed {
outline: 1px dashed #4c5d77;
outline-offset: 2px;
}
.wiki-readable-diff .wiki-diff-line {
min-height: 1.4em;
}
.wiki-compare-explain {
font-size: 0.9em;
}
82 changes: 82 additions & 0 deletions tests/unit/readable_diff_test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php
/**
*
* Wiki extension for the phpBB Forum Software package
*
* @copyright (c) 2026, phpBB Modders, https://www.phpbbmodders.com/
* @license GNU General Public License, version 2 (GPL-2.0)
*
*/

namespace phpbbmodders\wiki\tests\unit;

use phpbbmodders\wiki\wiki\readable_diff;

/**
* The readable compare: two rendered versions compared block by block, and word by word
* inside a block that changed.
*/
class readable_diff_test extends \PHPUnit\Framework\TestCase
{
public static function setUpBeforeClass(): void
{
global $phpbb_root_path, $phpEx;

require_once $phpbb_root_path . 'includes/diff/diff.' . $phpEx;
require_once $phpbb_root_path . 'includes/diff/engine.' . $phpEx;
}

public function test_a_changed_line_shows_its_changed_words()
{
$html = readable_diff::render('Hello world<br>Second line', 'Hello wide world<br>Second line');

$this->assertSame('<div class="wiki-diff-line">Hello <ins class="wiki-diff-ins">wide</ins> world</div><div class="wiki-diff-line">Second line</div>', $html);
}

public function test_an_added_heading_is_marked_whole_and_the_rest_is_unchanged()
{
$html = readable_diff::render('Intro<br>End', 'Intro<h2>New</h2>End');

$this->assertSame('<div class="wiki-diff-line">Intro</div><h2 class="wiki-diff-ins">New</h2><div class="wiki-diff-line">End</div>', $html);
}

public function test_a_list_is_compared_item_by_item()
{
$html = readable_diff::render('<ul><li>one</li><li>two</li></ul>', '<ul><li>one</li><li>two!</li><li>three</li></ul>');

$this->assertSame('<ul><li>one</li><li><del class="wiki-diff-del">two</del> <ins class="wiki-diff-ins">two!</ins></li><li class="wiki-diff-ins">three</li></ul>', $html);
}

public function test_a_table_row_is_compared_cell_by_cell()
{
$html = readable_diff::render(
'<table><tr><th>A</th><td>1</td></tr><tr><th>B</th><td>2</td></tr></table>',
'<table><tr><th>A</th><td>1</td></tr><tr><th>B</th><td>3</td></tr></table>'
);

$this->assertSame('<table><tr><th>A</th><td>1</td></tr><tr><th>B</th><td><del class="wiki-diff-del">2</del> <ins class="wiki-diff-ins">3</ins></td></tr></table>', $html);
}

public function test_a_change_of_formatting_alone_is_marked_as_changed()
{
$this->assertSame('<div class="wiki-diff-line wiki-diff-changed"><em>plain</em> text</div>', readable_diff::render('plain text', '<em>plain</em> text'));
}

public function test_a_line_with_an_image_is_shown_whole_not_as_words()
{
$html = readable_diff::render('See <a href="x">Getting Started</a>.<br>End', '<img src="logo.png" alt="logo"><br>End');

$this->assertSame('<div class="wiki-diff-line wiki-diff-del">See <a href="x">Getting Started</a>.</div><div class="wiki-diff-line wiki-diff-ins"><img src="logo.png" alt="logo"></div><div class="wiki-diff-line">End</div>', $html);
}

public function test_text_is_escaped_and_unicode_kept()
{
$this->assertSame('<div class="wiki-diff-line">Grüße &lt;aus&gt; <del class="wiki-diff-del">Köln</del> <ins class="wiki-diff-ins">Berlin</ins></div>', readable_diff::render('Grüße &lt;aus&gt; Köln', 'Grüße &lt;aus&gt; Berlin'));
}

public function test_removing_everything_and_comparing_nothing()
{
$this->assertSame('<div class="wiki-diff-line wiki-diff-del">gone</div>', readable_diff::render('gone', ''));
$this->assertSame('', readable_diff::render('', ''));
}
}
62 changes: 57 additions & 5 deletions wiki/compare.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ class compare
/** @var string php_ext */
protected $php_ext;

/**
* A version's text rendered as the article page shows it.
*
* @param string $article The article's name, as in its address
* @param array $row The version, with article_text, bbcode_uid and bbcode_bitfield
* @return string HTML
*/
protected function rendered($article, array $row)
{
// [[File:name]] and the PAGENAME words refer to this article; categories found
// while rendering are not this page's to list
\phpbbmodders\wiki\wiki\files::set_context($article);
\phpbbmodders\wiki\wiki\categories::reset();
try
{
return generate_text_for_display($row['article_text'], $row['bbcode_uid'], $row['bbcode_bitfield'], OPTION_FLAG_BBCODE | OPTION_FLAG_SMILIES | OPTION_FLAG_LINKS);
}
finally
{
\phpbbmodders\wiki\wiki\files::set_context(null);
\phpbbmodders\wiki\wiki\categories::reset();
}
}

/**
* Constructor
*
Expand Down Expand Up @@ -78,6 +102,12 @@ public function __construct(\phpbb\auth\auth $auth, \phpbb\config\config $config

public function compare_versions($article, $from, $to)
{
// Comparing shows the text of versions, as the version history does
if (!$this->auth->acl_get('u_wiki_versions'))
{
trigger_error('NOT_AUTHORISED');
}

if ($from == 0 || $to == 0)
{
trigger_error('NO_VERSIONS_SELECTED');
Expand All @@ -87,17 +117,25 @@ public function compare_versions($article, $from, $to)
require($this->phpbb_root_path . 'includes/diff/engine.' . $this->php_ext);
require($this->phpbb_root_path . 'includes/diff/renderer.' . $this->php_ext);

$sql = 'SELECT article_text, bbcode_uid, bbcode_bitfield, article_sources, article_description, article_approved
$sql = 'SELECT article_url, article_text, bbcode_uid, bbcode_bitfield, article_sources, article_description, article_approved
FROM ' . $this->article_table . '
WHERE article_id = ' . (int) $from;
$result = $this->db->sql_query($sql);
$from_row = $this->db->sql_fetchrow($result);
$this->db->sql_freeresult($result);

$sql = 'SELECT article_text, bbcode_uid, bbcode_bitfield, article_sources, article_description, article_approved
$sql = 'SELECT article_url, article_text, bbcode_uid, bbcode_bitfield, article_sources, article_description, article_approved
FROM ' . $this->article_table . '
WHERE article_id = ' . (int) $to;
$result = $this->db->sql_query($sql);
$to_row = $this->db->sql_fetchrow($result);
$this->db->sql_freeresult($result);

// Both versions must exist and belong to the article in the address
if (!$from_row || !$to_row || $from_row['article_url'] !== $article || $to_row['article_url'] !== $article)
{
trigger_error('NO_VERSIONS_SELECTED');
}

$from_article = generate_text_for_edit($from_row['article_text'], $from_row['bbcode_uid'], OPTION_FLAG_BBCODE | OPTION_FLAG_SMILIES | OPTION_FLAG_LINKS);
$to_article = generate_text_for_edit($to_row['article_text'], $to_row['bbcode_uid'], OPTION_FLAG_BBCODE | OPTION_FLAG_SMILIES | OPTION_FLAG_LINKS);
Expand All @@ -111,18 +149,32 @@ public function compare_versions($article, $from, $to)
$sources_diff_empty = $sources_diff->is_empty();

$description_diff = new \diff($from_row['article_description'], $to_row['article_description']);
$descriptiondiff_empty = $sources_diff->is_empty();
$description_diff_empty = $description_diff->is_empty();

$renderer = new \diff_renderer_inline();

// The readable view compares the versions as readers see them. A template page's
// text does not make sense rendered on its own, so it is compared as written.
$s_readable_possible = (strpos($article, \phpbbmodders\wiki\wiki\templates::PREFIX) !== 0);
$s_readable = $s_readable_possible && ($this->request->variable('view', 'readable') !== 'source');
$readable = '';
if ($s_readable && !$article_diff_empty)
{
$readable = \phpbbmodders\wiki\wiki\readable_diff::render($this->rendered($article, $from_row), $this->rendered($article, $to_row));
}

$s_set_active = $this->auth->acl_get('u_wiki_set_active');
$s_delete = $this->auth->acl_get('m_wiki_delete');

$this->template->assign_vars(array(
'HEADLINE' => sprintf($this->user->lang('VERSION_COMPARE_HEADLINE'), $from, $to, $u_from, $u_to),
'DIFF' => ($article_diff_empty) ? '' : $renderer->get_diff_content($article_diff),
'DIFF' => ($article_diff_empty) ? '' : ($s_readable ? $readable : $renderer->get_diff_content($article_diff)),
'DIFF_SOURCE' => ($sources_diff_empty) ? '' : $renderer->get_diff_content($sources_diff),
'DIFF_DESCRIPTION' => ($descriptiondiff_empty) ? '' : $renderer->get_diff_content($description_diff),
'DIFF_DESCRIPTION' => ($description_diff_empty) ? '' : $renderer->get_diff_content($description_diff),
'S_READABLE' => $s_readable,
'S_READABLE_POSSIBLE' => $s_readable_possible,
'U_VIEW_READABLE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $article, 'action' => 'compare', 'from' => (int) $from, 'to' => (int) $to)),
'U_VIEW_SOURCE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $article, 'action' => 'compare', 'from' => (int) $from, 'to' => (int) $to, 'view' => 'source')),

'APPROVE_VERSION_FROM' => sprintf($this->user->lang('APPROVE_VERSION'), $from),
'REJECT_VERSION_FROM' => sprintf($this->user->lang('REJECT_VERSION'), $from),
Expand Down
Loading
Loading