Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fragments #91

Merged
merged 16 commits into from
Nov 19, 2024
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);
librasteve marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading