NAME

App::FuguWeb - a static documentation site for a Perl project

SYNOPSIS

fuguweb build --out web/build
fuguweb check --out web/build

use App::FuguWeb;

my $text = App::FuguWeb::decode($bytes, $path, \my $reason)
    // die "$reason\n";
my $safe = App::FuguWeb::escape_html($text);
my $page = App::FuguWeb::encode($safe);
my $file = App::FuguWeb::CONFIG_FILE;      # .fuguwebrc

DESCRIPTION

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 mandoc, lowdown, and pod2man, and wraps each result in one shared chrome.

A project needs no build recipe. It writes one .fuguwebrc at its root, keeps its body fragments and its assets in one source directory, and runs fuguweb build. See fuguweb(1) for the subcommands and the exit codes.

This file holds what more than one module in the namespace needs: the constants and the functions documented below. The rest of the work is split by concern:

App::FuguWeb::Config

The site description: the settings, the languages, the navigation, the pages, and the manual groups.

App::FuguWeb::Language

One declared language: its tag, its name, its direction, and the path of its tree.

App::FuguWeb::Fragment

The fit of a body fragment to its page: the outline, the heading ids of a translation and the links to them, and the fit of the fragment of each renderer.

App::FuguWeb::Page

The chrome around one body fragment.

App::FuguWeb::Render

The three external renderers.

App::FuguWeb::Manual

One manual source: its path, name, section, page, and description.

App::FuguWeb::Index

The body of the manual index.

App::FuguWeb::Site

The whole build.

App::FuguWeb::Check

The checks over a built site.

App::FuguWeb::Status

The record of the translations, and each part of a translation that does not follow its source.

App::FuguWeb::CLI

Subcommand dispatch over Fugu::CLI.

THE CONFIGURATION FILE

.fuguwebrc sits at the project root and uses the Fugu::Config grammar: a setting on a line of its own, and a block that opens with a brace at the end of its header line. A # starts a comment, and the grammar has no escape for it, so a value may not hold one.

site       = OpenHAP
url        = https://openhap.example.org/
footer     = footer.body.html
out_dir    = web/build
source_dir = web
entry      = index.html

language en {
        name = English
}

language de {
        name = Deutsch
}

nav "fugu.html" {
        label = Fugu
}

page "install.html" {
        title    = Install
        markdown = INSTALL.md
}

manuals fugu {
        heading   = Fugu
        dir       = man/fugu
        namespace = "Fugu::"
}

modules modules {
        heading = OpenHAP modules
        dir     = lib/App/OpenHAP
}

page de "install.html" {
        title    = Installation
        markdown = INSTALL.de.md
}

A page block names exactly one source: body for a fragment in the source directory, markdown for a Markdown file, or index = yes for the generated manual index. An index page can take a body that opens it. unlinked = yes marks a page that no other page links to, such as 404.html.

The block name of a group is its anchor on the index page, and heading holds the text. A language block declares one language, and the first one is the default. A block with the tag of a declared language in front of its name is a translation into that language. Each language of the site takes one tree of the output, below the path of its tag, and "Languages" in App::FuguWeb::Config states the rules.

Three rules keep a list out of the file. A manuals block reads its directory. A modules block finds every .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; "ASSETS" in App::FuguWeb::Site says exactly which those are.

A keys block and its key blocks describe the published key directory of an organization. App::FuguWeb::Keys documents them.

App::FuguWeb::Config documents every setting and every default.

FUNCTIONS

Text is UTF-8 at each edge, and characters inside, per WEB-SITE-5. The build decodes the description, each source and each output of a renderer, and it encodes each page that it writes. A name of a file stays bytes, because the file system reads bytes.

decode

my $text = App::FuguWeb::decode($bytes, $name, \my $reason);

The characters of UTF-8 bytes. The function returns undef when the bytes are not UTF-8, with the reason in $reason. The reason names $name, the file or the output, and the offset of the first byte that is wrong. The decode is strict: it refuses an overlong form, a surrogate, a noncharacter and a code point above U+10FFFF.

encode

my $bytes = App::FuguWeb::encode($text);

The UTF-8 bytes of characters. Each page goes through it on its way to the disk.

as_text

my $text = App::FuguWeb::as_text($name);

The characters of a name or a field that a page shows: a name of the description, a name of a file, or a field of a key. The description is UTF-8, so each of its names decodes. A name of a file that is not UTF-8 takes U+FFFD for each wrong byte. Its link then names no file, and the check reports it.

fold_name

App::FuguWeb::fold_name($name) eq App::FuguWeb::fold_name($other)

The fold of a name or a path of the output, per WEB-OUTPUT-17: its characters in NFD, the canonical decomposition of Unicode, then the full case fold of fc. A file system without case, such as APFS on Darwin, keeps two names of one fold as one entry. APFS joins an NFC name and its NFD form, two cases of a name, and the sharp s U+00DF and SS. The fold joins each such pair.

