' . esc_html(!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins')) . '
' . esc_html(!empty($metadata['description']) ? $metadata['description'] : (!empty($repo['description']) ? $repo['description'] : __('Ingen beskrivelse angivet.', 'ansico-plugins'))) . '
';
$meta_rows = array(
__('Installeret version', 'ansico-plugins') => $plugin_state['installed_version'] !== '' ? $plugin_state['installed_version'] : '—',
__('Seneste release', 'ansico-plugins') => !is_wp_error($release) ? $this->normalize_release_version($release) : '—',
@@ -592,8 +599,71 @@ class Ansico_Plugins_Admin {
__('Plugin-URL', 'ansico-plugins') => !empty($metadata['plugin_headers']['PluginURI']) ? $metadata['plugin_headers']['PluginURI'] : '—',
__('Kræver WordPress', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresWP']) ? $metadata['plugin_headers']['RequiresWP'] : '—',
__('Kræver PHP', 'ansico-plugins') => !empty($metadata['plugin_headers']['RequiresPHP']) ? $metadata['plugin_headers']['RequiresPHP'] : '—',
- __('Text domain', 'ansico-plugins') => !empty($metadata['plugin_headers']['TextDomain']) ? $metadata['plugin_headers']['TextDomain'] : '—',
);
+ if (!empty($metadata['plugin_headers']['TextDomain'])) {
+ $meta_rows[__('Text domain', 'ansico-plugins')] = $metadata['plugin_headers']['TextDomain'];
+ }
+
+ $tabs = array();
+ $tabs[] = array(
+ 'slug' => 'description',
+ 'label' => __('Beskrivelse', 'ansico-plugins'),
+ 'content' => $this->render_readme_section(!empty($sections['description']) ? $sections['description'] : $description, $is_wordpress_readme),
+ );
+ if (!empty($sections['installation'])) {
+ $tabs[] = array(
+ 'slug' => 'installation',
+ 'label' => __('Installation', 'ansico-plugins'),
+ 'content' => $this->render_readme_section($sections['installation'], $is_wordpress_readme),
+ );
+ }
+ if (!empty($sections['changelog'])) {
+ $tabs[] = array(
+ 'slug' => 'changelog',
+ 'label' => __('Changelog', 'ansico-plugins'),
+ 'content' => $this->render_readme_section($sections['changelog'], $is_wordpress_readme),
+ );
+ }
+ if (!empty($metadata['screenshot_url']) || !empty($sections['screenshots'])) {
+ $screen = '';
+ if (!empty($metadata['screenshot_url'])) {
+ $screen .= '
 . ')
';
+ }
+ if (!empty($sections['screenshots'])) {
+ $screen .= $this->render_readme_section($sections['screenshots'], $is_wordpress_readme);
+ }
+ $tabs[] = array(
+ 'slug' => 'screenshots',
+ 'label' => __('Screenshots', 'ansico-plugins'),
+ 'content' => $screen,
+ );
+ }
+ $tabs[] = array(
+ 'slug' => 'metadata',
+ 'label' => __('Metadata', 'ansico-plugins'),
+ 'content' => $this->render_metadata_grid($meta_rows),
+ );
+ if (!is_wp_error($release) && !empty($release['body'])) {
+ $tabs[] = array(
+ 'slug' => 'release-notes',
+ 'label' => __('Release-noter', 'ansico-plugins'),
+ 'content' => '
' . wp_kses_post(wpautop($release['body'])) . '
',
+ );
+ }
+
+ $html .= $this->render_details_tabs($tabs, $repo['name']);
+ return $html;
+ }
+
+ private function get_plugin_icon_markup($metadata, $display_name) {
+ if (!empty($metadata['icon_url'])) {
+ return '
';
+ }
+ return '
AP
';
+ }
+
+ private function render_metadata_grid($meta_rows) {
+ $html = '
';
-
- if (!empty($metadata['screenshot_url'])) {
- $html .= '
';
- $html .= '
' . esc_html__('Screenshot', 'ansico-plugins') . '
';
- $html .= '
 . ')
';
- $html .= '
';
- }
-
- if (!is_wp_error($release) && !empty($release['body'])) {
- $html .= '
' . esc_html__('Release-noter', 'ansico-plugins') . '
';
- $html .= '
' . wp_kses_post(wpautop($release['body'])) . '
';
- }
-
return $html;
}
+ private function render_details_tabs($tabs, $repo_name) {
+ $id_base = 'ansico-tabs-' . sanitize_title($repo_name);
+ $html = '
';
+ foreach ($tabs as $index => $tab) {
+ $active = 0 === $index ? ' is-active' : '';
+ $html .= '';
+ }
+ $html .= '
';
+ foreach ($tabs as $index => $tab) {
+ $active = 0 === $index ? ' is-active' : '';
+ $html .= '
' . $tab['content'] . '
';
+ }
+ $html .= '
';
+ return $html;
+ }
+
+ private function render_readme_section($content, $is_wordpress_readme = false) {
+ $content = trim((string) $content);
+ if ('' === $content) {
+ return '
' . esc_html__('Ingen oplysninger tilgængelige.', 'ansico-plugins') . '
';
+ }
+ if ($is_wordpress_readme) {
+ return wp_kses_post(wpautop($this->format_wordpress_readme_text($content)));
+ }
+ return wp_kses_post(wpautop($this->format_markdown_text($content)));
+ }
+
+ private function format_wordpress_readme_text($content) {
+ $content = preg_replace('/^=\s*(.+?)\s*=\s*$/m', "
+
$1
+", $content);
+ $content = preg_replace('/^\*\s+/m', '• ', $content);
+ return $content;
+ }
+
+ private function format_markdown_text($content) {
+ $content = preg_replace('/^###\s+(.+)$/m', "
+
$1
+", $content);
+ $content = preg_replace('/^##\s+(.+)$/m', "
+
$1
+", $content);
+ $content = preg_replace('/^#\s+(.+)$/m', "
+
$1
+", $content);
+ $content = preg_replace('/^[-*]\s+/m', '• ', $content);
+ $content = preg_replace('/`([^`]+)`/', '$1', $content);
+ return $content;
+ }
+
private function render_catalog_styles() {
echo '';
+ ';
}
}
diff --git a/ansico-plugins/includes/class-ansico-plugins-admin.php:Zone.Identifier b/ansico-plugins/includes/class-ansico-plugins-admin.php:Zone.Identifier
index 343fd8c..20f0320 100644
Binary files a/ansico-plugins/includes/class-ansico-plugins-admin.php:Zone.Identifier and b/ansico-plugins/includes/class-ansico-plugins-admin.php:Zone.Identifier differ
diff --git a/ansico-plugins/includes/class-ansico-plugins-client.php b/ansico-plugins/includes/class-ansico-plugins-client.php
index 9f6d06d..cfe5f7a 100644
--- a/ansico-plugins/includes/class-ansico-plugins-client.php
+++ b/ansico-plugins/includes/class-ansico-plugins-client.php
@@ -303,23 +303,188 @@ class Ansico_Plugins_Client {
public function get_repository_readme_title($owner, $repo_name, $ref = '') {
- $candidates = array('README.md', 'readme.md', 'Readme.md');
+ $documentation = $this->get_repository_documentation($owner, $repo_name, $ref);
+ if (is_wp_error($documentation)) {
+ return '';
+ }
+ return !empty($documentation['title']) ? (string) $documentation['title'] : '';
+ }
+
+ public function get_repository_documentation($owner, $repo_name, $ref = '') {
+ $candidates = array('readme.txt', 'README.txt', 'README.md', 'readme.md', 'Readme.md');
foreach ($candidates as $candidate) {
- $file = $this->get_repository_contents($owner, $repo_name, $candidate, $ref);
- if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) {
+ $raw = $this->get_repository_text_file($owner, $repo_name, $candidate, $ref);
+ if ('' === $raw) {
continue;
}
- $raw = base64_decode((string) $file['content'], true);
- if (false === $raw) {
- continue;
+ if (preg_match('/\.txt$/i', $candidate)) {
+ $parsed = $this->parse_wordpress_readme($raw);
+ } else {
+ $parsed = $this->parse_markdown_readme($raw);
}
- if (preg_match('/^\#\s+(.+)$/m', (string) $raw, $matches)) {
- return trim(wp_strip_all_tags($matches[1]));
+ if (!empty($parsed['title']) || !empty($parsed['short_description']) || !empty($parsed['sections'])) {
+ $parsed['source'] = $candidate;
+ return $parsed;
}
}
+ return array(
+ 'title' => '',
+ 'short_description' => '',
+ 'sections' => array(),
+ 'source' => '',
+ );
+ }
+
+ private function get_repository_text_file($owner, $repo_name, $path, $ref = '') {
+ $file = $this->get_repository_contents($owner, $repo_name, $path, $ref);
+ if (is_wp_error($file) || !is_array($file) || empty($file['content']) || empty($file['encoding']) || 'base64' !== $file['encoding']) {
+ return '';
+ }
+
+ $raw = base64_decode((string) $file['content'], true);
+ if (false === $raw) {
+ return '';
+ }
+
+ return str_replace(array("
+", "
"), "
+", (string) $raw);
+ }
+
+ private function parse_wordpress_readme($raw) {
+ $raw = trim((string) $raw);
+ $lines = preg_split('/
+/', $raw);
+ $title = '';
+ $short_description = '';
+ $sections = array();
+ $current = '';
+ $buffer = array();
+ $header_done = false;
+
+ foreach ($lines as $line) {
+ if ('' === $title && preg_match('/^===\s*(.+?)\s*===\s*$/', $line, $m)) {
+ $title = trim($m[1]);
+ continue;
+ }
+
+ if (!$header_done && preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) {
+ $header_done = true;
+ $current = strtolower(trim($m[1]));
+ $buffer = array();
+ continue;
+ }
+
+ if (!$header_done) {
+ if ('' === $short_description && '' !== trim($line) && false === strpos($line, ':')) {
+ $short_description = trim($line);
+ }
+ continue;
+ }
+
+ if (preg_match('/^==\s*(.+?)\s*==\s*$/', $line, $m)) {
+ if ('' !== $current) {
+ $sections[$current] = trim(implode("
+", $buffer));
+ }
+ $current = strtolower(trim($m[1]));
+ $buffer = array();
+ continue;
+ }
+
+ $buffer[] = $line;
+ }
+
+ if ('' !== $current) {
+ $sections[$current] = trim(implode("
+", $buffer));
+ }
+
+ if ('' === $short_description && !empty($sections['description'])) {
+ $short_description = $this->first_paragraph($sections['description']);
+ }
+
+ return array(
+ 'title' => $title,
+ 'short_description' => $short_description,
+ 'sections' => $sections,
+ );
+ }
+
+ private function parse_markdown_readme($raw) {
+ $title = '';
+ $short_description = '';
+ $sections = array();
+ $current = '';
+ $buffer = array();
+ $after_title = '';
+
+ foreach (preg_split('/
+/', (string) $raw) as $line) {
+ if ('' === $title && preg_match('/^#\s+(.+)$/', $line, $m)) {
+ $title = trim(wp_strip_all_tags($m[1]));
+ continue;
+ }
+
+ if (preg_match('/^##\s+(.+)$/', $line, $m)) {
+ if ('' !== $current) {
+ $sections[$current] = trim(implode("
+", $buffer));
+ }
+ $current = strtolower(trim(wp_strip_all_tags($m[1])));
+ $buffer = array();
+ continue;
+ }
+
+ if ('' === $current) {
+ $after_title .= $line . "
+";
+ } else {
+ $buffer[] = $line;
+ }
+ }
+
+ if ('' !== $current) {
+ $sections[$current] = trim(implode("
+", $buffer));
+ }
+
+ $short_description = $this->first_paragraph($after_title);
+ if ('' === $short_description && !empty($sections['description'])) {
+ $short_description = $this->first_paragraph($sections['description']);
+ }
+
+ return array(
+ 'title' => $title,
+ 'short_description' => $short_description,
+ 'sections' => $sections,
+ );
+ }
+
+ private function first_paragraph($text) {
+ $text = trim((string) $text);
+ if ('' === $text) {
+ return '';
+ }
+ $parts = preg_split("/
+\s*
+/", $text);
+ if (empty($parts[0])) {
+ return '';
+ }
+ return trim(wp_strip_all_tags($parts[0]));
+ }
+
+ private function find_repository_asset_url($owner, $repo_name, $default_branch, $candidates) {
+ foreach ((array) $candidates as $candidate) {
+ $file = $this->get_repository_contents($owner, $repo_name, $candidate, $default_branch);
+ if (!is_wp_error($file) && is_array($file) && !empty($file['download_url'])) {
+ return (string) $file['download_url'];
+ }
+ }
return '';
}
@@ -331,54 +496,53 @@ class Ansico_Plugins_Client {
$default_branch = !empty($repo['default_branch']) ? (string) $repo['default_branch'] : '';
$root = $this->get_repository_contents($owner, $repo_name, '', $default_branch);
- if (is_wp_error($root) || !is_array($root)) {
- return array(
- 'readme_title' => '',
- 'author_name' => !empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''),
- 'author_url' => !empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : ''),
- 'plugin_headers' => array(),
- 'support_url' => !empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : '',
- 'license' => !empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : '',
- 'screenshot_url' => '',
- 'default_branch' => $default_branch,
- );
- }
-
- $screenshot_url = '';
$plugin_headers = array();
- foreach ($root as $item) {
- if (!is_array($item) || empty($item['name'])) {
- continue;
- }
- $name = (string) $item['name'];
- if (strtolower($name) === 'screenshot.png' && !empty($item['download_url'])) {
- $screenshot_url = (string) $item['download_url'];
- }
- if (empty($plugin_headers) && !empty($item['type']) && 'file' === $item['type'] && preg_match('/\.php$/i', $name)) {
- $file = $this->get_repository_contents($owner, $repo_name, $name, $default_branch);
- if (!is_wp_error($file) && is_array($file) && !empty($file['content']) && !empty($file['encoding']) && 'base64' === $file['encoding']) {
- $raw = base64_decode((string) $file['content'], true);
- if (false !== $raw) {
- $plugin_headers = $this->parse_plugin_headers($raw);
+
+ if (!is_wp_error($root) && is_array($root)) {
+ foreach ($root as $item) {
+ if (!is_array($item) || empty($item['name'])) {
+ continue;
+ }
+ $name = (string) $item['name'];
+ if (empty($plugin_headers) && !empty($item['type']) && 'file' === $item['type'] && preg_match('/\.php$/i', $name)) {
+ $file = $this->get_repository_contents($owner, $repo_name, $name, $default_branch);
+ if (!is_wp_error($file) && is_array($file) && !empty($file['content']) && !empty($file['encoding']) && 'base64' === $file['encoding']) {
+ $raw = base64_decode((string) $file['content'], true);
+ if (false !== $raw) {
+ $plugin_headers = $this->parse_plugin_headers($raw);
+ }
}
}
}
}
- $readme_title = $this->get_repository_readme_title($owner, $repo_name, $default_branch);
+ $documentation = $this->get_repository_documentation($owner, $repo_name, $default_branch);
+ if (is_wp_error($documentation)) {
+ $documentation = array('title' => '', 'short_description' => '', 'sections' => array(), 'source' => '');
+ }
+
$author_name = !empty($plugin_headers['Author']) ? (string) $plugin_headers['Author'] : (!empty($repo['owner']['full_name']) ? (string) $repo['owner']['full_name'] : (!empty($repo['owner']['login']) ? (string) $repo['owner']['login'] : ''));
$author_url = !empty($plugin_headers['AuthorURI']) ? (string) $plugin_headers['AuthorURI'] : (!empty($repo['owner']['website']) ? (string) $repo['owner']['website'] : (!empty($repo['owner']['html_url']) ? (string) $repo['owner']['html_url'] : ''));
$support_url = !empty($plugin_headers['SupportURI']) ? (string) $plugin_headers['SupportURI'] : (!empty($repo['html_url']) ? trailingslashit((string) $repo['html_url']) . 'issues' : '');
$license = !empty($plugin_headers['License']) ? (string) $plugin_headers['License'] : (!empty($repo['license']['spdx_id']) ? (string) $repo['license']['spdx_id'] : '');
+ $screenshot_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('screenshot.png', 'assets/screenshot.png'));
+ $icon_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('icon-256x256.png', 'icon-128x128.png', 'assets/icon-256x256.png', 'assets/icon-128x128.png'));
+ $banner_url = $this->find_repository_asset_url($owner, $repo_name, $default_branch, array('banner-1544x500.png', 'banner-772x250.png', 'assets/banner-1544x500.png', 'assets/banner-772x250.png'));
+
return array(
- 'readme_title' => $readme_title,
+ 'readme_title' => !empty($documentation['title']) ? $documentation['title'] : '',
+ 'description' => !empty($documentation['short_description']) ? $documentation['short_description'] : (!empty($repo['description']) ? (string) $repo['description'] : ''),
+ 'readme_sections' => !empty($documentation['sections']) && is_array($documentation['sections']) ? $documentation['sections'] : array(),
+ 'readme_source' => !empty($documentation['source']) ? $documentation['source'] : '',
'author_name' => $author_name,
'author_url' => $author_url,
'plugin_headers' => $plugin_headers,
'support_url' => $support_url,
'license' => $license,
'screenshot_url' => $screenshot_url,
+ 'icon_url' => $icon_url,
+ 'banner_url' => $banner_url,
'default_branch' => $default_branch,
);
}
diff --git a/ansico-plugins/includes/class-ansico-plugins-client.php:Zone.Identifier b/ansico-plugins/includes/class-ansico-plugins-client.php:Zone.Identifier
index 343fd8c..20f0320 100644
Binary files a/ansico-plugins/includes/class-ansico-plugins-client.php:Zone.Identifier and b/ansico-plugins/includes/class-ansico-plugins-client.php:Zone.Identifier differ
diff --git a/ansico-plugins/includes/class-ansico-plugins-installer.php:Zone.Identifier b/ansico-plugins/includes/class-ansico-plugins-installer.php:Zone.Identifier
index 343fd8c..20f0320 100644
Binary files a/ansico-plugins/includes/class-ansico-plugins-installer.php:Zone.Identifier and b/ansico-plugins/includes/class-ansico-plugins-installer.php:Zone.Identifier differ
diff --git a/ansico-plugins/includes/class-ansico-plugins-updater.php:Zone.Identifier b/ansico-plugins/includes/class-ansico-plugins-updater.php:Zone.Identifier
index 343fd8c..20f0320 100644
Binary files a/ansico-plugins/includes/class-ansico-plugins-updater.php:Zone.Identifier and b/ansico-plugins/includes/class-ansico-plugins-updater.php:Zone.Identifier differ
diff --git a/ansico-plugins/readme.txt b/ansico-plugins/readme.txt
index 94e28ad..045b82a 100644
--- a/ansico-plugins/readme.txt
+++ b/ansico-plugins/readme.txt
@@ -4,7 +4,7 @@ Tags: forgejo, plugins, private directory, ansico
Requires at least: 6.3
Tested up to: 6.9.4
Requires PHP: 7.0
-Stable tag: 1.0.0
+Stable tag: 1.0.0.2
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.html
@@ -12,7 +12,7 @@ Gives access to Ansico plugin directory with Ansico plugins.
== Description ==
-Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update, and delete internal plugins from Ansico releases.
+Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin directory and lets administrators install, activate, reinstall, update and delete internal plugins from Ansico releases.
= Features =
@@ -21,9 +21,10 @@ Ansico Plugins gives WordPress access to Ansico's private Forgejo-based plugin d
* Reads plugin metadata and README content from repositories.
* Opens a WordPress-style details popup for each plugin.
* Installs plugins from the latest Forgejo release ZIP.
-* Supports activate, reinstall, update, and delete actions.
+* Supports activate, reinstall, update and delete actions.
* Supports self-updates for Ansico Plugins itself.
* Includes Ansico default settings and a reset button.
+* Supports plugin presentation from `readme.txt` in WordPress format.
== Installation ==
@@ -43,12 +44,23 @@ Yes. Public Ansico repositories can be listed without a token. A token can still
Yes. Self-update support is built in and can check the configured Ansico Plugins repository for newer releases.
+== Screenshots ==
+
+1. The Ansico Plugins catalogue in WordPress admin.
+2. The plugin details modal with tabs and metadata.
+
== Changelog ==
+= 1.0.0.2 =
+* Added support for parsing `readme.txt` in WordPress format.
+* Added tabbed plugin details view with Description, Installation, Changelog and Screenshots.
+* Added generated plugin icon and plugin banner assets.
+* Improved metadata extraction priority so `readme.txt` can drive plugin presentation.
+
= 1.0.0 =
* First stable release.
* WordPress-style plugin catalogue UI.
* Details popup with metadata and optional screenshot.
-* Install, activate, reinstall, update, and delete actions.
+* Install, activate, reinstall, update and delete actions.
* Self-update support for Ansico Plugins.
* Ansico default configuration and reset button.
diff --git a/ansico-plugins/readme.txt:Zone.Identifier b/ansico-plugins/readme.txt:Zone.Identifier
index 343fd8c..20f0320 100644
Binary files a/ansico-plugins/readme.txt:Zone.Identifier and b/ansico-plugins/readme.txt:Zone.Identifier differ
diff --git a/banner-1544x500.png b/banner-1544x500.png
new file mode 100644
index 0000000..d186c3c
Binary files /dev/null and b/banner-1544x500.png differ
diff --git a/banner-1544x500.png:Zone.Identifier b/banner-1544x500.png:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/banner-1544x500.png:Zone.Identifier differ
diff --git a/icon-256x256.png b/icon-256x256.png
new file mode 100644
index 0000000..10f8760
Binary files /dev/null and b/icon-256x256.png differ
diff --git a/icon-256x256.png:Zone.Identifier b/icon-256x256.png:Zone.Identifier
new file mode 100644
index 0000000..d6c1ec6
Binary files /dev/null and b/icon-256x256.png:Zone.Identifier differ