NAME

App::FuguWeb::Config - the site description over Fugu::Config

SYNOPSIS

use App::FuguWeb::Config;

my $config = App::FuguWeb::Config->load(error => \my $reason)
    or die "$reason\n";

say $config->site;
say $_->{href} for $config->nav;
say $_->{file} for $config->pages;

DESCRIPTION

The class reads .fuguwebrc, applies the defaults, and validates the result. The grammar, the quoting, and the yes/no spellings come from Fugu::Config; this class holds what is true of a site.

The description is UTF-8, per WEB-SITE-5. Each value that a reader sees is characters: the site name, the name of a language, a title, a label and a heading. Each other value names a file or an address, so it stays bytes.

Languages

A language block declares one language. The block name is its tag, and the order of the blocks is the order of the languages. The first one is the default, per D-05. A description with no language block has the one language en.

language en {
        name = English
}

language pt-BR {
        name   = Portugues
        site   = Exemplo
        footer = pt-br/footer.body.html
}

A tag is a BCP 47 tag in its canonical case, per WEB-LANGUAGE-1. The block takes a name, and an optional dir of ltr or rtl. It can take a site and a footer, which take the place of those of the description in that language. App::FuguWeb::Language holds each language.

The object is immutable once loaded, and the module keeps no package state. Two sites in one process therefore share nothing.

Translations

A block of one argument is the default. A block of two is a translation: the first argument is the tag of a declared language other than the default, and the second names the default block that it translates.

page de "index.html" {
        title = Start
        body  = de/index.body.html
}

A page, a nav, a manuals and a modules block can take a translation. A translation of a page takes title, body and markdown. A translation of a nav takes label. A translation of a group takes heading and dir.

A page of a language takes each value of its translation, and the value of the default block where the translation holds none. The merged page obeys the rules of a default page. Thus a translation that names markdown for a page with a body fails, because the page then names two sources.

METHODS

load

App::FuguWeb::Config->load(
    root  => $dir,          # default: discover
    error => \my $reason,
)

Read and validate the description. The method returns the object, or undef with the reason in $reason.

Without root, the method walks up from the working directory to the first directory that holds .fuguwebrc, as fuguvm finds .fuguvmrc.

The reason travels through a reference because the object that would hold it does not exist when the load fails.

A load fails when the file is absent, when a line does not parse, or when the description would make the build do something it must not. Every message names the file, and the block when a block is at fault.

The description is rejected when:

  • the description is not UTF-8, per WEB-SITE-5;

  • it names a block type or a setting that fuguweb(1) does not document, per WEB-SITE-4. An old setting fails the same way;

  • there is no site setting, or no url. The url is the absolute URL of the site root: it starts with https:// or http://, and it ends in a solidus;

  • a language block names no tag of WEB-LANGUAGE-1, a tag that a block declares once more, no name, or a dir of another word;

  • a page block names no source, or more than one. An index page can take a body as well, which opens it;

  • two page blocks name the same file, or two manuals, or a manual and a page block, would become the same page. Each test folds each name, per WEB-OUTPUT-17;

  • a nav block has no label;

  • a translation names a tag that no language block declares, or the tag of the default language;

  • a translation takes more than two arguments, names no default block of its type, or comes twice for one name and one language;

  • a translation names a setting outside the set of its type;

  • a language, a keys or a key block takes a translation;

  • a group block names no heading, or its name is not one word of lowercase letters, digits and hyphens, or two groups share one name;

  • a source of the dir of a group translation has no namesake in the default dir;

  • a path setting steps out of the project with a .. component;

  • the source_dir is the project root, per WEB-SITE-7. The record of the translations sits there, per WEB-STATUS-1, and the build publishes each plain file of the source directory;

  • a page block name is absolute or steps out of the output directory: the name becomes a file there, so it is a path and gets the same guard the sources get;

  • a manuals namespace holds a path separator: it prefixes a manual name, and that name becomes both the staged file and the published page;

  • a modules directory is not below module_root, which would leave the module with no name to take but its whole path;

  • the path of a source from the project root holds ASCII white space or a parenthesis, per WEB-SITE-6. A source is a page source, a manual or a footer fragment, of any language;

  • a yes/no setting holds something that is neither;

  • the top level of the source directory holds a symlink, or the path of a fragment that the build reads holds one. The rule holds for the file, and for each directory between the source directory and the file. Every file at the top level that the build does not render is copied into the site. The fragments are the body fragment of each page in each language, and the footer of each language. A symlink would publish whatever it points at, from anywhere on the machine.

