Overview
Comment:Add markdown support, start of implementing web blocks
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | dev
Files: files | file ages | folders
SHA3-256: 9773a33ba42bc128a64a272b4802c900d99a029947c0c6cddc12c11a1ed742ed
User & Date: bohwaz on 2021-05-02 12:18:44
Other Links: branch diff | manifest | tags
Context
2021-05-20
03:31
Add markdown support (with TOC) check-in: 2965992701 user: bohwaz tags: trunk
2021-05-02
12:41
Don't allow rename of transactions and users files check-in: 36d8a33144 user: bohwaz tags: dev
12:18
Add markdown support, start of implementing web blocks check-in: 9773a33ba4 user: bohwaz tags: dev
2021-04-30
23:11
Rename tables and columns to english check-in: 3ba8ecbcc0 user: bohwaz tags: dev
Changes

Modified src/Makefile from [605948c091] to [4afd2ef457].

8
9
10
11
12
13
14



15
16
17
18
19
20
21
	wget ${KD2_FILE} -O ${TMP_KD2}/kd2.zip

	rm -rf "include/lib/KD2"
	unzip "${TMP_KD2}/kd2.zip" -d include/lib

	rm -rf ${TMP_KD2}




dev-server:
	php -S localhost:8082 -t www www/_route.php

test:
	find . -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null

phpstan:







>
>
>







8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	wget ${KD2_FILE} -O ${TMP_KD2}/kd2.zip

	rm -rf "include/lib/KD2"
	unzip "${TMP_KD2}/kd2.zip" -d include/lib

	rm -rf ${TMP_KD2}

	wget -O "include/lib/Parsedown.php" "https://raw.githubusercontent.com/erusev/parsedown/1.7.x/Parsedown.php"
	wget -O "include/lib/ParsedownExtra.php" "https://raw.githubusercontent.com/erusev/parsedown-extra/0.8.x/ParsedownExtra.php"

dev-server:
	php -S localhost:8082 -t www www/_route.php

test:
	find . -name '*.php' -print0 | xargs -0 -n1 php -l > /dev/null

phpstan:

Modified src/include/lib/Garradin/Entities/Web/Page.php from [c5b7261d79] to [7794458169].

45
46
47
48
49
50
51

52
53
54
55

