File Coverage: lib/Yukki/Web/Plugin/YukkiText.pm

# File Coverage

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 |  |  | 1 |  | 500 3 | use v5.24;
4 | 1 1 1 |  |  | 1 |  | 3 2 3 | use utf8;
5 | 1 1 1 |  |  | 1 |  | 10 4 2 | use Moo;
6 |
7 | 1 1 1 |  |  | 1 |  | 184 1 6 | use Type::Utils;
8 | 1 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 |  |  | 1 |  | 442 2 26 | use Text::MultiMarkdown;
15 | 1 1 1 |  |  | 1 |  | 2 3 24 | use Try::Tiny;
16 |
17 | 1 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 |  |  | 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 |  |  | 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 |  |  |  | 0 | my ($repo_name, $local_link) = split /:/, $link, 2 if $link =~ /:/;
110 | 0 | 0 | 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 |  |  |  | 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 |  |  |  | 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 |  |  |  | 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 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 |  |  |  | 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 |  |  | 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 |  |  |  | 0 | if (defined $helpers->{ $plugin_name }) {
161 |  |  |  |  |  |  | $text = try {
162 | 0 |  |  | 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 |  | 0 | warn "Plugin Error: $_";
173 | 0 |  |  |  |  | 0 | };
174 |
175 | 0 | 0 |  |  |  | 0 | last PLUGIN if defined $text;
176 |  |  |  |  |  |  | }
177 |  |  |  |  |  |  | }
178 |
179 | 0 |  | 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 | 1 | 2 | my ($self, $params) = @_;
204 |
205 | 1 |  |  |  |  | 3 | my $file = $params->{file};
206 | 1 |  | 50 |  |  | 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 | 50 |  |  |  | 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 |  |  | 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[&lt;span id="yukkitext-caret"&gt;&lt;/span&gt;]
286 |  |  |  |  |  |  | [<span id="yukkitext-caret"></span>];
287 |
288 | 1 |  |  |  |  | 22 | return $formatted;
289 |  |  |  |  |  |  | }
290 |
291 |  |  |  |  |  |  | 1;