A keys block adds its own rules. The description is rejected when:

  • two keys blocks name one directory, or one org word, or a key block stands with no keys block;

  • the keys block names no org, or an org that no key name can hold;

  • the keys name is not one segment below the source directory, names a directory of the path, names the staging directory of the build, is the path of the tree of a declared language, or names a file that the site already holds. Each test of the name against another name, and the test of two keys blocks of one directory, folds the two, per WEB-OUTPUT-17;

  • the keys block names a contact without an expires, or the other way round, or an expires that RFC 3339 does not hold;

  • the keys block names a url that is not absolute;

  • the key directory holds no SHA256, no SHA256.sig, or a symlink;

  • a key block names no file, more than one file, or a setting that no block defines;

  • a key block names a status outside current, next and retired, or a second block declares the same key;

  • a key block names a setting that its type does not take: an email or a fingerprint on a signify key, and an email on a certificate;

  • an email is not a local part and a domain, or a fingerprint does not hold the width of its type. The fingerprint of an OpenPGP key holds 40 hexadecimal characters, and the SHA-256 of the DER of a certificate holds 64.

App::FuguWeb::Keys documents the blocks and the published tree.

root, path

The project root, and the .fuguwebrc that this object read.

source_path

my $dir  = $config->source_path;
my $file = $config->source_path('footer.body.html');

The source directory, or one file in it.

site, url, out_dir, source_dir, entry, module_root, man_url, stylesheet

The settings. site and url are the ones a project must give.

site            (required)  the site name of each language whose
                            block names none, as characters
url             (required)  the absolute URL of the site root
footer          (none)      the footer fragment of each language
                            whose block names none, below the
                            source directory
out_dir         web/build   where the build writes
source_dir      web         the fragments and the assets
entry           index.html  the front page of each tree, and the
                            header link
module_root     lib         the prefix a module name drops
                            (a trailing slash is dropped)
man_url         https://man.openbsd.org/   where a remote .Xr goes
stylesheet      (searched)  the base stylesheet

"site" in App::FuguWeb::Language and "footer" in App::FuguWeb::Language answer the site name and the footer of each language.

stylesheet overrides the search that "share_path" in Fugu::File does. The search finds the sheet that ships with the tool, so most projects never set it. "THE STYLESHEET" in App::FuguWeb::Site states the search.

url_path

The path of the url: the part after the host, which starts and ends with a solidus. A page of the top level carries it as its base, per WEB-TOP-3, and the check takes that base as the one root-absolute href, per WEB-LINKS-4.

my @nav = $config->nav('de');

The navigation of one language, in file order. Each entry is a hashref with href, label and label_lang. The tag defaults to that of the default language, and an href is the same in each language. The label_lang is the tag of the language of the label, per WEB-LANGUAGE-8.

pages

my @pages = $config->pages('de');

The pages of one language, in file order. The tag defaults to that of the default language. Each entry is a hashref:

file        the name of the page in each language tree
title       the title, which defaults to the file name
source      one of body, markdown or index
value       what the source names; for index, the body
            that opens the page, or undef
unlinked    true when no other page links to it
lang        the language of the value: the tag of the
            page when its translation names the value,
            and the default tag otherwise
title_lang  the language of the title, by the same rule

groups

The manual groups, in file order. Each entry is an App::FuguWeb::Config::Group, and the two block types interleave as the description wrote them.

languages, default_language, language

my @languages = $config->languages;
my $default   = $config->default_language;
my $chinese   = $config->language('zh-Hans');

Each App::FuguWeb::Language of the site, in the order of the declaration, the default first. Each one names a tree of the output, below the path of its tag. language answers the declared language of a tag, or undef.

page_names

The name of each page of one language tree: the pages of the description, then one page for each manual of each group. Each tree holds the same names.

top_pages

The pages of the top level: index.html, which forwards to the entry page of the default tree, and 404.html when the description holds that page.

inventory

Every path that the output directory must hold after a build: each page of each language tree, the pages of the top level, the stylesheet, the assets, and each key directory. The build and the checks read the same list, so the two can never disagree about what the site holds.

A page of a tree is <path>/<page>, for example zh-hans/manuals.html. The pages of the top level, the stylesheet and the assets are file names. A key directory is one part below the root, so its entries hold a solidus.

keys_dirs

The name of each key directory, in file order. The list is empty when the description holds no keys block, so it is the test for one.

A description can hold several keys blocks, and each one names one directory. Each directory holds its own root of trust, per D-02, and a binding never crosses a directory. Two blocks must not name one directory.

keys_org, keys_contact, keys_expires, keys_url

my $org = $config->keys_org('keys');

The settings of one keys block. Each accessor takes the name of the directory, and each one answers undef for a name that no block holds.

keys_org     (required)  the first field of every key name
keys_contact             the Contact field of security.txt
keys_expires             the Expires field of security.txt
keys_url                 the published prefix of the directory,
                         with no trailing slash

RFC 9116 makes Contact and Expires both necessary, so contact and expires stand or fall together. The Encryption field needs an absolute URL, so a block that names no url writes no such field.

A site publishes one security.txt, so one block alone names the contact and the expiry. A second block that names either one fails the load.

keys_path

my $dir  = $config->keys_path('keys');
my $file = $config->keys_path( 'keys', 'SHA256' );

One source key directory, or one file in it. The method answers undef for a name that no keys block holds.

