diff --git a/README.md b/README.md index 1c15418..e800522 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,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/` 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. - Edits can wait for approval: moderators get a **Pending articles** list with Approve and Reject. 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. @@ -34,6 +35,27 @@ an active article offline or deleting it entirely: [![Moderator controls dropdown: set article inactive, delete article](docs/images/wiki-moderator-controls.png)](docs/images/wiki-moderator-controls.png) +## Linking between articles + +Write an article's name in double square brackets to link to it, in wiki articles and in forum posts: + +- `[[Installation]]` links to the article **Installation**. +- `[[Installation|how to install]]` links to the same article but shows *how to install*. + +Links to articles that don't exist yet are shown in red, and following one opens the editor to create that article. The **Wiki link** button in the editor toolbar inserts the brackets. Inside a `[code]` block the brackets stay plain text. + +Below each article, **What links here** lists the other articles whose current version links to it. + + + + + + + +
An article with links to other articles; links to articles that don't exist yet are red
Links, with missing articles in red
An article with its What links here list
What links here
The Wiki link button in the editor toolbar, outlined
The editor button (outlined)
+ +Articles and posts written before this feature existed get their links the next time they are edited. To convert them all at once, run phpBB's reparser from the board's folder: `php bin/phpbbcli.php reparser:reparse phpbbmodders_wiki_article` for articles and `php bin/phpbbcli.php reparser:reparse post_text` for forum posts. + ## Requirements - phpBB 3.3.19 or later (also tested on phpBB 4.0) diff --git a/config/services.yml b/config/services.yml index 885244c..189b4a7 100644 --- a/config/services.yml +++ b/config/services.yml @@ -94,6 +94,20 @@ services: - '%tables.wiki.article%' - '%core.root_path%' - '%core.php_ext%' + - '@phpbbmodders.wiki.links' + phpbbmodders.wiki.links: + class: phpbbmodders\wiki\wiki\links + arguments: + - '@dbal.conn' + - '%tables.wiki.article%' + phpbbmodders.wiki.wikilink_listener: + class: phpbbmodders\wiki\event\wikilink_listener + arguments: + - '@controller.helper' + - '@language' + - '@phpbbmodders.wiki.links' + tags: + - { name: event.listener } phpbbmodders.wiki.notification.type.article_edit: class: phpbbmodders\wiki\notification\article_edit diff --git a/docs/TODO.md b/docs/TODO.md index 9ca7b0d..f0fefa9 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -40,15 +40,6 @@ 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). -## Internal wiki links - -A lightweight `[[Article Title]]`-style shortcut (MediaWiki's signature -syntax) instead of requiring a full `[url=...]` BBCode every time you want -to link one article to another. Would need a custom BBCode or a message-parse -step that resolves `[[...]]` to the right article URL, including handling -the "target article doesn't exist yet" case (MediaWiki renders those as a -distinct "red link" style). - ## Move/rename an article The URL slug is fixed forever once an article is created — only the title @@ -107,8 +98,7 @@ 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 -and internal wiki links items above landing first, since a real wiki -layout wants both. +item above landing first, since a real wiki layout wants one. ## Recent Changes feed diff --git a/docs/images/wiki-article-links.png b/docs/images/wiki-article-links.png new file mode 100644 index 0000000..8046eed Binary files /dev/null and b/docs/images/wiki-article-links.png differ diff --git a/docs/images/wiki-article-view.png b/docs/images/wiki-article-view.png index bbb7b48..6161ce4 100644 Binary files a/docs/images/wiki-article-view.png and b/docs/images/wiki-article-view.png differ diff --git a/docs/images/wiki-compare.png b/docs/images/wiki-compare.png index 855911d..644812e 100644 Binary files a/docs/images/wiki-compare.png and b/docs/images/wiki-compare.png differ diff --git a/docs/images/wiki-edit.png b/docs/images/wiki-edit.png index 2315eec..09cc0fe 100644 Binary files a/docs/images/wiki-edit.png and b/docs/images/wiki-edit.png differ diff --git a/docs/images/wiki-link-button.png b/docs/images/wiki-link-button.png new file mode 100644 index 0000000..8150800 Binary files /dev/null and b/docs/images/wiki-link-button.png differ diff --git a/docs/images/wiki-moderator-controls.png b/docs/images/wiki-moderator-controls.png index d6e24b0..936e062 100644 Binary files a/docs/images/wiki-moderator-controls.png and b/docs/images/wiki-moderator-controls.png differ diff --git a/docs/images/wiki-overview.png b/docs/images/wiki-overview.png index 730f308..891ba38 100644 Binary files a/docs/images/wiki-overview.png and b/docs/images/wiki-overview.png differ diff --git a/docs/images/wiki-pending-articles.png b/docs/images/wiki-pending-articles.png index c644b03..e9e853b 100644 Binary files a/docs/images/wiki-pending-articles.png and b/docs/images/wiki-pending-articles.png differ diff --git a/docs/images/wiki-what-links-here.png b/docs/images/wiki-what-links-here.png new file mode 100644 index 0000000..3391081 Binary files /dev/null and b/docs/images/wiki-what-links-here.png differ diff --git a/event/wikilink_listener.php b/event/wikilink_listener.php new file mode 100644 index 0000000..bc1ffad --- /dev/null +++ b/event/wikilink_listener.php @@ -0,0 +1,126 @@ +. The link address is added + * here after rendering, not in the tag's template, so it comes from the + * wiki's own route; links to articles that don't exist yet are marked so + * they can be styled and lead to the editor. + */ +class wikilink_listener implements EventSubscriberInterface +{ + /** Characters an article name in [[ ]] may not contain. */ + const NAME_PATTERN = '[^\[\]|<>"#?%\/\\\\\n]{1,255}'; + + /** @var \phpbb\controller\helper */ + protected $helper; + + /** @var \phpbb\language\language */ + protected $language; + + /** @var \phpbbmodders\wiki\wiki\links */ + protected $links; + + /** + * @param \phpbb\controller\helper $helper Controller helper object + * @param \phpbb\language\language $language Language object + * @param \phpbbmodders\wiki\wiki\links $links Wiki links service + */ + public function __construct(\phpbb\controller\helper $helper, \phpbb\language\language $language, \phpbbmodders\wiki\wiki\links $links) + { + $this->helper = $helper; + $this->language = $language; + $this->links = $links; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + 'core.text_formatter_s9e_configure_after' => 'configure_wikilink', + 'core.text_formatter_s9e_render_after' => 'link_wikilinks', + ); + } + + /** + * Teach the text formatter [[Article name]] and [[Article name|text]]. + * + * @param \phpbb\event\data $event The event object + * @return void + */ + public function configure_wikilink($event) + { + $configurator = $event['configurator']; + if (isset($configurator->tags['WIKILINK'])) + { + return; + } + + $tag = $configurator->tags->add('WIKILINK'); + $tag->attributes->add('name'); + $tag->attributes->add('text')->required = false; + $tag->template = '' + . '' + . ''; + + $configurator->Preg->match( + '/\[\[\s*(?' . self::NAME_PATTERN . '?)\s*(?:\|\s*(?[^\[\]\n]{1,255}?)\s*)?\]\]/', + 'WIKILINK' + ); + } + + /** + * Point rendered wiki links at their articles, and mark the ones whose + * article doesn't exist yet. + * + * @param \phpbb\event\data $event The event object + * @return void + */ + public function link_wikilinks($event) + { + $html = $event['html']; + if (strpos($html, 'class="wikilink"') === false) + { + return; + } + + $pattern = '//'; + if (!preg_match_all($pattern, $html, $matches)) + { + return; + } + + $names = array_map(function ($name) { + return htmlspecialchars_decode($name, ENT_QUOTES); + }, $matches[1]); + $existing = $this->links->existing_articles($names); + + $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)); + if (isset($existing[$name])) + { + return ''; + } + + return ''; + }, $html); + } +} diff --git a/language/en/link.php b/language/en/link.php index 795e2bd..34bad89 100644 --- a/language/en/link.php +++ b/language/en/link.php @@ -36,5 +36,8 @@ $lang = array_merge($lang, array( 'WIKI' => 'Wiki', 'WIKI_NEW_ARTICLE_UPDATE' => 'A new version of the article %1$s in the Wiki is awaiting approval.', + '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', )); diff --git a/styles/prosilver/template/article_body.html b/styles/prosilver/template/article_body.html index 9b1016c..5c21be6 100644 --- a/styles/prosilver/template/article_body.html +++ b/styles/prosilver/template/article_body.html @@ -52,6 +52,17 @@

{L_SOURCES}

+ +

{L_WIKI_WHAT_LINKS_HERE}

+
+ +
diff --git a/styles/prosilver/template/event/posting_editor_buttons_custom_tags_before.html b/styles/prosilver/template/event/posting_editor_buttons_custom_tags_before.html new file mode 100644 index 0000000..bf9467f --- /dev/null +++ b/styles/prosilver/template/event/posting_editor_buttons_custom_tags_before.html @@ -0,0 +1,3 @@ + diff --git a/styles/prosilver/theme/style.css b/styles/prosilver/theme/style.css index 93b12cf..7ea1687 100644 --- a/styles/prosilver/theme/style.css +++ b/styles/prosilver/theme/style.css @@ -18,3 +18,8 @@ th.big{width:300px;} body[class~="section-app/kb/"] ul.topiclist dd { padding-left: 1em !important; } + +/* [[ ]] links to wiki articles that don't exist yet */ +a.wikilink-new { + color: #bc2a4d; +} diff --git a/wiki/links.php b/wiki/links.php new file mode 100644 index 0000000..9e7e045 --- /dev/null +++ b/wiki/links.php @@ -0,0 +1,128 @@ +db = $db; + $this->article_table = $article_table; + } + + /** + * Which of the given article names have an approved version. + * + * @param array $names Article names (their URL part), as written in [[ ]] + * @return array The names that exist, as keys + */ + public function existing_articles(array $names) + { + $names = array_values(array_unique(array_filter($names, 'strlen'))); + if (empty($names)) + { + return array(); + } + + $sql = 'SELECT DISTINCT article_url + FROM ' . $this->article_table . ' + WHERE article_approved = 1 + AND ' . $this->db->sql_in_set('article_url', $names); + $result = $this->db->sql_query($sql); + $existing = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $existing[$row['article_url']] = true; + } + $this->db->sql_freeresult($result); + + return $existing; + } + + /** + * Articles whose live version (the newest approved one) links to an article. + * + * @param string $name The linked article's name (its URL part) + * @return array Rows of article_url and article_title, sorted by title + */ + public function what_links_here($name) + { + // Links are stored in the parsed text as . + $needle = 'article_table . ' + WHERE article_approved = 1 + AND article_url <> \'' . $this->db->sql_escape($name) . '\' + AND article_text ' . $this->db->sql_like_expression($this->db->get_any_char() . $needle . $this->db->get_any_char()); + $result = $this->db->sql_query($sql); + $candidates = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + $candidates[(int) $row['article_id']] = $row; + } + $this->db->sql_freeresult($result); + + if (empty($candidates)) + { + return array(); + } + + // Keep only matches that are their article's live version. + $urls = array_values(array_unique(array_column($candidates, 'article_url'))); + $sql = 'SELECT article_id, article_url + FROM ' . $this->article_table . ' + WHERE article_approved = 1 + AND ' . $this->db->sql_in_set('article_url', $urls) . ' + ORDER BY article_last_edit DESC, article_id DESC'; + $result = $this->db->sql_query($sql); + $live = array(); + while ($row = $this->db->sql_fetchrow($result)) + { + if (!isset($live[$row['article_url']])) + { + $live[$row['article_url']] = (int) $row['article_id']; + } + } + $this->db->sql_freeresult($result); + + $linking = array(); + foreach ($live as $article_id) + { + if (isset($candidates[$article_id])) + { + $linking[] = array( + 'article_url' => $candidates[$article_id]['article_url'], + 'article_title' => $candidates[$article_id]['article_title'], + ); + } + } + usort($linking, function ($a, $b) { + return strcasecmp($a['article_title'], $b['article_title']); + }); + + return $linking; + } +} diff --git a/wiki/view.php b/wiki/view.php index 02e8c86..16e5ee5 100644 --- a/wiki/view.php +++ b/wiki/view.php @@ -24,6 +24,9 @@ class view extends \phpbbmodders\wiki\wiki\functions /** @var \parse_message */ protected $message_parser; + /** @var \phpbbmodders\wiki\wiki\links */ + protected $links; + /** @var \phpbb\template\template */ protected $template; @@ -59,8 +62,9 @@ class view extends \phpbbmodders\wiki\wiki\functions * @param string $phpbb_root_path * @param string $php_ext */ - public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_interface $db, \phpbb\controller\helper $helper, \phpbb\template\template $template, \phpbb\user $user, \phpbbmodders\wiki\wiki\compare $compare, \phpbbmodders\wiki\wiki\edit $edit, $article_table, $phpbb_root_path, $php_ext) + public function __construct(\phpbb\auth\auth $auth, \phpbb\db\driver\driver_interface $db, \phpbb\controller\helper $helper, \phpbb\template\template $template, \phpbb\user $user, \phpbbmodders\wiki\wiki\compare $compare, \phpbbmodders\wiki\wiki\edit $edit, $article_table, $phpbb_root_path, $php_ext, \phpbbmodders\wiki\wiki\links $links) { + $this->links = $links; $this->auth = $auth; $this->db = $db; $this->helper = $helper; @@ -214,6 +218,15 @@ public function view_article($article, $id = 0) 'EDIT_REASON' => ($id <> 0) ? $this->data['article_edit_reason'] : '', 'U_TOPIC' => ($this->data['article_topic_id'] <> 0) ? append_sid($this->phpbb_root_path . 'viewtopic.' . $this->php_ext, 't=' . $this->data['article_topic_id']) : '', )); + + // Other articles whose live version links here with [[ ]]. + foreach ($this->links->what_links_here($this->data['article_url']) as $linking) + { + $this->template->assign_block_vars('what_links_here', array( + 'TITLE' => $linking['article_title'], + 'U_ARTICLE' => $this->helper->route('phpbbmodders_wiki_article', array('article' => $linking['article_url'])), + )); + } } return $this->helper->render('article_body.html', $this->data['article_title']); }