Compare commits

...

42 commits
0.6.0 ... main

Author SHA1 Message Date
yggverse
597a105be7 update default options 2024-08-03 02:17:26 +03:00
yggverse
acad5e14cc fix query format 2024-07-08 04:36:04 +03:00
yggverse
ee3d403364 update readme 2024-06-26 20:42:50 +03:00
yggverse
f10537947d remove extra dependency, update tags 2024-06-26 20:15:08 +03:00
yggverse
d8c7b0668b update readme 2024-06-26 20:12:29 +03:00
yggverse
5b3a714194 update links 2024-06-26 20:05:42 +03:00
yggverse
8a895adbdf drop Dokuwiki features from future release 2024-06-26 20:04:16 +03:00
yggverse
639f384f29 drop Gtk3/Pango class from future release 2024-06-26 20:02:29 +03:00
yggverse
325ac1eb8c drop gemtext features (use gemtext-php library) 2024-06-26 20:01:35 +03:00
yggverse
3ee5de206f update readme 2024-06-24 23:34:38 +03:00
yggverse
2ba31f0373 add deprecation notice 2024-06-24 23:32:55 +03:00
yggverse
9368310e74 fix binary body detection in response 2024-06-24 23:27:15 +03:00
yggverse
0d5db685d1 add exception message 2024-06-24 23:16:35 +03:00
yggverse
5b6e645a4b fix exception namespace 2024-06-24 23:15:49 +03:00
yggverse
e36d9fb2a1 use gtk version dependent pango class implementation 2024-04-17 09:44:36 +03:00
yggverse
f12ff02616 escape all url entities 2024-04-16 20:34:27 +03:00
yggverse
3d86054f91 decode url in alt 2024-04-16 20:32:24 +03:00
yggverse
3677e090c4 append title attribute on address does not match alt value 2024-04-16 20:15:00 +03:00
yggverse
b0f8e62ce5 add title attribute to the links tag 2024-04-16 19:46:39 +03:00
yggverse
2cbdcbf255 add links support 2024-04-14 07:50:59 +03:00
yggverse
cef15510ea filter link tag on line construct 2024-04-14 07:48:46 +03:00
yggverse
675cdf3ac4 implement gemtext code format 2024-04-12 14:20:19 +03:00
yggverse
3265add74a implement getCode method 2024-04-12 11:52:33 +03:00
yggverse
482106edf4 add quote support 2024-04-12 09:46:12 +03:00
yggverse
1b996e043b add getQuote method 2024-04-12 09:45:23 +03:00
yggverse
284733886e add escape method 2024-04-12 09:36:27 +03:00
yggverse
3b371988ff update headers level 2024-04-12 08:03:14 +03:00
yggverse
faac656ab1 add pango markup converter 2024-04-12 08:01:43 +03:00
yggverse
fc3b82a052 add the line methods 2024-04-10 17:33:58 +03:00
yggverse
526d810406 return match line number as the array key in result 2024-04-10 17:25:15 +03:00
yggverse
55b13c4031 fix getAddress regex condition 2024-04-10 14:43:27 +03:00
yggverse
f5aebfe554 update readme 2024-04-09 10:37:56 +03:00
yggverse
1c54da73c3 add β-Doku integration 2024-04-09 10:35:46 +03:00
yggverse
595e0e6ada add integrations 2024-04-09 10:34:38 +03:00
yggverse
79e4de7434 update readme 2024-04-08 03:32:14 +03:00
yggverse
3a1d8aa83b update readme 2024-04-07 22:48:53 +03:00
yggverse
8bdd2341eb update readme 2024-04-07 21:04:33 +03:00
yggverse
ae779833cb update readme 2024-04-07 21:04:03 +03:00
yggverse
b395f5f17a update readme 2024-04-07 21:01:21 +03:00
yggverse
c8257641ce update readme 2024-04-07 20:55:58 +03:00
yggverse
ca0746f536 update readme 2024-04-07 20:55:17 +03:00
yggverse
06238a4ce3 add resolved request support 2024-04-07 20:53:35 +03:00
9 changed files with 105 additions and 1476 deletions

426
README.md
View file

