Skip to content

Commit

Permalink
Merge pull request #91 from librasteve/master
Browse files Browse the repository at this point in the history
Fragments
  • Loading branch information
patrickbkr authored Nov 19, 2024
2 parents 65ef381 + cb7e94c commit dcaca2a
Show file tree
Hide file tree
Showing 17 changed files with 173 additions and 58 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.precomp/
*.swp
.idea/workspace.xml
.idea/
*.iml
4 changes: 0 additions & 4 deletions .idea/misc.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Cro::WebApp ![Build Status](https://github.com/croservices/cro-webapp/actions/workflows/ci.yml/badge.svg)

This is a library to make it easier to build server-side web applications using
Cro. See the [Cro website](http://cro.services/) for further information and
documentation.
Cro. See the [Cro website](http://cro.raku.org/) for further information and
documentation.
10 changes: 0 additions & 10 deletions cro-webapp.iml

This file was deleted.

20 changes: 10 additions & 10 deletions lib/Cro/WebApp/Template.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ my $template-part-plugin = router-plugin-register("template-part");

#| Render the template at the specified path using the specified data, and
#| return the result as a C<Str>.
multi render-template(IO::Path $template-path, $initial-topic, :%parts --> Str) is export {
multi render-template(IO::Path $template-path, $initial-topic, :%parts, :$fragment --> Str) is export {
my $compiled-template = await get-template-repository.resolve-absolute($template-path.absolute);
Cro::WebApp::LogTimeline::RenderTemplate.log: :template($template-path), {
render-internal($compiled-template, $initial-topic, %parts)
render-internal($compiled-template, $initial-topic, %parts, :$fragment)
}
}

#| Render the template at the specified path, which will be resolved either in the
#| resources or via the file system, as configured by C<template-location> or
#| C<templates-from-resources>.
multi render-template(Str $template, $initial-topic, :%parts --> Str) is export {
multi render-template(Str $template, $initial-topic, :%parts, :$fragment --> Str) is export {
# Gather the route-specific locations and turn them into location descriptors
# for the resolver to use.
my @route-locations := try { router-plugin-get-configs($template-location-plugin) } // ();
Expand All @@ -52,15 +52,15 @@ multi render-template(Str $template, $initial-topic, :%parts --> Str) is export

# Finally, render it.
Cro::WebApp::LogTimeline::RenderTemplate.log: :$template, {
render-internal($compiled-template, $initial-topic, %parts)
render-internal($compiled-template, $initial-topic, %parts, :$fragment)
}
}

sub render-internal($compiled-template, $initial-topic, %parts) {
sub render-internal($compiled-template, $initial-topic, %parts, :$fragment) {
my $*CRO-TEMPLATE-MAIN-PART := $initial-topic;
my %*CRO-TEMPLATE-EXPLICIT-PARTS := %parts;
my %*WARNINGS;
my $result = $compiled-template.render($initial-topic);
my $result = $compiled-template.render($initial-topic, :$fragment);
if %*WARNINGS {
for %*WARNINGS.kv -> $text, $number {
warn "$text ($number time{ $number == 1 ?? '' !! 's' })";
Expand Down Expand Up @@ -184,14 +184,14 @@ sub template-part(Str $name, &provider --> Nil) is export {
#| Used in a Cro::HTTP::Router route handler to render a template and set it as
#| the response body. The initial topic is passed to the template to render. The
#| content type will default to text/html, but can be set explicitly also.
multi template($template, $initial-topic, :%parts, :$content-type = 'text/html' --> Nil) is export {
multi template($template, $initial-topic, :%parts, :$content-type = 'text/html', :$fragment --> Nil) is export {
my @*CRO-TEMPLATE-PART-PROVIDERS := router-plugin-get-configs($template-part-plugin, error-sub => 'template');
content $content-type, render-template($template, $initial-topic, :%parts);
content $content-type, render-template($template, $initial-topic, :%parts, :$fragment);
}

#| Used in a Cro::HTTP::Router route handler to render a template and set it as
#| the response body. The content type will default to text/html, but can be set
#| explicitly also.
multi template($template, :%parts, :$content-type = 'text/html' --> Nil) is export {
template($template, Nil, :%parts, :$content-type);
multi template($template, :%parts, :$content-type = 'text/html', :$fragment --> Nil) is export {
template($template, Nil, :%parts, :$content-type, :$fragment);
}
44 changes: 41 additions & 3 deletions lib/Cro/WebApp/Template/AST.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ my class Template does ContainerNode is export {

method compile() {
my $*IN-SUB = False;
my $*IN-FRAGMENT = False;
my $children-compiled = @!children.map(*.compile).join(", ");
use MONKEY-SEE-NO-EVAL;
multi sub trait_mod:<is>(Routine $r, :$TEMPLATE-EXPORT-SUB!) {
Expand All @@ -25,7 +26,10 @@ my class Template does ContainerNode is export {
multi sub trait_mod:<is>(Routine $r, :$TEMPLATE-EXPORT-MACRO!) {
%*TEMPLATE-EXPORTS<macro>{$r.name.substr('__TEMPLATE_MACRO__'.chars)} = $r;
}
my %*TEMPLATE-EXPORTS = :sub{}, :macro{};
multi sub trait_mod:<is>(Routine $r, :$TEMPLATE-EXPORT-FRAGMENT!) {
%*TEMPLATE-EXPORTS<fragment>{$r.name.substr('__TEMPLATE_FRAGMENT__'.chars)} = $r;
}
my %*TEMPLATE-EXPORTS = :sub{}, :macro{}, :fragment{};
my $renderer = EVAL 'sub ($_) { join "", (' ~ $children-compiled ~ ') }';
return Map.new((:$renderer, exports => %*TEMPLATE-EXPORTS, :@!used-files));
}
Expand Down Expand Up @@ -245,6 +249,36 @@ my class MacroApplication does ContainerNode is export {
}
}

my class TemplateFragment does ContainerNode is export {
has Str $.name is required;
has TemplateParameter @.parameters;

method compile() {
my $should-export = !$*IN-FRAGMENT;
{
my $*IN-FRAGMENT = True;
my $params = @!parameters.map(*.compile).join(", ");
my $trait = $should-export ?? 'is TEMPLATE-EXPORT-FRAGMENT' !! '';

'(sub __TEMPLATE_FRAGMENT__' ~ $!name ~ "($params) $trait \{\n" ~
'join "", (' ~ @!children.map(*.compile).join(", ") ~ ')' ~
"} && '')\n"
~
', (' ~ @!children.map(*.compile).join(", ") ~ ').join'
}
}
}

my class FragmentCall does ContainerNode is export {
has Str $.target is required;
has Node @.arguments;

method compile() {
'__TEMPLATE_FRAGMENT__' ~ $!target ~ '(' ~ @!arguments.map(*.compile).join(", ") ~ ')'

}
}

my class TemplatePart does ContainerNode is export {
has Str $.name is required;
has TemplateParameter @.parameters;
Expand All @@ -267,11 +301,13 @@ my class UseFile does Node is export {
has IO::Path $.path is required;
has @.exported-subs;
has @.exported-macros;
has @.exported-fragments;

method compile() {
my $decls = join ",", flat
@!exported-subs.map(-> $sym { '(my &__TEMPLATE_SUB__' ~ $sym ~ ' = .<sub><' ~ $sym ~ '>)' }),
@!exported-macros.map(-> $sym { '(my &__TEMPLATE_MACRO__' ~ $sym ~ ' = .<macro><' ~ $sym ~ '>)' });
@!exported-macros.map(-> $sym { '(my &__TEMPLATE_MACRO__' ~ $sym ~ ' = .<macro><' ~ $sym ~ '>)' }),
@!exported-fragments.map(-> $sym { '(my &__TEMPLATE_FRAGMENT__' ~ $sym ~ ' = .<fragment><' ~ $sym ~ '>)' });
'(BEGIN (((' ~ $decls ~ ') given await($*TEMPLATE-REPOSITORY.resolve-absolute(\'' ~
$!path.absolute ~ '\'.IO)).exports) && ""))'
}
Expand All @@ -280,11 +316,13 @@ my class UseFile does Node is export {
my class Prelude does Node is export {
has @.exported-subs;
has @.exported-macros;
has @.exported-fragments;

method compile() {
my $decls = join ",", flat
@!exported-subs.map(-> $sym { '(my &__TEMPLATE_SUB__' ~ $sym ~ ' = .<sub><' ~ $sym ~ '>)' }),
@!exported-macros.map(-> $sym { '(my &__TEMPLATE_MACRO__' ~ $sym ~ ' = .<macro><' ~ $sym ~ '>)' });
@!exported-macros.map(-> $sym { '(my &__TEMPLATE_MACRO__' ~ $sym ~ ' = .<macro><' ~ $sym ~ '>)' }),
@!exported-fragments.map(-> $sym { '(my &__TEMPLATE_FRAGMENT__' ~ $sym ~ ' = .<fragment><' ~ $sym ~ '>)' });
'(BEGIN (((' ~ $decls ~ ') given await($*TEMPLATE-REPOSITORY.resolve-prelude()).exports) && ""))'
}
}
Expand Down
21 changes: 19 additions & 2 deletions lib/Cro/WebApp/Template/ASTBuilder.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class Cro::WebApp::Template::ASTBuilder {
my $loaded-prelude = await $*TEMPLATE-REPOSITORY.resolve-prelude();
@prelude[0] = Prelude.new:
exported-subs => $loaded-prelude.exports<sub>.keys,
exported-macros => $loaded-prelude.exports<macro>.keys;
exported-macros => $loaded-prelude.exports<macro>.keys,
exported-fragments => $loaded-prelude.exports<fragment>.keys;
}
make Template.new:
children => [|@prelude, |flatten-literals($<sequence-element>.map(*.ast))],
Expand Down Expand Up @@ -205,6 +206,21 @@ class Cro::WebApp::Template::ASTBuilder {
make MacroBody.new;
}

method sigil-tag:sym<fragment>($/) {
make TemplateFragment.new:
name => ~$<name>,
parameters => $<signature> ?? $<signature>.ast !! (),
children => flatten-literals($<sequence-element>.map(*.ast),
:trim-trailing-horizontal($*lone-end-line)),
trim-trailing-horizontal-before => $*lone-start-line;
}

method sigil-tag:sym<fragment-call>($/) {
make FragmentCall.new:
target => ~$<target>,
arguments => $<arglist> ?? $<arglist>.ast !! ();
}

method sigil-tag:sym<part>($/) {
make TemplatePart.new:
name => ~$<name>,
Expand All @@ -221,7 +237,8 @@ class Cro::WebApp::Template::ASTBuilder {
@*USED-FILES.push($used);
make UseFile.new: :path($used.path),
exported-subs => $used.exports<sub>.keys,
exported-macros => $used.exports<macro>.keys;
exported-macros => $used.exports<macro>.keys,
exported-fragments => $used.exports<fragment>.keys;
}
orwith $<library> {
my $module-name = .ast;
Expand Down
7 changes: 7 additions & 0 deletions lib/Cro/WebApp/Template/Library.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ sub template-library(*@resources) is export {
}
%exports{$mangled} := $sub;
}
for %template-exports<fragment>.kv -> $sym, $sub {
my $mangled = "&__TEMPLATE_FRAGMENT__$sym";
if %exports{$mangled}:exists {
die "Duplicate export of fragment '$sym' in $*TEMPLATE-FILE";
}
%exports{$mangled} := $sub;
}
}
return %exports;
}
34 changes: 32 additions & 2 deletions lib/Cro/WebApp/Template/Parser.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ grammar Cro::WebApp::Template::Parser {
\h+
[
|| <name=.identifier> \h* <signature>? '>'
|| <.maformed: 'macro declaration tag'>
|| <.malformed: 'macro declaration tag'>
]
[ <?{ $*lone-start-line }> [ \h* \n | { $*lone-start-line = False } ] ]?

Expand Down Expand Up @@ -318,6 +318,36 @@ grammar Cro::WebApp::Template::Parser {
[ <?{ $*lone-end-line }> [ \h* \n | { $*lone-end-line = False } ] ]?
}

token sigil-tag:sym<fragment> {
:my $opener = $¢.clone;
:my $*lone-start-line = False;
'<:fragment'
[ <?after [^ | $ | \n] \h* '<:fragment'> { $*lone-start-line = True } ]?
\h+
[
|| <name=.identifier> \h* <signature>? '>'
|| <.malformed: 'fragment declaration tag'>
]
[ <?{ $*lone-start-line }> [ \h* \n | { $*lone-start-line = False } ] ]?

<sequence-element>*

:my $*lone-end-line = False;
[ '</:' || { $opener.unclosed('fragment declaration tag') } ]
[ <?after \n \h* '</:'> { $*lone-end-line = True } ]?
[ 'fragment'? \h* '>' || <.malformed: 'fragment declaration closing tag'> ]
[ <?{ $*lone-end-line }> [ \h* \n | { $*lone-end-line = False } ] ]?
}

token sigil-tag:sym<fragment-call> {
'<~'
[
|| <target=.identifier> \h* <arglist>? \h*
|| <.malformed: 'call tag'>
]
[ '>' || <.malformed: 'call tag'> ]
}

token sigil-tag:sym<use> {
'<:use' \h+
[
Expand Down Expand Up @@ -465,7 +495,7 @@ grammar Cro::WebApp::Template::Parser {

token sigil {
# Single characters we can always take as a tag sigil
| <[.$@&:|]>
| <[.$@&:|~]>
# The ? and ! for boolification must be followed by a . or $ tag sigil or
# { expression. <!DOCTYPE>, <?xml>, and <!--comment--> style things
# must be considered literal.
Expand Down
17 changes: 11 additions & 6 deletions lib/Cro/WebApp/Template/Repository.rakumod
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ class Cro::WebApp::Template::Compiled is implementation-detail {
has %.exports;

#| Renders the template, setting the provided argument as the topic.
method render($topic --> Str) {
method render($topic, :$fragment --> Str) {
my $*TEMPLATE-REPOSITORY = $!repository;
&!renderer($topic)

if $fragment {
%.exports<fragment>{$fragment}($topic);
} else {
&!renderer($topic)
}
}
}

Expand All @@ -46,11 +51,11 @@ role Cro::WebApp::Template::Repository {

#| Resolve a template name into a C<Promise> that will be kept with a
#| C<Cro::WebApp::Template::Compiled>.
method resolve(Str $template-name, Cro::WebApp::Template::Location @locations? --> Promise) { ... }
method resolve(Str $template-name, Cro::WebApp::Template::Location @locations? --> Promise) {...}

#| Resolve an absolute path into a C<Promise> that will be kept with a
#| C<Cro::WebApp::Template::Compiled>.
method resolve-absolute(IO() $abs-path, :@locations --> Promise) { ... }
method resolve-absolute(IO() $abs-path, :@locations --> Promise) {...}

#| Resolve the template prelude, which contains various built-ins.
method resolve-prelude(--> Promise) is implementation-detail {
Expand Down Expand Up @@ -155,8 +160,8 @@ monitor Cro::WebApp::Template::Repository::FileSystem::Reloading is Cro::WebApp:
}

my $template-repo = %*ENV<CRO_DEV>
?? Cro::WebApp::Template::Repository::FileSystem::Reloading.new
!! Cro::WebApp::Template::Repository::FileSystem.new;
?? Cro::WebApp::Template::Repository::FileSystem::Reloading.new
!! Cro::WebApp::Template::Repository::FileSystem.new;

#| Gets the currently active template repository. By default, this is
#| C<Cro::WebApp::Template::Repository::FileSystem>, however if the C<CRO_DEV>
Expand Down
5 changes: 4 additions & 1 deletion t/library-module/resources/test-template-library.crotmp
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,7 @@ Library: <$a> and <$b>
</:sub>
<:macro bs-container()>
<div class="container"><:body></div>
</:>
</:>
<:fragment frag-test($_)>
Fragment: <.a> and <.b>
</:>
2 changes: 2 additions & 0 deletions t/library-test-data/call-fragment.crotmp
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<:use Some::Library>
<~frag-test($_)>
6 changes: 6 additions & 0 deletions t/library.rakutest
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use lib $*PROGRAM.parent.add('library-module').Str;

my constant $base = $*PROGRAM.parent.add('library-test-data');


is render-template($base.add('use-only.crotmp'), {}), q:to/EXPECTED/, 'Use of a library compiles';

Everything is OK
Expand All @@ -19,4 +20,9 @@ is render-template($base.add('call-macro.crotmp'), {}), q:to/EXPECTED/, 'Can cal
<div class="container">Contained</div>
EXPECTED

is render-template($base.add('call-fragment.crotmp'), {a => 'hell', b=> 'dunkel'}), q:to/EXPECTED/, 'Can call a library fragment';

Fragment: hell and dunkel
EXPECTED

done-testing;
Loading

0 comments on commit dcaca2a

Please sign in to comment.