56
57
58
59
60
61
62
		'published' => 'DateTime',
		'modified'  => 'DateTime',
		'content'   => 'string',
	];

	const FORMAT_SKRIV = 'skriv';
	const FORMAT_ENCRYPTED = 'skriv/encrypted';


	const FORMATS_LIST = [
		self::FORMAT_SKRIV => 'SkrivML',
		self::FORMAT_ENCRYPTED => 'Chiffré',

	];

	const STATUS_ONLINE = 'online';
	const STATUS_DRAFT = 'draft';

	const STATUS_LIST = [
		self::STATUS_ONLINE => 'En ligne',







>




>







45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
		'published' => 'DateTime',
		'modified'  => 'DateTime',
		'content'   => 'string',
	];

	const FORMAT_SKRIV = 'skriv';
	const FORMAT_ENCRYPTED = 'skriv/encrypted';
	const FORMAT_MARKDOWN = 'markdown';

	const FORMATS_LIST = [
		self::FORMAT_SKRIV => 'SkrivML',
		self::FORMAT_ENCRYPTED => 'Chiffré',
		self::FORMAT_MARKDOWN => 'Markdown',
	];

	const STATUS_ONLINE = 'online';
	const STATUS_DRAFT = 'draft';

	const STATUS_LIST = [
		self::STATUS_ONLINE => 'En ligne',

Added src/include/lib/Garradin/Web/Render/AttachmentAwareTrait.php version [5455ee42ef].



























































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php

namespace Garradin\Web\Render;

use Garradin\Utils;
use Garradin\Entities\Files\File;


trait AttachmentAwareTrait
{
	protected $current_path;
	protected $context;
	protected $link_prefix;
	protected $link_suffix;

	protected function resolveAttachment(string $uri) {
		$prefix = $this->current_path;
		$pos = strpos($uri, '/');

		// "Image.jpg"
		if ($pos === false) {
			return WWW_URL . $prefix . '/' . $uri;
		}
		// "bla/Image.jpg" outside of web context
		elseif ($this->context !== File::CONTEXT_WEB && $pos !== 0) {
			return WWW_URL . $this->context . '/' . $uri;
		}
		// "bla/Image.jpg" in web context or absolute link, eg. "/transactions/2442/42.jpg"
		else {
			return WWW_URL . ltrim($uri, '/');
		}
	}

	protected function resolveLink(string $uri) {
		$first = substr($uri, 0, 1);
		if ($first == '/' || $first == '!') {
			return Utils::getLocalURL($uri);
		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= $this->link_suffix;
		}

		return $this->link_prefix . $uri;
	}

	public function isRelativeTo(File $file) {
		$this->current_path = Utils::dirname($file->path);
		$this->context = strtok($this->current_path, '/');
		$this->link_suffix = '';

		if ($this->context === File::CONTEXT_WEB) {
			$this->link_prefix = WWW_URL . '/';
			$this->current_path = Utils::basename(Utils::dirname($file->path));
		}
		else {
			$this->link_prefix = $options['prefix'] ?? sprintf(ADMIN_URL . 'common/files/preview.php?p=%s/', $this->context);
			$this->link_suffix = '.skriv';
		}
	}
}

Added src/include/lib/Garradin/Web/Render/Blocks.php version [7866908ee6].





















































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Squelette_Filtres;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Files\Files;
use Garradin\UserTemplate\CommonModifiers;

use Parsedown;
use Parsedown_Extra;

use const Garradin\{ADMIN_URL, WWW_URL};

/*
			display: grid;
			grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
			grid-gap: 10px;

 */

class Blocks
{
	static protected $parsedown;
	protected $_stack = [];

	public function render(?File $file, ?string $content = null, array $options = []): string
	{
		$str = $content ?? $file->fetch();

		$out = '<div class="web-blocks">';

		// Skip page metadata
		strtok($str, "\n\n----\n\n");

		while ($block = strtok("\n\n----\n\n")) {
			$out .= $this->block($block);
		}

		if ($block = strtok('')) {
			$out .= $this->block($block);
		}

		strtok('', ''); // Free memory

		foreach ($this->_stack as $type) {
			$out .= '</article></section>';
		}

		$out .= '</div>';

		return $out;
	}

	protected function block(string $block): string
	{
		$header = strtok($block, "\n\n");
		$content = strtok('');
		strtok('', ''); // Free memory
		$out  = '';

		$content = trim($content, "\n");
		$class = sprintf('web-block-%s', $type);

		switch ($type) {
			case 'columns':
				if (array_pop($this->_stack)) {
					$out .= '</article></section>';
				}

				$out .= '<section class="web-columns">';
				return $out;
			case 'column':
				if (array_pop($this->_stack)) {
					$out .= '</article>';
				}

				$out .= '<article class="web-column">';
				$this->_stack[] = 'column';
				return $out;
			case 'code':
				return sprintf('<pre class="%s">%s</pre>', $class, htmlspecialchars($content));
			case 'markdown':
				$md = new Markdown;
				return sprintf('<div class="web-content %s">%s</div>', $class, $md->render($content));
			case 'skriv':
				$skriv = new Skriv;
				return sprintf('<div class="web-content %s">%s</div>', $class, $skriv->render($content));
			case 'image':
				return sprintf('<div class="%s">%s</div>', $this->image($content));
			case 'gallery':
				return sprintf('<div class="%s">%s</div>', $this->gallery($content));
			case 'heading':
				return sprintf('<h2 class="%s">%s</h2>', $class, htmlspecialchars($content));
			case 'quote':
				return sprintf('<blockquote class="%s"><p>%s</p></blockquote>', $class, nl2br(htmlspecialchars($content)));
			case 'video':
				return sprintf('<iframe class="%s" src="%s" frameborder="0" allow="accelerometer; encrypted-media; gyroscope" allowfullscreen></iframe>', $class, htmlspecialchars($content));
			default:
				throw new \LogicException('Unknown type: ' . $type);
		}
	}
}

Modified src/include/lib/Garradin/Web/Render/EncryptedSkriv.php from [8b803e3b49] to [d0d2a69554].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;
use Garradin\Template;
use const Garradin\ADMIN_URL;

class EncryptedSkriv
{
	static public function render(File $file, ?string $content = null): string
	{
		$tpl = Template::getInstance();
		$content = $content ?? $file->fetch();
		$tpl->assign('admin_url', ADMIN_URL);
		$tpl->assign(compact('file', 'content'));
		return $tpl->fetch('common/files/_file_render_encrypted.tpl');
	}
}










|








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;
use Garradin\Template;
use const Garradin\ADMIN_URL;

class EncryptedSkriv
{
	public function render(File $file, ?string $content = null): string
	{
		$tpl = Template::getInstance();
		$content = $content ?? $file->fetch();
		$tpl->assign('admin_url', ADMIN_URL);
		$tpl->assign(compact('file', 'content'));
		return $tpl->fetch('common/files/_file_render_encrypted.tpl');
	}
}

Added src/include/lib/Garradin/Web/Render/Markdown.php version [7e10d16363].

























































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Squelette_Filtres;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Files\Files;
use Garradin\UserTemplate\CommonModifiers;

use Parsedown;
use Parsedown_Extra;

use const Garradin\{ADMIN_URL, WWW_URL};

class Markdown
{
	static protected $parsedown;

	public function render(?File $file, ?string $content = null, array $options = []): string
	{
		if (!self::$parsedown)
		{
			self::$parsedown = new ParsedownExtra;
			self::$parsedown->setBreaksEnabled(true);
			self::$parsedown->setUrlsLinked(true);
			self::$parsedown->setSafeMode(true);
		}

		$str = $content ?? $file->fetch();

		$str = self::$parsedown->text($str);

		$str = CommonModifiers::typo($str);

		$str = preg_replace_callback(';<a href="((?!https?://|\w+:).+)">;i', function ($matches) {
			return sprintf('<a href="%s" target="_parent">', $this->resolveLink($matches[1]));
		}, $str);

		return sprintf('<div class="web-content">%s</div>', $str);
	}
}

Modified src/include/lib/Garradin/Web/Render/Skriv.php from [0996e6e8e0] to [0ea4ef11c0].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;

use Garradin\Squelette_Filtres;
use Garradin\Plugin;
use Garradin\Utils;
use Garradin\Files\Files;
use Garradin\UserTemplate\CommonModifiers;

use KD2\SkrivLite;

use const Garradin\{ADMIN_URL, WWW_URL};

class Skriv
{
	static protected $skriv;

	static protected $current_path;
	static protected $context;
	static protected $link_prefix;
	static protected $link_suffix;

	static protected function resolveAttachment(string $uri) {
		$prefix = self::$current_path;
		$pos = strpos($uri, '/');

		// "Image.jpg"
		if ($pos === false) {
			return WWW_URL . $prefix . '/' . $uri;
		}
		// "bla/Image.jpg" outside of web context
		elseif (self::$context !== File::CONTEXT_WEB && $pos !== 0) {
			return WWW_URL . self::$context . '/' . $uri;
		}
		// "bla/Image.jpg" in web context or absolute link, eg. "/transactions/2442/42.jpg"
		else {
			return WWW_URL . ltrim($uri, '/');
		}
	}

	static public function resolveLink(string $uri) {
		$first = substr($uri, 0, 1);
		if ($first == '/' || $first == '!') {
			return Utils::getLocalURL($uri);
		}

		if (strpos(Utils::basename($uri), '.') === false) {
			$uri .= self::$link_suffix;
		}

		return self::$link_prefix . $uri;
	}

	static public function render(?File $file, ?string $content = null, array $options = []): string
	{
		if (!self::$skriv)
		{
			self::$skriv = new \KD2\SkrivLite;
			self::$skriv->registerExtension('file', [self::class, 'SkrivFile']);
			self::$skriv->registerExtension('image', [self::class, 'SkrivImage']);

			// Enregistrer d'autres extensions éventuellement
			Plugin::fireSignal('skriv.init', ['skriv' => self::$skriv]);
		}

		$skriv =& self::$skriv;

		if ($file) {
			self::$current_path = Utils::dirname($file->path);
			self::$context = strtok(self::$current_path, '/');
			self::$link_suffix = '';

			if (self::$context === File::CONTEXT_WEB) {
				self::$link_prefix = WWW_URL . '/';
				self::$current_path = Utils::basename(Utils::dirname($file->path));
			}
			else {
				self::$link_prefix = $options['prefix'] ?? sprintf(ADMIN_URL . 'common/files/preview.php?p=%s/', self::$context);
				self::$link_suffix = '.skriv';
			}
		}

		$str = $content ?? $file->fetch();

		$str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) {
			return self::resolveAttachment($match[1]);
		}, $str);

		$str = self::$skriv->render($str);

		$str = CommonModifiers::typo($str);

		$str = preg_replace_callback(';<a href="((?!https?://|\w+:).+)">;i', function ($matches) {
			return sprintf('<a href="%s" target="_parent">', self::resolveLink($matches[1]));
		}, $str);

		return sprintf('<div class="web-content">%s</div>', $str);
	}

	/**
	 * Callback utilisé pour l'extension <<file>> dans le wiki-texte
	 * @param array $args    Arguments passés à l'extension
	 * @param string $content Contenu éventuel (en mode bloc)
	 * @param SkrivLite $skriv   Objet SkrivLite
	 */
	static public function SkrivFile(array $args, ?string $content, SkrivLite $skriv): string
	{
		$name = $args[0] ?? null;
		$caption = $args[1] ?? null;

		if (!$name || !self::$current_path)
		{
			return $skriv->parseError('/!\ Tag file : aucun nom de fichier indiqué.');
		}

		if (empty($caption))
		{
			$caption = $name;
		}

		$url = self::resolveAttachment($name);
		$ext = substr($name, strrpos($name, '.')+1);

		return sprintf(
			'<aside class="file" data-type="%s"><a href="%s" class="internal-file">%s</a> <small>(%s)</small></aside>',
			htmlspecialchars($ext), htmlspecialchars($url), htmlspecialchars($caption), htmlspecialchars(strtoupper($ext))
		);
	}






