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; |