view release on metacpan or search on metacpan
lib/App/FuguWeb.pod view on Meta::CPAN
=head1 DESCRIPTION
C<fuguweb> renders one static site from the documentation that a Perl
project already keeps: mdoc(7) manuals, POD sidecars, and Markdown.
There is no templating language and no JavaScript. The tool runs
C<mandoc>, C<lowdown>, and C<pod2man>, and wraps each result in one
shared chrome.
A project needs no build recipe. It writes one F<.fuguwebrc> at its
root, keeps its body fragments and its assets in one source directory,
and runs C<fuguweb build>. See L<fuguweb(1)> for the subcommands and
the exit codes.
This file holds what more than one module in the namespace needs: two
constants and four functions, documented below. The rest of the work
is split by concern:
=over 4
=item L<App::FuguWeb::Config>
The site description: the settings, the navigation, the pages, and the
manual groups.
=item L<App::FuguWeb::Page>
The chrome around one body fragment.
=item L<App::FuguWeb::Render>
The three external renderers.
=item L<App::FuguWeb::Manual>
One manual source: its path, name, section, page, and description.
=item L<App::FuguWeb::Index>
lib/App/FuguWeb.pod view on Meta::CPAN
dir = man/fugu
anchor = fugu
namespace = "Fugu::"
}
modules "OpenHAP modules" {
dir = lib/App/OpenHAP
anchor = modules
}
A C<page> block names exactly one source: C<body> for a fragment in the
source directory, C<markdown> for a Markdown file, or C<index = yes>
for the generated manual index. C<unlinked = yes> marks a page that no
other page links to, such as F<404.html>.
Three rules keep a list out of the file. A C<manuals> block reads its
directory. A C<modules> block finds every F<.pod> file below its
directory, and the sidecar that names the directory itself. Every file
in the source directory that the build does not render is an asset, and
the build copies it; L<App::FuguWeb::Site/ASSETS> says exactly which
those are.
lib/App/FuguWeb/Check.pm view on Meta::CPAN
# A browser reads a relative URL whose first segment
# holds a colon as a scheme, so a page named
# Fugu::Daemon.3p.html needs its './'.
if ( $ref =~ /^[A-Za-z][A-Za-z0-9.+-]*:/ ) {
push @problems, "$page: $ref reads as a URL scheme;"
. ' a local link needs its ./';
next;
}
my ( $path, $fragment ) = split /#/, $ref, 2;
if ( defined $path && length $path ) {
$path = _resolve( $page, $path );
unless ( defined $path ) {
push @problems,
"$page: $ref names no page of the site";
next;
}
}
else {
$path = $page;
}
unless ( -e $self->{out} . "/$path" ) {
push @problems, "$page: $ref leads nowhere";
next;
}
next unless defined $fragment && length $fragment;
my $target = Fugu::File->read( $self->{out} . "/$path" ) // '';
push @problems, "$page: $ref has no such anchor"
unless $target =~ /\bid="\Q$fragment\E"/;
}
return @problems;
}
# _base_of($page):
# The step back from a page to the site root. It is the empty
# string for a page of the root, and one '../' for each
# directory below it. App::FuguWeb::Page writes the same step in
# front of every relative link of the chrome.
lib/App/FuguWeb/Check.pod view on Meta::CPAN
every relative reference resolves to a file in the output, a manual
cross-reference included;
=item *
a reference of a page below the root resolves against the directory of
that page;
=item *
every fragment resolves to an C<id> on the target page;
=item *
no relative href reads as a URL scheme, so every local link keeps its
C<./>;
=item *
every page is reachable from the entry page, except a page that the
description marks C<unlinked>;
lib/App/FuguWeb/Config.pm view on Meta::CPAN
# title, source, value and unlinked. The source is one of body,
# markdown or index, and the value is what that source names.
sub pages ($self)
{
return @{ $self->{page} };
}
# $self->assets:
# The names of the files in the source directory that the build
# copies as they stand, sorted. An asset is any file there that
# the build does not render: not a body fragment, not Markdown,
# and not a dot file.
#
# Thus robots.txt and CNAME need no entry in the description, and
# a CLAUDE.md beside them is not published. Markdown in the
# source directory is either a page source, which a page block
# names and lowdown renders, or notes for the maintainers.
# Neither belongs in the output as it stands.
#
# The build and the checks read the same list, so the two can
# never disagree about what the site holds.
lib/App/FuguWeb/Config.pod view on Meta::CPAN
The source directory, or one file in it.
=head2 site, lang, out_dir, source_dir, entry, module_root, mandoc_os, man_url, stylesheet
The settings. C<site> is the only one a project must give.
site (required) the name in the title of every page,
and the text of the header link
lang en the lang attribute of the document
out_dir web/build where the build writes
source_dir web the fragments and the assets
entry index.html the front page, and the header link
module_root lib the prefix a module name drops
(a trailing slash is dropped)
mandoc_os OpenBSD the mandoc -I os=, which pins the footer
man_url https://man.openbsd.org/ where a remote .Xr goes
stylesheet (searched) the base stylesheet
C<stylesheet> overrides the search that L<Fugu::File/share_path> does.
The search finds the sheet in a checkout and in an installed
App-FuguWeb distribution alike, so most projects never set it.
lib/App/FuguWeb/Index.pm view on Meta::CPAN
# App::FuguWeb::Index - the body of the manual index.
#
# The groups follow the source directories, which is already how a
# tree is organized. Thus the index cannot drift when a manual is
# added: no list here names one.
#
# The index never retypes a description either. It asks each
# App::FuguWeb::Manual, and that reads the source.
# The fragment that replaces the generated opening, when a project
# writes one.
use constant OPENING_FRAGMENT => 'manuals.body.html';
# App::FuguWeb::Index->new(%args):
# config => $config the site description (required)
sub new ( $class, %args )
{
my $config = $args{config};
die 'config parameter required'
unless defined $config;
lib/App/FuguWeb/Index.pm view on Meta::CPAN
sub title ($self)
{
for my $page ( $self->{config}->pages ) {
return $page->{title} if $page->{source} eq 'index';
}
return;
}
# $self->body:
# The body fragment of the index page.
sub body ($self)
{
my $html = $self->_opening;
$html .= $self->_group($_) for $self->{config}->groups;
return $html;
}
# $self->_opening:
# The heading and the paragraph above the groups. A project
# fragment in the source directory replaces both: the prose is
# the project's, and only the project knows what its manuals are.
sub _opening ($self)
{
my $path = $self->{config}->source_path(OPENING_FRAGMENT);
return Fugu::File->read($path) // '' if -f $path;
my $url = $self->{config}->man_url;
my $host = $url;
$host =~ s{^[a-z]+://}{};
$host =~ s{/$}{};
lib/App/FuguWeb/Index.pod view on Meta::CPAN
=head2 title
The title of the index page. It comes from the C<page> block that names
the index as its source, so the heading and the browser tab always
agree. The build renders the index for such a block only, so the block
is always there.
=head2 body
The body fragment. L<App::FuguWeb::Page> wraps it, exactly as it wraps
a body fragment that a project wrote by hand.
The fragment opens with the title and a paragraph about the manual
sources, which names the configured manual host. A
F<manuals.body.html> in the source directory replaces that opening: the
prose is the project's, and only the project knows what its manuals are.
Each group then emits an C<< <h2> >> that carries the group anchor, one
C<< <dl> >>, and one blank line. A group with no manual emits nothing,
so an empty group leaves no heading behind.
Each manual emits a term and a definition:
lib/App/FuguWeb/Keys.pm view on Meta::CPAN
# Record the reason and return undef, so each public method
# fails the same way.
sub _fail ( $self, $reason )
{
$self->{error} = $reason;
return;
}
# _index_body($rows):
# The body fragment of the human page: one row for each key, in
# publication order. Every value is escaped, and a value that
# the description left out becomes an empty cell.
sub _index_body ($rows)
{
my @head = (
'Key', 'Purpose', 'Serial', 'Type',
'Status', 'Fingerprint', 'Since', 'Until'
);
my $html = "<h1>Keys</h1>\n<table>\n<thead>\n<tr>";
lib/App/FuguWeb/Page.pm view on Meta::CPAN
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
use v5.36;
package App::FuguWeb::Page;
our $VERSION = '0.5.0';
use App::FuguWeb;
use Fugu::File;
# App::FuguWeb::Page - the shared chrome around one body fragment.
#
# Every source format is reduced to one HTML body fragment, and this
# module wraps it: the head, the header, the navigation, the fragment,
# and the footer. The layout is fixed. What a project decides is the
# site name, the language, the navigation, and the footer prose.
#
# The module builds the document with string operations and not with a
# substitution over a template. A title may therefore hold any
# character. The sed template that this replaced could not take a
# slash or an ampersand.
# The two separators that are not ASCII: an em dash between the page
# title and the site name, and a middle dot between navigation
# entries. No file in the namespace carries 'use utf8', and
# Fugu::File reads and writes bytes, so the constants are the UTF-8
# bytes themselves and reach the output unchanged.
use constant {
EM_DASH => "\xe2\x80\x94",
MIDDLE_DOT => "\xc2\xb7",
};
# The optional fragment that carries the footer prose. The text
# belongs to the project, not to the tool, so it is content in the
# source directory and not a setting.
use constant FOOTER_FRAGMENT => 'footer.body.html';
# App::FuguWeb::Page->new(%args):
# config => $config the site description (required)
# base => $prefix the step back to the site root
#
# A site is one flat directory, so the base is the empty string
# and every link of the chrome is a file name. The key directory
lib/App/FuguWeb/Page.pm view on Meta::CPAN
# name a file of the root.
sub new ( $class, %args )
{
my $config = $args{config};
die 'config parameter required'
unless defined $config;
return bless { config => $config, base => $args{base} // '' }, $class;
}
# $self->write($path, $title, $fragment):
# Write the whole page: the chrome around the fragment, as
# bytes. The fragment goes in unchanged: it is already HTML,
# from a renderer or from the project's own source directory.
# The method returns true on success, and undef with a message
# in the log otherwise.
sub write ( $self, $path, $title, $fragment )
{
return Fugu::File->write( $path, $self->document( $title, $fragment ) );
}
# $self->document($title, $fragment):
# The whole page as bytes. A caller that writes the file itself,
# or that holds the page beside other generated bytes, reads the
# document here and never repeats the chrome.
sub document ( $self, $title, $fragment )
{
return $self->_head($title) . ( $fragment // '' ) . $self->_foot;
}
# $self->_head($title):
# Everything before the fragment. Every value is escaped before
# the heredoc reads it, so nothing raw reaches the markup.
sub _head ( $self, $title )
{
my $config = $self->{config};
my $site = App::FuguWeb::escape_html( $config->site );
my $lang = App::FuguWeb::escape_attr( $config->lang );
my $entry = $self->_link( $config->entry );
my $full = App::FuguWeb::escape_html($title) . ' ' . EM_DASH . " $site";
my $sheet = $self->_link(App::FuguWeb::STYLESHEET);
lib/App/FuguWeb/Page.pm view on Meta::CPAN
"<nav>\n"
. join( ' ' . MIDDLE_DOT . "\n", @links )
. "\n</nav>\n";
}
# $self->_link($href):
# One href of the chrome, escaped, with the step back to the
# site root in front of it.
#
# The step goes in front of a relative name only. An absolute
# URL, a root-absolute path and a fragment each name a place of
# their own. A step in front of one would name a page that the
# site does not hold.
sub _link ( $self, $href )
{
my $written = App::FuguWeb::escape_attr($href);
return $written if $href =~ m{\A(?:[A-Za-z][A-Za-z0-9.+-]*:|/|\#)};
return $self->{base} . $written;
}
# $self->_foot:
# Everything after the fragment. A project with no footer
# fragment gets no footer element and no rule before it: an
# empty footer is not chrome, it is a gap.
sub _foot ($self)
{
my $html = "</main>\n";
# A page below the site root carries no footer. The fragment
# is the prose of the project, and this class copies it in
# unchanged. A relative link of it would resolve against the
# directory of the page. The link would then name a file of
# the key directory, or no file at all, and the writer of the
# fragment named neither.
my $path = $self->{config}->source_path(FOOTER_FRAGMENT);
if ( -f $path && !length $self->{base} ) {
my $prose = Fugu::File->read($path) // '';
$html .= "<hr>\n<footer>\n$prose</footer>\n";
}
$html .= "</body>\n</html>\n";
return $html;
}
lib/App/FuguWeb/Page.pod view on Meta::CPAN
=head1 NAME
App::FuguWeb::Page - the shared chrome around one body fragment
=head1 SYNOPSIS
use App::FuguWeb::Page;
my $page = App::FuguWeb::Page->new(config => $config);
$page->write("$out/install.html", 'Install', $fragment);
=head1 DESCRIPTION
Every source format is reduced to one HTML body fragment, and this
class wraps it: the head, the header, the navigation, the fragment, and
the footer. The layout is fixed. What a project decides is the site
name, the language, the navigation, and the footer prose.
The class builds the document with string operations, not with a
substitution over a template. A title may therefore hold any character,
including the slash and the ampersand that a C<sed> template could not
take.
Two separators are not ASCII: an em dash between the page title and the
site name, and a middle dot between navigation entries. Both are byte
lib/App/FuguWeb/Page.pod view on Meta::CPAN
C<config> is an L<App::FuguWeb::Config> and is required.
C<base> is the step back to the site root, and it defaults to the
empty string. A site is one flat directory, so every link of the
chrome is a file name. The key directory is the one part that sits
below the root. A page there passes C<'../'>, so the stylesheet, the
banner and the navigation then all name a file of the root.
=head2 write
$page->write($path, $title, $fragment)
Write the whole page with L<Fugu::File>: the chrome around the
fragment, as bytes. The fragment goes in unchanged: it is already
HTML, from a renderer or from the project's own source directory. The
method returns true on success, and C<undef> with a message in the log
otherwise.
=head2 document
$page->document($title, $fragment)
The whole page as bytes. A caller that writes the file itself, or that
holds the page beside other generated bytes, reads the document here
and never repeats the chrome.
=head1 THE OPTIONAL FOOTER
A F<footer.body.html> in the configured source directory becomes the
footer prose. It is project text, not tool text, so it is content and
not a setting. A project without it gets no C<< <footer> >> element
and no rule before one: an empty footer is a gap, not chrome.
A page with a non-empty C<base> carries no footer either. The prose is
the project's own, and this class copies it in unchanged. A relative
link inside it would resolve against the directory of that page. It
would then name a file of the key directory, or no file at all. The
writer of the fragment named neither.
=head1 SEE ALSO
L<App::FuguWeb>, L<App::FuguWeb::Config>, L<App::FuguWeb::Site>,
L<Fugu::File>
=head1 AUTHOR
Dick Olsson <hi@senzilla.io>
lib/App/FuguWeb/Render.pm view on Meta::CPAN
# to standard error. A caller that logged only stderr would
# report the failure with no line, no column and no reason.
my $said = join "\n", grep { defined } $result->{stdout},
$result->{stderr}, $result->{error};
$self->{log}->error( '%s', $_ ) for grep { length } split /\n/, $said;
return;
}
# $self->markdown($path):
# Render one Markdown file into an HTML body fragment.
sub markdown ( $self, $path )
{
return $self->_capture( 'lowdown',
[ $self->{lowdown}, '-Thtml', $path ] );
}
# $self->mdoc($file, $dir):
# Render one staged mdoc source into an HTML body fragment. The
# child runs in $dir, because mandoc decides between a local link
# and a link to the manual host by looking for a file named %N.%S
# in its working directory.
sub mdoc ( $self, $file, $dir )
{
return $self->_capture(
'mandoc',
[ $self->{mandoc}, $self->html_options, $file ],
cwd => $dir
);
}
# $self->pod($path, $name, $date):
# Render one POD sidecar into an HTML body fragment. pod2man
# writes mdoc, and mandoc finishes the job, so a module page
# carries the same chrome as a hand-written manual.
#
# The date comes from the caller and never from the file time.
# git does not preserve file times, so a build that read one
# would give different bytes on every checkout.
sub pod ( $self, $path, $name, $date )
{
my $man = Fugu::Process->run(
cmd => [
lib/App/FuguWeb/Render.pm view on Meta::CPAN
#
# The './' in the man= template matters. A module page is named
# Fugu::Daemon.3p.html, and a browser reads a relative URL whose
# first segment holds a colon as a scheme.
sub html_options ($self)
{
my $config = $self->{config};
return (
'-Thtml', '-I', 'os=' . $config->mandoc_os,
'-O', 'fragment,man=./%N.%S.html;' . $config->man_url . '%N.%S',
);
}
# $self->_capture($tool, $cmd, %args):
# Run one renderer and return its output. A failure names the
# tool and carries its diagnostics into the log.
sub _capture ( $self, $tool, $cmd, %args )
{
my $result = Fugu::Process->run( cmd => $cmd, %args );
unless ( $result->{success} ) {
lib/App/FuguWeb/Render.pod view on Meta::CPAN
Run C<mandoc -Tlint -W warning> over every mdoc source. The method
returns true when every page is clean, and C<undef> with the diagnostics
in the log otherwise. A malformed page must fail the build, not render
badly.
=head2 markdown
$render->markdown($path)
Render one Markdown file into an HTML body fragment, with
C<lowdown -Thtml>.
=head2 mdoc
$render->mdoc($file, $dir)
Render one staged mdoc source into an HTML body fragment. The child runs
in C<$dir>, through the C<cwd> option of L<Fugu::Process/run>.
C<mandoc> decides between a local link and a link to the manual host by
looking for a file named C<%N.%S> in its working directory. That is why
L<App::FuguWeb::Site> stages every source in one directory under its
L<App::FuguWeb::Manual/staged_name>, and why the child has to start
there.
=head2 pod
$render->pod($path, $name, $date)
Render one POD sidecar into an HTML body fragment. C<pod2man> writes
mdoc, and C<mandoc> finishes the job, so a module page carries the same
chrome as a hand-written manual.
C<$date> comes from the caller and never from the file time: git does not
preserve file times, so a build that read one would give different bytes
on every checkout. L<App::FuguWeb::Site/pod_date> supplies it.
C<pod2man> reports a malformed directive on standard error and still
writes the page. The page is what the site needs, so a diagnostic is a
warning and only empty output is fatal.
lib/App/FuguWeb/Site.pm view on Meta::CPAN
my $dir = $target =~ s{/[^/]+\z}{}r;
Fugu::File->ensure_dir($dir) or return;
my $data = $bytes->() // return;
return Fugu::File->write( $target, $data );
}
# $self->_render_pages:
# Render every page block. A body fragment is already HTML, a
# Markdown file goes through lowdown, and the index comes from
# App::FuguWeb::Index.
sub _render_pages ($self)
{
my $config = $self->{config};
my $page = App::FuguWeb::Page->new( config => $config );
my $index = App::FuguWeb::Index->new( config => $config );
for my $entry ( $config->pages ) {
my $fragment;
if ( $entry->{source} eq 'body' ) {
my $path = $config->source_path( $entry->{value} );
$fragment = Fugu::File->read($path);
unless ( defined $fragment ) {
$self->{log}->error( 'Cannot read %s', $path );
return;
}
}
elsif ( $entry->{source} eq 'markdown' ) {
$fragment =
$self->{render}
->markdown( $config->root . '/' . $entry->{value} );
return unless defined $fragment;
}
else {
$fragment = $index->body;
}
$page->write( $self->{out} . '/' . $entry->{file},
$entry->{title}, $fragment )
or return;
}
return 1;
}
# $self->_render_manuals:
# Render one page for each manual of each group.
sub _render_manuals ($self)
{
my $config = $self->{config};
my $page = App::FuguWeb::Page->new( config => $config );
my $date = $self->pod_date;
for my $group ( $config->groups ) {
for my $manual ( $group->manuals ) {
my $fragment =
$manual->is_pod
? $self->{render}
->pod( $manual->path, $manual->name, $date )
: $self->{render}
->mdoc( $manual->staged_name, $self->staging );
return unless defined $fragment;
$page->write(
$self->{out} . '/' . $manual->page,
$manual->name . '(' . $manual->section . ')',
$fragment
) or return;
}
}
return 1;
}
1;
lib/App/FuguWeb/Site.pod view on Meta::CPAN
The site build cannot sign, so the manifest pair is a source file. A
build that signed would prove that the builder holds the key, and
never that the release does.
A description with no C<keys> block writes no key directory, so every
site that predates it keeps its output.
=head1 ASSETS
An asset is any file directly in the source directory that the build
does not render: not a C<*.body.html> fragment, not Markdown, and not a
dot file. F<robots.txt> and F<CNAME> therefore need no entry in the
description, and a F<CLAUDE.md> beside them is not published.
Markdown in the source directory is either a page source, which a
C<page> block names and C<lowdown> renders, or notes for the
maintainers. Neither belongs in the output as it stands.
=head1 THE STYLESHEET
The base stylesheet ships with the tool, at F<share/fuguweb/style.css>,
man/fuguweb/fuguweb.1 view on Meta::CPAN
Every build writes it,
so a target without it is the output of no build.
.Pp
The staging directory holds one flat directory of plain files.
The command refuses a staging directory of another shape,
and the build refuses the same one and leaves it.
.It Cm check Oo Fl Fl out Ar dir Oc Op Fl Fl verbose
Check a built site:
that every page and asset is there and is not empty,
that every page has a title and the whole navigation,
that every local link and every fragment resolves,
that no reference is root-absolute or a
.Ql file:
URL,
that no local link reads as a URL scheme,
that no manual cross-reference dangles,
that every page is reachable from the front page,
and that the output holds the site and nothing else.
.Pp
The command prints one line for each problem and exits 5.
It says nothing and exits 0 when the site is good.
t/fuguweb/check.t view on Meta::CPAN
print {$rc} <<'PAGE';
page "other.html" {
title = Other
body = other.body.html
}
PAGE
close $rc;
open my $body, '>', "$root/web/other.body.html"
or die "Cannot write the fragment: $!";
print {$body} "<h2 id=\"here\">Here</h2>\n";
close $body;
my $out = "$root/out";
make_path($out);
my %page = ( %GOOD, %override );
for my $name ( sort keys %page ) {
unless ( defined $page{$name} ) {
unlink "$out/$name";
t/fuguweb/check.t view on Meta::CPAN
@problems = built( 'other.html' => <<'HTML' )->run;
<title>Other — Example</title>
<a href="index.html">Home</a>
<a href="https://example.org/">Elsewhere</a>
<a href="./index.html#absent">Absent</a>
<h2 id="here">Here</h2>
HTML
like( join( "\n", @problems ),
qr{other\.html: \./index\.html#absent has no such anchor},
'a dead fragment' );
};
subtest 'a reference that leaves the site' => sub {
my @problems = built( 'other.html' => <<'HTML' )->run;
<title>Other — Example</title>
<a href="index.html">Home</a>
<a href="https://example.org/">Elsewhere</a>
<a href="/index.html">Rooted</a>
<a href="file:///etc/passwd">Local</a>
<h2 id="here">Here</h2>
t/fuguweb/config.t view on Meta::CPAN
or die "Cannot write $pair->[0]: $!";
print {$fh} $pair->[1];
close $fh;
}
my $config =
App::FuguWeb::Config->load( root => $root, error => \my $reason );
ok( $config, 'the description loads' ) or diag $reason;
# The pages, one page per manual, the stylesheet, and the
# assets. The body fragment is a source and never an asset.
is_deeply(
[ $config->inventory ],
[ qw(index.html manuals.html tool.1.html style.css
robots.txt) ],
'every name the output must hold, and nothing else'
);
};
subtest 'the navigation keeps its file order' => sub {
my ( $config, $reason ) = load_rc( <<'RC' );
t/fuguweb/config.t view on Meta::CPAN
}
RC
ok( $config, 'the description loads' ) or diag $reason;
my @pages = $config->pages;
is( scalar @pages, 4, 'four pages' );
is( $pages[0]{file}, 'index.html', 'the first file' );
is( $pages[0]{title}, 'Home', 'the first title' );
is( $pages[0]{source}, 'body', 'a body source' );
is( $pages[0]{value}, 'index.body.html', 'the fragment' );
ok( !$pages[0]{unlinked}, 'the front page is linked' );
is( $pages[1]{source}, 'markdown', 'a markdown source' );
is( $pages[1]{value}, 'INSTALL.md', 'the markdown file' );
is( $pages[2]{source}, 'index', 'an index source' );
is( $pages[2]{value}, undef, 'the index names no file' );
is( $pages[3]{title}, '404.html',
'a page with no title takes its file name' );
t/fuguweb/index.t view on Meta::CPAN
site = Example
modules "No anchor" {
dir = lib/Thing
}
RC
is( $config, undef, 'a group with no anchor fails the load' );
like( $reason, qr{has no anchor}, 'the message says which' );
};
subtest 'a project fragment replaces the opening' => sub {
my $root = build_root($RC);
open my $fh, '>', "$root/web/manuals.body.html"
or die "Cannot write the fragment: $!";
print {$fh} "<h1>The manuals</h1>\n\n<p>Read them.</p>\n\n";
close $fh;
my $config =
App::FuguWeb::Config->load( root => $root, error => \my $reason );
ok( $config, 'the description loads' ) or diag $reason;
my $body = App::FuguWeb::Index->new( config => $config )->body;
like( $body, qr{^<h1>The manuals</h1>\n}, 'the fragment opens' );
unlike( $body, qr/These pages come from/, 'and replaces the prose' );
like( $body, qr{<h2 id="tool">Tool</h2>}, 'the groups still follow' );
};
done_testing();
t/fuguweb/page.t view on Meta::CPAN
#!/usr/bin/env perl
# ex:ts=8 sw=4:
# App::FuguWeb::Page: the chrome, the two byte separators, the
# escaping, and the optional footer fragment.
#
# The test builds each site in a File::Temp directory. It never reads
# the repository, so a change to web/ cannot break it.
use v5.36;
use Test::More;
use FindBin qw($RealBin);
use lib "$RealBin/../../lib";
use File::Path qw(make_path);
use File::Temp qw(tempdir);
t/fuguweb/page.t view on Meta::CPAN
close $out;
}
my $config =
App::FuguWeb::Config->load( root => $root, error => \my $reason );
die "$reason\n" unless $config;
return $config;
}
# render($page, $title, $fragment):
# Write one page into a scratch file and return its bytes.
# write is the one public method, so the test reads back what
# it wrote.
sub render ( $page, $title, $fragment )
{
my $path = tempdir( CLEANUP => 1 ) . '/page.html';
$page->write( $path, $title, $fragment )
or die 'write failed';
open my $fh, '<', $path or die "Cannot read $path: $!";
binmode $fh;
local $/ = undef;
my $html = <$fh>;
close $fh;
return $html;
}
t/fuguweb/page.t view on Meta::CPAN
unlike( $html, qr/onload="x"/, 'no attribute was injected' );
# An ampersand is not markup, but it is not valid in an
# attribute either, and the same escape covers both.
like( $html, qr{href="index\.html\?a&b"},
'the header link is escaped' );
like( $html, qr{href="search\.html\?q=1&r=2"},
'a navigation href is escaped' );
};
subtest 'the footer fragment is optional' => sub {
my $page = App::FuguWeb::Page->new( config => site($RC) );
my $html = render( $page, 'Install', '' );
unlike( $html, qr/<footer>/, 'no fragment, no footer element' );
like( $html, qr/<\/main>\n<\/body>/, 'and no rule before one' );
$page = App::FuguWeb::Page->new(
config => site( $RC, 'footer.body.html' => "<p>ISC.</p>\n" ) );
$html = render( $page, 'Install', '' );
like( $html, qr{</main>\n<hr>\n<footer>\n<p>ISC\.</p>\n</footer>\n},
'the fragment becomes the footer' );
};
done_testing();
t/fuguweb/render.t view on Meta::CPAN
my $joined = join ' ', @options;
# -I os= pins the footer. Without it the page would name the
# operating system of the machine that built the site.
like( $joined, qr/\Qos=Example OS\E/, 'the OS is pinned' );
# The './' matters: a browser reads a relative URL whose first
# segment holds a colon as a scheme.
like( $joined, qr{\Qman=./%N.%S.html;https://man.example.org/%N.%S\E},
'a local link stays local and a remote one leaves' );
like( $joined, qr/fragment/, 'the output is a body fragment' );
};
SKIP: {
skip 'mandoc not found', 2 unless have('mandoc');
subtest 'lint accepts a clean page and rejects a malformed one' =>
sub {
my $dir = tempdir( CLEANUP => 1 );
open my $good, '>', "$dir/good.1"