<

<
<








|

|
<
<
<

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|














<
<
<
|
<
<
<
<
<
<
<
<





|


|




|
















|









|







1
2
3
4
5
6

7


8
9
10
11
12
13
14
15
16
17
18



19































20
21
22
23
24
25
26
27
28
29
30
31
32
33
34



35








36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<?php

namespace Garradin\Web\Render;

use Garradin\Entities\Files\File;


use Garradin\Plugin;


use Garradin\UserTemplate\CommonModifiers;

use KD2\SkrivLite;

use const Garradin\{ADMIN_URL, WWW_URL};

class Skriv
{
	use AttachmentAwareTrait;

	static protected $skriv;



































	public function render(?File $file, ?string $content = null, array $options = []): string
	{
		if (!self::$skriv)
		{
			self::$skriv = new \KD2\SkrivLite;
			self::$skriv->registerExtension('file', [self::class, 'SkrivFile']);
			self::$skriv->registerExtension('image', [self::class, 'SkrivImage']);

			// Enregistrer d'autres extensions éventuellement
			Plugin::fireSignal('skriv.init', ['skriv' => self::$skriv]);
		}

		$skriv =& self::$skriv;

		if ($file) {



			$this->isRelativeTo($file);








		}

		$str = $content ?? $file->fetch();

		$str = preg_replace_callback('/#file:\[([^\]\h]+)\]/', function ($match) {
			return $this->resolveAttachment($match[1]);
		}, $str);

		$str = $skriv->render($str);

		$str = CommonModifiers::typo($str);

		$str = preg_replace_callback(';<a href="((?!https?://|\w+:).+)">;i', function ($matches) {
			return sprintf('<a href="%s" target="_parent">', $this->resolveLink($matches[1]));
		}, $str);

		return sprintf('<div class="web-content">%s</div>', $str);
	}