The fold reads the Unicode version of this Perl. A file system of an earlier version joins fewer pairs, and a guard then refuses more than it must, which is safe. A file system of a later version can fold a character that this Perl does not know, and a guard then misses that pair.

escape_html

my $safe = App::FuguWeb::escape_html($text);

Escape &, < and >, in that order. The function reads characters and returns characters, so a character outside ASCII passes through untouched.

escape_attr

my $safe = App::FuguWeb::escape_attr($text);

The same, plus the double quote. Use it for a value on its way into a double-quoted attribute: a value that holds a quote ends the attribute early, and everything after it becomes markup. "escape_html" alone does not guard an attribute.

edit_attributes

my $fitted = App::FuguWeb::edit_attributes($attributes,
    sub ($attribute, $name, $value) { return $attribute });

The attributes of a start tag, each one as the function returns it. The function takes the attribute as it stands, its name, and its value, or undef when the attribute has none. A text after the last attribute, such as a slash, stays.

This function and "attribute" are the one reader of an attribute. App::FuguWeb::Fragment and App::FuguWeb::Check read each attribute here, so the two agree. An attribute can take its value in double quotes, in single quotes, or with no quotes. The reader reads the attributes of a tag in order, from the first one, and it steps over each value as a whole. So the text of a value, such as title="see id=x", is no attribute, and a name such as data-id is no id. HTML separates two attributes with ASCII white space alone, so a character or a byte outside ASCII never ends a value.

attribute

my $id = App::FuguWeb::attribute($attributes, 'id');

The value of the first attribute of the name in a start tag, through "edit_attributes", or undef when no attribute of the name has a value. The name is in lowercase, and the name of each attribute compares in lowercase.

list_dir

my $names = App::FuguWeb::list_dir($dir) or die "cannot read: $!";

The names in one directory, sorted, without . and ... The function returns an array reference, or undef with the reason in $!, so a caller can tell an empty directory from one it cannot read.

The sort compares bytes and never reads the locale of the builder: a site must not depend on the machine that built it.

list_tree

my $paths = App::FuguWeb::list_tree($dir) or die "cannot read: $!";

Every leaf below the directory, as paths relative to it: each file, each symlink, and each directory that holds nothing. The function returns an array reference, or undef with the reason in $!.

The function recurses into a plain directory, and never through a symlink. A symlinked directory is one entry: the build owns neither the target of the link nor what sits under it.

An empty directory is a leaf, so a caller sees it. A walk that answered with files alone would hide a stray directory from the checks, and the clean refuses one.

Each language tree and each key directory sits below the top level of a site. The output therefore needs a walk of the tree wherever a walk of one level would serve one directory.

path_below

App::FuguWeb::path_below($path, $root)

Report whether $path is $root or lies below it. A trailing slash on either does not change the answer. Both paths must be of the same kind: both absolute, or both relative to the same directory.

canonical_path

my $path = App::FuguWeb::canonical_path('./web//zh-hans/');  # web/zh-hans

The one form of a path from the project root: no . step, no empty step, and no final solidus. The record of the translations names each file in this form, per WEB-STATUS-1. The form of the project root is the empty string.

ref_parts

my ($path, $fragment) = App::FuguWeb::ref_parts($ref);

The path and the fragment of one reference. A query is no part of the name of a file, so the path holds none, per WEB-LINKS-3. The chrome and the check read each reference here, so the two agree.

resolve_ref

my $path = App::FuguWeb::resolve_ref($dir, $ref);

One relative reference, as a path below the site root. The reference is relative to $dir, which is a path below the site root. The function returns undef when the reference steps above the site root, or names the site root.

CONSTANTS

CONFIG_FILE

The name of the configuration file, .fuguwebrc. The name and the discovery match .fuguvmrc.

STAGING_DIR

The staging directory for the mdoc sources, inside the output directory. The build makes it, uses it, and removes it again.

The name lives here because two modules need it. App::FuguWeb::Site owns the directory, and App::FuguWeb::Config must refuse a key directory of the same name: the build would remove the published keys with the staging.

STYLESHEET

The name of the stylesheet in the output directory, style.css. The build writes it at the top level, outside every language tree. Each page but the forward index.html links it with a relative path, and the forward loads nothing.

FORWARD_PAGE, NOT_FOUND_PAGE

The two pages of the top level, index.html and 404.html. The first forwards to the entry page of the default tree. The second holds the body of the 404.html of each tree, for the host.

SEE ALSO

fuguweb(1), App::FuguWeb::Language, Fugu::Config, mandoc(1), lowdown(1), pod2man

AUTHOR

Dick Olsson <hi@senzilla.io>