| File: | lib/Yukki/Web/Plugin/YukkiText.pm |
| Coverage: | 39.4% |
| line | stmt | bran | cond | sub | pod | time | code |
|---|---|---|---|---|---|---|---|
| 1 | package Yukki::Web::Plugin::YukkiText; | ||||||
| 2 | |||||||
| 3 | 1 1 | 500 3 | use v5.24; | ||||
| 4 | 1 1 1 | 3 2 3 | use utf8; | ||||
| 5 | 1 1 1 | 10 4 2 | use Moo; | ||||
| 6 | |||||||
| 7 | 1 1 1 | 184 1 6 | use Type::Utils; | ||||
| 8 | 1 1 1 | 986 1 6 | use Types::Standard qw( HashRef Str ); | ||||
| 9 | |||||||
| 10 | extends 'Yukki::Web::Plugin'; | ||||||
| 11 | |||||||
| 12 | # ABSTRACT: format text/yukki files using markdown, etc. | ||||||
| 13 | |||||||
| 14 | 1 1 1 | 442 2 26 | use Text::MultiMarkdown; | ||||
| 15 | 1 1 1 | 2 3 24 | use Try::Tiny; | ||||
| 16 | |||||||
| 17 | 1 1 1 | 2 1 5 | use namespace::clean; | ||||
| 18 | |||||||
| 19 - 49 | =head1 SYNOPSIS
# Plugins are not used directly...
my $repo = $self->model('Repository', { name => 'main' });
my $file = $repo->file({ full_path => "some-file.yukki' });
my $html = $file->fetch_formatted($ctx);
=head1 DESCRIPTION
Yukkitext formatting is based on Multi-Markdown, which is an extension to regular markdown that adds tables, metadata, and a few other tidbits. In addition to this, yukkitext adds linking using double-bracket notation:
[[ A Link ]]
[[ ./A Sub-Page Link ]]
[[ ./A Sub-Dir/Sub-Page Link ]]
[[ ./a-sub-dir/sub-page-link.pdf | Sub-Page PDF ]]
This link format is based loosely upon the format used by MojoMojo, which I was using prior to developing Yukki.
It also adds support for format helpers usinga double-curly brace notation:
{{attachment:Path/To/Attachment.pdf}}
{{=:5 + 5}}
=head1 ATTRIBUTES
=head2 html_formatters
This returns the yukkitext formatter for "text/yukki".
=cut | ||||||
| 50 | |||||||
| 51 | has html_formatters => ( | ||||||
| 52 | is => 'ro', | ||||||
| 53 | isa => HashRef[Str], | ||||||
| 54 | required => 1, | ||||||
| 55 | default => sub { +{ | ||||||
| 56 | 'text/yukki' => 'yukkitext', | ||||||
| 57 | } }, | ||||||
| 58 | ); | ||||||
| 59 | |||||||
| 60 | with 'Yukki::Web::Plugin::Role::Formatter'; | ||||||
| 61 | |||||||
| 62 - 69 | =head2 markdown This is the L<Text::MultiMarkdown> object for rendering L</yukkitext>. Do not use. Provides a C<format_markdown> method delegated to C<markdown>. Do not use. =cut | ||||||
| 70 | |||||||
| 71 | has markdown => ( | ||||||
| 72 | is => 'ro', | ||||||
| 73 | isa => class_type('Text::MultiMarkdown'), | ||||||
| 74 | required => 1, | ||||||
| 75 | lazy => 1, | ||||||
| 76 | builder => '_build_markdown', | ||||||
| 77 | handles => { | ||||||
| 78 | 'format_markdown' => 'markdown', | ||||||
| 79 | }, | ||||||
| 80 | ); | ||||||
| 81 | |||||||
| 82 | sub _build_markdown { | ||||||
| 83 | 1 | 40 | Text::MultiMarkdown->new( | ||||
| 84 | markdown_in_html_blocks => 1, | ||||||
| 85 | heading_ids => 0, | ||||||
| 86 | strip_metadata => 1, | ||||||
| 87 | ); | ||||||
| 88 | } | ||||||
| 89 | |||||||
| 90 - 96 | =head1 METHODS =head2 yukkilink Used to help render yukkilinks. Do not use. =cut | ||||||
| 97 | |||||||
| 98 | sub yukkilink { | ||||||
| 99 | 0 | 1 | 0 | my ($self, $params) = @_; | |||
| 100 | |||||||
| 101 | 0 | 0 | my $file = $params->{file}; | ||||
| 102 | 0 | 0 | my $ctx = $params->{context}; | ||||
| 103 | 0 | 0 | my $repository = $file->repository_name; | ||||
| 104 | 0 | 0 | my $link = $params->{link}; | ||||
| 105 | 0 | 0 | my $label = $params->{label}; | ||||
| 106 | |||||||
| 107 | 0 0 | 0 0 | $link =~ s/^\s+//; $link =~ s/\s+$//; | ||||
| 108 | |||||||
| 109 | 0 | 0 | my ($repo_name, $local_link) = split /:/, $link, 2 if $link =~ /:/; | ||||
| 110 | 0 | 0 | if (defined $repo_name and defined $self->app->settings->{repositories}{$repo_name}) { | ||||
| 111 | 0 | 0 | $repository = $repo_name; | ||||
| 112 | 0 | 0 | $link = $local_link; | ||||
| 113 | } | ||||||
| 114 | |||||||
| 115 | # If we did not get a label, make the label into the link | ||||||
| 116 | 0 | 0 | if (not defined $label) { | ||||
| 117 | 0 | 0 | ($label) = $link =~ m{([^/]+)$}; | ||||
| 118 | 0 | 0 | $link = $self->app->munge_label($link); | ||||
| 119 | } | ||||||
| 120 | |||||||
| 121 | 0 | 0 | my @base_name; | ||||
| 122 | 0 | 0 | if ($file->full_path) { | ||||
| 123 | 0 | 0 | $base_name[0] = $file->full_path; | ||||
| 124 | 0 | 0 | $base_name[0] =~ s/\.yukki$//g; | ||||
| 125 | } | ||||||
| 126 | |||||||
| 127 | 0 | 0 | $link = join '/', @base_name, $link if $link =~ m{^\./}; | ||||
| 128 | 0 | 0 | $link =~ s{^/}{}; | ||||
| 129 | 0 | 0 | $link =~ s{/\./}{/}g; | ||||
| 130 | |||||||
| 131 | 0 0 | 0 0 | $label =~ s/^\s*//; $label =~ s/\s*$//; | ||||
| 132 | |||||||
| 133 | 0 0 | 0 0 | my $b = sub { $ctx->rebase_url($_[0]) }; | ||||
| 134 | |||||||
| 135 | 0 | 0 | my $link_repo = $self->model('Repository', { name => $repository }); | ||||
| 136 | 0 | 0 | my $link_file = $link_repo->file({ full_path => $link }); | ||||
| 137 | |||||||
| 138 | 0 | 0 | my $class = $link_file->exists ? 'exists' : 'not-exists'; | ||||
| 139 | 0 | 0 | return qq{<a class="$class" href="}.$b->("page/view/$repository/$link").qq{">$label</a>}; | ||||
| 140 | } | ||||||
| 141 | |||||||
| 142 - 146 | =head2 yukkiplugin Used to render plugged in markup. Do not use. =cut | ||||||
| 147 | |||||||
| 148 | sub yukkiplugin { | ||||||
| 149 | 0 | 1 | 0 | my ($self, $params) = @_; | |||
| 150 | |||||||
| 151 | 0 | 0 | my $ctx = $params->{context}; | ||||
| 152 | 0 | 0 | my $plugin_name = $params->{plugin_name}; | ||||
| 153 | 0 | 0 | my $arg = $params->{arg}; | ||||
| 154 | |||||||
| 155 | 0 | 0 | my $text; | ||||
| 156 | |||||||
| 157 | 0 | 0 | my @plugins = $self->app->format_helper_plugins; | ||||
| 158 | 0 | 0 | PLUGIN: for my $plugin (@plugins) { | ||||
| 159 | 0 | 0 | my $helpers = $plugin->format_helpers; | ||||
| 160 | 0 | 0 | if (defined $helpers->{ $plugin_name }) { | ||||
| 161 | $text = try { | ||||||
| 162 | 0 | 0 | my $helper = $helpers->{ $plugin_name }; | ||||
| 163 | $plugin->$helper({ | ||||||
| 164 | context => $ctx, | ||||||
| 165 | file => $params->{file}, | ||||||
| 166 | 0 | 0 | helper_name => $plugin_name, | ||||
| 167 | arg => $arg, | ||||||
| 168 | }); | ||||||
| 169 | } | ||||||
| 170 | |||||||
| 171 | catch { | ||||||
| 172 | 0 | 0 | warn "Plugin Error: $_"; | ||||
| 173 | 0 | 0 | }; | ||||
| 174 | |||||||
| 175 | 0 | 0 | last PLUGIN if defined $text; | ||||
| 176 | } | ||||||
| 177 | } | ||||||
| 178 | |||||||
| 179 | 0 | 0 | $text //= "{{$plugin_name:$arg}}"; | ||||
| 180 | 0 | 0 | return $text; | ||||
| 181 | } | ||||||
| 182 | |||||||
| 183 - 200 | =head2 yukkitext
my $html = $view->yukkitext({
context => $ctx,
repository => $repository_name,
page => $page,
file => $file,
});
Yukkitext is markdown plus some extra stuff. The extra stuff is:
[[ main:/link/to/page.yukki | Link Title ]] - wiki link
[[ /link/to/page.yukki | Link Title ]] - wiki link
[[ /link/to/page.yukki ]] - wiki link
{{attachment:file.pdf}} - attachment URL
=cut | ||||||
| 201 | |||||||
| 202 | sub yukkitext { | ||||||
| 203 | 1 | 1 | 2 | my ($self, $params) = @_; | |||
| 204 | |||||||
| 205 | 1 | 3 | my $file = $params->{file}; | ||||
| 206 | 1 | 4 | my $position = 0 + ($params->{position} // -1); | ||||
| 207 | 1 | 17 | my $repository = $file->repository_name; | ||||
| 208 | 1 | 31 | my $yukkitext = $file->fetch; | ||||
| 209 | |||||||
| 210 | 1 | 14196 | $yukkitext =~ s[(.{$position}.*?)$][$1<span id="yukkitext-caret"></span>]sm | ||||
| 211 | if $position >= 0; | ||||||
| 212 | |||||||
| 213 | # Yukki Links | ||||||
| 214 | 1 | 7 | $yukkitext =~ s{ | ||||
| 215 | (?<!\\) # \ will escape the link | ||||||
| 216 | \[\[ \s* # [[ to start it | ||||||
| 217 | |||||||
| 218 | (?: ([\w]+) : )? # repository: is optional | ||||||
| 219 | ([^|\]]+) \s* # link/to/page is mandatory | ||||||
| 220 | |||||||
| 221 | (?: \| # | to split link from label | ||||||
| 222 | ([^\]]+) # a pretty label (needs trimming) | ||||||
| 223 | )? # is optional | ||||||
| 224 | |||||||
| 225 | \]\] # ]] to end | ||||||
| 226 | }{ | ||||||
| 227 | 0 | 0 | $self->yukkilink({ | ||||
| 228 | %$params, | ||||||
| 229 | |||||||
| 230 | repository => $1 // $repository, | ||||||
| 231 | link => $2, | ||||||
| 232 | label => $3, | ||||||
| 233 | }); | ||||||
| 234 | }xeg; | ||||||
| 235 | |||||||
| 236 | # Handle escaped links, hide the escape | ||||||
| 237 | 1 | 4 | $yukkitext =~ s{ | ||||
| 238 | \\ # \ will escape the link | ||||||
| 239 | (\[\[ \s* # [[ to start it | ||||||
| 240 | |||||||
| 241 | (?: [\w]+ : )? # repository: is optional | ||||||
| 242 | [^|\]]+ \s* # link/to/page is mandatory | ||||||
| 243 | |||||||
| 244 | (?: \| # | to split link from label | ||||||
| 245 | [^\]]+ # a pretty label (needs trimming) | ||||||
| 246 | )? # is optional | ||||||
| 247 | |||||||
| 248 | \]\]) # ]] to end | ||||||
| 249 | }{$1}gx; | ||||||
| 250 | |||||||
| 251 | # Yukki Plugins | ||||||
| 252 | 1 | 3 | $yukkitext =~ s{ | ||||
| 253 | (?<!\\) # \ will escape the plugin | ||||||
| 254 | \{\{ \s* # {{ to start it | ||||||
| 255 | |||||||
| 256 | ([^:]+) : # plugin_name: is required | ||||||
| 257 | |||||||
| 258 | (.*?) # plugin arguments | ||||||
| 259 | |||||||
| 260 | \}\} # }} to end | ||||||
| 261 | }{ | ||||||
| 262 | 0 | 0 | $self->yukkiplugin({ | ||||
| 263 | %$params, | ||||||
| 264 | |||||||
| 265 | plugin_name => $1, | ||||||
| 266 | arg => $2, | ||||||
| 267 | }); | ||||||
| 268 | }xegms; | ||||||
| 269 | |||||||
| 270 | # Handle the escaped plugin thing | ||||||
| 271 | 1 | 4 | $yukkitext =~ s{ | ||||
| 272 | \\ # \ will escape the plugin | ||||||
| 273 | (\{\{ \s* # {{ to start it | ||||||
| 274 | |||||||
| 275 | [^:]+ : # plugin_name: is required | ||||||
| 276 | |||||||
| 277 | .*? # plugin arguments | ||||||
| 278 | |||||||
| 279 | \}\}) # }} to end | ||||||
| 280 | }{$1}xgms; | ||||||
| 281 | |||||||
| 282 | 1 | 49 | my $formatted = '<div>' . $self->format_markdown($yukkitext) . '</div>'; | ||||
| 283 | |||||||
| 284 | # Just in case markdown mangled the caret marker: | ||||||
| 285 | 1 | 3318 | $formatted =~ s[<span id="yukkitext-caret"></span>] | ||||
| 286 | [<span id="yukkitext-caret"></span>]; | ||||||
| 287 | |||||||
| 288 | 1 | 22 | return $formatted; | ||||
| 289 | } | ||||||
| 290 | |||||||
| 291 | 1; | ||||||