	/**
	 * Callback utilisé pour l'extension <<file>> dans le wiki-texte
	 * @param array $args    Arguments passés à l'extension
	 * @param string $content Contenu éventuel (en mode bloc)
	 * @param SkrivLite $skriv   Objet SkrivLite
	 */
	static public function SkrivFile(array $args, ?string $content, SkrivLite $skriv): string
	{
		$name = $args[0] ?? null;
		$caption = $args[1] ?? null;

		if (!$name || !$this->current_path)
		{
			return $skriv->parseError('/!\ Tag file : aucun nom de fichier indiqué.');
		}

		if (empty($caption))
		{
			$caption = $name;
		}

		$url = $this->resolveAttachment($name);
		$ext = substr($name, strrpos($name, '.')+1);

		return sprintf(
			'<aside class="file" data-type="%s"><a href="%s" class="internal-file">%s</a> <small>(%s)</small></aside>',
			htmlspecialchars($ext), htmlspecialchars($url), htmlspecialchars($caption), htmlspecialchars(strtoupper($ext))
		);
	}
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
	{
		static $align_values = ['left', 'right', 'center'];

		$name = $args[0] ?? null;
		$align = $args[1] ?? null;
		$caption = $args[2] ?? null;

		if (!$name || !self::$current_path)
		{
			return $skriv->parseError('/!\ Tag image : aucun nom de fichier indiqué.');
		}

		$url = self::resolveAttachment($name);
		$thumb_url = sprintf('%s?%dpx', $url, $align == 'center' ? 500 : 200);

		$out = sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>',
			htmlspecialchars($url),
			htmlspecialchars($thumb_url),
			htmlspecialchars($caption ?? $name)
		);







|




|







92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
	{
		static $align_values = ['left', 'right', 'center'];

		$name = $args[0] ?? null;
		$align = $args[1] ?? null;
		$caption = $args[2] ?? null;

		if (!$name || !$this->current_path)
		{
			return $skriv->parseError('/!\ Tag image : aucun nom de fichier indiqué.');
		}

		$url = $this->resolveAttachment($name);
		$thumb_url = sprintf('%s?%dpx', $url, $align == 'center' ? 500 : 200);

		$out = sprintf('<a href="%s" class="internal-image" target="_image"><img src="%s" alt="%s" loading="lazy" /></a>',
			htmlspecialchars($url),
			htmlspecialchars($thumb_url),
			htmlspecialchars($caption ?? $name)
		);

Added src/www/admin/static/scripts/web_editor.js version [882b005b1b].





































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
(function () {

const TYPES = ['code', ];

window.WebEditor = class {
	constructor(textarea) {
		this.textarea = textarea;
		this.parse(textarea.value)
		this.editor = 
	}

	build() {

	}

	parse(text) {
		var blocks = text.split("\n\n----\n\n");
		this.blocks = [];

		for (var i = 0; i < blocks.length; i++) {
			let block = blocks[i];

			if ((i % 1) == 0) {
				this.blocks.push(this.parseMeta(block));
			}
			else {
				this.blocks[this.blocks.length - 1].content = block.trim("\r\n");
			}
		}
	}

	parseMeta(text) {
		var lines = text.split("\n");
		var meta = {};

		for (var i = 0; i < lines.length; i++) {
			var line = lines[i].split(':', 2);

			if (line.length != 2) {
				continue;
			}

			meta[line[0].trim().toLowerCase()] = line[1].trim();
		}

		return meta;
	}
}

})();