mirror of
https://github.com/KevinMidboe/linguist.git
synced 2025-10-29 09:40:21 +00:00
631 lines
19 KiB
Raku
Executable File
631 lines
19 KiB
Raku
Executable File
#!/usr/bin/env perl6
|
|
use v6;
|
|
|
|
# This script isn't in bin/ because it's not meant to be installed.
|
|
|
|
BEGIN say 'Initializing ...';
|
|
|
|
use Pod::To::HTML;
|
|
use URI::Escape;
|
|
use lib 'lib';
|
|
use Perl6::TypeGraph;
|
|
use Perl6::TypeGraph::Viz;
|
|
use Perl6::Documentable::Registry;
|
|
|
|
my $*DEBUG = False;
|
|
|
|
my $tg;
|
|
my %methods-by-type;
|
|
my $footer = footer-html;
|
|
my $head = q[
|
|
<link rel="icon" href="/favicon.ico" type="favicon.ico" />
|
|
<link rel="stylesheet" type="text/css" href="/style.css" media="screen" title="default" />
|
|
];
|
|
|
|
|
|
sub url-munge($_) {
|
|
return $_ if m{^ <[a..z]>+ '://'};
|
|
return "/type/$_" if m/^<[A..Z]>/;
|
|
return "/routine/$_" if m/^<[a..z]>/;
|
|
# poor man's <identifier>
|
|
if m/ ^ '&'( \w <[[\w'-]>* ) $/ {
|
|
return "/routine/$0";
|
|
}
|
|
return $_;
|
|
}
|
|
|
|
sub p2h($pod) {
|
|
pod2html($pod, :url(&url-munge), :$footer, :$head);
|
|
}
|
|
|
|
sub pod-gist(Pod::Block $pod, $level = 0) {
|
|
my $leading = ' ' x $level;
|
|
my %confs;
|
|
my @chunks;
|
|
for <config name level caption type> {
|
|
my $thing = $pod.?"$_"();
|
|
if $thing {
|
|
%confs{$_} = $thing ~~ Iterable ?? $thing.perl !! $thing.Str;
|
|
}
|
|
}
|
|
@chunks = $leading, $pod.^name, (%confs.perl if %confs), "\n";
|
|
for $pod.content.list -> $c {
|
|
if $c ~~ Pod::Block {
|
|
@chunks.push: pod-gist($c, $level + 2);
|
|
}
|
|
elsif $c ~~ Str {
|
|
@chunks.push: $c.indent($level + 2), "\n";
|
|
} elsif $c ~~ Positional {
|
|
@chunks.push: $c.map: {
|
|
if $_ ~~ Pod::Block {
|
|
*.&pod-gist
|
|
} elsif $_ ~~ Str {
|
|
$_
|
|
}
|
|
}
|
|
}
|
|
}
|
|
@chunks.join;
|
|
}
|
|
|
|
sub recursive-dir($dir) {
|
|
my @todo = $dir;
|
|
gather while @todo {
|
|
my $d = @todo.shift;
|
|
for dir($d) -> $f {
|
|
if $f.f {
|
|
take $f;
|
|
}
|
|
else {
|
|
@todo.push($f.path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
sub first-code-block(@pod) {
|
|
if @pod[1] ~~ Pod::Block::Code {
|
|
return @pod[1].content.grep(Str).join;
|
|
}
|
|
'';
|
|
}
|
|
|
|
sub MAIN(Bool :$debug, Bool :$typegraph = False) {
|
|
$*DEBUG = $debug;
|
|
|
|
say 'Creating html/ subdirectories ...';
|
|
for '', <type language routine images op op/prefix op/postfix op/infix
|
|
op/circumfix op/postcircumfix op/listop> {
|
|
mkdir "html/$_" unless "html/$_".IO ~~ :e;
|
|
}
|
|
|
|
say 'Reading lib/ ...';
|
|
my @source = recursive-dir('lib').grep(*.f).grep(rx{\.pod$});
|
|
@source .= map: {; .path.subst('lib/', '').subst(rx{\.pod$}, '').subst(:g, '/', '::') => $_ };
|
|
|
|
say 'Reading type graph ...';
|
|
$tg = Perl6::TypeGraph.new-from-file('type-graph.txt');
|
|
{
|
|
my %h = $tg.sorted.kv.flat.reverse;
|
|
@source .= sort: { %h{.key} // -1 };
|
|
}
|
|
|
|
my $dr = Perl6::Documentable::Registry.new;
|
|
|
|
say 'Processing Pod files ...';
|
|
for (0..* Z @source) -> $num, $_ {
|
|
my $podname = .key;
|
|
my $file = .value;
|
|
my $what = $podname ~~ /^<[A..Z]> | '::'/ ?? 'type' !! 'language';
|
|
printf "% 4d/%d: % -40s => %s\n", $num, +@source, $file.path, "$what/$podname";
|
|
|
|
my $pod = eval slurp($file.path) ~ "\n\$=pod";
|
|
$pod .= [0];
|
|
|
|
if $what eq 'language' {
|
|
write-language-file(:$dr, :$what, :$pod, :$podname);
|
|
}
|
|
else {
|
|
say pod-gist($pod[0]) if $*DEBUG;
|
|
write-type-file(:$dr, :$what, :pod($pod[0]), :$podname);
|
|
}
|
|
}
|
|
|
|
say 'Composing doc registry ...';
|
|
$dr.compose;
|
|
|
|
write-disambiguation-files($dr);
|
|
write-op-disambiguation-files($dr);
|
|
write-operator-files($dr);
|
|
write-type-graph-images(:force($typegraph));
|
|
write-search-file($dr);
|
|
write-index-file($dr);
|
|
|
|
say 'Writing per-routine files ...';
|
|
my %routine-seen;
|
|
for $dr.lookup('routine', :by<kind>).list -> $d {
|
|
next if %routine-seen{$d.name}++;
|
|
write-routine-file($dr, $d.name);
|
|
print '.'
|
|
}
|
|
say '';
|
|
|
|
say 'Processing complete.';
|
|
}
|
|
|
|
sub write-language-file(:$dr, :$what, :$pod, :$podname) {
|
|
spurt "html/$what/$podname.html", p2h($pod);
|
|
if $podname eq 'operators' {
|
|
my @chunks = chunks-grep($pod.content,
|
|
:from({ $_ ~~ Pod::Heading and .level == 2}),
|
|
:to({ $^b ~~ Pod::Heading and $^b.level <= $^a.level}),
|
|
);
|
|
for @chunks -> $chunk {
|
|
my $heading = $chunk[0].content[0].content[0];
|
|
next unless $heading ~~ / ^ [in | pre | post | circum | postcircum ] fix | listop /;
|
|
my $what = ~$/;
|
|
my $operator = $heading.split(' ', 2)[1];
|
|
$dr.add-new(
|
|
:kind<operator>,
|
|
:subkind($what),
|
|
:name($operator),
|
|
:pod($chunk),
|
|
:!pod-is-complete,
|
|
);
|
|
}
|
|
}
|
|
$dr.add-new(
|
|
:kind<language>,
|
|
:name($podname),
|
|
:$pod,
|
|
:pod-is-complete,
|
|
);
|
|
}
|
|
|
|
sub write-type-file(:$dr, :$what, :$pod, :$podname) {
|
|
my @chunks = chunks-grep($pod.content,
|
|
:from({ $_ ~~ Pod::Heading and .level == 2}),
|
|
:to({ $^b ~~ Pod::Heading and $^b.level <= $^a.level}),
|
|
);
|
|
|
|
if $tg.types{$podname} -> $t {
|
|
$pod.content.push: Pod::Block::Named.new(
|
|
name => 'Image',
|
|
content => [ "/images/type-graph-$podname.png"],
|
|
);
|
|
$pod.content.push: pod-link(
|
|
'Full-size type graph image as SVG',
|
|
"/images/type-graph-$podname.svg",
|
|
);
|
|
|
|
my @mro = $t.mro;
|
|
@mro.shift; # current type is already taken care of
|
|
|
|
for $t.roles -> $r {
|
|
next unless %methods-by-type{$r};
|
|
$pod.content.push:
|
|
pod-heading("Methods supplied by role $r"),
|
|
pod-block(
|
|
"$podname does role ",
|
|
pod-link($r.name, "/type/$r"),
|
|
", which provides the following methods:",
|
|
),
|
|
%methods-by-type{$r}.list,
|
|
;
|
|
}
|
|
for @mro -> $c {
|
|
next unless %methods-by-type{$c};
|
|
$pod.content.push:
|
|
pod-heading("Methods supplied by class $c"),
|
|
pod-block(
|
|
"$podname inherits from class ",
|
|
pod-link($c.name, "/type/$c"),
|
|
", which provides the following methods:",
|
|
),
|
|
%methods-by-type{$c}.list,
|
|
;
|
|
for $c.roles -> $r {
|
|
next unless %methods-by-type{$r};
|
|
$pod.content.push:
|
|
pod-heading("Methods supplied by role $r"),
|
|
pod-block(
|
|
"$podname inherits from class ",
|
|
pod-link($c.name, "/type/$c"),
|
|
", which does role ",
|
|
pod-link($r.name, "/type/$r"),
|
|
", which provides the following methods:",
|
|
),
|
|
%methods-by-type{$r}.list,
|
|
;
|
|
}
|
|
}
|
|
}
|
|
my $d = $dr.add-new(
|
|
:kind<type>,
|
|
# TODO: subkind
|
|
:$pod,
|
|
:pod-is-complete,
|
|
:name($podname),
|
|
);
|
|
|
|
for @chunks -> $chunk {
|
|
my $name = $chunk[0].content[0].content[0];
|
|
say "$podname.$name" if $*DEBUG;
|
|
next if $name ~~ /\s/;
|
|
%methods-by-type{$podname}.push: $chunk;
|
|
# determine whether it's a sub or method
|
|
my Str $subkind;
|
|
{
|
|
my %counter;
|
|
for first-code-block($chunk).lines {
|
|
if ms/^ 'multi'? (sub|method)»/ {
|
|
%counter{$0}++;
|
|
}
|
|
}
|
|
if %counter == 1 {
|
|
($subkind,) = %counter.keys;
|
|
}
|
|
if %counter<method> {
|
|
write-qualified-method-call(
|
|
:$name,
|
|
:pod($chunk),
|
|
:type($podname),
|
|
);
|
|
}
|
|
}
|
|
|
|
$dr.add-new(
|
|
:kind<routine>,
|
|
:$subkind,
|
|
:$name,
|
|
:pod($chunk),
|
|
:!pod-is-complete,
|
|
:origin($d),
|
|
);
|
|
}
|
|
|
|
spurt "html/$what/$podname.html", p2h($pod);
|
|
}
|
|
|
|
sub chunks-grep(:$from!, :&to!, *@elems) {
|
|
my @current;
|
|
|
|
gather {
|
|
for @elems -> $c {
|
|
if @current && ($c ~~ $from || to(@current[0], $c)) {
|
|
take [@current];
|
|
@current = ();
|
|
@current.push: $c if $c ~~ $from;
|
|
}
|
|
elsif @current or $c ~~ $from {
|
|
@current.push: $c;
|
|
}
|
|
}
|
|
take [@current] if @current;
|
|
}
|
|
}
|
|
|
|
sub pod-with-title($title, *@blocks) {
|
|
Pod::Block::Named.new(
|
|
name => "pod",
|
|
content => [
|
|
Pod::Block::Named.new(
|
|
name => "TITLE",
|
|
content => Array.new(
|
|
Pod::Block::Para.new(
|
|
content => [$title],
|
|
)
|
|
)
|
|
),
|
|
@blocks.flat,
|
|
]
|
|
);
|
|
}
|
|
|
|
sub pod-block(*@content) {
|
|
Pod::Block::Para.new(:@content);
|
|
}
|
|
|
|
sub pod-link($text, $url) {
|
|
Pod::FormattingCode.new(
|
|
type => 'L',
|
|
content => [
|
|
join('|', $text, $url),
|
|
],
|
|
);
|
|
}
|
|
|
|
sub pod-item(*@content, :$level = 1) {
|
|
Pod::Item.new(
|
|
:$level,
|
|
:@content,
|
|
);
|
|
}
|
|
|
|
sub pod-heading($name, :$level = 1) {
|
|
Pod::Heading.new(
|
|
:$level,
|
|
:content[pod-block($name)],
|
|
);
|
|
}
|
|
|
|
sub write-type-graph-images(:$force) {
|
|
unless $force {
|
|
my $dest = 'html/images/type-graph-Any.svg'.path;
|
|
if $dest.e && $dest.modified >= 'type-graph.txt'.path.modified {
|
|
say "Not writing type graph images, it seems to be up-to-date";
|
|
say "To force writing of type graph images, supply the --typegraph";
|
|
say "option at the command line, or delete";
|
|
say "file 'html/images/type-graph-Any.svg'";
|
|
return;
|
|
}
|
|
}
|
|
say 'Writing type graph images to html/images/ ...';
|
|
for $tg.sorted -> $type {
|
|
my $viz = Perl6::TypeGraph::Viz.new-for-type($type);
|
|
$viz.to-file("html/images/type-graph-{$type}.svg", format => 'svg');
|
|
$viz.to-file("html/images/type-graph-{$type}.png", format => 'png', size => '8,3');
|
|
print '.'
|
|
}
|
|
say '';
|
|
|
|
say 'Writing specialized visualizations to html/images/ ...';
|
|
my %by-group = $tg.sorted.classify(&viz-group);
|
|
%by-group<Exception>.push: $tg.types< Exception Any Mu >;
|
|
%by-group<Metamodel>.push: $tg.types< Any Mu >;
|
|
|
|
for %by-group.kv -> $group, @types {
|
|
my $viz = Perl6::TypeGraph::Viz.new(:types(@types),
|
|
:dot-hints(viz-hints($group)),
|
|
:rank-dir('LR'));
|
|
$viz.to-file("html/images/type-graph-{$group}.svg", format => 'svg');
|
|
$viz.to-file("html/images/type-graph-{$group}.png", format => 'png', size => '8,3');
|
|
}
|
|
}
|
|
|
|
sub viz-group ($type) {
|
|
return 'Metamodel' if $type.name ~~ /^ 'Perl6::Metamodel' /;
|
|
return 'Exception' if $type.name ~~ /^ 'X::' /;
|
|
return 'Any';
|
|
}
|
|
|
|
sub viz-hints ($group) {
|
|
return '' unless $group eq 'Any';
|
|
|
|
return '
|
|
subgraph "cluster: Mu children" {
|
|
rank=same;
|
|
style=invis;
|
|
"Any";
|
|
"Junction";
|
|
}
|
|
subgraph "cluster: Pod:: top level" {
|
|
rank=same;
|
|
style=invis;
|
|
"Pod::Config";
|
|
"Pod::Block";
|
|
}
|
|
subgraph "cluster: Date/time handling" {
|
|
rank=same;
|
|
style=invis;
|
|
"Date";
|
|
"DateTime";
|
|
"DateTime-local-timezone";
|
|
}
|
|
subgraph "cluster: Collection roles" {
|
|
rank=same;
|
|
style=invis;
|
|
"Positional";
|
|
"Associative";
|
|
"Baggy";
|
|
}
|
|
';
|
|
}
|
|
|
|
sub write-search-file($dr) {
|
|
say 'Writing html/search.html ...';
|
|
my $template = slurp("search_template.html");
|
|
my @items;
|
|
my sub fix-url ($raw) { $raw.substr(1) ~ '.html' };
|
|
@items.push: $dr.lookup('language', :by<kind>).sort(*.name).map({
|
|
"\{ label: \"Language: {.name}\", value: \"{.name}\", url: \"{ fix-url(.url) }\" \}"
|
|
});
|
|
@items.push: $dr.lookup('type', :by<kind>).sort(*.name).map({
|
|
"\{ label: \"Type: {.name}\", value: \"{.name}\", url: \"{ fix-url(.url) }\" \}"
|
|
});
|
|
my %seen;
|
|
@items.push: $dr.lookup('routine', :by<kind>).grep({!%seen{.name}++}).sort(*.name).map({
|
|
"\{ label: \"{ (.subkind // 'Routine').tclc }: {.name}\", value: \"{.name}\", url: \"{ fix-url(.url) }\" \}"
|
|
});
|
|
sub escape(Str $s) {
|
|
$s.trans([</ \\ ">] => [<\\/ \\\\ \\">]);
|
|
}
|
|
@items.push: $dr.lookup('operator', :by<kind>).map({
|
|
qq[\{ label: "$_.human-kind() {escape .name}", value: "{escape .name}", url: "{ fix-url .url }"\}]
|
|
});
|
|
|
|
my $items = @items.join(",\n");
|
|
spurt("html/search.html", $template.subst("ITEMS", $items));
|
|
}
|
|
|
|
my %operator_disambiguation_file_written;
|
|
|
|
sub write-disambiguation-files($dr) {
|
|
say 'Writing disambiguation files ...';
|
|
for $dr.grouped-by('name').kv -> $name, $p is copy {
|
|
print '.';
|
|
my $pod = pod-with-title("Disambiguation for '$name'");
|
|
if $p.elems == 1 {
|
|
$p.=[0] if $p ~~ Array;
|
|
if $p.origin -> $o {
|
|
$pod.content.push:
|
|
pod-block(
|
|
pod-link("'$name' is a $p.human-kind()", $p.url),
|
|
' from ',
|
|
pod-link($o.human-kind() ~ ' ' ~ $o.name, $o.url),
|
|
);
|
|
}
|
|
else {
|
|
$pod.content.push:
|
|
pod-block(
|
|
pod-link("'$name' is a $p.human-kind()", $p.url)
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
$pod.content.push:
|
|
pod-block("'$name' can be anything of the following"),
|
|
$p.map({
|
|
if .origin -> $o {
|
|
pod-item(
|
|
pod-link(.human-kind, .url),
|
|
' from ',
|
|
pod-link($o.human-kind() ~ ' ' ~ $o.name, $o.url),
|
|
)
|
|
}
|
|
else {
|
|
pod-item( pod-link(.human-kind, .url) )
|
|
}
|
|
});
|
|
}
|
|
my $html = p2h($pod);
|
|
spurt "html/$name.html", $html;
|
|
if all($p>>.kind) eq 'operator' {
|
|
spurt "html/op/$name.html", $html;
|
|
%operator_disambiguation_file_written{$p[0].name} = 1;
|
|
}
|
|
}
|
|
say '';
|
|
}
|
|
|
|
sub write-op-disambiguation-files($dr) {
|
|
say 'Writing operator disambiguation files ...';
|
|
for $dr.lookup('operator', :by<kind>).classify(*.name).kv -> $name, @ops {
|
|
next unless %operator_disambiguation_file_written{$name};
|
|
my $pod = pod-with-title("Disambiguation for '$name'");
|
|
if @ops == 1 {
|
|
my $p = @ops[0];
|
|
if $p.origin -> $o {
|
|
$pod.content.push:
|
|
pod-block(
|
|
pod-link("'$name' is a $p.human-kind()", $p.url),
|
|
' from ',
|
|
pod-link($o.human-kind() ~ ' ' ~ $o.name, $o.url),
|
|
);
|
|
}
|
|
else {
|
|
$pod.content.push:
|
|
pod-block(
|
|
pod-link("'$name' is a $p.human-kind()", $p.url)
|
|
);
|
|
}
|
|
}
|
|
else {
|
|
$pod.content.push:
|
|
pod-block("'$name' can be anything of the following"),
|
|
@ops.map({
|
|
if .origin -> $o {
|
|
pod-item(
|
|
pod-link(.human-kind, .url),
|
|
' from ',
|
|
pod-link($o.human-kind() ~ ' ' ~ $o.name, $o.url),
|
|
)
|
|
}
|
|
else {
|
|
pod-item( pod-link(.human-kind, .url) )
|
|
}
|
|
});
|
|
}
|
|
my $html = p2h($pod);
|
|
spurt "html/$name.html", $html;
|
|
}
|
|
|
|
}
|
|
|
|
sub write-operator-files($dr) {
|
|
say 'Writing operator files ...';
|
|
for $dr.lookup('operator', :by<kind>).list -> $doc {
|
|
my $what = $doc.subkind;
|
|
my $op = $doc.name;
|
|
my $pod = pod-with-title(
|
|
"$what.tclc() $op operator",
|
|
pod-block(
|
|
"Documentation for $what $op, extracted from ",
|
|
pod-link("the operators language documentation", "/language/operators")
|
|
),
|
|
@($doc.pod),
|
|
);
|
|
spurt "html/op/$what/$op.html", p2h($pod);
|
|
}
|
|
}
|
|
|
|
sub write-index-file($dr) {
|
|
say 'Writing html/index.html ...';
|
|
my %routine-seen;
|
|
my $pod = pod-with-title('Perl 6 Documentation',
|
|
Pod::Block::Para.new(
|
|
content => ['Official Perl 6 documentation'],
|
|
),
|
|
# TODO: add more
|
|
pod-heading("Language Documentation"),
|
|
$dr.lookup('language', :by<kind>).sort(*.name).map({
|
|
pod-item( pod-link(.name, .url) )
|
|
}),
|
|
pod-heading('Types'),
|
|
$dr.lookup('type', :by<kind>).sort(*.name).map({
|
|
pod-item(pod-link(.name, .url))
|
|
}),
|
|
pod-heading('Routines'),
|
|
$dr.lookup('routine', :by<kind>).sort(*.name).map({
|
|
next if %routine-seen{.name}++;
|
|
pod-item(pod-link(.name, .url))
|
|
}),
|
|
);
|
|
spurt 'html/index.html', p2h($pod);
|
|
}
|
|
|
|
sub write-routine-file($dr, $name) {
|
|
say 'Writing html/routine/$name.html ...' if $*DEBUG;
|
|
my @docs = $dr.lookup($name, :by<name>).grep(*.kind eq 'routine');
|
|
my $subkind = 'routine';
|
|
{
|
|
my @subkinds = @docs>>.subkind;
|
|
$subkind = @subkinds[0] if all(@subkinds>>.defined) && [eq] @subkinds;
|
|
}
|
|
my $pod = pod-with-title("Documentation for $subkind $name",
|
|
pod-block("Documentation for $subkind $name, assembled from the
|
|
following types:"),
|
|
@docs.map({
|
|
pod-heading(.origin.name ~ '.' ~ .name),
|
|
pod-block("From ", pod-link(.origin.name, .origin.url ~ '#' ~ .name)),
|
|
.pod.list,
|
|
})
|
|
);
|
|
spurt "html/routine/$name.html", p2h($pod);
|
|
}
|
|
|
|
sub write-qualified-method-call(:$name!, :$pod!, :$type!) {
|
|
my $p = pod-with-title(
|
|
"Documentation for method $type.$name",
|
|
pod-block('From ', pod-link($type, "/type/{$type}#$name")),
|
|
@$pod,
|
|
);
|
|
spurt "html/{$type}.{$name}.html", p2h($p);
|
|
}
|
|
|
|
sub footer-html() {
|
|
state $dt = ~DateTime.now;
|
|
qq[
|
|
<div id="footer">
|
|
<p>
|
|
Generated on $dt from the sources at
|
|
<a href="https://github.com/perl6/doc">perl6/doc on github</a>.
|
|
</p>
|
|
<p>
|
|
This is a work in progress to document Perl 6, and known to be
|
|
incomplete. Your contribution is appreciated.
|
|
</p>
|
|
</div>
|
|
];
|
|
}
|