#!/usr/bin/perl # # cs::GNUInfo: a module for parsing GNU info files. # - Cameron Simpson 22sep2000 # =head1 NAME cs::GNUInfo - parse and transcribe GNU info files =head1 SYNOPSIS use cs::GNUInfo; =head1 DESCRIPTION The B module parses GNU info(1) files and will transcribe them to perlpod(1) format for ready conversion to other useful formats. =cut use strict qw(vars); ##BEGIN { use cs::DEBUG; cs::DEBUG::using(__FILE__); } use cs::Pathname; use cs::Source; use cs::GNUInfo::Node; package cs::GNUInfo; require Exporter; @cs::GNUInfo::ISA=qw(); $cs::GNUInfo::DoDebug=defined($ENV{DEBUG_GNUINFO}) && length($ENV{DEBUG_GNUINFO}); sub dbg { return $cs::GNUInfo::DoDebug if ! @_; local($_)="@_"; chomp; warn "$_\n" if $cs::GNUInfo::DoDebug; } =head1 GENERAL FUNCTIONS =over 4 =item parseTypeLine(I) Extract the block type and following parameters from the header line of an info block. Returns an array of B<(I,I,I,I,I,...)>. =cut sub parseTypeLine($) { local($_)=@_; my $type; my %fields; /^[^:]*/; $type=uc($&); while (/^([^:]+):\s*([^,]+)(,\s*)?/) { my($op,$arg)=(uc($1),$2); $_=$'; $fields{$op}=$arg; } dbg("parseTypeLine: type=$type, fields=[".join("|",%fields)."]"); ($type,\%fields); } =back =head1 OBJECT CREATION =over 4 =item new cs::GNUInfo I Instantiate a new B object based upon the named I. =cut sub new($$) { my($class,$file)=@_; my $dir = cs::Pathname::dirname($file); $file=cs::Pathname::absname($file,$dir); my $this= bless { ROOTFILE => $file, # context ROOTDIR => $dir, FILEQUEUE => [], # pending files FILESEEN => {}, # files queued before NODEMAP => {}, # node mapping NODELIST => [], # node list NAME => cs::Pathname::basename($file), }, $class; $this->NoteFile($file); $this; } =back =head1 OBJECT METHODS =over 4 =item RunQueue() After instantiation the object is initially empty, with the named file queued for processing (via the B method below). This method processes every queued file, which should result in processing of the entire info section because subsidiary files are queued during this procedure and processed before return. =cut sub RunQueue($) { my($this)=@_; my $Q = $this->{FILEQUEUE}; FILE: while (@$Q) { my $file = shift(@$Q); dbg("RunQueue $file ...\n"); my $s = new cs::Source(PATH,$file,1); if (! defined $s) { warn "$::cmd: can't open $file: $!"; next FILE; } $this->ParseSource($s,$file); } } =item NoteFile(I) Queue the named I for processing. I is resolved into a full pathname with respect to the root file of the object. =cut sub NoteFile($$) { my($this,$file)=@_; $file=cs::Pathname::absname($file,$this->{ROOTDIR}); if (! exists $this->{FILESEEN}->{$file}) { dbg("NoteFile($file)"); push(@{$this->{FILEQUEUE}}, $file); $this->{FILESEEN}->{$file}=1; } } =item ParseSource(I,I) Read lines from the B object I (associated with the file I), assembling them into info structures: text, menus, etc. =cut sub ParseSource($$$) { my($this,$s,$fname)=@_; local($_); BLOCK: while (defined($_=$s->GetLine()) && length) { if (/^\037$/) # commence block { # get header line if (! defined ($_=$s->GetLine()) || ! length) # end of file { dbg("EOF"); last BLOCK; } # commence next block if (/^\037$/) { $s->UnGet($_); next BLOCK; } chomp; my($type,$F)=parseTypeLine($_); my $N = new cs::GNUInfo::Node $type; $N->Fields($F); if (exists $F->{NODE}) { dbg("Nodename is \"$F->{NODE}\""); $N->Name($F->{NODE}) } my $data = $N->Data(); LINE: while (defined ($_=$s->GetLine()) && length) { # beginning of next block if (/^\037$/) { $s->UnGet($_); last LINE; } if ($type eq FILE) { ## dbg("FILE: addline $_"); $N->AddLine($_,$s,$fname); } elsif ($type eq INDIRECT) { $this->_LineINDIRECT($_,$s,$fname,$F,$data); } else { chomp; dbg("$type: push \"$_\""); push(@$data, $_); } } my $nd = scalar(@$data); dbg("AddNode type=$type"); $this->AddNode($N); } else # lines outside structure - ignore { dbg("SKIP: $_"); } } $s->UnGet($_) if defined && length; } sub _LineINDIRECT($$$$$) { my($this)=shift; local($_)=shift; my($s,$fname,$F,$data)=@_; # file: byte offset if (/^([^:]+):\s*\d+$/) { $this->NoteFile($1); } else { warn "$::cmd: $fname: unparsed INDIRECT block line: $_\n"; push(@$data,$_); } } sub AddNode($$) { my($this,$N)=@_; # back reference $N->Info($this); my $nl = $this->Nodes(); push(@$nl, $N); my $name = $N->Name(); if (defined $name) { my $nm = $this->NodeMap(); if (exists $nm->{$name}) { warn "$::cmd: AddNode(): repeated nodes named \"$name\", keeping last"; } $nm->{$name}=$N; } $N; } sub Nodes($) { my($this)=@_; my $nl = $this->{NODELIST}; wantarray ? @$nl : $nl; } sub NodeMap($) { my($this)=@_; my $nm = $this->{NODEMAP}; wantarray ? %$nm : $nm; } =item Node(I) Return the B object for the supplied I. =cut sub Node($$) { my($this,$name)=@_; my $nm = $this->NodeMap(); return undef if ! exists $nm->{$name}; $nm->{$name}; } =item Pod2s(I) Write a perlpod(1) transcription of the info object to the B object I. =cut sub Pod2s($$) { my($this,$s)=@_; $s->Put("=head1 NAME\n\n".$this->{NAME}." - $this->{NAME}\n\n"); local($_); local (%::SeenNode); my $neednl=0; my $nl = $this->Nodes(); NODE: for my $N (@$nl) { my $type = $N->Type(); if ($type eq FILE) { $N->Pod2s($s); } elsif (grep($_ eq $type, "INDIRECT", "TAG TABLE","END TAG TABLE")) { dbg("skip node of type \"$type\""); } else { warn "$::cmd: Pod2s(): unhandled node type \"$type\""; } } } =back =head1 BUGS "B<*note>" tags spanning two lines are not recognised, and remain in the text. =head1 SEE ALSO info2pod(1), info2man(1), pod2man(1), perlpod(1) =head1 AUTHOR Cameron Simpson Ecs@zip.com.auE =cut 1;