site_keys

my @key = $config->site_keys('keys');

The key blocks of one key directory, in file order. The directory that holds the file owns the block, as the extension of that file owns the type. A stem that two directories hold fails the load.

Each entry is a hashref:

name         the key file in the directory
stem         the block name, which is the name without the
             extension
type         signify, openpgp or x509, from the extension
serial       the rotation count of the purpose
purpose      what the key signs
status       current, next or retired
since        the date the key took its status, or undef
until        the date it left it, or undef
email        the address of an OpenPGP key, or undef
wkd          the Web Key Directory hash of that address
fingerprint  the declared fingerprint, in upper case, or undef

The list is named site_keys and not keys, because a method named keys in the package makes every call of the builtin ambiguous.

site_bindings

my @binding = $config->site_bindings('keys');

The binding files of one key directory, in name order. A binding is the signature of one key file by another key of the same directory. Each entry is a hashref:

name     the binding file in the directory
target   the key file that the signature covers
signer   the key file of the key that signed it
type     the type of the signer

A binding carries no block: its name holds every field, and Fugu::KeyDir parses it. The list holds a binding whose target and whose signer are both keys of that directory, and App::FuguWeb::Keys reports every other name as a stray file.

key_paths

Every path of every key directory in the output, relative to the output directory, or the empty list when the description holds no keys block. "inventory" holds the same paths.

The site holds one well-known tree, and each directory writes into it. The list therefore holds one entry for the policy file, and one for an address that two directories name.

sources

my @paths = $config->sources;

The path of each source that a build reads, below the project root, per WEB-OUTPUT-18. The files come first: each body, Markdown page, manual, POD sidecar and footer of each language, and the stylesheet that the description names. The directories follow: the source directory, each key directory, and the dir of each group in each language. App::FuguWeb::Site refuses an output directory that holds one, and names the first one that it meets.

GROUPS

A manuals or modules block becomes one group. A group never holds a list of manuals: it reads its directory, so a manual that is added reaches the site with no edit anywhere.

A group whose directory does not exist fails the load and names the directory. A silent empty group hides a typo in a path, and a rename that nothing catches is what this file exists to prevent.

kind, name, namespace, lang

kind is manuals or modules. name is the block name, which becomes the id of the <h2>. Every language shares it. lang is the tag of the default language, and a manual of the default dir is of that language.

heading

my $text = $group->heading('de');

The text of the <h2> in one language, as characters: the heading of the translation, or the default heading. The tag defaults to that of the default language.

heading_lang

my $tag = $group->heading_lang('de');

The language of that text: the tag when the translation names a heading, and the default tag when the group takes the default heading, per WEB-LANGUAGE-8. The tag defaults to that of the default language. The index marks a heading of another language than the page, per "body" in App::FuguWeb::Index.

translate

$group->translate( 'de', heading => $text, dir => $path );

Give the group the heading and the directory of one language. A heading of undef keeps the default heading, and a dir of undef keeps the default sources for that language. The description calls it once for each translation of the block.

dirs

my @dirs = $group->dirs;

The directory of the group in each language: the default dir, then the dir of each translation that names one, in the order of the tags.

manuals

my @manuals = $group->manuals('de');

The manuals of the group in one language, in the order the index shows them, as App::FuguWeb::Manual objects. The tag defaults to that of the default language. The method reads each directory once and keeps the answer.

A translation dir mirrors the default dir. The namesake of a source is the source of the same path below the other directory, and the umbrella of a modules group is the namesake of the umbrella. A manual of a language comes from its namesake when the translation dir holds one, and from the default source otherwise. It keeps the name and the page of the default source, so each language holds the same pages.

A manuals group reads its directory for *.1, *.3p, *.5, *.7 and *.8, and takes a plain file only. It does not glob: Perl's glob splits its pattern on whitespace and reads [ ] { } ? ~, so a project whose path holds one of them would lose its manuals or collect a sibling directory's. It sorts by section, in the order 1, 3p, 5, 7, 8, and then by file name. The namespace setting prefixes the name and the staged file name, so man/fugu/Daemon.3p becomes Fugu::Daemon(3p).

A modules group finds every .pod file below its directory, and the sidecar that names the directory itself: lib/App/FuguWeb.pod is the umbrella of lib/App/FuguWeb/. It sorts by path, so Store.pod stays before Store/Memory.pod.

Both sorts compare bytes and never read the locale of the builder. A site must not depend on the machine that built it.

Never name a directory that holds more than one namespace. In this repository lib/App is such a directory, and one group there would swallow three namespaces.

orphan

my $path = $group->orphan('de');

The first source of the translation dir of one language that has no namesake in the default dir, or undef. The description refuses a group with an orphan.

SEE ALSO

App::FuguWeb, App::FuguWeb::Keys, App::FuguWeb::Manual, Fugu::Config, fuguweb(1)

AUTHOR

Dick Olsson <hi@senzilla.io>