@ -1,6 +1,12 @@
# gemini-php # gemini-php
PHP 8 Library for Gemini Protocol PHP 8 Library for [Gemini Protocol](https://geminiprotocol.net)
_For optimization reasons, some experimental features like `Dokuwiki` and `GTK3/Pango` was dropped from `1.0.0` release, but available in [previous versions](https://github.com/YGGverse/gemini-php/releases/tag/0.10.1). `Gemtext` component re-implemented as separated library (see [Extras](#extras))_
## Extras
* [gemtext-php](https://github.com/YGGverse/gemtext-php) - Object-oriented PHP 8 library for Gemini / Gemtext operations
## Usage ## Usage
@ -14,12 +20,39 @@ PHP interface for Gemini protocol queries by TLS socket connection
### Request ### Request
``` ``` php
$request = new \Yggverse\Gemini\Client\Request( $request = new \Yggverse\Gemini\Client\Request(
'gemini://betahowto.duckdns.org:1965/archive' 'gemini://yggverse.cities.yesterweb.org:1965/index.gmi'
); );
``` ```
**Resolved request (SNI)**
For direct connection provide resolved IP as the second argument
``` php
$request = new \Yggverse\Gemini\Client\Request(
'gemini://yggverse.cities.yesterweb.org:1965/index.gmi' // target URL
'68.133.1.71' // resolved IP, skip to use system-wide resolver
);
```
Alternatively, use `setResolvedHost` method of `Request` object before `getResponse`
#### Request::setResolvedHost
``` php
$request->setResolvedHost(
'68.133.1.71'
)
```
* to resolve network address with PHP, take a look on the [net-php](https://github.com/YGGverse/net-php) library!
#### Request::getResolvedHost
Get resolved host back
#### Request::setHost #### Request::setHost
#### Request::getHost #### Request::getHost
#### Request::setPort #### Request::setPort
@ -32,17 +65,38 @@ $request = new \Yggverse\Gemini\Client\Request(
Execute requested URL and return raw response Execute requested URL and return raw response
``` ``` php
var_dump( var_dump(
$request->getResponse() $request->getResponse()
); );
``` ```
#### Request::getOptions
#### Request::setOptions
``` php
$request = new \Yggverse\Gemini\Client\Request(
'gemini://yggverse.cities.yesterweb.org',
'68.133.1.71' // make direct request to the resolved host
);
$request->setOptions(
[
'ssl' =>
[
'peer_name' => 'yggverse.cities.yesterweb.org', // SNI
'verify_peer' => false,
'verify_peer_name' => false
]
]
);
```
### Response ### Response
This class provides additional features for the raw response operations This class provides additional features for the raw response operations
``` ``` php
$response = new \Yggverse\Gemini\Client\Response( $response = new \Yggverse\Gemini\Client\Response(
$request->getResponse() $request->getResponse()
); );
@ -55,367 +109,15 @@ $response = new \Yggverse\Gemini\Client\Response(
#### Response::setBody #### Response::setBody
#### Response::getBody #### Response::getBody
``` ``` php
var_dump( var_dump(
$response->getBody() $response->getBody()
); );
``` ```
## Gemtext ## Integrations
Object-oriented API for Gemtext
### Body
Basic methods to work with `text/gemini` documents
```
$body = new \Yggverse\Gemini\Gemtext\Body(
$response->getBody() // gemtext body from client response or .gmi file
);
```
#### Body::getH1
#### Body::getH2
#### Body::getH3
#### Body::getLinks
```
var_dump(
$body->getLinks() // returns array of inline links
);
```
#### Body::findLinks
Find context links by protocol as argument, `gemini` by default
```
var_dump(
$body->findLinks('http') // returns array of http links found
);
```
#### Body::skipTags
Strip gemini tags from Gemini document
```
var_dump(
$body->skipTags() // strip all tags
);
var_dump(
$body->skipTags(
[ // 1- and 2- level headers only
"##",
"###"
]
)
);
```
### Link
Inline links parser.
Allows to extract address, date with timestamp and alt text from link line given
```
foreach ($body->getLinks() as $line)
{
$link = new \Yggverse\Gemini\Gemtext\Link(
$line
);
var_dump(
$link->getAddress()
);
var_dump(
$link->getAlt()
);
}
```
#### Link::getAddress
#### Link::getDate
This method also validates time format and returns the unix timestamp as linked argument
```
var_dump(
$link->getDate(
$timestamp // get unix time from this variable
)
);
var_dump(
$timestamp
);
```
#### Link::getAlt
## DokuWiki
Toolkit provides DokuWiki API for Gemini.
Allows to simple deploy new apps or make existing website mirror
### Examples
* [gemini-dl](https://github.com/YGGverse/gemini-dl) - CLI Batch downloader for Gemini Protocol
* [Yo!](https://github.com/YGGverse/Yo/tree/gemini) - Crawler for different networks
* [Yoda](https://github.com/YGGverse/Yoda) - PHP-GTK browser for Gemini Protocol
* [β-Doku](https://github.com/YGGverse/bdoku) - DokuWiki Satellite for Gemini Protocol * [β-Doku](https://github.com/YGGverse/bdoku) - DokuWiki Satellite for Gemini Protocol
### Reader
Read DokuWiki and convert to Gemini
```
$reader = new \Yggverse\Gemini\Dokuwiki\Reader(
// optional regex rule set array
);
```
#### Reader::getRules
#### Reader::setRules
#### Reader::getRule
#### Reader::setRule
Get or change existing regex rule (or just skip by using build-in set)
```
echo $reader->setRule(
'/subject/ui',
'replacement'
);
```
#### Reader::getMacroses
#### Reader::setMacroses
#### Reader::getMacros
#### Reader::setMacros
```
echo $reader->setMacros(
'~my-macros-key~',
'~my-macros-value~',
);
```
#### Reader::toGemini
Convert DokuWiki text to Gemini markup
As wiki has lot of inline links, to make converted document well-readable, this method does not replace links with new line `=>` macros, but uses inline context: `Name ( URL )`. This model useful with `Reader::getLinks` method, that for example appends all those related links to the document footer.
If you don't like this implementation, feel free to change it by `Reader::setRule` method!
```
echo $reader->toGemini(
file_get_contents(
'/host/data/pages/index.txt'
)
);
```
#### Reader::getH1
Get document title
```
$gemini = $reader->toGemini(
file_get_contents(
'/host/data/pages/index.txt'
)
);
echo $reader->getH1(
$gemini
);
```
#### Reader::getLinks
Get document links
```
$gemini = $reader->toGemini(
file_get_contents(
'/host/data/pages/index.txt'
)
);
echo $reader->getLinks(
$gemini
);
```
### Filesystem
Provides methods for simple and secure interaction with DokuWiki file storage
```
$filesystem = new \Yggverse\Gemini\Dokuwiki\Filesystem(
'/host/data' // storage location
);
```
#### Filesystem::getList
Return simple array of all files in storage
```
var_dump (
$filesystem->getList(
'hello:world'
)
);
```
#### Filesystem::getTree
Return all files under the storage folder in tree format
```
var_dump (
$filesystem->getTree(
'hello:world'
)
);
```
#### Filesystem::getPagePathsByPath
Return pages under the given data directory
```
var_dump (
$filesystem->getPagePathsByPath(
// absolute path to target data directory (e.g. Filesystem::getDirectoryPathByUri)
)
);
```
#### Filesystem::getDirectoryPathByUri
#### Filesystem::getPagePathByUri
Return absolute path to stored page file
```
var_dump (
$filesystem->getPagePathByUri(
'hello:world'
)
);
```
#### Filesystem::getDirectoryUriByPath
#### Filesystem::getPageUriByPath
Return page URI in `dokuwiki:format`
```
var_dump (
$filesystem->getPageUriByPath(
'/full/path/to/page.txt'
)
);
```
#### Filesystem::getMediaPathByUri
Return absolute path to stored media file
```
var_dump (
$filesystem->getMediaPathByUri(
'hello:world'
)
);
```
#### Filesystem::getMimeByPath
Return file MIME if path match storage item
```
var_dump (
$filesystem->getMimeByPath(
'/full/path/to/page.txt'
)
);
```
#### Filesystem::getDataByPath
Return file content if path match storage item
```
var_dump (
$filesystem->getDataByPath(
'/full/path/to/page.txt'
)
);
```
#### Filesystem::isPath
Check path exist and match storage item
```
var_dump (
$filesystem->isPath(
'/full/path/to/page.txt'
)
);
```
### Helper
Useful methods to minify controller codebase
```
$helper = new \Yggverse\Gemini\Dokuwiki\Helper(
new \Yggverse\Gemini\Dokuwiki\Filesystem(),
new \Yggverse\Gemini\Dokuwiki\Reader()
);
```
#### Helper::getChildrenSectionLinksByUri
Return simple array of children section links in Gemini format
```
var_dump (
$helper->getChildrenSectionLinksByUri(
'hello:world'
)
);
```
#### Helper::getChildrenPageLinksByUri
Return simple array of children page links in Gemini format
```
var_dump (
$helper->getChildrenPageLinksByUri(
'hello:world'
)
);
```
#### Helper::getPageLinkByPath
Return page link (that contain document name) in Gemini format
```
var_dump (
$helper->getPageLinkByPath(
$filesystem->getPagePathByUri(
'hello:world'
)
)
);
```

View file

@ -1,7 +1,7 @@
{ {
"name": "yggverse/gemini", "name": "yggverse/gemini",
"description": "PHP 8 Library for Gemini Protocol", "description": "PHP 8 Library for Gemini Protocol",
"keywords": [ "yggverse", "gemini", "wiki", "dokuwiki", "markdown" ], "keywords": [ "yggverse", "gemini", "gemini-protocol", "client", "request", "response" ],
"homepage": "https://github.com/yggverse/gemini-php", "homepage": "https://github.com/yggverse/gemini-php",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
@ -10,7 +10,5 @@
"Yggverse\\Gemini\\": "src/" "Yggverse\\Gemini\\": "src/"
} }
}, },
"require": { "require": {}
"dekor/php-array-table": "^2.0"
}
} }

View file

@ -6,6 +6,8 @@ namespace Yggverse\Gemini\Client;
class Request class Request
{ {
private ?string $_ip = null;
private string $_host; private string $_host;
private int $_port; private int $_port;
private string $_path; private string $_path;
@ -15,12 +17,14 @@ class Request
[ [
'ssl' => 'ssl' =>
[ [
'verify_peer' => false, 'allow_self_signed' => true,
'verify_peer_name' => false 'disable_compression' => true,
'verify_peer_name' => false,
'verify_peer' => false
] ]
]; ];
public function __construct(string $url) public function __construct(string $url, ?string $ip = null)
{ {
if ($host = parse_url($url, PHP_URL_HOST)) if ($host = parse_url($url, PHP_URL_HOST))
{ {
@ -31,7 +35,9 @@ class Request
else else
{ {
throw new Exception(); // @TODO throw new \Exception(
_('Host required')
);
} }
if ($port = parse_url($url, PHP_URL_PORT)) if ($port = parse_url($url, PHP_URL_PORT))
@ -75,6 +81,13 @@ class Request
'' ''
); );
} }
if ($ip && false !== filter_var($ip, FILTER_VALIDATE_IP))
{
$this->setResolvedHost(
$ip
);
}
} }
public function setOptions(array $value): void public function setOptions(array $value): void
@ -127,6 +140,16 @@ class Request
return $this->_query; return $this->_query;
} }
public function setResolvedHost(?string $value): void
{
$this->_ip = $value;
}
public function getResolvedHost(): ?string
{
return $this->_ip;
}
public function getResponse( public function getResponse(
int $timeout = 30, // socket timeout, useful for offline resources int $timeout = 30, // socket timeout, useful for offline resources
?int $limit = null, // content length, null for unlimited ?int $limit = null, // content length, null for unlimited
@ -139,7 +162,7 @@ class Request
$connection = stream_socket_client( $connection = stream_socket_client(
sprintf( sprintf(
'tls://%s:%d', 'tls://%s:%d',
$this->_host, $this->_ip ? $this->_ip : $this->_host,
$this->_port $this->_port
), ),
$code, $code,
@ -163,7 +186,10 @@ class Request
$this->_host, $this->_host,
$this->_port, $this->_port,
$this->_path, $this->_path,
$this->_query $this->_query ? sprintf(
'?%s',
$this->_query
) : null
) )
); );

View file

@ -17,7 +17,7 @@ class Response
$match = []; $match = [];
preg_match( preg_match(
'/(?<code>\d{2})\s(?<meta>.*)\r\n(?<body>.*)/su', '/^(?<code>\d{2})(?<meta>.*)$/m',
$data, $data,
$match $match
); );
@ -37,14 +37,16 @@ class Response
if (isset($match['meta']) && mb_strlen($match['meta']) <= 1024) if (isset($match['meta']) && mb_strlen($match['meta']) <= 1024)
{ {
$this->setMeta( $this->setMeta(
(string) $match['meta'] trim(
(string) $match['meta']
)
); );
} }
if (isset($match['body'])) if ($body = substr($data, strpos($data, chr(10)) + 1))
{ {
$this->setBody( $this->setBody(
(string) (string) $match['body'] (string) $body
); );
} }
} }

View file

@ -1,277 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Gemini\Dokuwiki;
class Filesystem
{
private $_path;
private $_tree = [];
private $_list = [];
public function __construct(string $path)
{
$this->_path = rtrim(
$path,
'/'
);
$this->_index(
$this->_path
);
}
public function getTree(): array
{
return $this->_tree;
}
public function getList(): array
{
return $this->_list;
}
public function getPagePathsByPath(string $path): ?array
{
if (isset($this->_tree[$path]))
{
return $this->_tree[$path];
}
return null;
}
public function getPagePathByUri(string $uri): ?string
{
$path = sprintf(
'%s/pages/%s.txt',
$this->_path,
str_replace(
':',
'/',
mb_strtolower(
urldecode(
$uri
)
)
)
);
if (!$this->isPath($path))
{
return null;
}
return $path;
}
public function getPageUriByPath(string $path): ?string
{
if (!$this->isPath($path))
{
return null;
}
$path = str_replace(
sprintf(
'%s/pages/',
$this->_path
),
'',
$path
);
$path = trim(
$path,
'/'
);
$path = str_replace(
[
'/',
'.txt'
],
[
':',
null
],
$path
);
return $path;
}
public function getDirectoryPathByUri(string $uri = ''): ?string
{
$path = rtrim(
sprintf(
'%s/pages/%s',
$this->_path,
str_replace(
':',
'/',
mb_strtolower(
urldecode(
$uri
)
)
)
),
'/'
);
if (!isset($this->_tree[$path]) || !is_dir($path) || !is_readable($path))
{
return null;
}
return $path;
}
public function getDirectoryUriByPath(string $path): ?string
{
if (!isset($this->_tree[$path]) || !is_dir($path) || !is_readable($path))
{
return null;
}
$path = str_replace(
sprintf(
'%s/pages',
$this->_path
),
'',
$path
);
$path = trim(
$path,
'/'
);
$path = str_replace(
[
'/'
],
[
':'
],
$path
);
return $path;
}
public function getMediaPathByUri(string $uri): ?string
{
$path = sprintf(
'%s/media/%s',
$this->_path,
str_replace(
':',
'/',
mb_strtolower(
urldecode(
$uri
)
)
)
);
if (!$this->isPath($path))
{
return null;
}
return $path;
}
public function getMimeByPath(?string $path): ?string
{
if ($this->isPath($path))
{
if ($mime = mime_content_type($path))
{
return $mime;
}
}
return null;
}
public function getDataByPath(?string $path): ?string
{
if ($this->isPath($path))
{
if ($data = file_get_contents($path))
{
return $data;
}
}
return null;
}
public function isPath(?string $path): bool
{
if (in_array($path, $this->_list) && is_file($path) && is_readable($path))
{
return true;
}
return false;
}
private function _index(
string $path,
?array $blacklist = ['sidebar.txt', '__template.txt']
): void
{
foreach ((array) scandir($path) as $file)
{
if (str_starts_with($file, '.'))
{
continue;
}
if (is_link($file))
{
continue;
}
if (in_array($file, $blacklist))
{
continue;
}
$file = sprintf(
'%s/%s',
$path,
$file
);
switch (true)
{
case is_dir($file):
if (!isset($this->_tree[$path]))
{
$this->_tree[$path] = [];
}
$this->_index($file);
break;
case is_file($file):
$this->_tree[$path][] = $file;
$this->_list[] = $file;
break;
}
}
}
}

View file

@ -1,154 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Gemini\Dokuwiki;
class Helper
{
private \Yggverse\Gemini\Dokuwiki\Filesystem $_filesystem;
private \Yggverse\Gemini\Dokuwiki\Reader $_reader;
public function __construct(
\Yggverse\Gemini\Dokuwiki\Filesystem $filesystem,
\Yggverse\Gemini\Dokuwiki\Reader $reader
) {
$this->_filesystem = $filesystem;
$this->_reader = $reader;
}
public function getChildrenSectionLinksByUri(?string $uri = ''): array
{
$sections = [];
if ($directory = $this->_filesystem->getDirectoryPathByUri($uri))
{
foreach ((array) $this->_filesystem->getTree() as $path => $files)
{
if (str_starts_with($path, $directory) && $path != $directory)
{
// Init link name
$h1 = null;
// Init this directory URI
$thisUri = $this->_filesystem->getDirectoryUriByPath(
$path
);
// Skip sections deeper this level
if (substr_count($thisUri, ':') > ($uri ? substr_count($uri, ':') + 1 : 0))
{
continue;
}
// Get section names
$segments = [];
foreach ((array) explode(':', $thisUri) as $segment)
{
$segments[] = $segment;
// Find section index if exists
if ($file = $this->_filesystem->getPagePathByUri(implode(':', $segments) . ':' . $segment))
{
$h1 = $this->_reader->getH1(
$this->_reader->toGemini(
$this->_filesystem->getDataByPath(
$file
)
)
);
}
// Find section page if exists
else if ($file = $this->_filesystem->getPagePathByUri(implode(':', $segments)))
{
$h1 = $this->_reader->getH1(
$this->_reader->toGemini(
$this->_filesystem->getDataByPath(
$file
)
)
);
}
// Reset title of undefined segment
else
{
$h1 = null;
}
}
// Register section link
$sections[] = sprintf(
'=> /%s %s',
$thisUri,
$h1
);
}
}
}
// Keep unique
$sections = array_unique(
$sections
);
// Sort asc
sort(
$sections
);
return $sections;
}
public function getChildrenPageLinksByUri(?string $uri = ''): array
{
$pages = [];
if ($directory = $this->_filesystem->getDirectoryPathByUri($uri))
{
foreach ((array) $this->_filesystem->getPagePathsByPath($directory) as $file)
{
if ($link = $this->getPageLinkByPath($file))
{
$pages[] = $link;
}
}
}
// Keep unique
$pages = array_unique(
$pages
);
// Sort asc
sort(
$pages
);
return $pages;
}
public function getPageLinkByPath(string $path): ?string
{
if (in_array($path, $this->_filesystem->getList()) && is_file($path) && is_readable($path))
{
return sprintf(
'=> /%s %s',
$this->_filesystem->getPageUriByPath(
$path
),
$this->_reader->getH1(
$this->_reader->toGemini(
$this->_filesystem->getDataByPath(
$path
)
)
)
);
}
return null;
}
}

View file

@ -1,412 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Gemini\Dokuwiki;
use dekor\ArrayToTextTable;
class Reader
{
private array $_macros =
[
'~URL:base~' => null,
'~IPv6:open~' => '[',
'~IPv6:close~' => ']',
'~LINE:break~' => PHP_EOL
];
private array $_rule =
[
// Headers
'/^([\s]*)#([^#]+)/' => '$1#$2' . PHP_EOL,
'/^([\s]*)##([^#]+)/' => '$1##$2' . PHP_EOL,
'/^([\s]*)###([^#]+)/' => '$1###$2' . PHP_EOL,
'/^([\s]*)####([^#]+)/' => '$1###$2' . PHP_EOL,
'/^([\s]*)#####([^#]+)/' => '$1###$2' . PHP_EOL,
'/^([\s]*)######([^#]+)/' => '$1###$2' . PHP_EOL,
'/^[\s]*[=]{6}([^=]+)[=]{6}/' => '# $1' . PHP_EOL,
'/^[\s]*[=]{5}([^=]+)[=]{5}/' => '## $1' . PHP_EOL,
'/^[\s]*[=]{4}([^=]+)[=]{4}/' => '### $1' . PHP_EOL,
'/^[\s]*[=]{3}([^=]+)[=]{3}/' => '### $1' . PHP_EOL,
'/^[\s]*[=]{2}([^=]+)[=]{2}/' => '### $1' . PHP_EOL,
'/^[\s]*[=]{1}([^=]+)[=]{1}/' => '### $1' . PHP_EOL,
// Tags
'/\*\*/' => '',
'/\'\'/' => '',
'/\%\%/' => '',
'/(?<!:)\/\//' => '',
// Remove extra spaces
'/(\s)\s+/' => '$1',
// Links
/// Detect IPv6 (used as no idea how to resolve square quotes in rules below)
'/\[\[([^\[]+)\[([A-f:0-9]*)\]([^\]]+)\]\]/' => '$1~IPv6:open~$2~IPv6:close~$3',
/// Remove extra chars
'/\[\[\s*\:?([^\|]+)\s*\|\s*([^\]]+)\s*\]\]/' => '[[$1|$2]]',
'/\[\[\s*\:?([^\]]+)\s*\]\]/' => '[[$1]]',
'/\{\{\s*\:?([^\|]+)\s*\|\s*([^\}]+)\s*\}\}/' => '{{$1|$2}}',
'/\{\{\s*\:?([^\}]+)\s*\}\}/' => '{{$1}}',
/// Wikipedia
'/\[\[wp([A-z]{2,})>([^\|]+)\|([^\]]+)\]\]/ui' => '$3 ( https://$1.wikipedia.org/wiki/$2 )',
'/\[\[wp>([^\|]+)\|([^\]]+)\]\]/i' => '$2 ( https://en.wikipedia.org/wiki/$1 )',
'/\[\[wp([A-z]{2,})>([^\]]+)\]\]/i' => '$2 ( https://$1.wikipedia.org/wiki/$2 )',
'/\[\[wp>([^\]]+)\]\]/i' => '$1 ( https://en.wikipedia.org/wiki/$1 )',
/// Dokuwiki
'/\[\[doku>([^\|]+)\|([^\]]+)\]\]/i' => '$2( https://www.dokuwiki.org/$1 )',
'/\[\[doku>([^\]]+)\]\]/i' => '$1( https://www.dokuwiki.org/$1 )',
/// Index
/// Useful with src/Dokuwiki/Helper.php
'/\{\{indexmenu>:([^\}]+)\}\}/i' => '',
'/\{\{indexmenu_n>[\d]+\}\}/i' => '',
// Related
'/\[\[this>([^\|]+)\|([^\]]+)\]\]/i' => '$2',
/// Relative
'/\[\[(?!https?:|this|doku|wp[A-z]{0,2})([^\|]+)\|([^\]]+)\]\]/i' => ' $2$3 ( ~URL:base~$1 )',
'/\[\[(?!https?:|this|doku|wp[A-z]{0,2})([^\]]+)\]\]/i' => ' $2 ( ~URL:base~$1 )',
/// Absolute
'/\[\[(https?:)([^\|]+)\|([^\]]+)\]\]/i' => '$3 ( $1$2 )',
'/\[\[(https?:)([^\]]+)\]\]/i' => '$1$2', // @TODO
/// Media
'/\{\{(?!https?:)([^\|]+)\|([^\}]+)\}\}/i' => PHP_EOL . '=> /$1$2' . PHP_EOL,
'/\{\{(?!https?:)([^\}]+)\}\}/i' => PHP_EOL . '=> /$1$2' . PHP_EOL,
// List
'/^[\s]?-/' => '* ',
'/^[\s]+\*/' => '*',
// Separators
'/[\\\]{2}/' => '~LINE:break~',
// Plugins
'/~~DISCUSSION~~/' => '', // @TODO
'/~~INFO:syntaxplugins~~/' => '', // @TODO
// Final corrections
'/[\n\r]+[.,;:]+/' => PHP_EOL
];
public function __construct(?array $rules = null)
{
if ($rules)
{
$this->_rule = $rules;
}
}
// Macros operations
public function getMacroses(): array
{
$this->_macros;
}
public function setMacroses(array $macros)
{
$this->_macros = $macros;
}
public function getMacros(string $key, string $value): ?string
{
$this->_macros[$key] = isset($this->_macros[$key]) ? $value : null;
}
public function setMacros(string $key, ?string $value): void
{
if ($value)
{
$this->_macros[$key] = $value;
}
else
{
unset(
$this->_macros[$key]
);
}
}
// Rule operations
public function getRules(): array
{
$this->_rule;
}
public function setRules(array $rules)
{
$this->_rule = $rules;
}
public function getRule(string $key, string $value): ?string
{
$this->_rule[$key] = isset($this->_rule[$key]) ? $value : null;
}
public function setRule(string $key, ?string $value): void
{
if ($value)
{
$this->_rule[$key] = $value;
}
else
{
unset(
$this->_rule[$key]
);
}
}
// Convert DokuWiki text to Gemini
public function toGemini(?string $data, ?array &$lines = []): ?string
{
if (empty($data))
{
return null;
}
$raw = false;
$lines = [];
foreach ((array) explode(PHP_EOL, $data) as $line)
{
// Skip any formatting in lines between code tag
if (!$raw && preg_match('/<(code|file)([^>]*)>/i', $line, $matches))
{
// Prepend tag meta or filename as plain description
if (!empty($matches[0]))
{
$lines[] = preg_replace(
'/<(code|file)[\s-]*([^>]*)>/i',
'$2',
$matches[0]
);
}
$lines[] = '```';
$lines[] = preg_replace(
'/<\/?(code|file)[^>]*>/i',
'',
$line
);
$raw = true;
// Make sure inline tag closed
if (preg_match('/<\/(code|file)>/i', $line))
{
$lines[] = '```';
$raw = false;
continue;
}
continue;
}
if ($raw && preg_match('/<\/(code|file)>/i', $line))
{
$lines[] = preg_replace(
'/<\/(code|file)>/i',
'',
$line
);
$lines[] = '```';
$raw = false;
continue;
}
if ($raw)
{
$lines[] = preg_replace(
'/^```/',
' ```',
$line
);
continue;
}
// Apply config
$lines[] = preg_replace(
array_keys(
$this->_rule
),
array_values(
$this->_rule
),
strip_tags(
$line
)
);
}
// ASCII table
$table = false;
$rows = [];
$th = [];
foreach ($lines as $index => $line)
{
// Strip line breaks
$line = str_replace(
'~LINE:break~',
' ',
$line
);
// Header
if (!$table && preg_match_all('/\^([^\^]+)/', $line, $matches))
{
if (!empty($matches[1]))
{
$table = true;
$rows = [];
$th = [];
foreach ($matches[1] as $value)
{
$th[] = trim(
$value
);
}
unset(
$lines[$index]
);
continue;
}
}
// Body
if ($table)
{
$table = false;
if (preg_match(sprintf('/%s\|/', str_repeat('\|(.*)', count($th))), $line, $matches))
{
if (count($matches) == count($th) + 1)
{
$table = true;
$row = [];
foreach ($th as $offset => $column)
{
$row[$column] = trim(
$matches[$offset + 1]
);
}
$rows[] = $row;
unset(
$lines[$index]
);
}
}
if (!$table && $rows)
{
$builder = new ArrayToTextTable(
$rows
);
$lines[$index] = '```' . PHP_EOL . $builder->render() . PHP_EOL . '```';
}
}
}
// Merge lines
return preg_replace(
'/[\n\r]{2,}/',
PHP_EOL . PHP_EOL,
str_replace(
array_keys(
$this->_macros
),
array_values(
$this->_macros
),
implode(
PHP_EOL,
$lines
)
)
);
}
public function getH1(?string $gemini, ?string $regex = '/^[\s]?#([^#]+)/'): ?string
{
foreach ((array) explode(PHP_EOL, (string) $gemini) as $line)
{
preg_match(
$regex,
$line,
$matches
);
if (!empty($matches[1]))
{
return trim(
$matches[1]
);
break;
}
}
return null;
}
public function getLinks(?string $gemini, ?string $regex = '/(https?|gemini):\/\/\S+/'): array
{
$links = [];
if (empty($gemini))
{
return $links;
}
preg_match_all(
$regex,
$gemini,
$matches
);
if (!empty($matches[0]))
{
foreach ((array) $matches[0] as $link)
{
$links[] = trim(
$link
);
}
}
return array_unique(
$links
);
}
}

View file

@ -1,193 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Gemini\Gemtext;
class Body
{
private array $_lines = [];
public function __construct(string $gemtext)
{
foreach ((array) explode(PHP_EOL, $gemtext) as $line)
{
$this->_lines[] = $line;
}
}
public function getH1(): array
{
$matches = [];
foreach ($this->_lines as $line)
{
if (preg_match('/^#([^#]+)/', trim($line), $match))
{
$matches[] = trim(
$match[1]
);
}
}
return $matches;
}
public function getH2(): array
{
$matches = [];
foreach ($this->_lines as $line)
{
if (preg_match('/^##([^#]+)/', trim($line), $match))
{
$matches[] = trim(
$match[1]
);
}
}
return $matches;
}
public function getH3(): array
{
$matches = [];
foreach ($this->_lines as $line)
{
if (preg_match('/^###([^#]+)/', trim($line), $match))
{
$matches[] = trim(
$match[1]
);
}
}
return $matches;
}
public function getLinks(): array
{
$matches = [];
foreach ($this->_lines as $line)
{
if (preg_match('/^=>(.*)/', trim($line), $match))
{
$matches[] = trim(
$match[1]
);
}
}
return $matches;
}
public function findLinks(string $protocol = 'gemini'): array
{
$matches = [];
foreach ($this->_lines as $line)
{
if (preg_match('/' . $protocol . ':\/\/(.*)[\s\S\'"]*/', trim($line), $match))
{
$matches[] =
sprintf(
'%s://%s',
$protocol,
trim(
$match[1]
)
);
}
}
return $matches;
}
public function skipTags(array $tags = []): string
{
$lines = [];
foreach ($this->_lines as $line)
{
$line = trim(
$line
);
if ($tags)
{
foreach ($tags as $tag)
{
if(!in_array($tag, ['#', '##', '###', '=>', '*', '```']))
{
continue;
}
switch (true)
{
case str_starts_with($line, '#'):
$line = preg_replace(
sprintf(
'/^%s([^#]+)/ui',
$tag
),
'$1',
$line
);
break;
case str_starts_with($line, '*'):
$line = preg_replace(
'/^\*(.*)/ui',
'$1',
$line
);
break;
default:
$line = preg_replace(
sprintf(
'/^%s(.*)/ui',
$tag
),
'$1',
$line
);
}
}
}
else
{
$line = preg_replace(
[
'/^#([^#]+)/ui',
'/^##([^#]+)/ui',
'/^###([^#]+)/ui',
'/^=>(.*)/ui',
'/^\*(.*)/ui',
'/^```(.*)/ui',
],
'$1',
$line
);
}
$lines[] = trim(
$line
);
}
return implode(
PHP_EOL,
$lines
);
}
}

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace Yggverse\Gemini\Gemtext;
class Link
{
private string $_line;
public function __construct(string $line)
{
$this->_line = $line;
}
public function getAddress(): ?string
{
if (preg_match('/^([^\s]+)\s.*/', trim($this->_line), $match))
{
return trim(
$match[1]
);
}
return null;
}
public function getDate(?int &$timestamp = null): ?string
{
if (preg_match('/\s([\d]+-[\d+]+-[\d]+)\s/', trim($this->_line), $match))
{
if ($result = strtotime($match[1]))
{
$timestamp = $result;
return trim(
$match[1]
);
}
}
return null;
}
public function getAlt(): ?string
{
if (preg_match('/\s[\d]+-[\d+]+-[\d]+\s(.*)$/', trim($this->_line), $match))
{
return trim(
$match[1]
);
}
else if (preg_match('/\s(.*)$/', trim($this->_line), $match))
{
return trim(
$match[1]
);
}
return null;
}
}