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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ A wiki for phpBB with version history, side-by-side compare, and a moderation qu
- Files can be uploaded to an article by members who hold the **Can upload files to articles** permission. They are checked against the board's allowed file types and size limit, stored outside public view under `store/phpbbmodders_wiki/`, and read only through the wiki, which checks that the reader may see the article. In the text, `[[File:Diagram.png]]` shows an uploaded image and `[[File:notes.zip|the notes]]` links to any other file. The member who uploaded a file, or a moderator who may delete versions, can remove it, and deleting an article deletes its files.
- Notifications when an edit is waiting for approval.
- **Watch article** on every article page, for signed-in members: they are notified when a new version of a watched article goes live (a moderator approved an edit, or a trusted editor's edit went live directly), never about their own edit. It is a notification type in phpBB's notification settings, so members choose the bell, email or both. With the ACP setting **Watch edited articles** (on by default, under **Extensions > Wiki > Settings**), saving an edit also starts watching the article. Watches are removed with the article and with the member.
- Wikipedia-style headings: `== Heading ==` to `====== Heading ======` in an article become section headings with anchors, `[[Article#Section]]` and `[[#Section]]` link to them, and a table of contents shows after the intro when an article has four or more headings (`__TOC__` places it, `__FORCETOC__` forces it, `__NOTOC__` hides it). A first line `#REDIRECT [[Article]]` sets the redirect for members who may set redirects. This works in wiki articles only; in a forum post `== text ==` stays plain text.
- The Wiki link in the board header shows the Wikipedia icon by default. Under **Extensions > Wiki > Settings** in the ACP the icon can be changed to any [Font Awesome](https://fontawesome.com/v4.7.0/icons/) icon name, or left blank for no icon.
- A **Start discussion** button on each article creates its discussion topic in a forum chosen under **Extensions > Wiki > Settings** in the ACP, and links the topic to the article. It shows to members who hold the "Can change the discussion topic for an article" permission and may post in that forum, and stays hidden until a forum is chosen.
- Separate permissions for viewing, editing, version history, deleting and moderating.
Expand Down
16 changes: 2 additions & 14 deletions docs/TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@ Still needs deciding before implementation:
can build on, so it shouldn't get a free pass on the same checks normal
article content goes through.

## Table of contents

The `article_toc` column has existed in the schema since early on, but
`wiki/edit.php` always inserts an empty string into it and nothing anywhere
reads it — it's scaffolded and abandoned. Finish it: auto-generate a TOC
from the article's heading structure and render it at the top of the
article (likely only worth showing past some minimum heading count, so
short articles don't get a token one-entry TOC).

Needs headings first, since phpBB has no heading tag; see
[Wiki markup](TODO/todo-wiki-markup.md).

## Move/rename an article

The URL slug is fixed forever once an article is created — only the title
Expand Down Expand Up @@ -92,8 +80,8 @@ A per-board style choice, set from an ACP page for this extension (admin
picks "Forum-style" or "Wiki-style" for how the extension's own templates
render) rather than ripping out the current look — some boards will prefer
wiki content to visually match the rest of the forum, others will want it
to look like an actual wiki. Scope depends heavily on the table of contents
item above landing first, since a real wiki layout wants one.
to look like an actual wiki. A real wiki layout wants the table of contents, which now exists, as a sidebar
or floating box, so the contents box would move too.

## Per-article edit protection

Expand Down
12 changes: 11 additions & 1 deletion docs/TODO/todo-wiki-markup.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ phpBB already has BBCode, so every extra syntax is a second way to write
something. This plan adds only what BBCode can't do, and only inside wiki
articles.

## Built so far

Headings (`==` to `======`), `[[Article#Section]]` and `[[#Section]]` links, the
table of contents (`__TOC__`, `__FORCETOC__`, `__NOTOC__`, automatic from four
headings) and the `#REDIRECT [[Article]]` first line are done. They are read only
while a wiki article is saved or previewed, by switching the `WIKIHEADING` and
`WIKIMAGIC` tags on in `core.text_formatter_s9e_parse_before` (see `wiki/markup.php`),
and a migration reparses saved articles. What is left below is tables, the text
styles and references (not wanted yet), `[[Category:Name]]`, and templates.

## Scope: wiki articles only

`[[Article name]]` links are safe everywhere and stay enabled in forum
Expand Down Expand Up @@ -45,7 +55,7 @@ Still to confirm on the test board before building on it.

| Syntax | What it does | Notes |
|---|---|---|
| `== Section ==` | Headings | phpBB has no heading tag. Also needed for the table of contents. |
| `== Section ==` | Headings | Built; see "Built so far". |
| `[[Article#Section]]` | Link to a heading in an article | `#` isn't allowed inside `[[ ]]` today, so this shows as plain text. |
| HTML tables | Tables | See below. |

Expand Down
76 changes: 69 additions & 7 deletions event/wikilink_listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
class wikilink_listener implements EventSubscriberInterface
{
/** Characters an article name in [[ ]] may not contain. */
const NAME_PATTERN = '[^\[\]|<>"#?%\/\\\\\n]{1,255}';
const NAME_CLASS = '[^\[\]|<>"#?%\/\\\\\n]';

const NAME_PATTERN = self::NAME_CLASS . '{1,255}';

/** @var \phpbb\controller\helper */
protected $helper;
Expand Down Expand Up @@ -58,6 +60,7 @@ public static function getSubscribedEvents()
{
return array(
'core.text_formatter_s9e_configure_after' => 'configure_wikilink',
'core.text_formatter_s9e_parse_before' => 'toggle_wiki_markup',
'core.text_formatter_s9e_render_after' => 'link_wikilinks',
);
}
Expand Down Expand Up @@ -88,15 +91,42 @@ public function configure_wikilink($event)
'WIKIFILE'
);

// == Heading == to ====== Heading ======, one per line. The parser reads these only
// while a wiki article is saved (see toggle_wiki_markup()); the anchors and the
// table of contents are added when the article is shown (see markup::finish()).
$heading = $configurator->tags->add('WIKIHEADING');
$heading->attributes->add('marks');
$heading->attributes->add('title');
$heading->template = '<xsl:choose>'
. '<xsl:when test="string-length(@marks) = 2"><h2 class="wikiheading"><xsl:value-of select="@title"/></h2></xsl:when>'
. '<xsl:when test="string-length(@marks) = 3"><h3 class="wikiheading"><xsl:value-of select="@title"/></h3></xsl:when>'
. '<xsl:when test="string-length(@marks) = 4"><h4 class="wikiheading"><xsl:value-of select="@title"/></h4></xsl:when>'
. '<xsl:when test="string-length(@marks) = 5"><h5 class="wikiheading"><xsl:value-of select="@title"/></h5></xsl:when>'
. '<xsl:otherwise><h6 class="wikiheading"><xsl:value-of select="@title"/></h6></xsl:otherwise>'
. '</xsl:choose>';

$configurator->Preg->match(
'/^(?<marks>={2,6})[ \t]*(?<title>[^=\n][^\n]*?)[ \t]*\k<marks>[ \t]*$/m',
'WIKIHEADING'
);

// __TOC__, __NOTOC__ and __FORCETOC__ steer the table of contents
$magic = $configurator->tags->add('WIKIMAGIC');
$magic->attributes->add('word');
$magic->template = '<span class="wikimagic" data-wiki-magic="{@word}"></span>';

$configurator->Preg->match('/__(?<word>FORCETOC|NOTOC|TOC)__/', 'WIKIMAGIC');

$tag = $configurator->tags->add('WIKILINK');
$tag->attributes->add('name');
$tag->attributes->add('name')->required = false;
$tag->attributes->add('section')->required = false;
$tag->attributes->add('text')->required = false;
$tag->template = '<a class="wikilink" data-wiki-article="{@name}">'
$tag->template = '<a class="wikilink" data-wiki-article="{@name}" data-wiki-section="{@section}">'
. '<xsl:choose><xsl:when test="@text"><xsl:value-of select="@text"/></xsl:when>'
. '<xsl:otherwise><xsl:value-of select="@name"/></xsl:otherwise></xsl:choose></a>';

$configurator->Preg->match(
'/\[\[\s*(?!File:)(?<name>' . self::NAME_PATTERN . '?)\s*(?:\|\s*(?<text>[^\[\]\n]{1,255}?)\s*)?\]\]/',
'/\[\[\s*(?!File:)(?=[^\]|])(?<name>' . self::NAME_CLASS . '{0,255}?)\s*(?:#\s*(?<section>[^\[\]|<>"\n]{1,255}?)\s*)?(?:\|\s*(?<text>[^\[\]\n]{1,255}?)\s*)?\]\]/',
'WIKILINK'
);
}
Expand All @@ -108,10 +138,33 @@ public function configure_wikilink($event)
* @param \phpbb\event\data $event The event object
* @return void
*/
public function toggle_wiki_markup($event)
{
$parser = $event['parser']->get_parser();

foreach (array('WIKIHEADING', 'WIKIMAGIC') as $tag)
{
if (\phpbbmodders\wiki\wiki\markup::is_active())
{
$parser->enableTag($tag);
}
else
{
$parser->disableTag($tag);
}
}
}

public function link_wikilinks($event)
{
$html = $event['html'];

if (strpos($html, 'class="wikiheading"') !== false || strpos($html, 'class="wikimagic"') !== false)
{
$html = \phpbbmodders\wiki\wiki\markup::finish($html, $this->language->lang('WIKI_TOC'));
$event['html'] = $html;
}

if (strpos($html, 'class="wikifile"') !== false)
{
$html = $this->embed_files($html);
Expand All @@ -123,7 +176,7 @@ public function link_wikilinks($event)
return;
}

$pattern = '/<a class="wikilink" data-wiki-article="([^"]*)">/';
$pattern = '/<a class="wikilink" data-wiki-article="([^"]*)" data-wiki-section="([^"]*)">/';
if (!preg_match_all($pattern, $html, $matches))
{
return;
Expand All @@ -132,11 +185,20 @@ public function link_wikilinks($event)
$names = array_map(function ($name) {
return htmlspecialchars_decode($name, ENT_QUOTES);
}, $matches[1]);
$existing = $this->links->existing_articles($names);
$existing = $this->links->existing_articles(array_filter($names, 'strlen'));

$event['html'] = preg_replace_callback($pattern, function ($match) use ($existing) {
$name = htmlspecialchars_decode($match[1], ENT_QUOTES);
$url = $this->helper->route('phpbbmodders_wiki_article', array('article' => $name));
$section = htmlspecialchars_decode($match[2], ENT_QUOTES);
$fragment = ($section !== '') ? '#' . htmlspecialchars(\phpbbmodders\wiki\wiki\markup::anchor($section), ENT_QUOTES, 'UTF-8') : '';

// [[#Section]] points into the page it is on
if ($name === '')
{
return '<a class="wikilink" href="' . $fragment . '" data-wiki-article="">';
}

$url = $this->helper->route('phpbbmodders_wiki_article', array('article' => $name)) . $fragment;
if (isset($existing[$name]))
{
return '<a class="wikilink" href="' . $url . '" data-wiki-article="' . $match[1] . '">';
Expand Down
1 change: 1 addition & 0 deletions language/de/link.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@
'WIKI_NEW_ARTICLE_UPDATE' => 'Ein neue Version zum Artikel <strong>%1$s</strong> in der KB wartet auf Freischaltung.',
'WIKI_WATCHED_ARTICLE' => 'Ein Wiki-Artikel, den du beobachtest, hat eine neue Version',
'WIKI_WATCHED_ARTICLE_LIVE' => 'Eine neue Version des Wiki-Artikels <strong>%1$s</strong> ist jetzt aktiv.',
'WIKI_TOC' => 'Inhaltsverzeichnis',

));
1 change: 1 addition & 0 deletions language/en/link.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
'WIKI_ARTICLE_UPDATE' => 'An edit of a wiki article is awaiting approval',
'WIKI_WATCHED_ARTICLE' => 'A wiki article you watch has a new version',
'WIKI_WATCHED_ARTICLE_LIVE' => 'A new version of the wiki article <strong>%1$s</strong> is now live.',
'WIKI_TOC' => 'Contents',
'WIKI_LINK_BUTTON' => 'Wiki link: [[Article name]] or [[Article name|link text]]',
'WIKI_LINK_NEW_ARTICLE' => 'This article doesn’t exist yet. Follow the link to create it.',
'WIKI_WHAT_LINKS_HERE' => 'What links here',
Expand Down
72 changes: 72 additions & 0 deletions migrations/add_wiki_markup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?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\migrations;

/**
* Makes the text formatter learn the headings and table of contents markup, then
* reparses the saved articles so "== Heading ==" lines in them become headings.
*/
class add_wiki_markup extends \phpbb\db\migration\container_aware_migration
{
public static function depends_on()
{
return array(
'\phpbbmodders\wiki\migrations\add_recent_changes',
);
}

public function update_data()
{
return array(
array('custom', array(array($this, 'regenerate_formatter'))),
array('custom', array(array($this, 'reparse'))),
);
}

/**
* The text formatter keeps its tag list in a cache; rebuild it so the new tags exist.
*
* @return void
*/
public function regenerate_formatter()
{
$this->container->get('text_formatter.s9e.factory')->regenerate();
}

/**
* Reparse the articles, newest rows first, fifty a step.
*
* @param int $current A row id to continue from, 0 to start
* @return bool|int The next row id, or true when finished
*/
public function reparse($current = 0)
{
$reparser = new \phpbbmodders\wiki\textreparser\plugins\article_text(
$this->db,
$this->container->getParameter('core.table_prefix') . 'wiki_article'
);

if (empty($current))
{
$current = $reparser->get_max_id();
}

$limit = 50;
$start = max(1, $current + 1 - $limit);
$end = max(1, $current);

$reparser->reparse_range($start, $end);

$current = $start - 1;

return ($current === 0) ? true : $current;
}
}
43 changes: 43 additions & 0 deletions styles/prosilver/theme/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,46 @@ a.wikilink-new {
.forumbg ul.topiclist dd.lastpost.wiki-pending-action {
width: 230px;
}

/* Headings and the table of contents written with == Heading == */
.wiki h2.wikiheading,
.content h2.wikiheading {
margin: 1.2em 0 0.5em;
border-bottom: 1px solid #ccc;
padding-bottom: 0.2em;
}

.wiki h3.wikiheading,
.wiki h4.wikiheading,
.wiki h5.wikiheading,
.wiki h6.wikiheading,
.content h3.wikiheading,
.content h4.wikiheading,
.content h5.wikiheading,
.content h6.wikiheading {
margin: 1em 0 0.4em;
}

.wikitoc {
display: table;
margin: 0.8em 0;
padding: 0.6em 1em;
border: 1px solid #ccc;
background-color: rgba(128, 128, 128, 0.08);
}

.wikitoc ul {
margin: 0.3em 0 0 1.2em;
padding: 0;
list-style: none;
}

.wikitoc .wikitoc-title {
margin: 0;
text-align: center;
}

.wikitoc .wikitoc-number {
margin-right: 0.3em;
opacity: 0.7;
}
Loading
Loading