5
0
mirror of git://git.proxmox.com/git/pve-docs.git synced 2025-01-06 13:17:48 +03:00
pve-docs/asciidoc-pve.in
Stefan Sterz 1f0e8a1624 asciidoc-pve: disallow certain characters in onlineHelp keys
this removes the lazily evaluated part of the regex used to detect
`onlineHelp` keys in order to match them with asciidoc anchors in the
documentation. it uses a pattern that excludes curly braces, brackets
and quotations instead. this allows for greedy matching and also
removes an issue where keys that are set via CBind were included
incorrectly. the script would try to find an anchor name that was
equivalent to the CBind placeholder, which would fail.

since brackets, curly braces and quotation marks aren't allowed in
asciidoc anchors anyway, excluding them like this should be safe.

Signed-off-by: Stefan Sterz <s.sterz@proxmox.com>
2022-11-14 14:27:16 +01:00

635 lines
14 KiB
Perl

#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
use File::Path;
use File::Basename;
use IO::File;
use Cwd;
use JSON;
my $verbose;
my $keep_artifacts;
my $release = '@RELEASE@';
my $clicmd = shift or
die "no command specified\n";
my $data_str = "";
while (<main::DATA>) { $data_str .= $_; }
my $fileinfo = decode_json($data_str);
my $tmpprefix = '.asciidoc-pve-tmp'.$$.'_';
my $adoc_source_dir = "/usr/share/pve-doc-generator";
# inside pve-docs source dir?
if (-f "asciidoc-pve.in" && -f "pve-admin-guide.adoc") {
$adoc_source_dir = getcwd();
}
my $prepared_files = {};
my $man_target = 'man';
my $env_stack = [];
my $env_skip = 0;
my $online_help_links = {
'pve_service_daemons' => {
link => '/pve-docs/index.html#_service_daemons',
title => 'Service Daemons',
},
'pve_documentation_index' => {
link => '/pve-docs/index.html',
title => 'Proxmox VE Documentation Index',
},
'pve_admin_guide' => {
link => '/pve-docs/pve-admin-guide.html',
title => 'Proxmox VE Administration Guide',
},
};
sub debug {
my $msg = shift;
return if !$verbose;
print STDERR "asciidoc-pve: $msg\n";
}
sub push_environment {
my ($env, $skip) = @_;
$skip = 1 if $env_skip;
$skip = 0 if !defined($skip);
push @$env_stack, [$env, $skip];
$env_skip = $skip;
}
sub pop_environment {
my ($env) = @_;
my $last_stack_entry = pop @$env_stack;
die "unable to pop env '$env'" if !defined($last_stack_entry);
my ($last_env, $skip) = @$last_stack_entry;
die "environment missmatch (${last_env} != $env)\n" if $last_env ne $env;
if (!scalar(@$env_stack)) {
$env_skip = 0;
} else {
my (undef, $skip) = @{$env_stack->[-1]};
$env_skip = $skip;
}
}
my $files_for_cleanup = [];
sub cleanup {
return if $keep_artifacts;
foreach my $file (@$files_for_cleanup) {
unlink $file;
}
}
sub replace_wiki_xref {
my ($blockid, $text) = @_;
my $link = $fileinfo->{blockid_target}->{wiki}->{$blockid};
my $reftext = $fileinfo->{reftext}->{wiki}->{$blockid};
die "unable to resolve wiki link (xref:$blockid)\n"
if !defined($link);
$text = $reftext if !length($text);
die "xref: no text for wiki link '$blockid'\n" if !$text;
return "$link\[$text\]";
}
sub replace_default_xref {
my ($blockid, $text) = @_;
my $link = $fileinfo->{blockid_target}->{default}->{$blockid};
my $reftext = $fileinfo->{reftext}->{default}->{$blockid};
die "unable to resolve chapter link (xref:$blockid)\n"
if !defined($link);
$text = $reftext if !length($text);
die "xref: no text for chapter link '$blockid'\n" if !$text;
return "$link\[$text\]";
}
sub replace_man_xref {
my ($blockid, $text) = @_;
my $link = $fileinfo->{blockid_target}->{manvolnum}->{$blockid};
my $reftext = $fileinfo->{reftext}->{manvolnum}->{$blockid};
die "unable to resolve man page link (xref:$blockid)\n"
if !defined($link);
$text = $reftext if !length($text);
die "xref: no text for man page link '$blockid'\n" if !$text;
my $section = $fileinfo->{mansection}->{manvolnum}->{$link};
if (!defined($section)) {
warn "link '$blockid' target '$link' is not a manual page, ignoring\n";
return "$text";
}
if ($man_target eq 'html') {
my $target = $link;
$target =~ s/\.adoc//;
$target .= ".$section";
return "link:${target}.html#${blockid}\[$text\]";
} elsif ($man_target eq 'man') {
my $command = $link;
$command =~ s/\.adoc//;
return "\*${text}\* (man \*${command}\*($section))";
} else {
die "internal error"
}
}
sub replace_xref {
my ($env, $blockid, $text) = @_;
if ($env eq 'wiki') {
return replace_wiki_xref($blockid, $text);
} elsif ($env eq 'manvolnum') {
if (($man_target eq 'man') || ($man_target eq 'html')) {
return replace_man_xref($blockid, $text);
} elsif ($man_target eq 'wiki') {
return replace_wiki_xref($blockid, $text);
} else {
die "internal error"
}
} elsif ($env eq 'default') {
return replace_default_xref($blockid, $text);
} else {
die "internal error";
}
}
sub prepare_adoc_file {
my ($target_env, $filename, $attributes) = @_;
return $prepared_files->{$filename} if defined($prepared_files->{$filename});
debug("prepare $filename");
my $dirname = dirname($filename);
my $basename = basename($filename);
my $outfilename = "$dirname/${tmpprefix}$basename";
$prepared_files->{$filename} = $outfilename;
my $fh = IO::File->new("$filename", "r") or
die "unable to open file '$filename' - $!\n";
my $outfh = IO::File->new("$outfilename", "w") or
die "unable to open temporary file '$outfilename'\n";
push @$files_for_cleanup, $outfilename;
while (defined (my $line = <$fh>)) {
chomp $line;
if ($line =~ m/^if(n?)def::(\S+)\[(.*)\]\s*$/) {
my ($not, $env, $text) = ($1, $2, $3);
die "unsuported ifdef usage - implement me" if $text;
my $skip = !exists($attributes->{$env}) ? 1 : 0;
$skip = ($skip ? 0 : 1 ) if $not;
push_environment($env, $skip);
next;
} elsif ($line =~ m/^endif::(\S+)\[(.*)\]\s*$/) {
my ($env, $text) = ($1, $2);
die "unsuported ifdef usage - implement me" if $text;
pop_environment($env);
next;
}
next if $env_skip;
if ($line =~ m/^include::(\S+)(\[.*\]\s*)$/) {
my ($fn, $rest) = ($1, $2);
debug("include $fn");
my $new_fn = prepare_adoc_file($target_env, $fn, $attributes);
print $outfh "include::${new_fn}$rest\n";
next;
}
if ($line =~ m/xref:\S+?\[[^\]]*$/) {
die "possible xref spanning multiple lines in '$filename':\n(line $.): $line\n";
}
if ($line =~ m/<<((?!\>\>).)*$/) {
die "possible xref spanning multiple lines in '$filename':\n(line $.): $line\n";
}
# fix xrefs
$line =~ s/xref:([^\s\[\]]+)\[([^\]]*)\]/replace_xref(${target_env},$1,$2)/ge;
$line =~ s/<<([^\s,\[\]]+)(?:,(.*?))?>>/replace_xref(${target_env},$1,$2)/ge;
print $outfh $line . "\n";
}
return $outfilename;
}
sub compile_asciidoc {
my ($env) = @_;
my $outfile;
GetOptions ("outfile=s" => \$outfile,
"keep-artifacts" => \$keep_artifacts,
"verbose" => \$verbose) or
die("Error in command line arguments\n");
my $infile = shift(@ARGV) or
die "no input file specified\n";
scalar(@ARGV) == 0 or
die "too many arguments...\n";
my $outfilemap = $fileinfo->{outfile}->{$env}->{$infile} ||
die "no output file mapping for '$infile' ($env)";
if ($man_target eq 'html') {
$outfilemap .= '.html';
} elsif ($man_target eq 'wiki') {
$outfilemap .= '-plain.html';
}
if (defined($outfile)) {
die "wrong output file name '$outfile != $outfilemap' ($env)"
if $outfile ne $outfilemap;
} else {
$outfile = $outfilemap;
}
defined($fileinfo->{titles}->{$env}) ||
die "unknown environment '$env'";
my $title = $fileinfo->{titles}->{$env}->{$infile} or
die "unable to get title for '$infile'$env\n";
debug("compile $title");
my $leveloffset = 0;
my $doctype = $fileinfo->{doctype}->{$env}->{$infile};
die "unable to get document type for '$infile'\n"
if !defined($doctype);
$leveloffset = - $doctype;
my $date;
if (defined($ENV{SOURCE_DATE_EPOCH})) {
$date = `date -d "\@$ENV{SOURCE_DATE_EPOCH}"`;
} else {
$date = `date`;
}
chomp $date;
my $attributes = {
$env => undef,
leveloffset => $leveloffset,
revnumber => $release,
revdate => $date,
'footer-style' => 'revdate',
};
my $mansection = $fileinfo->{mansection}->{$env}->{$infile};
if ($env eq 'wiki') {
} elsif ($env eq 'manvolnum') {
die "undefined man section" if !defined($mansection);
$attributes->{manvolnum} = $mansection;
} elsif ($env eq 'default') {
die "$infile: wrong doctype\n" if $doctype != 0;
$attributes->{toc2} = undef;
}
if (!defined($outfile)) {
$outfile = $infile;
$outfile =~ s/\.adoc$//;
if ($env eq 'manvolnum') {
if (($man_target eq 'html') || ($man_target eq 'wiki')) {
$outfile .= ".$mansection.html";
} else {
$outfile .= ".$mansection";
}
} else {
$outfile .= ".html";
}
}
if (($env eq 'manvolnum') && ($man_target eq 'man')) {
# asciidoc /etc/asciidoc/docbook-xsl/manpage.xsl skip REFERENCES
# section like footnotes, so we cannot use a2x.
# We use xmlto instead.
my $cmd = ['asciidoc', '-dmanpage', '-bdocbook',
'-f', "$adoc_source_dir/asciidoc/asciidoc-pve.conf",
'-a', 'docinfo1'];
foreach my $key (keys %$attributes) {
my $value = $attributes->{$key};
if (defined($value)) {
push @$cmd, '-a', "$key=$value";
} else {
push @$cmd, '-a', $key;
}
}
push @$cmd, '--verbose' if $verbose;
my $tmpxmlfile = "${outfile}.xml.tmp";
push @$cmd, '--out-file', $tmpxmlfile;
push @$files_for_cleanup, $tmpxmlfile;
my $new_infile = prepare_adoc_file($env, $infile, $attributes);
push @$cmd, $new_infile;
debug("run " . join(' ', @$cmd));
system(@$cmd) == 0 or
die "aciidoc error";
$cmd = ['xmlto', 'man', $tmpxmlfile];
push @$cmd, '-v' if $verbose;
debug("run " . join(' ', @$cmd));
system(@$cmd) == 0 or
die "xmlto error";
} else {
$attributes->{icons} = undef;
$attributes->{'data-uri'} = undef;
my $cmd = ['asciidoc',
'-f', "$adoc_source_dir/asciidoc/asciidoc-pve.conf",
];
if (($env eq 'wiki') ||
(($env eq 'manvolnum') && ($man_target eq 'wiki'))) {
push @$cmd, '-b', "$adoc_source_dir/asciidoc/mediawiki";
} else {
push @$cmd, '-b', "$adoc_source_dir/asciidoc/pve-html";
}
foreach my $key (keys %$attributes) {
my $value = $attributes->{$key};
if (defined($value)) {
push @$cmd, '-a', "$key=$value";
} else {
push @$cmd, '-a', $key;
}
}
push @$cmd, '--verbose' if $verbose;
push @$cmd, '--out-file', $outfile;
my $new_infile = prepare_adoc_file($env, $infile, $attributes);
push @$cmd, $new_infile;
debug("run " . join(' ', @$cmd));
system(@$cmd) == 0 or
die "aciidoc error";
}
}
sub get_links {
my $data = {};
foreach my $blockid (sort keys %{$fileinfo->{blockid_target}->{default}}) {
my $link = $fileinfo->{blockid_target}->{default}->{$blockid};
my $reftitle = $fileinfo->{reftitle}->{default}->{$blockid};
my $reftext = $fileinfo->{reftext}->{default}->{$blockid};
die "internal error" if $link !~ m/^link:/;
$link =~ s/^link://;
my $file = $fileinfo->{blockid}->{default}->{$blockid};
die "internal error - no filename" if ! defined($file);
my $title = $fileinfo->{titles}->{default}->{$file} ||
die "internal error - no title";
$data->{$blockid}->{title} = $title;
$data->{$blockid}->{link} = $link;
my $subtitle = $reftitle || $reftext;
$data->{$blockid}->{subtitle} = $subtitle
if $subtitle && ($title ne $subtitle);
}
return $data;
}
sub scan_extjs_file {
my ($filename, $res_data) = @_;
my $fh = IO::File->new($filename, "r") ||
die "unable to open '$filename' - $!\n";
debug("scan-extjs $filename");
while(defined(my $line = <$fh>)) {
if ($line =~ m/\s+onlineHelp:\s*[\'\"]([^{}\[\]\'\"]+)[\'\"]/) {
my $blockid = $1;
my $link = $fileinfo->{blockid_target}->{default}->{$blockid};
die "undefined blockid '$blockid' ($filename, line $.)\n"
if !(defined($link) || defined($online_help_links->{$blockid}));
$res_data->{$blockid} = 1;
}
}
}
if ($clicmd eq 'compile-wiki') {
eval { compile_asciidoc('wiki'); };
my $err = $@;
cleanup();
die $err if $err;
} elsif ($clicmd eq 'compile-chapter') {
eval { compile_asciidoc('default'); };
my $err = $@;
cleanup();
die $err if $err;
} elsif ($clicmd eq 'compile-man-html') {
$man_target = 'html';
eval { compile_asciidoc('manvolnum'); };
my $err = $@;
cleanup();
die $err if $err;
} elsif ($clicmd eq 'compile-man-wiki') {
$man_target = 'wiki';
eval { compile_asciidoc('manvolnum'); };
my $err = $@;
cleanup();
die $err if $err;
} elsif ($clicmd eq 'compile-man') {
eval { compile_asciidoc('manvolnum'); };
my $err = $@;
cleanup();
die $err if $err;
} elsif ($clicmd eq 'print-links') {
my $outfile;
GetOptions("outfile=s" => \$outfile,
"verbose" => \$verbose) or
die("Error in command line arguments\n");
scalar(@ARGV) == 0 or
die "too many arguments...\n";
my $data = get_links();
my $res = to_json($data, { pretty => 1, canonical => 1 } );
if (defined($outfile)) {
my $outfh = IO::File->new("$outfile", "w") or
die "unable to open temporary file '$outfile'\n";
print $outfh $res;
} else {
print $res;
}
} elsif ($clicmd eq 'scan-extjs') {
GetOptions("verbose" => \$verbose) or
die("Error in command line arguments\n");
my $link_hash = {};
my $scanned_files = {};
while (my $filename = shift) {
die "got strange file name '$filename'\n"
if $filename !~ m/\.js$/;
next if $scanned_files->{$filename};
scan_extjs_file($filename, $link_hash);
$scanned_files->{$filename} = 1;
}
my $data = get_links();
my $res_data = {};
foreach my $blockid (keys %$link_hash) {
$res_data->{$blockid} = $data->{$blockid} || $online_help_links->{$blockid} ||
die "internal error - no data for '$blockid'";
}
my $data_str = to_json($res_data, { pretty => 1, canonical => 1 });
chomp $data_str;
print "const pveOnlineHelpInfo = ${data_str};\n";
} elsif ($clicmd eq 'chapter-table') {
print '[width="100%",options="header"]' . "\n";
print "|====\n";
print "|Title|Link\n";
my $filelist = $fileinfo->{outfile}->{default};
foreach my $sourcefile (sort keys %$filelist) {
my $target = $filelist->{$sourcefile};
next if $target eq 'pve-admin-guide.html';
my $title = $fileinfo->{titles}->{default}->{$sourcefile} ||
die "not title for '$sourcefile'";
print "|$title|link:$target\[\]\n";
}
print "|====\n";
} elsif ($clicmd =~ m/^man([158])page-table$/) {
my $section = $1;
print '[width="100%",cols="5*d",options="header"]' . "\n";
print "|====\n";
print "|Name 3+|Title|Link\n";
my $filelist = $fileinfo->{outfile}->{manvolnum};
foreach my $manpage (sort keys %$filelist) {
next if $section ne $fileinfo->{mansection}->{manvolnum}->{$manpage};
my $mantitle = $fileinfo->{titles}->{manvolnum}->{$manpage} ||
die "not manual title for '$manpage'";
my $title = $fileinfo->{titles}->{default}->{$manpage} ||
die "not title for '$manpage'";
# hack - remove command name prefix from titles
$title =~ s/^[a-z]+\s*-\s*//;
my $target = $filelist->{$manpage};
print "|$mantitle 3+|$title|link:$target.html\[$target\]\n";
}
print "|====\n";
} else {
die "unknown command '$clicmd'\n";
}
exit 0;
__END__