The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# $Id$
$VERSION{''.__FILE__} = '$Revision$';
#
# >>Title::     MIF Format Driver
#
# >>Copyright::
# Copyright (c) 1992-1996, Ian Clatworthy (ianc@mincom.com).
# You may distribute under the terms specified in the LICENSE file.
#
# >>History::
# -----------------------------------------------------------------------
# Date      Who     Change
# 01-Mar-98 ianc    Inner and Outer alignment of tables now supported
#                   David Schooley's patch to MifPathName also applied.
# 29-Feb-96 ianc    SDF 2.000
# -----------------------------------------------------------------------
#
# >>Purpose::
# This library provides an [[SDF_DRIVER]] which generates
# [[FrameMaker]]'s [[MIF]] (Maker Interchange Format) files.
#
# >>Description::
# Variables supported include:
# 
# * {{MIF_EXT}} - the extension to use for Frame files in jumps
# * {{MIF_BOOK_MODE}} - build a book, rather than a normal document
# * {{MIF_ALL_STYLES}} - output all paragraph styles, rather than
#   just the used ones.
#
# >>Limitations::
# Changing a font family or style does not update the FPostScriptName
# attribute. In practice, this may not be a problem as it may be
# fixed by [[FrameMaker]] during the loading of a [[MIF]] file.
#
# Is _MifProcessControls() the best way to handle document control settings?
# (There may be problems with only processing DOC_* variables at the end.)
#
# We do not support a Running header/footer type of {{paratag}} as
# inserting the name of a Frame paragraph tag would probably be
# meaningless.
#
# Only the predefined colours are currently supported. Ultimately,
# the RGB value of an arbitary color could be converted to CMYK and
# added to the color catalog.
#
# >>Resources::
#
# >>Implementation::
#

##### Constants #####

# FrameMaker 5.0 adds blank pages for tables with titles, so
# this configuration flag controls whether
# we produce real or simple/simulated table titles.
# Unless a book is being generated, generating a list of tables
# only works when table titles are separate paragraphs.
$_MIF_SIMPLE_TBL_TITLES = 1;

# Define the set of paragraph attributes.
@MIF_PARA_ATTRS = (
    'AcrobatLevel',
    'Alignment',
    'AutoNum',
    'BlockSize',
    'BotSepAtIndent',
    'BotSepOffset',
    'BotSeparator',
    'CellAlignment',
    'CellBMarginFixed',
    'CellLMarginFixed',
    'CellMargins',
    'CellRMarginFixed',
    'CellTMarginFixed',
    'FIndent',
    'FIndentOffset',
    'FIndentRelative',
    'FontAngle',
    'FontCase',
    'FontChangeBar',
    'FontColor',
    'FontDW',
    'FontDX',
    'FontDY',
    'FontFamily',
    'FontLocked',
    'FontOutline',
    'FontOverline',
    'FontPairKern',
    'FontPosition',
    'FontSeparation',
    'FontShadow',
    'FontSize',
    'FontStrike',
    'FontUnderlining',
    'FontVar',
    'FontWeight',
    'HyphenMaxLines',
    'HyphenMinPrefix',
    'HyphenMinSuffix',
    'HyphenMinWord',
    'Hyphenate',
    'LIndent',
    'Language',
    'Leading',
    'LetterSpace',
    'LineSpacing',
    'Locked',
    'MaxWordSpace',
    'MinWordSpace',
    'NextTag',
    'NumFormat',
    'NumberFont',
    'NumAtEnd',
    'OptWordSpace',
    'Placement',
    'PlacementStyle',
    'RIndent',
    'RunInDefaultPunct',
    'SpAfter',
    'SpBefore',
    'TabStop',
    'TopSepAtIndent',
    'TopSepOffset',
    'TopSeparator',
    'UseNextTag',
    'WithNext',
    'WithPrev',
);

# Define the set of font attributes.
@MIF_FONT_ATTRS = (
    'Family',
    'Var',
    'Weight',
    'Angle',
    'PostScriptName',
    'Size',
    'Underlining',
    'Overline',
    'Strike',
    'ChangeBar',
    'Outline',
    'Shadow',
    'PairKern',
    'Case',
    'Position',
    'DX',
    'DY',
    'DW',
    'Locked',
    'Separation',
    'Color');

# This is the gap in points between the actual border and the
# decoration for an area on a master page
$_MIF_BORDER_GAP = 6;

# This is the text flow id of the main text
$_MIF_TEXTFLOW_MAIN = 9999;

# This is where we start numbering our text flows from to ensure there
# are no conflicts with those already in templates
$_MIF_TEXTFLOW_START = 10000;

# This is where we start numbering our objects from to ensure there
# are no conflicts with those already in templates
$_MIF_OBJ_REF_START = 11000;

# This is where we start numbering TOC cross-references
$_MIF_TOC_XREF_START = 2000;

# The character mapping table (SDF -> MIF)
%_MIF_CHAR = (
    'bullet',       'Bullet',
    'c',            '\xa9 ',
    'cent',         'Cent',
    'dagger',       'Dagger',
    'doubledagger', 'DoubleDagger',
    'emdash',       'EmDash',
    'endash',       'EnDash',
    'emspace',      'EmSpace',
    'enspace',      'EnSpace',
    'lbrace',       '{',
    'lbracket',     '[',
    'nbdash',       'HardHyphen',
    'nbspace',      'HardSpace',
    'nl',           'HardReturn',
    'pound',        'Pound',
    'r',            '\xa8 ',
    'rbrace',       '}',
    'rbracket',     ']',
    'tab',          'Tab',
    'tm',           '\xaa ',
    'yen',          'Yen',

    # From pod2fm ...

    'amp'       =>      '&',    #   ampersand
    'lt'        =>      '<',    #   left chevron, less-than
    'gt'        =>      '\\>',    #   right chevron, greater-than
    'quot'      =>      '"',    #   double quote

    "Aacute"    =>      "\\xe7 ",    #   capital A, acute accent
    "aacute"    =>      "\\x87 ",    #   small a, acute accent
    "Acirc"     =>      "\\xe5 ",    #   capital A, circumflex accent
    "acirc"     =>      "\\x89 ",    #   small a, circumflex accent
    "AElig"     =>      '\\xae ',    #   capital AE diphthong (ligature)
    "aelig"     =>      '\\xbe ',    #   small ae diphthong (ligature)
    "Agrave"    =>      "\\xcb ",    #   capital A, grave accent
    "agrave"    =>      "\\x88 ",    #   small a, grave accent
    "Aring"     =>      '\\x81 ',    #   capital A, ring
    "aring"     =>      '\\x8c ',    #   small a, ring
    "Atilde"    =>      '\\xcc ',    #   capital A, tilde
    "atilde"    =>      '\\x8b ',    #   small a, tilde
    "Auml"      =>      '\\x80 ',    #   capital A, dieresis or umlaut mark
    "auml"      =>      '\\x8a ',    #   small a, dieresis or umlaut mark
    "Ccedil"    =>      '\\x82 ',    #   capital C, cedilla
    "ccedil"    =>      '\\x8d ',    #   small c, cedilla
    "Eacute"    =>      "\\x83 ",    #   capital E, acute accent
    "eacute"    =>      "\\x8e ",    #   small e, acute accent
    "Ecirc"     =>      "\\xe6 ",    #   capital E, circumflex accent
    "ecirc"     =>      "\\x90 ",    #   small e, circumflex accent
    "Egrave"    =>      "\\xe9 ",    #   capital E, grave accent
    "egrave"    =>      "\\x8f ",    #   small e, grave accent
    "Euml"      =>      "\\xe8 ",    #   capital E, dieresis or umlaut mark
    "euml"      =>      "\\x91 ",    #   small e, dieresis or umlaut mark
    "Iacute"    =>      "\\xea ",    #   capital I, acute accent
    "iacute"    =>      "\\x92 ",    #   small i, acute accent
    "Icirc"     =>      "\\xeb ",    #   capital I, circumflex accent
    "icirc"     =>      "\\x90 ",    #   small i, circumflex accent
    "Igrave"    =>      "\\xe9 ",    #   capital I, grave accent
    "igrave"    =>      "\\x93 ",    #   small i, grave accent
    "Iuml"      =>      "\\xec ",    #   capital I, dieresis or umlaut mark
    "iuml"      =>      "\\x95 ",    #   small i, dieresis or umlaut mark
    "Ntilde"    =>      '\\x84 ',    #   capital N, tilde
    "ntilde"    =>      '\\x96 ',    #   small n, tilde
    "Oacute"    =>      "\\xee ",    #   capital O, acute accent
    "oacute"    =>      "\\x97 ",    #   small o, acute accent
    "Ocirc"     =>      "\\xef ",    #   capital O, circumflex accent
    "ocirc"     =>      "\\x99 ",    #   small o, circumflex accent
    "Ograve"    =>      "\\xf1 ",    #   capital O, grave accent
    "ograve"    =>      "\\x98 ",    #   small o, grave accent
    "Oslash"    =>      "\\xaf ",    #   capital O, slash
    "oslash"    =>      "\\xbf ",    #   small o, slash
    "Otilde"    =>      "\\xcd ",    #   capital O, tilde
    "otilde"    =>      "\\x9b ",    #   small o, tilde
    "Ouml"      =>      "\\x85 ",    #   capital O, dieresis or umlaut mark
    "ouml"      =>      "\\x9a ",    #   small o, dieresis or umlaut mark
    "Uacute"    =>      "\\xf2 ",    #   capital U, acute accent
    "uacute"    =>      "\\x9c ",    #   small u, acute accent
    "Ucirc"     =>      "\\xf3 ",    #   capital U, circumflex accent
    "ucirc"     =>      "\\x9e ",    #   small u, circumflex accent
    "Ugrave"    =>      "\\xf4 ",    #   capital U, grave accent
    "ugrave"    =>      "\\x9d ",    #   small u, grave accent
    "Uuml"      =>      "\\x86 ",    #   capital U, dieresis or umlaut mark
    "uuml"      =>      "\\x9f ",    #   small u, dieresis or umlaut mark
    "yuml"      =>      "\\xd8 ",    #   small y, dieresis or umlaut mark
);

# Lookup table of colour values used by _MifMapColor()
%_MIF_COLOR = (
    'black',    'Black',
    '000000',   'Black',
    'white',    'White',
    'ffffff',   'White',
    'red',      'Red',
    'ff0000',   'Red',
    'green',    'Green',
    '008000',   'Green',
    'blue',     'Blue',
    '0000ff',   'Blue',
    'yellow',   'Yellow',
    'ffff00',   'Yellow',
    'magenta',  'Magenta',
    'ff00ff',   'Magenta',
    'cyan',     'Cyan',
    '00ffff',   'Cyan',
);

# Lookup table of fill values
%_MIF_FILL_CODE = (
    100,    0,
    90,     1,
    70,     2,
    50,     3,
    30,     4,
    10,     5,
    3,      6,
    0,      15,
);

# Lookup table of known index types
%_MIF_INDEX_CODE = (
    "standard",     2,
    "comment",      3,
    "subject",      4,
    "author",       5,
);

# This is the numeric code matching the HardReturn character
$_MIF_HARDRETURN_CODE = 10;

#
# >>_Description::
# {{Y:%_MIF_REF}} maps logical names to MIF reference names.
#
%_MIF_REF = (
    'table',        'ATbl',
    'figure',       'AFrame',
);

# Lookup table of title tags for objects
%_MIF_TITLE_TAG = (
    'table',        'TT',
    'figure',       'FT',
);

# Lookup table of no-title tags for objects
%_MIF_NOTITLE_TAG = (
    'table',        'TA',
    'figure',       'FA',
);

# The text at the end of each paragraph and its length
$_MIF_PARA_END = " >\n> # end of Para";
$_MIF_PARA_END_LEN = length($_MIF_PARA_END);

#
# >>_Description::
# {{Y:%_MIF_DFLT_BOOK_ATTRS}} contains the default set of
# book attributes.
#
%_MIF_DFLT_BOOK_ATTRS = (
    "StartPageSide", "ReadFromFile",
    "PageNumbering", "Continue",
    "PgfNumbering", "Continue",
    "PageNumPrefix", "",
    "PageNumSuffix", "",
    "DefaultPrint", "Yes",
    "DefaultApply", "Yes",
);

# Mapping table for logical variable names to Frame equivalents
%_MIF_VAR_MAP = (
    'Running_H_F_1',    'Running H/F 1',
    'Running_H_F_2',    'Running H/F 2',
    'Running_H_F_3',    'Running H/F 3',
    'Running_H_F_4',    'Running H/F 4',
);

# Set of legal tab types
%_MIF_TAB_TYPE = (
    'Left',         1,
    'Center',       1,
    'Right',        1,
    'Decimal',      1,
);

# Directive mapping table
%_MIF_HANDLER = (
    'tuning',         '_MifHandlerTuning',
    'endtuning',      '_MifHandlerEndTuning',
    'table',            '_MifHandlerTable',
    'row',              '_MifHandlerRow',
    'cell',             '_MifHandlerCell',
    'endtable',         '_MifHandlerEndTable',
    'import',           '_MifHandlerImport',
    'inline',           '_MifHandlerInline',
    'output',           '_MifHandlerOutput',
    'object',           '_MifHandlerObject',
);

# Phrase directive mapping table
%_MIF_PHRASE_HANDLER = (
    'char',             '_MifPhraseHandlerChar',
    'import',           '_MifPhraseHandlerImport',
    'inline',           '_MifPhraseHandlerInline',
    'variable',         '_MifPhraseHandlerVariable',
    'xref',             '_MifPhraseHandlerXRef',
    'pagenum',          '_MifPhraseHandlerPageNum',
    'pagecount',        '_MifPhraseHandlerPageCount',
    'paratext',         '_MifPhraseHandlerParaText',
    'paranum',          '_MifPhraseHandlerParaNum',
    'paranumonly',      '_MifPhraseHandlerParaNumOnly',
    'parashort',        '_MifPhraseHandlerParaShort',
    'paralast',         '_MifPhraseHandlerParaLast',
);

# Table states
$_MIF_INTABLE = 1;
$_MIF_INROW   = 2;
$_MIF_INCELL  = 3;

# Table row suffixes
%_MIF_ROW_SUFFIX = (
    'Heading',  'H',
    'Body',     'Body',
    'Group',    'Body',
    'Footing',  'F',
);

# Parts in a book before numbering starts resetting
%_MIF_FRONT_PART = (
    'front',        1,
    'pretoc',       1,
    'toc',          1,
    'lof',          1,
    'lot',          1,
    'prechapter',   1,
);

##### Variables #####

# Counter to ensure stream processing functions are re-entrant
$_mif_cnt = 0;

# The current template stuff
@_mif_template = ();
%_mif_tpl_vars = ();
%_mif_tpl_xrefs = ();
%_mif_tpl_paras = ();
%_mif_tpl_fonts = ();
%_mif_tpl_tbls = ();
%_mif_tpl_ctrls = ();

# The current variables, cross-references, paragraph/font/table catalogs,
# control settings, reference frames and generated lists
%_mif_vars = ();
%_mif_xrefs = ();
%_mif_paras = ();
%_mif_fonts = ();
%_mif_tbls = ();
%_mif_ctrls = ();
%_mif_frames = ();
%_mif_lists = ();
%_mif_indexes = ();

# Counter of Running H/F variables used
$_mif_runninghf_cnt = 0;

# Style usage counters. We don't bother keeping usage counts for
# font styles because they are small to build and are referenced
# in lots of places (e.g. autonumber fonts).
%_mif_parastyle_used = ();
%_mif_tblstyle_used = ();

# The cover page name and rectangles
$_mif_cover = '';
%_mif_cover_rect = ();

# Buffers for collection of special objects
@_mif_toc_list = ();
@_mif_lof_list = ();
@_mif_lof_list = ();
@_mif_figure = ();
@_mif_table = ();
@_mif_textflows = ();
@_mif_pages = ();
$_mif_bodypage = '';

# Stacks for tables
@_mif_tbl_start = ();
@_mif_tbl_state = ();
@_mif_tbl_wide = ();
@_mif_tbl_id = ();
@_mif_tbl_title = ();
@_mif_tbl_style = ();
@_mif_tbl_landscape = ();

# Type of current row
@_mif_row_type = ();

# Alignment of current table cell
$_mif_cell_align = '';
$_mif_cell_valign = '';

# Margin around current cell (and defaults for current table)
$_mif_cell_lmargin = 0;
$_mif_cell_tmargin = 0;
$_mif_cell_rmargin = 0;
$_mif_cell_bmargin = 0;
$_mif_tbl_lmargin = 0;
$_mif_tbl_tmargin = 0;
$_mif_tbl_rmargin = 0;
$_mif_tbl_bmargin = 0;

#
# >>_Description::
# {{Y:_mif_buffered_fname}} and {{Y:_mif_buffered_file}} contain the
# filename and contents of the last album file fetched.
#
$_mif_buffered_fname = '';
@_mif_buffered_file = ();

# Used to ensure text flows are unique
$_mif_textflow_cnt = $_MIF_TEXTFLOW_START;

# Used to ensure cross-reference targets are unique
$_mif_xref_cnt = $_MIF_TOC_XREF_START;

# Arrays of paragraph attribute information
@_mif_paraattr_name = ();
@_mif_paraattr_full = ();
@_mif_paraattr_type = ();

# Root objects for faster catalog building
$_mif_pararoot_name = '';
%_mif_pararoot_attr = ();
$_mif_fontroot_name = '';
%_mif_fontroot_attr = ();
$_mif_tblroot_name = '';
%_mif_tblroot_attr = ();
$_mif_frameroot_name = '';
%_mif_frameroot_attr = ();
$_mif_listroot_name = '';
%_mif_listroot_attr = ();
$_mif_indexroot_name = '';
%_mif_indexroot_attr = ();

# Stacks of component file offsets and filenames
@_mif_component_offset = ();
@_mif_component_file = ();
@_mif_component_type = ();

# Table of component names and types
@_mif_component_tbl = ();

# Counter for building derived component names
$_mif_component_cntr = 0;

# Cursor in component type array
$_mif_component_cursor= 0;

##### Routines #####

#
# >>Description::
# {{Y:MifFormat}} is an SDF driver which outputs MIF.
#
sub MifFormat {
    local(*data) = @_;
    local(@result);
    local($msg_cursor, %msg_counts);

    # Init global variables/buffers
    $_mif_textflow_cnt = $_MIF_TEXTFLOW_START;
    $_mif_xref_cnt = $_MIF_TOC_XREF_START;
    %_mif_vars = ();
    %_mif_xrefs = ();
    %_mif_paras = ();
    %_mif_fonts = ();
    %_mif_tbls = ();
    %_mif_ctrls = ();
    %_mif_frames = ();
    %_mif_lists = ();
    %_mif_indexes = ();
    $_mif_runninghf_cnt = 0;
    %_mif_parastyle_used = ();
    %_mif_tblstyle_used = ();
    $_mif_cover = '';
    %_mif_cover_rect = ();
    @_mif_toc_list = ();
    @_mif_lof_list = ();
    @_mif_lot_list = ();
    @_mif_figure = ();
    @_mif_table = ();
    @_mif_textflows = ();
    @_mif_pages = ();
    $_mif_bodypage = '';
    @_mif_tbl_start = ();
    @_mif_tbl_wide = ();
    @_mif_tbl_state = ();
    @_mif_tbl_id = ();
    @_mif_tbl_title = ();
    @_mif_tbl_style = ();
    @_mif_tbl_landscape = ();
    @_mif_row_type = ();
    @_mif_component_offset = ();
    @_mif_component_file = ();
    @_mif_component_type = ();
    @_mif_component_tbl = ();
    $_mif_component_cntr = 0;
    $_mif_component_cursor = 0;

    # Initialise things for building a book, if necessary
    if ($SDF_USER'var{'MIF_BOOK_MODE'}) {
        @_mif_component_tbl = ('Part|Type');
    }

    # Get the current message cursor - we skip the finalisation stuff
    # if errors are found
    $msg_cursor = &AppMsgNextIndex();

    # Process the paragraphs
    @result = ();
    &_MifAddSection(*result, *data);

    # Save away any unclosed components
    while (@_mif_component_file) {
        &_MifHandlerOutput(*result, '-');
    }

    # Finalise the output, provided that no errors/aborts/fatals were found
    %msg_counts = &AppMsgCounts($msg_cursor);
    if ($msg_counts{'error'} || $msg_counts{'abort'} || $msg_counts{'fatal'} ) {
        # do nothing
    }
    elsif ($SDF_USER'var{'MIF_BOOK_MODE'}) {
        @_mif_component_tbl = &TableParse(@_mif_component_tbl);
        @result = &_MifBookBuild(*_mif_component_tbl, $SDF_USER'var{'DOC_BASE'});
    }
    else {
        @result = &_MifFinalise(*result);
    }

    # Return result
    return @result;
}

#
# >>_Description::
# {{Y:_MifAddSection}} formats a block of SDF (@data) into MIF and
# adds it to a buffer (@outbuf).
#
sub _MifAddSection {
    local(*outbuf, *data) = @_;
#   local();
    local($prev_tag, %prev_attrs);
    local($para_tag, $para_text, %para_attrs);
    local($directive);

    # Process the paragraphs
    $prev_tag = '';
    %prev_attrs = ();
#print "data: ", join("<\n", @data), "<\n" if $mif_debug;
    while (($para_text, $para_tag, %para_attrs) = &SdfNextPara(*data)) {

        # handle directives
        if ($para_tag =~ /^__(\w+)$/) {
            $directive = $_MIF_HANDLER{$1};
            if (defined &$directive) {
                &$directive(*outbuf, $para_text, %para_attrs);
            }
            else {
                &AppTrace("mif", 5, "ignoring internal directive '$1'");
            }
            next;
        }

        # Add the paragraph
        &_MifParaAdd(*outbuf, $para_tag, $para_text, *para_attrs, $prev_tag,
          *prev_attrs);
    }

    # Do this stuff before starting next loop iteration
    continue {
        $prev_tag = $para_tag;
        %prev_attrs = %para_attrs;
    }
}
       
#
# >>_Description::
# {{Y:_MifEscape}} escapes special characters in a MIF string.
# If {{escape_space}} is true, space characters are also escaped.
#
sub _MifEscape {
    local(*text, $escape_space) = @_;
#   local();
    local($orig_linematch_flag);

    $orig_linematch_flag = $*;
    $* = 1;
    $text =~ s/([\\>])/\\$1/g;
    $text =~ s/\t/\\t/g;
    $text =~ s/'/\\q/g;
    $text =~ s/`/\\Q/g;
    $text =~ s/ /\\ /g if $escape_space;
    $* = $orig_linematch_flag;
}

#
# >>_Description::
# {{Y:_MifFmtMarker}} formats a mif marker.
#
sub _MifFmtMarker {
    local($prefix, $code, $text) = @_;
    local($marker);
    local($rest);

    # For long markers, make each entry a marker until the rest
    # is short enough to fit into one marker
    $marker = '';
    while (length($text) > 255) {
        if ($text =~ /[^\\];/) {
            $rest = $`;
            $text = $';
            &_MifEscape(*rest);
            $marker .=  "$prefix<Marker\n" .
                        "$prefix <MType $code>\n" .
                        "$prefix <MText `$rest'>\n" .
                        "$prefix> # end of Marker\n";
        }
        else {
            # If we reach here, we are unable to split the marker text.
            # Therefore, the best we can do is take the first 255 chars
            # and let the user know. :-(
            $rest = substr($text, 255);
            $text = substr($text, 0, 255);
            &AppMsg("warning", "ignoring marker text - '$rest'");
        }
    }

    # Handle the simple case
    &_MifEscape(*text);
    $marker .=  "$prefix<Marker\n" .
                "$prefix <MType $code>\n" .
                "$prefix <MText `$text'>\n" .
                "$prefix> # end of Marker\n";
    return $marker;
}
       
#
# >>_Description::
# {{Y:_MifParaAdd}} adds a paragraph.
#
sub _MifParaAdd {
    local(*result, $para_tag, $para_text, *para_attrs, $prev_tag, *prev_attrs) = @_;
#   local();
    local($is_example, $para_fmt);
    local($para_override);
    local($para);
    local($hdg_level);
    local($id, $hlpinfo);
    local($index, $index_code);

    # Get the example flag
    $is_example = $SDF_USER'parastyles_category{$para_tag} eq 'example';

    # After headings, use a FIRST tag instead of a normal tag
    if ($prev_tag =~ /^[HAP]\d$/ && $para_tag eq 'N' &&
      $SDF_USER'parastyles_to{'FIRST'} ne '') {
        $para_tag = 'FIRST';
    }

    # Get the Frame format name
    $para_fmt = $SDF_USER'parastyles_to{$para_tag};
    $para_fmt = $para_tag if $para_fmt eq '';
    $para_fmt .= 'NoTOC' if $para_attrs{'notoc'};
    $_mif_parastyle_used{$para_fmt}++;

    # Inherit the alignment and margins from the table cell, if applicable
    if (@_mif_tbl_state) {
        if ($_mif_cell_align ne $para_attrs{'align'}) {
            $para_attrs{'align'} = $_mif_cell_align;
        }
        if ($_mif_cell_valign ne '') {
            $para_attrs{'mif.CellAlignment'} = $_mif_cell_valign;
        }
        # Get the cell margins, if necessary
        if ($_mif_cell_lmargin  ne '' || $_mif_cell_tmargin ne '' ||
            $_mif_cell_rmargin  ne '' || $_mif_cell_bmargin ne '') {
            $_mif_cell_lmargin = $_mif_tbl_lmargin if $_mif_cell_lmargin eq '';
            $_mif_cell_tmargin = $_mif_tbl_tmargin if $_mif_cell_tmargin eq '';
            $_mif_cell_rmargin = $_mif_tbl_rmargin if $_mif_cell_rmargin eq '';
            $_mif_cell_bmargin = $_mif_tbl_bmargin if $_mif_cell_bmargin eq '';
            $para_attrs{'mif.CellMargins'} =
              "${_mif_cell_lmargin}pt ${_mif_cell_tmargin}pt " .
              "${_mif_cell_rmargin}pt ${_mif_cell_bmargin}pt";
            $para_attrs{'mif.CellLMarginFixed'} = 1;
            $para_attrs{'mif.CellTMarginFixed'} = 1;
            $para_attrs{'mif.CellRMarginFixed'} = 1;
            $para_attrs{'mif.CellBMarginFixed'} = 1;
        }
    }

    # Get the format overrides
    &SdfAttrMap(*para_attrs, 'mif', *SDF_USER'paraattrs_to,
      *SDF_USER'paraattrs_map, *SDF_USER'paraattrs_attrs,
      $SDF_USER'parastyles_attrs{$para_tag});
    $para_override = &_MifParaSdfAttr(*para_attrs, "  ");

    # Build the paragraph header
    $para  = "<Para\n" .
         " <PgfTag `$para_fmt'>\n";
    if ($para_override ne '') {
        $para .= " <Pgf\n$para_override >\n";
    }
    $para .= " <ParaLine\n";

    # Build a hypertext marker, if necessary
    if ($para_attrs{"id"}) {
        $id = &_MifEscapeNewlink($para_attrs{"id"});
        # We need this one for hypertext
        $para .= "  <Marker\n" .
             "   <MType 8>\n" .
             "   <MText `newlink $id'>\n" .
             "  > # end of Marker\n";
        # And we need this one for cross-references
        $para .= "  <Marker\n" .
             "   <MType 9>\n" .
             "   <MText `$id'>\n" .
             "  > # end of Marker\n";
        # And this one is for short text within a header/footer
        if ($para_attrs{"short"}) {
            $id = &_MifEscapeNewlink($para_attrs{"short"});
            $para .= "  <Marker\n" .
                 "   <MType 0>\n" .
                 "   <MText `$id'>\n" .
                 "  > # end of Marker\n";
        }
        $id = '';
    }
    for $hlpinfo ('context', 'header', 'topic', 'window', 'endwindow') {
        $id = $para_attrs{"hlp.$hlpinfo"};
        $id = &_MifEscapeNewlink($id);
        if ($id ne '') {
            $para .= "  <Marker\n" .
                 "   <MType 8>\n" .
                 "   <MText `sdf $hlpinfo=$id'>\n" .
                 "  > # end of Marker\n";
        }
    }

    # If this paragraph is in a generated list, add a cross reference,
    # unless we're in book mode
    unless ($SDF_USER'var{'MIF_BOOK_MODE'}) {
        if ($para_tag =~ /^[HAP](\d)$/) {
            $hdg_level = $1;
            if ($hdg_level <= $SDF_USER'var{'DOC_TOC'} &&
              !$para_attrs{'notoc'}) {
                &_MifAddListXref(*para, $para_fmt, *_mif_toc_list, 'TOC');
            }
        }
        elsif ($para_tag eq 'FT') {
            &_MifAddListXref(*para, $para_fmt, *_mif_lof_list, 'LOF');
        }
        elsif ($para_tag eq 'TT') {
            &_MifAddListXref(*para, $para_fmt, *_mif_lot_list, 'LOT');
        }
    }

    # Process index-related attributes
    $index = $para_attrs{"index"};
    if ($index) {
        $index_code = $para_attrs{"index_type"};
        if ($index_code eq '') {
            $index_code = 2;
        }
        elsif ($_MIF_INDEX_CODE{$index_code}) {
            $index_code = $_MIF_INDEX_CODE{$index_code};
        }
        elsif ($index_code !~ /^\d+/) {
            &AppMsg("warning", "unknown index type '$index_code' - assuming standard");
            $index_code = 2;
        }
        $para .= &_MifFmtMarker("  ", $index_code, $index);
    }

    # Indent examples, if necessary
    if ($is_example && $para_attrs{'in'}) {
        $para_text = " " x ($para_attrs{'in'} * 4) . $para_text;
        delete $para_attrs{'in'};
    }

    # Add the paragraph body
    if ($para_attrs{'verbatim'}) {
        delete $para_attrs{'verbatim'};
        &_MifEscape(*para_text, $is_example);
        $para .= "  <String `$para_text'>\n";
    }
    else {
        $para .= &_MifParaText($para_text, $is_example);
    }

    # Build result
    if ($is_example && $para_tag eq $prev_tag &&
      join('', %para_attrs) eq join('', %prev_attrs)) {
        &_MifParaAppend(*result, $para);
    }
    else {
        push(@result, $para . $_MIF_PARA_END);
    }
}

#
# >>_Description::
# {{Y:_MifAddListXref}} adds a paragraph to a generated list.
#
sub _MifAddListXref {
    local(*para, $para_fmt, *list, $list_type) = @_;
#   local();

    # Add the xref destination marker
    $para .=
         "  <Marker\n" .
         "   <MType 9>\n" .
         "   <MText `$_mif_xref_cnt'>\n" .
         "  > # end of Marker\n";

    # And a special marker for HLP format
    $para .= "  <Marker\n" .
         "   <MType 11>\n" .
         "   <MText `$_mif_xref_cnt'>\n" .
         "  > # end of Marker\n";

    # Add the xref to the global list
    $_mif_parastyle_used{"$para_fmt$list_type"}++;
    push(@list,
        "<Para\n" .
        " <PgfTag `$para_fmt$list_type'>\n" .
        " <ParaLine\n" .
        "  <XRef\n" .
        "   <XRefName `$list_type'>\n" .
        "   <XRefSrcText `$_mif_xref_cnt'>\n" .
        "  > # end of XRef\n" .
        " >\n" .
        "> # end of Para");

    # Update the global xref counter (which makes xref unique)
    $_mif_xref_cnt++;
}
       
#
# >>_Description::
# {{Y:_MifInitTemplate}} initialises the global template variables.
#
sub _MifInitTemplate {
#   local() = @_;
#   local();

    @_mif_template = ();
    %_mif_tpl_paras = ();
    %_mif_tpl_fonts = ();
    %_mif_tpl_vars = ();
    %_mif_tpl_xrefs = ();
    %_mif_tpl_tbls = ();
    %_mif_tpl_ctrls = ();
}
       
#
# >>_Description::
# {{Y:_MifFetchTemplate}} loads a MIF template.
# The following global variables are updated:
#
# * @_mif_template - each element contains one and only one main MIF object
# * %_mif_tpl_vars - the set of variables
# * %_mif_tpl_xrefs - the set of cross references
# * %_mif_tpl_paras - the paragraph catalog
# * %_mif_tpl_fonts - the font catalog
# * %_mif_tpl_tbls - the table catalog (soon)
# * %_mif_tpl_ctrls - the document control settings
#
sub _MifFetchTemplate {
    local($template_file) = @_;
    local($ok);
    local($strm, @rec);

    # Open the input stream
    $strm = sprintf("mif_s%d", $_mif_cnt++);
    open($strm, $template_file) || return 0;

    # Read the data. Objects taking a whole line get a record to
    # themselves. A slight variation of this is needed to cover the
    #  opening <MIFFile n> line as it often has a trailing comment.
    #. Otherwise, we use a > at the start of a line to detect
    # the end of objects.
    line:
    while (<$strm>) {
        next line if /^#/;
        chop;
        if (/^\<.*\>$/ || /^\<.*\>\s*\#/) {
            push(@_mif_template, $_);
        }
        else {
            push(@rec, $_); 
            if (/^\>/) {
                $text = join("\n", @rec);
                push(@_mif_template, $text);
                if ($rec[0] =~ /^\<VariableFormats/) {
                    %_mif_tpl_vars = &_MifVarsFromText(*rec);
                }
                elsif ($rec[0] =~ /^\<XRefFormats/) {
                    %_mif_tpl_xrefs = &_MifXRefsFromText(*rec);
                }
                elsif ($rec[0] =~ /^\<PgfCatalog/) {
$igc_start = time;
                    %_mif_tpl_paras = &_MifParasFromText(*rec);
#printf STDERR "para->text: %d seconds\n", time - $igc_start;
                }
                elsif ($rec[0] =~ /^\<FontCatalog/) {
                    %_mif_tpl_fonts = &_MifFontsFromText(*rec);
                }
                elsif ($rec[0] =~ /^\<TblCatalog/) {
                    %_mif_tpl_tbls = &_MifTblsFromText(*rec);
                }
                elsif ($rec[0] =~ /^\<Document/) {
                    %_mif_tpl_ctrls = &_MifCtrlsFromText(*rec);
                }
                @rec = ();
            }
        }
    }
    close($strm);
    
    # Return result
    return 1;
}       

#
# >>_Description::
# {{Y:_MifFetchAlbum}} handles fetching of album files.
#
sub _MifFetchAlbum {
    local($fname) = @_;
    local($ok, @file);
    local($strm);

    # Check if the file is buffered
    if ($fname eq $_mif_buffered_fname) {
        return (1, @_mif_buffered_file);
    }

    # Fetch the file
    $strm = sprintf("mif_s%d", $_mif_cnt++);
    $ok = open($strm, $fname);
    if ($ok) {
        @file = <$strm>;
        chop(@file);
        close($strm);
    }

    # Buffer the file
    $_mif_buffered_fname = $fname;
    @_mif_buffered_file = @file;

    # Return result
    return ($ok, @file);
}

#
# >>_Description::
# {{Y:_MifVarsFromText}} converts text records (@recs)
# that represent variable declarations in a MIF file to
# a set of name value pairs.
#
sub _MifVarsFromText {
    local(*recs) = @_;
    local(%vars);
    local($line);
    local($name, $value);

    for $line (@recs) {
        if ($line =~ /VariableName\s+`(.+)'\>/) {
            $name = $1;
        }
        elsif ($line =~ /VariableDef\s+`(.+)'\>/) {
            $value = $1;
            $value =~ s/\\q/'/g;
            $value =~ s/\\Q/`/g;
            $value =~ s/\\t/\t/g;
            $value =~ s/\\([\\\>])/$1/g;
            $vars{$name} = $value;
        }
    }

    # Return result
    return %vars;
}

#
# >>_Description::
# {{Y:_MifVarsToText}} converts {{%vars}} to the text string
# that represents those declarations in a MIF file.
#
sub _MifVarsToText {
    local(*vars) = @_;
    local($text);
    local($var, $value);

    # Build the result
    $text = "<VariableFormats\n";
    for $var (sort keys %vars) {
        # Escape special characters in the value
        $value = $vars{$var};
        &_MifEscape(*value);

        # Add this variable
        $text .= " <VariableFormat\n" .
             "  <VariableName `$var'>\n" .
             "  <VariableDef `$value'>\n" .
             " > # end of VariableFormat\n";
    }
    $text .= "> # end of VariableFormats";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifXRefsFromText}} converts text records (@recs)
# that represent cross-reference declarations in a MIF file to
# a set of name value pairs.
#
sub _MifXRefsFromText {
    local(*recs) = @_;
    local(%xrefs);
    local($line);
    local($name, $value);

    for $line (@recs) {
        if ($line =~ /XRefName\s+`(.+)'\>/) {
            $name = $1;
        }
        elsif ($line =~ /XRefDef\s+`(.+)'\>/) {
            $value = $1;
            $xrefs{$name} = $value;
        }
    }

    # Return result
    return %xrefs;
}

#
# >>_Description::
# {{Y:_MifXRefsToText}} converts {{%xrefs}} to the text string
# that represents those declarations in a MIF file.
#
sub _MifXRefsToText {
    local(*xrefs) = @_;
    local($text);
    local($xref, $value);

    # Build the result
    $text = "<XRefFormats\n";
    for $xref (sort keys %xrefs) {
        $value = $xrefs{$xref};

        # Add this definition
        $text .= " <XRefFormat\n" .
             "  <XRefName `$xref'>\n" .
             "  <XRefDef `$value'>\n" .
             " > # end of XRefFormat\n";
    }
    $text .= "> # end of XRefFormats";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifParasFromText}} converts text records (@recs) that represent
# paragraph definitions in a MIF file to a set of name value pairs.
# Use {{Y:_MifAttrSplit}} and {{Y:_MifAttrSplit}} to convert
# the value to and from an associative array.
#
sub _MifParasFromText {
    local(*recs) = @_;
    local(%result);
    local($line);
    local($name, %values);
    local($in_font, $in_tabstop, %tab, $tab);
    local($atname, $atvalue);

    $in_font = 0;
    $in_tabstop = 0;
    for $line (@recs) {
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        if ($line =~ /^\<PgfTag\s+`(.+)'\>$/) {
            $name = $1;
            %values = ();
        }
        elsif ($line =~ /^\<(\w+)\s+(.+)\>$/) {
            $atname = $1;
            $atvalue = &_MifAttrToText($2);
            if ($in_tabstop) {
                $tab{$atname} = $atvalue;
            }
            elsif($in_font) {
                $values{'Font' . substr($atname, 1)} = $atvalue;
            }
            else {
                $atname =~ s/^Pgf//;
                $values{$atname} = $atvalue;
            }
        }
        elsif ($line =~ /^\> # end of Pgf$/) {
            # Finalise the set of attributes
            delete $values{'FontTag'};
            delete $values{'FontPostScriptName'};
            delete $values{'NumTabs'};
            $values{'TabStop'} = '' unless defined($values{'TabStop'});

            # Store the attributes for this paragraph format
            $result{$name} = &_MifAttrJoin(*values);
        }
        elsif ($line =~ /^<PgfFont$/) {
            $in_font = 1;
        }
        elsif ($line =~ /^\> # end of PgfFont$/) {
            $in_font = 0;
        }
        elsif ($line =~ /^<TabStop$/) {
            $in_tabstop = 1;
            %tab = ();
        }
        elsif ($line =~ /^\> # end of TabStop$/) {
            $in_tabstop = 0;
            $tab{'TSType'}      = '' if $tab{'TSType'} eq 'Left';
            $tab{'TSLeaderStr'} = '' if $tab{'TSLeaderStr'} eq ' ';
            $tab = join('/', @tab{'TSX', 'TSType', 'TSLeaderStr'});
            $tab =~ s/\/+$//;
            if (defined($values{'TabStop'})) {
                $values{'TabStop'} .= "," . $tab;
                next;
            }
            $values{'TabStop'} = $tab;
        }
    }

    # Return result
    return %result;
}

#
# >>_Description::
# {{Y:_MifParasToText}} converts {{%paras}} to the text string
# that represents those declarations in a MIF file.
#
sub _MifParasToText {
    local(*paras) = @_;
    local($text);
    local(@styles);
    local($name, %attr);

    # Build the attribute information arrays
    &_MifBuildAttrInfo();

    # Decide on the styles to output
    @styles = $SDF_USER'var{'MIF_ALL_STYLES'} ? sort keys %paras :
                sort keys %_mif_parastyle_used;

    # Build the result
    $text = "<PgfCatalog\n";
    for $name (@styles) {
        %attr = &_MifAttrSplit($paras{$name});

        # If this paragraph is not defined, don't output an entry for it
        next unless %attr;

        # Add this paragraph
        # (join is used in preference to . for performance)
        $text .= join('',
             " <Pgf \n",
             "  <PgfTag `$name'>\n",
             &_MifParaMifAttr(*attr, '  '),
             " > # end of Pgf\n");
    }
    $text .= "> # end of PgfCatalog";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifFontsFromText}} converts text records (@recs) that represent
# font definitions in a MIF file to a set of name value pairs.
# Use {{Y:_MifAttrJoin}} and {{Y:_MifAttrSplit}} to convert
# the value to and from an associative array.
#
sub _MifFontsFromText {
    local(*recs) = @_;
    local(%result);
    local($line);
    local($name, %values);

    for $line (@recs) {
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        if ($line =~ /^<FTag\s+`(.+)'\>$/) {
            $name = $1;
            %values = ();
        }
        elsif ($line =~ /^<F(\w+)\s+(.+)\>$/) {
            $values{$1} = &_MifAttrToText($2);
        }
        elsif ($line =~ /^\> # end of Font$/) {
            delete $values{'PostScriptName'};
            $result{$name} = &_MifAttrJoin(*values);
        }
    }

    # Return result
    return %result;
}

#
# >>_Description::
# {{Y:_MifFontsToText}} converts {{%fonts}} to the text string
# that represents those declarations in a MIF file.
#
sub _MifFontsToText {
    local(*fonts) = @_;
    local($text);
    local($name, %attr);

    # Build the result
    $text = "<FontCatalog\n";
    for $name (sort keys %fonts) {
        %attr = &_MifAttrSplit($fonts{$name});

        # Add this font
        $text .= &_MifFontFormat($name, ' ', %attr);
    }
    $text .= "> # end of FontCatalog";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifTblsFromText}} converts text records (@recs) that represent
# table format definitions in a MIF file to a set of name value pairs.
# Use {{Y:_MifAttrJoin}} and {{Y:_MifAttrSplit}} to convert
# the value to and from an associative array.
#
sub _MifTblsFromText {
    local(*recs) = @_;
    local(%result);
    local($line);
    local($name, %values);

    for $line (@recs) {
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        if ($line =~ /^<TblTag\s+`(.+)'\>$/) {
            $name = $1;
            %values = ();
            $nested = 0;
        }
        elsif ($line =~ /^<Tbl(\w+)\s+(.+)\>$/) {
            $values{$1} = &_MifAttrToText($2) unless $nested;
        }
        elsif ($line =~ /^<Tbl(Column|TitlePgf1)$/) {
            $nested = 1;
        }
        elsif ($line =~ /^\> # end of Tbl(Column|TitlePgf1)$/) {
            $nested = 0;
        }
        elsif ($line =~ /^\> # end of TblFormat$/) {
            $result{$name} = &_MifAttrJoin(*values);
        }
    }

    # Return result
    return %result;
}

#
# >>_Description::
# {{Y:_MifTblsToText}} converts {{%tbls}} to the text string
# that represents those declarations in a MIF file.
#
sub _MifTblsToText {
    local(*tbls) = @_;
    local($text);
    local(@styles);
    local($name, %attr);

    # Decide on the styles to output
    @styles = $SDF_USER'var{'MIF_ALL_STYLES'} ? sort keys %tbls :
                sort keys %_mif_tblstyle_used;

    # Build the result
    $text = "<TblCatalog\n";
    for $name (@styles) {
        %attr = &_MifAttrSplit($tbls{$name});

        # Add this table format
        $text .= &_MifTblFormat($name, ' ', *attr);
    }
    $text .= "> # end of TblCatalog";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifTblFormat}} formats a table format name and
# set of attributes (%attr) into MIF.
# {{prefix}} is a string of spaces to put at the front of each line.
#
sub _MifTblFormat {
    local($name, $prefix, *attr) = @_;
#   local($result);
    local(@text);
    local($id, $type, $value);

    # Build the header
    @text = ("$prefix<TblFormat ", " <TblTag `$name'>");

    # Add the attributes
    for $id (sort keys %attr) {
        $type = $SDF_USER'tableparams_type{"mif.$id"};
        $value = &_MifAttrFromText($attr{$id}, $type);
        push(@text, " <Tbl$id $value>");
    }

    # Add a dummy table column
    push(@text,
        " <TblColumn",
        "  <TblColumnNum 0>",
        "  <TblColumnWidth 72pt>",
        "  <TblColumnH",
        "   <PgfTag `Cell'>",
        "  > # end of TblColumnH",
        "  <TblColumnBody",
        "   <PgfTag `Cell'>",
        "  > # end of TblColumnBody",
        "  <TblColumnF",
        "   <PgfTag `Cell'>",
        "  > # end of TblColumnF",
        " > # end of TblColumn");

    # Add the footer
    push(@text, "> # end of TblFormat\n");

    # Return result
    return join("\n$prefix", @text);
}

#
# >>_Description::
# {{Y:_MifCtrlsFromText}} converts text records (@recs) that represent
# document contorl settings in a MIF file to a set of name value pairs.
#
sub _MifCtrlsFromText {
    local(*recs) = @_;
    local(%result);
    local($line);

    for $line (@recs) {
        $line =~ s/^\s+//;
        $line =~ s/\s+$//;
        if ($line =~ /^<(\w+)\s+(.+)\>$/) {
            $result{$1} = &_MifAttrToText($2);
        }
    }

    # Return result
    return %result;
}

#
# >>_Description::
# {{Y:_MifCtrlsToText}} converts {{%ctrls}} to the text string
# that represents those settings in a MIF file.
#
sub _MifCtrlsToText {
    local(*ctrls) = @_;
    local($text);
    local($name, $var_name, $type, $value);

    # Build the result
    $text = "<Document\n";
    for $name (sort keys %ctrls) {

        # Get the matching variable name and type
        $var_name = "MIF_" . &MiscMixedToUpper(substr($name, 1));
        $type = $SDF_USER'variables_type{$var_name};

        # format the value
        $value = &_MifAttrFromText($ctrls{$name}, $type);

        # build the result
        $text .= " <$name $value>\n";
    }
    $text .= "> # end of Document";

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifAttrSplit}} split a string of attributes into an associative array.
# The string format is a null-separated list of "name=value" strings.
#
sub _MifAttrSplit {
    local($str) = @_;
    local(%attr);
    local($nv);

    for $nv (split(/\000/, $str)) {
        $nv =~ /\=/;
        $attr{$`} = $';
    }

    # Return result
    %attr;
}

#
# >>_Description::
# {{Y:_MifAttrJoin}} joins an associative array into a string of attributes.
# (A "reference" to the array is passed for performance.)
# The string format is a null-separated list of "name=value" strings.
#
sub _MifAttrJoin {
    local(*attr) = @_;
    local($str);
    local($name, $value);

    # Build the string
    $str .= "$name=$value\000" while ($name, $value) = each %attr;

    # Return result
    $str;
}

#
# >>_Description::
# {{Y:_MifAttrToText}} formats a MIF attribute into text.
# As this routine can have a major impact on performance,
# the implementation favors performance over readability.
#
sub _MifAttrToText {
    local($mif) = @_;
#   local($text);

    # Trim leading/trailing whitespace
    $mif =~ s/^\s+//;
    $mif =~ s/\s+$//;

    # Convert boolean values to numeric
    $mif = 0 if $mif eq 'No';

    # Trim surrounding quotes, if any
    $mif =~ s/^\`(.*)\'$/$1/;

    # Return result
    $mif;
}

#
# >>_Description::
# {{Y:_MifAttrFromText}} formats an attribute into MIF.
# {{type}} can be one of the following:
#
# * {{boolean}} - output is Yes or No
# * {{string}} - output is `data'
# * {{tabstop}} - output is a tabstop record
# * {{keyword}} - output has a trailing space (matches MIF conventions)
#
# Other types return the value as it is. If the type is unknown:
#
# * 0 is converted to No
# * other values are returned as is.
#
# This algorithm minimises the problems associated with unknown attributes.
#
# As this routine can have a major impact on performance,
# the implementation favors performance over readability.
#
sub _MifAttrFromText {
    local($value, $type) = @_;
#   local($text);

    # Handle special cases
    return $value ? 'Yes ' : 'No '  if $type =~ /^b/;   # boolean
    return "`$value'"               if $type =~ /^s/;   # string
    return "$value "                if $type =~ /^k/;   # keyword
    return &_MifTabFromText($value) if $type =~ /^t/;   # tabstop
    return $value                   if $type =~ /^\w/;  # other

    # Otherwise, return the value as is, or No
    return $value eq '0' ? 'No ' : $value;
}

#
# >>_Description::
# {{Y:_MifTabFromText}} formats a tabstop attribute into MIF.
#
sub _MifTabFromText {
    local($value) = @_;
    local($text);
    local(@tabs, $tab, $tsx, $tstype, $tsleader);

    @tabs = split(/\s*,\s*/, $value);
    $text = '';
    for $tab (@tabs) {
        $text .= ">\n  <TabStop " if $text;
        ($tsx, $tstype, $tsleader) = split(/\//, $tab, 3);
        if ($tstype) {
            unless ($_MIF_TAB_TYPE{$tstype}) {
                &AppMsg("warning", "tab type '$tstype' not in (Left,Center,Right,Decimal)");
            }
        }
        else {
            $tstype = 'Left';
        }
        $tsleader = ' ' if $tsleader eq '';
        $text .= "<TSX $tsx>";
        $text .= "<TSType $tstype>";
        $text .= "<TSLeaderStr `$tsleader'>";
        if ($tstype eq 'Decimal') {
            $text .= "<TSDecimalChar `.'>";
        }
    }

    # Return result
    return $text;
}

#
# >>_Description::
# {{Y:_MifParaSdfAttr}} formats a set of SDF paragraph attributes into MIF.
# {{prefix}} is a string of spaces to put at the front of each line.
#
sub _MifParaSdfAttr {
    local(*attrs, $prefix) = @_;
    local($mif);
    local($attr, $value, $type, $fm_prefix, $font);

    for $attr (sort keys %attrs) {

        # get the attribute value & type
        $value = $attrs{$attr};
        $type = $SDF_USER'paraattrs_type{$attr};

        # Get the Frame prefix
        $fm_prefix = $SDF_USER'phraseattrs_name{$attr} ? 'F' : 'Pgf';

        # Check it's a Frame attribute 
        next unless $attr =~ s/^mif\.//;

        # Map to the MIF name
        $attr = "$fm_prefix$attr" unless
          ($attr =~ /^HyphenM/ || $attr eq 'TabStop');

        # format the value
        $value = &_MifAttrFromText($value, $type);

        # build the result, separating the font attributes for now
        if ($fm_prefix eq 'F') {
             $font .= "$prefix <$attr $value>\n";
        }
        else {
            $mif .= "$prefix<$attr $value>\n";
        }
    }

    # Combine normal and font attributes
    $mif .= "$prefix<PgfFont\n$prefix <FTag `'>\n$font$prefix>\n" if $font;

    # Return result
    $mif;
}

#
# >>_Description::
# {{Y:_MifBuildAttrInfo}} builds the table of paragraph attribute
# information. Each record contains the the attribute name & type,
# separated by a NULL character.
#
sub _MifBuildAttrInfo {
#   local() = @_;
#   local();
    local($name, $short_name, $new_name);

    # If the work has already been done, don't bother doing it again
    return if @_mif_paraattr_name;

    # Add the attribute information
    for $name (sort keys %SDF_USER'paraattrs_type) {
        next unless $name =~ /^mif\./;

        # Skip attributes we don't weant to output
        next if $name eq 'mif.NumTabs';
        next if $name eq 'mif.component';

        # Convert the name
        $short_name = $name;
        $short_name =~ s/^mif\.//;
        if ($short_name =~ /^Font/) {
            $new_name = "F" . $';
        }
        elsif ($short_name =~ /^HyphenM|^TabS/) {
            $new_name = $short_name;
        }
        else {
            $new_name = "Pgf" . $short_name;
        }

        # Store the information
        push(@_mif_paraattr_name, $short_name);
        push(@_mif_paraattr_full, $new_name);
        push(@_mif_paraattr_type, $SDF_USER'paraattrs_type{$name});
    }
}

#
# >>_Description::
# {{Y:_MifParaMifAttr}} formats a set of MIF paragraph attributes into MIF.
# {{prefix}} is a string of spaces to put at the front of each line.
#
sub _MifParaMifAttr {
    local(*attrs, $prefix) = @_;
    local($mif);
    local($i, $attr, $value, $fullname, $type, $font);

    # This routine has a big impact on performance, so we
    # get the list of names from a prebuilt array, rather than by
    # sorting the keys of %attrs. Likewise, the fullname and type
    # come from prebuilt arrays, rather than from assocative arrays
    # indexed on attribute name, to improve performance.
    for ($i = 0; $i <= $#_mif_paraattr_name; $i++) {

        # get the attribute information
        $attr     = $_mif_paraattr_name[$i];
        $fullname = $_mif_paraattr_full[$i];
        $type     = $_mif_paraattr_type[$i];

        # format the value
        $value = &_MifAttrFromText($attrs{$attr}, $type);

        # build the result, separating the font attributes for now
        # and ignoring empty tab stops
        ## Note that we explicitly test for the most common case first
        ## in order to improve performance.
        #if ($fullname =~ /^P/) {
        #    $mif .= "$prefix<$fullname $value>\n";
        #}
        #elsif ($fullname =~ /^F/) {
        if ($fullname =~ /^F/) {
             $font .= "$prefix <$fullname $value>\n";
        }
        elsif ($fullname =~ /^T/ && $value eq '') {
            next;
        }
        else {
            $mif .= "$prefix<$fullname $value>\n";
        }
    }

    # Combine normal and font attributes
    $mif .= "$prefix<PgfFont\n$prefix <FTag `'>\n$font$prefix>\n" if $font;

    # Return result
    $mif;
}

#
# >>_Description::
# {{Y:_MifFontFormat}} formats a MIF font specification from a
# tag and set of MIF attributes.
#
sub _MifFontFormat {
    local($tag, $prefix, %attr) = @_;
    local($mif);
    local($attr);
    local($short_name);
    local($type, $value);

    # Init things
    $mif = "$prefix<Font\n" .
           "$prefix <FTag `$tag'>\n";

    # Process the attributes
    for $attr (sort keys %attr) {

        # Get the short name
        $short_name = $attr;
        $short_name =~ s/^mif\.//;

        # get the attribute type and value
        $type = $SDF_USER'phraseattrs_type{"mif.$short_name"};
        $value = $attr{$attr};

        # For MIF font definitions, an empty value implies "as-is"
        # and as-is attributes are not including in the definition
        next if $value eq '';

        # format the value
        $value = &_MifAttrFromText($value, $type);

        # build the result
        $mif .= "$prefix <F$attr $value>\n";
    }
    $mif .= "$prefix>\n";

    # Return result
    return $mif;
}

#
# >>_Description::
# {{Y:_MifCharFont}} formats a MIF font specification from a
# tag and set of SDF attributes.
#
sub _MifCharFont {
    local($tag, $prefix, %attr) = @_;
    local($mif);
    local($attr);
    local($type, $value);

    # Init things
    $mif = "$prefix<Font\n" .
           "$prefix <FTag `$tag'>\n";

    # Process the attributes
    for $attr (sort keys %attr) {

        # get the attribute type and value
        $type = $SDF_USER'phraseattrs_type{$attr};
        $value = $attr{$attr};

        # Check it's a Frame attribute and map it to a MIF name
        next unless $attr =~ s/^mif\./F/;

        # format the value
        $value = &_MifAttrFromText($value, $type);

        # build the result
        $mif .= "$prefix <$attr $value>\n";
    }
    $mif .= "$prefix>\n";

    # Return result
    return $mif;
}

#
# >>_Description::
# {{Y:_MifParaText}} converts SDF text into MIF "paragraph lines".
# 
sub _MifParaText {
    local($para_text, $is_example) = @_;
    local($para);
    local($state);
    local($sect_type, $char_tag, $text, %sect_attrs);
    local(@char_fonts);
    local($char_font, $id, $index, $index_code);
    local($directive);

    # Handle blank lines
    $para_text = " " if $para_text eq '';

    # Process the text
    $para = '';
    while (($sect_type, $text, $char_tag, %sect_attrs) =
      &SdfNextSection(*para_text, *state)) {

        # Escape any special characters
        if ($sect_type eq 'phrase') {
            ($text) = &SDF_USER'ExpandLink($text) if $char_tag eq 'L';
            &_MifEscape(*text, $is_example);
        }
        elsif ($sect_type eq 'string') {
            &_MifEscape(*text, $is_example);
        }

        # Build the paragraph
        if ($sect_type eq 'string') {
            # Convert hyphens to hard-hyphens, if in a phrase
            $text =~ s/\-/\\x15 /g if @char_fonts;

            $para .= "  <String `$text'>\n";
        }

        elsif ($sect_type eq 'phrase') {

            # Map colors to safe values
            if ($sect_attrs{'color'} ne '') {
                my $color = &_MifMapColor($sect_attrs{'color'});
                if ($color ne '') {
                    $sect_attrs{'color'} = $color;
                }
                else {
                    delete $sect_attrs{'color'};
                }
            }

            # Process formatting attributes
            &SdfAttrMap(*sect_attrs, 'mif', *SDF_USER'phraseattrs_to,
              *SDF_USER'phraseattrs_map, *SDF_USER'phraseattrs_attrs,
              $SDF_USER'phrasestyles_attrs{$char_tag});
            $char_font = &_MifCharFont($SDF_USER'phrasestyles_to{$char_tag}, "  ",
              %sect_attrs);
            push(@char_fonts, $char_font);
            $para .= $char_font;

            # Process hypertext-related attributes
            $id = &_MifEscapeNewlink($sect_attrs{"id"});
            if ($id) {
                # We need this one for hypertext
                $para .= "  <Marker\n" .
                    "   <MType 8>\n" .
                    "   <MText `newlink $id'>\n" .
                    "  > # end of Marker\n";
                # And we need this one for cross-references
                $para .= "  <Marker\n" .
                    "   <MType 9>\n" .
                    "   <MText `$id'>\n" .
                    "  > # end of Marker\n";
            }
            &_MifAddLink(*para, $sect_attrs{'jump'});
            $id = &_MifEscapeNewlink($sect_attrs{"hlp.popup"});
            if ($id ne '') {
                $para .= "  <Marker\n" .
                     "   <MType 8>\n" .
                     "   <MText `sdf popup=$id'>\n" .
                     "  > # end of Marker\n";
            }

            # Process index-related attributes
            $index = $sect_attrs{"index"};
            if ($index) {
                $index_code = $sect_attrs{"index_type"};
                if ($index_code eq '') {
                    $index_code = 2;
                }
                elsif ($_MIF_INDEX_CODE{$index_code}) {
                    $index_code = $_MIF_INDEX_CODE{$index_code};
                }
                elsif ($index_code !~ /^\d+/) {
                    &AppMsg("warning", "unknown index type '$index_code' - assuming standard");
                    $index_code = 2;
                }
                $para .= &_MifFmtMarker("  ", $index_code, $index);
            }

            # Convert hyphens to hard-hyphens
            $text =~ s/\-/\\x15 /g;

            # Convert spaces to non-breaking spaces, if necessary
            $text =~ s/ /\\x11 /g if $char_tag eq 'S';

            # Add the text for this phrase
            $para .= "  <String `$text'>\n";
        }

        elsif ($sect_type eq 'phrase_end') {
            pop(@char_fonts);
            $char_font = $char_fonts[$#char_fonts];
            $para .= $char_font ne '' ? $char_font :
			  "  <Font\n   <FTag `'>\n  > # end of Font\n";
        }

        elsif ($sect_type eq 'special') {
            $directive = $_MIF_PHRASE_HANDLER{$char_tag};
            if (defined &$directive) {
                &$directive(*para, $text, %sect_attrs);
            }
            else {
                &AppMsg("warning", "ignoring special phrase '$1' in MIF driver");
            }
        }
    }

    # Return result
    return $para;
}

#
# >>_Description::
# {{Y:_MifMapColor}} maps Web (CSS) colors to MIF colors.
#
sub _MifMapColor {
    local($color) = @_;
    local($mif_color);

    # Ignore leading # on rrggbb value, if any
    $color =~ s/^#//;

    # For now, if it's not in the lookup table, ignore it
    $mif_color = $_MIF_COLOR{"\L$color"};

    # Return result
    return $mif_color;
}

#
# >>_Description::
# {{Y:_MifAddLink}} adds link information for a paragraph.
#
sub _MifAddLink {
    local(*para, $link) = @_;
#   local();

    if ($link) {
        $link = &_MifLink($link, $SDF_USER'var{'MIF_EXT'});
        $para .= "  <Marker\n" .
            "   <MType 8>\n" .
            "   <MText `$link'>\n" .
            "  > # end of Marker\n";
    }
}

#
# >>_Description::
# {{Y:_MifFinalise}} generates a MIF file.
# If a template has been loaded, the result is a merge of the buffered
# MIF objects with the current template. Otherwise, the output is the
# buffered MIF objects prepended onto the text records. (In the latter
# case, the generated MIF must be imported into a template to produce
# the final document.) If the output is a component in a book,
# {{component_type}} should be set accordingly (e.g. FRONT, TOC,
# CHAPTER, etc.)
#
sub _MifFinalise {
    local(*text, $component_type) = @_;
    local(@out_result);
    local($pwidth, $pheight);
    local($component_prefix);
    local(%offset, $old_match_rule);
    local(%merged_ctrls, %merged_vars, %merged_xrefs);
    local(%merged_paras, %merged_fonts, %merged_tbls);
    local($mainflow);

    # Process document control settings
    &_MifProcessControls(*SDF_USER'var, $component_type);

    # Get the page width and height
    $pwidth = $SDF_USER'var{'DOC_PAGE_WIDTH'};
    $pheight = $SDF_USER'var{'DOC_PAGE_HEIGHT'};

    # Generate the master pages
    $component_prefix = $component_type eq '' ? '' : $component_type . "_";
    &_MifAddMasterPage('First', $component_prefix . "FIRST", $pwidth, $pheight,
      $_MIF_TEXTFLOW_MAIN);
    &_MifAddMasterPage('Right', $component_prefix . "RIGHT", $pwidth, $pheight);
    if ($SDF_USER'var{'DOC_TWO_SIDES'}) {
        &_MifAddMasterPage('Left', $component_prefix . "LEFT", $pwidth, $pheight);
    }

    # Generate the reference pages
    &_MifAddRefPages($component_type);

    # Add the generated lists (Table of Contents, etc.) unless this is
    # a part of a book
    &_MifAddLists(*text) if $component_type eq '';

    #
    # Build the import table. Note that each record in the
    # import table contains a single MIF main statement.
    # As we go, we also build an offset table which is
    # required by MifMerge so it can merge the import
    # table with the nominated template.
    #       
    @out_result = ("<MIFFile 5.00>");
    if (%_mif_ctrls) {
        %merged_ctrls = %_mif_tpl_ctrls;
        @merged_ctrls{keys %_mif_ctrls} = values %_mif_ctrls;
        push(@out_result, &_MifCtrlsToText(*merged_ctrls));
        $offset{'Document'} = $#out_result;
    }
    if (%_mif_vars) {
        %merged_vars = %_mif_tpl_vars;
        @merged_vars{keys %_mif_vars} = values %_mif_vars;
        push(@out_result, &_MifVarsToText(*merged_vars));
        $offset{'VariableFormats'} = $#out_result;
    }
    if (%_mif_xrefs) {
        %merged_xrefs = %_mif_tpl_xrefs;
        @merged_xrefs{keys %_mif_xrefs} = values %_mif_xrefs;
        push(@out_result, &_MifXRefsToText(*merged_xrefs));
        $offset{'XRefFormats'} = $#out_result;
    }
    if (%_mif_paras) {
$igc_start = time;
        %merged_paras = %_mif_tpl_paras;
        @merged_paras{keys %_mif_paras} = values %_mif_paras;
        push(@out_result, &_MifParasToText(*merged_paras));
#printf STDERR "text->para: %d seconds\n", time - $igc_start;
        $offset{'PgfCatalog'} = $#out_result;
    }
    if (%_mif_fonts) {
$igc_start = time;
        %merged_fonts = %_mif_tpl_fonts;
        @merged_fonts{keys %_mif_fonts} = values %_mif_fonts;
        push(@out_result, &_MifFontsToText(*merged_fonts));
#printf STDERR "text->font: %d seconds\n", time - $igc_start;
        $offset{'FontCatalog'} = $#out_result;
    }
    if (%_mif_tbls) {
$igc_start = time;
        %merged_tbls = %_mif_tpl_tbls;
        @merged_tbls{keys %_mif_tbls} = values %_mif_tbls;
        push(@out_result, &_MifTblsToText(*merged_tbls));
#printf STDERR "text->tbl: %d seconds\n", time - $igc_start;
        $offset{'TblCatalog'} = $#out_result;
    }
    if (@_mif_figure) {
        push(@out_result, join("\n",
          '<AFrames',
          @_mif_figure,
          '> # end of AFrames'
        ));
        $offset{'AFrames'} = $#out_result;
    }
    if (@_mif_table) {
        push(@out_result, join("\n",
          '<Tbls ',
          @_mif_table,
          '> # end of Tbls'
        ));
        $offset{'Tbls'} = $#out_result;
    }
    if (@_mif_pages) {
        push(@out_result, join("\n", @_mif_pages, $_mif_bodypage));
        $offset{'Page'} = $#out_result;
    }

    # Build the main text flow:
    # * add the header
    # * patch in the TextRect ID for the main flow
    # * ensure that nested lines are indented
    # * add the footer.
    $mainflow = join("\n",
        "<TextFlow",
        "<TFTag `A'>",
        "<TFAutoConnect Yes>",
        "<TFSideheads Yes>",
        "<TFSideheadPlacement Left>",
        "<TFSideheadGap $SDF_USER'var{OPT_SIDEHEAD_GAP}>",
        "<TFSideheadWidth $SDF_USER'var{OPT_SIDEHEAD_WIDTH}>",
        "<Notes ",
        "> # end of Notes",
        @text);
    $old_match_rule = $*;
    $* = 1;
    $mainflow =~ s/\<ParaLine/$&\n  <TextRectID $_MIF_TEXTFLOW_MAIN>/;
    $mainflow =~ s/\n/\n /g;
    $mainflow .= "\n> # end of TextFlow";
    $* = $old_match_rule;

    # Add the text flows to the  import table
    push(@out_result, join("\n", @_mif_textflows, $mainflow));
    $offset{'TextFlow'} = $#out_result;

    # If we're building a book, reset the buffers ready for the next part
    if ($component_type ne '') {
        @_mif_figure = ();
        @_mif_table = ();
        @_mif_pages = ();
        @_mif_textflows = ();
    }

    # Merge with the current template, if necessary
    if (scalar(@_mif_template) > 0) {
        @out_result = &_MifMerge(*_mif_template, *out_result, %offset);
    }

    # Return result
    push(@out_result, "# End of MIFFile");
    return @out_result;
}

#
# >>_Description::
# {{Y:_MifProcessControls}} checks the document control variables
# and sets the MIF control settings appropriately.
#
sub _MifProcessControls {
    local(*vars, $component_type) = @_;
#   local();
    local($page_size, $page_width, $page_height);
    local($sdf_tag, $mif_tag, $prefix);

    # Set the page size
    $page_size = $sdf_pagesize{$vars{'OPT_PAGE_SIZE'}};
    if ($page_size ne '') {
        ($page_width, $page_height) = split(/\000/, $page_size, 2);
    }
    else {
        # Custom size
        ($page_width, $page_height) = split(/x/, $vars{'OPT_PAGE_SIZE'}, 2);
    }
    $page_width  .= "pt" if $page_width =~ /^[\d\.]+$/;
    $page_width  .= "pt" if $page_width =~ /^[\d\.]+$/;
    $_mif_ctrls{'DPageSize'} = "$page_width $page_height";

    # Set the number of sides
    $_mif_ctrls{'DTwoSides'} = $vars{'DOC_TWO_SIDES'} || $vars{'MIF_TWO_SIDES'};

    # If numbering per section is enabled, adjust the 'Current Page #'
    # variable definition, if necessary
    if ($vars{'MIF_BOOK_MODE'} && $vars{'OPT_NUMBER_PER_COMPONENT'}) {
        $_mif_ctrls{'DPageNumStyle'} = 'Arabic';
        if ($component_type eq 'CHAPTER') {
            $sdf_tag = $vars{'OPT_COMPONENT_COVER'} ? 'H1NUM' : 'H1';
            $mif_tag = $SDF_USER'parastyles_to{$sdf_tag};
            $prefix = "<\$paranumonly[$mif_tag]>";
        }
        elsif ($component_type eq 'APPENDIX') {
            $sdf_tag = $vars{'OPT_COMPONENT_COVER'} ? 'A1NUM' : 'A1';
            $mif_tag = $SDF_USER'parastyles_to{$sdf_tag};
            $prefix = "<\$paranumonly[$mif_tag]>";
        }
        elsif ($component_type eq 'IX') {
            $prefix = "Index";
        }
        elsif (! $_MIF_FRONT_PART{"\L$component_type"}) {
            $prefix = "\u\L$component_type";
        }
        else {
            # Use roman numerals for page numbers of front matter
            $_mif_ctrls{'DPageNumStyle'} = 'LCRoman';
        }
        if ($prefix) {
            $_mif_vars{'Current Page #'} = "$prefix-<\$curpagenum>";
        }
        else {
            $_mif_vars{'Current Page #'} = "<\$curpagenum>";
        }
    }
}

#
# >>_Description::
# {{Y:_MifAddMasterPage}} adds a master page to the internal buffers.
# {{name}} is either Left, Right or a 'user' name.
# (In the latter case, the MIF type is implicitly 'OtherMasterPage'.)
# {{sdf_type}} is the SDF page type (e.g. FIRST, FRONT_RIGHT).
# {{page_width}} and {{page_height}} are the page width and height
# in points.
# The text flows for the header and footer, if any, are added to
# internal buffers used to generate the final document.
# If you want a matching body page for this master page, then set
# {{bodypage_id}} to the id of the TextRect within that body page.
#
sub _MifAddMasterPage {
    local($name, $sdf_type, $page_width, $page_height, $bodypage_id) = @_;
#   local();
    local(@page);
    local($mif_type);
    local($left, $width);
    local($h_top, $h_height);
    local($m_top, $m_height);
    local($f_top, $f_height);
    local($prefix);
    local($sh_width, $sh_gap);
    local($col_count, $col_gap);
    local(@body);
    local(@text, $id);
    local($background);

    # Build the header
    if ($name eq 'Left' || $name eq 'Right') {
        $mif_type = $name . "MasterPage";
    }
    else {
        $mif_type = "OtherMasterPage";
    }
    @page = (
        "<Page",
        " <PageType $mif_type>",
        " <PageTag `$name'>",
        " <PageAngle 0.0>");

    # Calculate the left & width (used by all rectangles on the page)
    $left   = ($name eq 'Left') ? &SdfVarPoints("OPT_MARGIN_OUTER") :
              &SdfVarPoints("OPT_MARGIN_INNER");
    $width  = $page_width - &SdfVarPoints("OPT_MARGIN_OUTER") -
              &SdfVarPoints("OPT_MARGIN_INNER");

    # Calculate the tops and heights
    $h_top    = &SdfVarPoints("OPT_MARGIN_TOP");
    $h_height = &SdfPageInfo($sdf_type, "HEADER_HEIGHT", "pt");
    $f_height = &SdfPageInfo($sdf_type, "FOOTER_HEIGHT", "pt");
    $f_top    = $page_height - $f_height - &SdfVarPoints("OPT_MARGIN_BOTTOM");
    $m_top    = $h_top + $h_height +
                &SdfPageInfo($sdf_type, "HEADER_GAP", "pt");
    $m_height = $f_top - $m_top -
                &SdfPageInfo($sdf_type, "FOOTER_GAP", "pt");

    # Get the sidehead and column details
    $prefix    = $sdf_type =~ /^IX/ ? 'OPT_IX' : 'OPT';
    $sh_width  = &SdfVarPoints("${prefix}_SIDEHEAD_WIDTH");
    $sh_gap    = &SdfVarPoints("${prefix}_SIDEHEAD_GAP");
    $col_count = $SDF_USER'var{"${prefix}_COLUMNS"};
    $col_gap   = &SdfVarPoints("${prefix}_COLUMN_GAP");

    # Add the main section
    @text = ();
    $id = &_MifAddTextFlow(*text, 'A');
    &_MifAddTextArea(*page, $id, $left, $m_top, $width, $m_height,
      $sh_width, $sh_gap, &SdfPageInfo($sdf_type, "MAIN_BORDER"),
      $col_count, $col_gap);
    if ($bodypage_id ne '') {
        @body = (
            "<Page",
            " <PageType BodyPage>",
            " <PageBackground `$name'>");
        &_MifAddTextArea(*body, $bodypage_id, $left, $m_top, $width, $m_height,
          $sh_width, $sh_gap, '', $col_count, $col_gap);
        push(@body, "> # end of Page");
        $_mif_bodypage = join("\n", @body);
    }

    # Add the header, if any
    if ($h_height) {
        @text = split("\n", &SdfPageInfo($sdf_type, "HEADER", "macro"));
        @text = ('HEADER:') unless @text;
        $id = &_MifAddTextFlow(*text, '');
        &_MifAddTextArea(*page, $id, $left, $h_top, $width, $h_height,
          $sh_width, $sh_gap, &SdfPageInfo($sdf_type, "HEADER_BORDER"));
    }

    # Add the footer, if any
    if ($f_height) {
        @text = split("\n", &SdfPageInfo($sdf_type, "FOOTER", "macro"));
        @text = ('FOOTER:') unless @text;
#$mif_debug = 1;
        $id = &_MifAddTextFlow(*text, '');
$mif_debug = 0;
        &_MifAddTextArea(*page, $id, $left, $f_top, $width, $f_height,
          $sh_width, $sh_gap, &SdfPageInfo($sdf_type, "FOOTER_BORDER"));
    }

    # Add background objects, if any
    $background = &SdfPageInfo($sdf_type, "BACKGROUND");
    if ($background ne '') {
        &_MifAddPageBackground(*page, $background, $background);
    }

    # Add the object footer
    push(@page, "> # end of Page");

    # Add the page to the internal buffers
    push(@_mif_pages, join("\n", @page));
}

#
# >>_Description::
# {{Y:_MifAddTextFlow}} adds a text flow to the internal buffers.
# {{@text}} is the sdf for the text in the text flow,
# unless {{mif}} is true, in which case {{@text}} is assumed to be MIF.
# {{tag}} is the tag of the text flow, if any.
# The {{id}} of the text flow added is returned.
#
sub _MifAddTextFlow {
    local(*text, $tag, $mif) = @_;
    local($id);
    local(@hdr, @flow);
    local($textflow, $old_match_rule);

    # Get the next text flow id
    $id = $_mif_textflow_cnt++;

    # Build the text flow header
    @hdr = ("<TextFlow");
    if ($tag ne '') {
        push(@hdr,
            " <TFTag `$tag'>",
            " <TFAutoConnect Yes>");
    }

    # Convert the SDF to a MIF, if necessary
    if ($mif) {
        @flow = @text;
    }
    else {
        @flow = ();
        &_MifAddSection(*flow, *text);
    }

    # Convert to a text flow
    if (@flow) {
        $textflow = join("\n", @flow);
        $old_match_rule = $*;
        $* = 1;
        $textflow =~ s/\<ParaLine/$&\n  <TextRectID $id>/;
        $textflow =~ s/\n/\n /g;
        $textflow .= "\n> # end of TextFlow";
        $* = $old_match_rule;
    }

    # If nothing was generated, build the text flow with a dummy paragraph
    else {
        $textflow = join("\n",
            " <Para",
            "  <ParaLine",
            "   <TextRectID $id>",
            "  > # end of ParaLine",
            " > # end of Para",
            "> # end of TextFlow");
    }

    # Add the text flow to the internal buffers
    push(@_mif_textflows, join("\n", @hdr, $textflow));

    # Return result
    return $id;
}

#
# >>_Description::
# {{Y:_MifAddTextArea}} adds a text area to a master or reference page.
# {{@page}} is the MIF page data (so far).
# {{id}} is the id of the text flow for this area.
# {{left}}, {{top}}, {{width}} and {{height}} give the
# rectangle's position. {{shwidth}} and {{shgap}} give
# the sidehead width and gap respectively.
# {{border}} is a comma-separated list of
# attributes which collectively describe the border.
# The format of each attribute is name[=value].
# The supported attributes are:
#
# * {{top}} - a line above the area
# * {{bottom}} - a line below the area
# * {{box}} - a box around the area
# * {{radius}} - for a box, the radius of the corner.
#
# For {{top}}, {{bottom}} and {{box}}, the value of the
# attribute is the line width in points.
#
sub _MifAddTextArea {
    local(*page, $id, $left, $top, $width, $height, $shwidth, $shgap, $border, $colcnt, $colgap) = @_;
#   local();
    local(%border, $nv, $name, $value);
    local($left2, $top2, $width2, $height2);
    local($right, $bottom);

    # Convert the border attribute to a set of name-value pairs
    %border = ();
    for $nv (split(/\s*,\s*/, $border)) {
        if ($nv =~ /\=/) {
            $name = $`;
            $value = $';
        }
        else {
            $name = $nv;
            $value = 1;
        }
        $border{$name} = $value;
    }

    # Add the border, if any
    for $name (sort keys %border) {
        $value = $border{$name};
        if ($name eq 'top') {
            # put the line just above the actual border
            $top2 = $top - $_MIF_BORDER_GAP;
            $right = $left + $width;
            push(@page, &_MifLine($left, $top2, $right, $top2,
                " ", 0, $value));
        }
        elsif ($name eq 'bottom') {
            # put the line just below the actual border
            $right = $left + $width;
            $bottom = $top + $height + $_MIF_BORDER_GAP;
            push(@page, &_MifLine($left, $bottom, $right, $bottom,
                " ", 0, $value));
        }
        elsif ($name eq 'box') {
            # put the rectangle just around the actual border
            $left2 = $left - $_MIF_BORDER_GAP;
            $top2 = $top - $_MIF_BORDER_GAP;
            $width2 = $width + $_MIF_BORDER_GAP * 2;
            $height2 = $height + $_MIF_BORDER_GAP * 2;
            push(@page, &_MifRectangle($left2, $top2, $width2, $height2,
                $border{'radius'}, " ", 0, $value));
        }
    }

    # Add the text rectangle. Note that we explicitly do this AFTER
    # the border stuff so that MIF attribute inheritance will work
    # correctly for background objects (if any).
    $colcnt = 1 if $colcnt < 1;
    $colgap = 0 if $colgap eq '';
    push(@page,
        " <TextRect",
        "  <ID $id>",
        "  <Pen 15>",
        "  <Fill 15>",
        "  <ShapeRect  ${left}pt ${top}pt ${width}pt ${height}pt>",
        "  <BRect  ${left}pt ${top}pt ${width}pt ${height}pt>",
        "  <TRSideheadWidth  ${shwidth}pt>",
        "  <TRSideheadGap  ${shgap}pt>",
        "  <TRNumColumns  ${colcnt}>",
        "  <TRColumnGap  ${colgap}pt>",
        " > # end of TextRect");
}

#
# >>_Description::
# {{Y:_MifLine}} creates a MIF line object.
#
sub _MifLine {
    local($x1, $y1, $x2, $y2, $prefix, $pen, $pen_width, $color) = @_;
#   local($result);
    local(@line);

    # Apply defaults
    $pen       = 0          if $pen       eq '';
    $pen_width = 1          if $pen_width eq '';
    $color     = 'Black'    if $color     eq '';

    # Build the header
    @line = ("$prefix<PolyLine");

    # Add the attributes
    push(@line,
        " <Pen $pen>",
        " <PenWidth  $pen_width pt>",
        " <ObColor `$color'>",
        " <HeadCap Square>",
        " <TailCap Square>");

    # Add the points
    push(@line,
        " <NumPoints 2>",
        " <Point  ${x1}pt ${y1}pt>",
        " <Point  ${x2}pt ${y2}pt>",
        "> # end of PolyLine");

    # Return result
    return join("\n$prefix", @line);
}

#
# >>_Description::
# {{Y:_MifRectangle}} creates a MIF rectangle object.
# {{radius}} is the radius of the corner (0=square corner).
#
sub _MifRectangle {
    local($left, $top, $width, $height, $radius, $prefix, $pen, $pen_width) = @_;
#   local($result);
    local(@rect);

    # Build the header
    @rect = ("$prefix<RoundRect");

    # Add the attributes
    push(@rect,
        " <Pen $pen>",
        " <PenWidth  $pen_width pt>");

    # Add the shape
    push(@rect,
        " <ShapeRect ${left}pt ${top}pt ${width}pt ${height}pt>",
        " <Radius ${radius}pt>",
        "> # end of RoundRect");

    # Return result
    return join("\n$prefix", @rect);
}

#
# >>_Description::
# {{Y:_MifAddPageBackground}} adds background objects to a page.
# {{master}} is the name of the master page to get the
# objects from. The master page is assumed to be in a file called
# {{background}}.{{mif}}.
# If {{required}} is true, then a warning is output if the
# master page is not found.
#
sub _MifAddPageBackground {
    local(*page, $background, $master, $required) = @_;
#   local();
    local($bg_ext, $bg_short, $bg_file);
    local($_, $in_object, $in_file);

    # Find the file
    #$bg_ext = 'bg' . substr($sdf_fmext, -1);
    $bg_ext = 'mif';
    $bg_short = &NameJoin('', $background, $bg_ext);
    $bg_file = &SDF_USER'FindFile($bg_short);
    if ($bg_file eq '') {
        &AppMsg("warning", "unable to find background file '$bg_short'");
        return;
    }

    # Open the file
    unless (open(BGFILE, $bg_file)) {
        &AppMsg("warning", "unable to open background file '$bg_file'");
        return;
    }

    # Copy the objects
    $in_object = 0;
    $in_file = 0;
    while (<BGFILE>) {
        chop;
        if ($in_object) {
            push(@page, $_);
            $in_object = 0 if /^ \>/;
        }
        elsif ($in_file) {
            last if /^\>/;
            next if /\>$/;
            next if /^ \<TextRect/;
            if (/^ \<(\w+)/) {
                push(@page, $_);
                $in_object = 1;
            }
        }
        elsif (/^ \<PageTag\s+`$master'\>/) {
            $in_file = 1;
        }
    }
    close(BGFILE);

    # Output a warning, if necessary
    if ($required && !$in_file) {
        &AppMsg("warning", "master page '$master' not found in '$bg_file'");
    }
}

#
# >>_Description::
# {{Y:_MifAddRefPages}} adds the reference pages to the internal buffers.
# {{part_type}} is the book part type. If {{part_type}} is a derived
# list (e.g. TOC), the relevant special text flow is added.
#
sub _MifAddRefPages {
    local($part_type) = @_;
#   local();
    local(@page);
    local($name);
    local(%attr);
    local($page_x, $page_y, $width, $height);
    local($objects);
    local($pen, $pen_width);
    local($y, $length);

    # Build the reference page header
    @page = (
        "<Page",
        " <PageType ReferencePage>",
        " <PageTag `Reference'>",
        " <PageAngle 0.0>");

    # Add the frames
    $page_x = 72;
    $page_y = 72;
    for $name (sort keys %_mif_frames) {
        %attr = &_MifAttrSplit($_mif_frames{$name});
        $width  = $attr{'Width'};
        $width  = $SDF_USER'var{'DOC_FULL_WIDTH'} if $width eq '';
        $height = $attr{'Height'};
        $page_y += $height + 36;

        # Build the object in the frame, if necessary
        if ($attr{'LineLength'}) {
            $pen       = $attr{'Pen'};
            $pen_width = $attr{'PenWidth'};
            $x         = $attr{'LineX'};
            $x         = 0 if $x eq '';
            $y         = $attr{'LineY'};
            $y         = 0 if $y eq '';
            $length    = $attr{'LineLength'};
            $objects   = &_MifLine($x, $y, $length+$x, $y, "  ", $pen,
                         $pen_width, $attr{'Color'});
        }
        else {
            $objects = $attr{'Objects'};
        }

        # Add the frame
        push(@page,
            " <Frame ",
            "  <Pen 15>",
            "  <Fill 15>",
            "  <PenWidth  1.0 pt>",
            "  <ObColor `Black'>",
            "  <ShapeRect  ${page_x}pt ${page_y}pt ${width}pt ${height}pt>",
            "  <FrameType NotAnchored>",
            "  <Tag `$name'>",
            $objects,
            " > # end of Frame");
    }

    # Add the object footer
    push(@page, "> # end of Page");

    # Add the page to the internal buffers
    push(@_mif_pages, join("\n", @page));

    # Add the special text flow, if necessary
    &_MifAddSpecialTextFlow($part_type);
}
sub _MifAddSpecialTextFlow {
    local($name) = @_;
#   local();
    local(%attr, $layout);
    local(@mif_text, $j, $hdgtag, $tag, $tagtype, $tagbase);
    local($id);
    local(@page);
    local($left, $top, $width, $height, $sh_width, $sh_gap);

    # Get the attributes, if any
    if ($_mif_lists{$part_type}) {
        %attr = &_MifAttrSplit($_mif_lists{$name});
    }
    elsif ($_mif_indexes{$part_type}) {
        %attr = &_MifAttrSplit($_mif_indexes{$name});
    }
    else {
        return;
    }

    # Build the MIF for the text flow.
    # Note: Frame core dumps(!!) if a layout includes a paranumonly
    # or paranum when the matching paragraph has no autonumber, so
    # we make sure this cannot happen!!!
    @mif_text = ();
    if ($name eq 'TOC') {
        for ($j = 1; $j <= $SDF_USER'var{'DOC_TOC'}; $j++) {
            for $tagtype ('H', 'A', 'P') {
                $hdgtag = $SDF_USER'parastyles_to{"$tagtype$j"};
                $tag = $hdgtag . $name;
                $_mif_parastyle_used{$tag}++;
                $layout = &_MifFixLayout($attr{'Layout'}, $hdgtag);
                push(@mif_text,
                     "<Para ",
                     " <PgfTag `$tag'>",
                     " <ParaLine ",
                     "  <String `$layout'>",
                     " >",
                     "> # end of Para");
            }
        }

        # Ensure the index makes it into the contents
        $hdgtag = $SDF_USER'parastyles_to{"IXT"};
        $tag = $hdgtag . $name;
        $_mif_parastyle_used{$tag}++;
        $layout = &_MifFixLayout($attr{'Layout'}, $hdgtag);
        push(@mif_text,
             "<Para ",
             " <PgfTag `$tag'>",
             " <ParaLine ",
             "  <String `$layout'>",
             " >",
             "> # end of Para");
    }
    elsif ($name eq 'LOF') {
        $hdgtag = $SDF_USER'parastyles_to{"FT"};
        $tag = $hdgtag . $name;
        $_mif_parastyle_used{$tag}++;
        $layout = &_MifFixLayout($attr{'Layout'}, $hdgtag);
        push(@mif_text,
             "<Para ",
             " <PgfTag `$tag'>",
             " <ParaLine ",
             "  <String `$layout'>",
             " >",
             "> # end of Para");
    }
    elsif ($name eq 'LOT') {
        $hdgtag = $SDF_USER'parastyles_to{"TT"};
        $tag = $hdgtag . $name;
        $_mif_parastyle_used{$tag}++;
        $layout = &_MifFixLayout($attr{'Layout'}, $hdgtag);
        push(@mif_text,
             "<Para ",
             " <PgfTag `$tag'>",
             " <ParaLine ",
             "  <String `$layout'>",
             " >",
             "> # end of Para");
    }
    elsif ($name eq 'IX') {
        for $tagbase ('GroupTitles', 'Index', 'Level1', 'Level2') {
            $tag = $tagbase . $name;
            $_mif_parastyle_used{$tag}++ if $_mif_paras{$tag};
        }
    }

    # Add the text flow to the internal buffers
    $id = &_MifAddTextFlow(*mif_text, $name, 1);

    # Build the reference page header
    @page = (
        "<Page",
        " <PageType ReferencePage>",
        " <PageTag `$name'>",
        " <PageAngle 0.0>");

    # Calculate the page dimensions
    $left     = &SdfVarPoints("OPT_MARGIN_INNER");
    $width    = &SdfVarPoints("DOC_FULL_WIDTH");
    $top      = &SdfVarPoints("OPT_MARGIN_TOP");
    $height   = &SdfVarPoints("DOC_TEXT_HEIGHT");
    $sh_width = &SdfVarPoints("OPT_SIDEHEAD_WIDTH");
    $sh_gap   = &SdfVarPoints("OPT_SIDEHEAD_GAP");

    # Add the text area
    &_MifAddTextArea(*page, $id, $left, $top, $width, $height, $sh_width,
      $sh_gap);

    # Add the object footer
    push(@page, "> # end of Page");

    # Add the page to the internal buffers
    push(@_mif_pages, join("\n", @page));
}

#
# >>_Description::
# {{Y:_MifFixLayout}} removes paranum/paranumonly building blocks
# from a layout for a paragraph which does not have an autonumber.
#
sub _MifFixLayout {
    local($layout, $paratag) = @_;
    local($result);
    local(%attr);

    # Build the result
    $result = $layout;
    %attr = &_MifAttrSplit($_mif_paras{$paratag});
    if ($attr{'NumFormat'} eq '') {
        $result =~ s/\<\$paranumonly\\\>//g;
        $result =~ s/\<\$paranum\\\>//g;
    }
#print STDERR "$paratag:$result.\n";

    # Return result
    return $result;
}

#
# >>_Description::
# {{Y:_MifAddLists}} adds the generated lists (i.e. table of contents, etc.).
#
sub _MifAddLists {
    local(*text) = @_;
#   local();
    local($target, $soft);
    local($name);
    local(%attr);
    local($layout);
    local($i);
    local($old_match_rule, $toc_offset);

    # Set some flags based on the output ultimately generated
    $target = $SDF_USER'var{'OPT_TARGET'};
    $soft = $target eq 'help' || $target eq 'html';

    # Process the list definitions (add xrefs, etc.)
    for $name (sort keys %_mif_lists) {
        %attr = &_MifAttrSplit($_mif_lists{$name});
        $layout = $attr{'Layout'};
        $layout = '<$paratext\>' if $soft;
        $_mif_xrefs{$name} = $layout;
    }

    # Add titles
    if (@_mif_toc_list) {
        unshift(@_mif_toc_list, &_MifListTitle('TOC', 'Table of Contents'));
    }
    if (@_mif_lof_list) {
        unshift(@_mif_lof_list, &_MifListTitle('LOF', 'List of Figures'));
    }
    if (@_mif_lot_list) {
        unshift(@_mif_lot_list, &_MifListTitle('LOT', 'List of Tables'));
    }

    # Insert the generated lists before the first level 1 heading
    push(@_mif_toc_list, @_mif_lof_list, @_mif_lot_list);
    if (@_mif_toc_list) {
        $old_match_rule = $*;
        $* = 1;
        $toc_offset = 0;
        para:
        for ($i = 0; $i <= $#text; $i++) {
            if ($text[$i] =~ /\<MText \`$_MIF_TOC_XREF_START\'\>/) {
                $toc_offset = $i;
                last para;
            }
        }
        $* = $old_match_rule;
        splice(@text, $toc_offset, 0, @_mif_toc_list);
    }
}

#
# >>_Description::
# {{Y:_MifListTitle}} builds a title for a generated list.
# The mif for the title is returned. {{default_title}} is used if
# a title hasn't been specified via the appropriate SDF variable.
#
sub _MifListTitle {
    local($type, $default_title) = @_;
    local($mif);
    local($title, $tag);
    local(@sdf_data, @mif_data);

    # If we're building component covers, make sure that the relevant SDF
    # macro gets called
    $mif = '';
    if ($SDF_USER'var{'OPT_COMPONENT_COVER'} &&
        ($type eq 'TOC' || $type eq 'IX')) {
        @sdf_data = ('!DOC_COMPONENT_COVER_BEGIN');
        &_MifAddSection(*mif_data, *sdf_data);
        $mif = join("\n", @mif_data) . "\n";
    }

    $tag = $SDF_USER'parastyles_to{$type . 'T'};
    $_mif_parastyle_used{$tag}++;
    $title = $SDF_USER'var{"DOC_${type}_TITLE"};
    $title = $default_title if $title eq '';
    &_MifEscape(*title);
    $mif .=
        "<Para\n" .
        " <PgfTag `$tag'>\n" .
        " <ParaLine\n" .
        "  <String `$title'>\n" .
        " >\n" .
        "> # end of Para";

    # Return result
    return $mif;
}

#
# >>_Description::
# {{Y:_MifMerge}} merges an import table into a MIF template.
# The import table must be generated so that each
# main MIF object contains one record only. {{offset}} contains
# the indices of each main MIF object which exists in {{@import}}.
# Reference pages and their textflows are retained from {{@template}}.
# The other pages (i.e. the master and body pages) must be supplied
# by {{@import}}.
#
sub _MifMerge {
    local(*template, *import, %offset) = @_;
    local(@new);
    local($record, $obj);
    local($old_match_rule);
    local($merged_pages, $merged_textflows, %ref_textflow);
    local($side_width);
    local($page_type, $page_name, $page_size, $cover_rect);
    
    # To permit multi-line matching, save the old state here and
    # restore it later      
    $old_match_rule = $*;
    $* = 1;

    #
    # Do the merge. We ignore BookComponent objects
    # in @import as this simplifies the code somewhat.
    # As a Tbls section may or may not exist in the template,
    # we ignore existing Tbls and place new tables, if any,
    # immediately after figures (AFrames).
    #
    # We use while/shift, rather than for, to save memory.
    #
    $merged_pages = 0;
    $merged_textflows = 0;
    %ref_textflow = ();
    while($record = shift(@template)) {
    
        # Find the object 'name'
        unless ($record =~ /^\<(\w+)/) {
            &AppExit("fatal", "MIF template error - expecting object");
        }       
        $obj = $1;

        # Patch the comment to claim responsibility
        if ($obj eq 'MIFFile') {
            $record =~ s/\>.*$/>/;
            $record .= " # Generated by $app_product_name $app_product_version";
        }
        
        # Patch in records from import  
        if ($obj eq 'TextFlow') {

            # If this is the first text flow, merge the import text flows
            unless ($merged_textflows) {
                $merged_textflows = 1;
                push(@new, $import[$offset{'TextFlow'}]);
            }

            # If this is a text flow for a reference page, keep it
            if ($record =~ /\<TextRectID\s+(\d+)/) {
                push(@new, $record) if $ref_textflow{$1};
            }       
            else {
                &AppExit("warning", "MIF template error - no TextRectID for TextFlow");
            }
        }
        elsif ($obj eq 'Page') {
            $record =~ /\<PageType\s+(\w+)/;
            $page_type = $1;

            # If this is the first page, merge the import pages
            unless ($merged_pages) {
                $merged_pages = 1;
                push(@new, $import[$offset{'Page'}]);
            }

            # If this is a reference page, use it and
            # remember the textflow id, if any
            if ($page_type eq 'ReferencePage') {
                push(@new, $record);
                if ($record =~ /\<TextRect\s+\<ID\s+(\d+)\>/) {
                    $ref_textflow{$1} = 1;
                }
                #else {
                #    &AppMsg("warning", "MIF template error - no TextRect ID for reference page");
                #}
            }
        }
        elsif ($obj eq 'AFrames') {
            if ($offset{$obj}) {
                push(@new, $import[$offset{$obj}]);
            }
            if ($offset{"Tbls"}) {
                push(@new, $import[$offset{"Tbls"}]);
            }
        }
        elsif ($obj ne 'BookComponent' && $obj ne 'Tbls' && $offset{$obj}) {
            push(@new, $import[$offset{$obj}]);
        }
        else {
            push(@new, $record);
        }
    }
    
    # Return result
    $* = $old_match_rule;
    return @new;    
}

#
# >>_Description::
# {{Y:_MifHandlerTuning}} handles the 'tuning' directive.
#
sub _MifHandlerTuning {
    local(*outbuffer, $tuning, %attr) = @_;
#   local();
    local($template_file);

    # Regardless of what happens, make sure we init things
    &_MifInitTemplate();

    # A tuning of '.' means to skip merging (used in testing)
    if ($tuning eq '.') {
        return;
    }

    # Find the template file. A template file is searched for by
    # looking for {{tuning}}.{{fmver}} along the SDF include
    # path, where {{sdfver}} is fm4 or fm5 (typically).
    $template_file = &SDF_USER'FindFile(&NameJoin('', $tuning, $sdf_fmext));
    if ($template_file eq '') {
        #&AppMsg("warning", "unable to find template '$tuning'");
        return;
    }

    # Load the template file
    unless (&_MifFetchTemplate($template_file)) {
        &AppMsg("warning", "unable to load template file '$template_file'");
    }
}

#
# >>_Description::
# {{Y:_MifHandlerEndTuning}} handles the 'endtuning' directive.
#
sub _MifHandlerEndTuning {
    local(*outbuffer, $text, %attr) = @_;
#   local();

    # do nothing
}

#
# >>_Description::
# {{Y:_MifHandlerTable}} handles the 'table' directive.
#
sub _MifHandlerTable {
    local(*outbuffer, $columns, %attr) = @_;
#   local();
    local($tbl_id);
    local($style, $style_tag);
    local($tbl_title);
    local($tbl_width, $unit_width, $width, @widths);
    local($rest);
    local($target);
    local(@equal_cols, $col);
    local($lower, $sep, $upper);
    local($indent);
    local($align, $placement);
    local($margins);

    # Update the state
    push(@_mif_tbl_state, $_MIF_INTABLE);
    push(@_mif_tbl_start, $#outbuffer + 1);
    push(@_mif_tbl_wide, $attr{'wide'});
    push(@_mif_tbl_style, $attr{'style'});
    push(@_mif_tbl_landscape, $attr{'landscape'});
    push(@_mif_row_type, '');

    # Get the table id. Note that we keep a queue of these (rather
    # than a stack) to ensure that nested table are output in the
    # right order.
    $tbl_id = $#_mif_table + scalar(@_mif_tbl_id) + $_MIF_OBJ_REF_START;
    unshift(@_mif_tbl_id, $tbl_id);

    # Get the style
    $style = $attr{'style'};
    $style_tag = $SDF_USER'tablestyles_to{$style};
    if ($style_tag eq '') {
        &AppMsg("warning", "unknown table style '$style'");
        $style_tag = $style;
    }

    # Get the title, if any. FrameMaker 5.0 adds blank pages for
    # tables with titles, so a configuration flag controls whether
    # we produce real or simulated table titles.
    $tbl_title = $attr{'title'};
    push(@_mif_tbl_title, $tbl_title);
    $tbl_title = '' if $_MIF_SIMPLE_TBL_TITLES;

    # Get the indent
    if (!$attr{'wide'} && $attr{'listitem'} ne '') {
        $indent = &SdfVarPoints('OPT_LIST_INDENT') * $attr{'listitem'};
    }
    else {
        $indent = 0;
    }

    # Get the positioning details
    $align = $attr{'align'};
    $align = 'Inside' if $align eq 'Inner';
    $align = 'Outside' if $align eq 'Outer';
    $placement = $attr{'placement'};
    $placement =~ s/top/Top/ if $placement ne '';

    # For landscape tables, the default alignment is centered, i.e.
    # the table is centered BEFORE rotation
    if ($attr{'landscape'} && $align eq '') {
        $align = 'Center';
    }

    # Get the table width and 1% (i.e. unit) width
    if ($attr{'wide'}) {
        $tbl_width = $SDF_USER'var{'DOC_FULL_WIDTH'} - $indent;
    }
    else {
        $tbl_width = $SDF_USER'var{'DOC_TEXT_WIDTH'} - $indent;
    }
    $unit_width = $tbl_width / 100;

    # For margins not supplied, we use some defaults. Ideally,
    # the default values should come from the table format used.
    ($_mif_tbl_lmargin, $_mif_tbl_tmargin, $_mif_tbl_rmargin,
      $_mif_tbl_bmargin) = (6, 4, 6, 2);

    # Get the cell margins, if necessary
    if ($attr{'lmargin'} ne '' || $attr{'tmargin'} ne '' ||
        $attr{'rmargin'} ne '' || $attr{'bmargin'} ne '') {
        $_mif_tbl_lmargin = $attr{'lmargin'} if defined $attr{'lmargin'};
        $_mif_tbl_tmargin = $attr{'tmargin'} if defined $attr{'tmargin'};
        $_mif_tbl_rmargin = $attr{'rmargin'} if defined $attr{'rmargin'};
        $_mif_tbl_bmargin = $attr{'bmargin'} if defined $attr{'bmargin'};
        $margins =
          "${_mif_tbl_lmargin}pt ${_mif_tbl_tmargin}pt " .
          "${_mif_tbl_rmargin}pt ${_mif_tbl_bmargin}pt";
    }

    # Update the output buffer
    $_mif_tblstyle_used{$style_tag}++;
    push(@outbuffer, 
        " <Tbl",
        "  <TblID $tbl_id>",
        "  <TblTag `$style_tag'>");

    # If we're ultimately going to rtf or hlp, output a simple set of
    # column widths
    $target = $SDF_USER'var{'OPT_TARGET'};
    if ($target eq 'hlp' || $target eq 'rtf') {
        push(@outbuffer, "  <TblNumColumns $columns>");
        @widths = split(/,/, $attr{'format'});
        $rest = 100;
        for ($col = 0; $col < $columns; $col++) {
            $width = $widths[$col];
            if ($width =~ /([-=])/) {
                # Assume 30% for now :-(
                $width = (30 * $unit_width) . 'pt';
                $rest -= 30;
            }
            elsif ($width =~ /\*$/) {
                # Assume the rest for now. This will work provided
                # that it is last column and that preceding dimensions are 
                # guessed (e.g. -) or percentages. :-(
                $rest = 10 if $rest <= 0;
                $width = ($rest * $unit_width) . 'pt';
                $rest = 0;
            }
            else {
                # Convert the measurement to points if it's a percentage
                if ($width =~ /\%$/) {
                    $rest -= $`;
                    $width = ($` * $unit_width) . 'pt';
                }
            }
            push(@outbuffer, "  <TblColumnWidth $width>");
        }
    }

    else {
        # Override the table format
        push(@outbuffer, "  <TblFormat");
        push(@outbuffer, "   <TblTitlePlacement InHeader>") if $tbl_title;
        push(@outbuffer, "   <TblLIndent ${indent}pt>") if $indent ne '';
        push(@outbuffer, "   <TblAlignment $align >") if $align;
        push(@outbuffer, "   <TblPlacement $placement >") if $placement;
        push(@outbuffer, "   <TblCellMargins $margins >") if $margins;
        push(@outbuffer, "   <TblWidth ${tbl_width}pt>");

        # Output the column widths
        @widths = split(/,/, $attr{'format'});
        @equal_cols = ();
        for ($col = 0; $col < $columns; $col++) {
            $width = $widths[$col];
            push(@outbuffer, "   <TblColumn", "    <TblColumnNum $col>");
            if ($width =~ /([-=])/) {
                $lower = $`;
                $sep   = $1;
                $upper = $';
                $lower = ($` * $unit_width) . 'pt' if $lower =~ /\%$/;
                $upper = ($` * $unit_width) . 'pt' if $upper =~ /\%$/;
                push(@outbuffer, "    <TblColumnWidthA $lower $upper>");
                push(@equal_cols, $col) if $sep eq '=';
            }
            elsif ($width =~ /\*$/) {
                push(@outbuffer, "    <TblColumnWidthP $`>");
            }
            else {
                $width = ($` * $unit_width) . 'pt' if $width =~ /\%$/;
                push(@outbuffer, "    <TblColumnWidth $width>");
            }
            push(@outbuffer, "   > # end of TblColumn");
        }
        push(@outbuffer, "  > # end of TblFormat");

        # Output the column details (those not already in the table format)
        push(@outbuffer, "  <TblNumColumns $columns>");
        if (@equal_cols) {
            push(@outbuffer, "  <EqualizeWidths");
            for $col (@equal_cols) {
                push(@outbuffer, "   <TblColumnNum $col>");
            }
            push(@outbuffer, "  > # end of EqualizeWidths");
        }
    }

    # Output the table title, if any
    if ($tbl_title) {
        push(@outbuffer, "  <TblTitleContent");
        &_MifParaAdd(*outbuffer, 'TT', $tbl_title);
        push(@outbuffer, "  > # end of TblTitleContent");
    }    
}

#
# >>_Description::
# {{Y:_MifHandlerRow}} handles the 'row' directive.
#
sub _MifHandlerRow {
    local(*outbuffer, $text, %attr) = @_;
#   local();
    local($state);

    # Finalise the old cell/row, if any
    $state = $_mif_tbl_state[$#_mif_tbl_state];
    if ($state eq $_MIF_INCELL) {
        push(@outbuffer,
            "     > # end of CellContent",
            "    > # end of Cell",
            "   > # end of Row",
            "  > # end of TblH/TblBody/TblF");
    }
    elsif ($state eq $_MIF_INROW) {
        push(@outbuffer,
            "   > # end of Row",
            "  > # end of TblH/TblBody/TblF");
    }

    # Update the state
    $_mif_tbl_state[$#_mif_tbl_state] = $_MIF_INROW;
    $_mif_row_type[$#_mif_row_type] = $text;

    # Update the output buffer
    push(@outbuffer,
            "  <Tbl$_MIF_ROW_SUFFIX{$text}",
            "   <Row");
    push(@outbuffer, "    <RowWithNext Yes>") if $text eq 'Group';
}

#
# >>_Description::
# {{Y:_MifHandlerCell}} handles the 'cell' directive.
#
sub _MifHandlerCell {
    local(*outbuffer, $text, %attr) = @_;
#   local();
    local($state);
    local($fill, $color, $cols, $rows, $angle);
    local($lruling, $rruling, $truling, $bruling);

    # Finalise the old cell, if any
    $state = $_mif_tbl_state[$#_mif_tbl_state];
    if ($state eq $_MIF_INCELL) {
        push(@outbuffer,
            "     > # end of CellContent",
            "    > # end of Cell");
    }

    # Update the state
    $_mif_tbl_state[$#_mif_tbl_state] = $_MIF_INCELL;
    $_mif_cell_align = $attr{'align'};
    $_mif_cell_valign = $attr{'valign'};
    if ($_mif_cell_valign eq 'Baseline') {
        $_mif_cell_valign = '';
    }
    $_mif_cell_tmargin = $attr{'tmargin'};
    $_mif_cell_bmargin = $attr{'bmargin'};
    $_mif_cell_lmargin = $attr{'lmargin'};
    $_mif_cell_rmargin = $attr{'rmargin'};

    # Get the attributes
    $color = &_MifMapColor($attr{'bgcolor'});
    $fill = $attr{'fill'} ne '' ? $_MIF_FILL_CODE{$attr{'fill'}} :
            ($color ne '' ? 0 : '');
    $cols = $attr{'cols'};
    $rows = $attr{'rows'};
    $angle = $attr{'angle'};
    $lruling = $attr{'lruling'};
    $lruling = 'Very Thin' if $lruling eq 'Vthin';
    $rruling = $attr{'rruling'};
    $rruling = 'Very Thin' if $rruling eq 'Vthin';
    $truling = $attr{'truling'};
    $truling = 'Very Thin' if $truling eq 'Vthin';
    $bruling = $attr{'bruling'};
    $bruling = 'Very Thin' if $bruling eq 'Vthin';

    # For tables of style "columns", the default top ruling of
    # group rows is 'Thin'
    if ($_mif_row_type[$#_mif_row_type] eq 'Group' && $truling eq '') {
        $truling = 'Thin' if $_mif_tbl_style[$#_mif_tbl_style] eq 'columns';
    }

    # Update the output buffer
    push(@outbuffer, "    <Cell");
    push(@outbuffer, "     <CellFill $fill>") if $fill ne '';
    push(@outbuffer, "     <CellColor `$color'>") if $color ne '';
    push(@outbuffer, "     <CellColumns $cols>") if $cols > 1;
    push(@outbuffer, "     <CellRows $rows>") if $rows > 1;
    push(@outbuffer, "     <CellAngle $angle>") if $angle != 0;
    push(@outbuffer, "     <CellLRuling `$lruling'>") if $lruling ne '';
    push(@outbuffer, "     <CellRRuling `$rruling'>") if $rruling ne '';
    push(@outbuffer, "     <CellTRuling `$truling'>") if $truling ne '';
    push(@outbuffer, "     <CellBRuling `$bruling'>") if $bruling ne '';
    push(@outbuffer, "     <CellContent");
}

#
# >>_Description::
# {{Y:_MifHandlerEndTable}} handles the 'endtable' directive.
#
sub _MifHandlerEndTable {
    local(*outbuffer, $text, %attr) = @_;
#   local();
    local($state, $start, $tbl_id, $tbl_title, $tbl_wide);
    local($nested, $angle);

    # Finalise the table
    $state = pop(@_mif_tbl_state);
    if ($state eq $_MIF_INCELL) {
        push(@outbuffer,
            "     > # end of CellContent",
            "    > # end of Cell",
            "   > # end of Row",
            "  > # end of TblH/TblBody/TblF");
    }
    elsif ($state eq $_MIF_INROW) {
        push(@outbuffer,
            "   > # end of Row",
            "  > # end of TblH/TblBody/TblF");
    }
    push(@outbuffer,
            " > # end of Tbl");

    # Move the table to the table buffer
    $start = pop(@_mif_tbl_start);
    push(@_mif_table, join("\n", @outbuffer[$start .. $#outbuffer]));
    $#outbuffer = $start - 1;

    # Update the main text flow
    $tbl_id = pop(@_mif_tbl_id);
    $tbl_wide = pop(@_mif_tbl_wide);
    $tbl_title = pop(@_mif_tbl_title);
    $landscape = pop(@_mif_tbl_landscape);
    if (@_mif_tbl_id) {
        &'AppMsg("warning", "ignoring nested MIF table");
    }
    else {
        if ($landscape) {
            $nested = $landscape;
            $angle = 90;
        }
        &_MifUpdateMainFlow(*outbuffer, "table", $tbl_id, $tbl_title, $tbl_wide,
          $nested, $angle);
    }

    # Cleanup the state
    pop(@_mif_tbl_style);
    pop(@_mif_row_type);
}

#
# >>_Description::
# {{Y:_MifHandlerInline}} handles the inline directive.
#
sub _MifHandlerInline {
    local(*outbuffer, $text, %attr) = @_;
#   local();

    # Check we can handle this format
    my $target = $attr{'target'};
    return unless $target eq 'mif' || $target eq 'ps';

    # Build the result
    push(@outbuffer, $text);
}

#
# >>_Description::
# {{Y:_MifHandlerOutput}} handles the output directive.
#
sub _MifHandlerOutput {
    local(*outbuffer, $text, %attr) = @_;
#   local();
    local($offset, @component_data);
    local($component_type);
    local($file);

    # Finalise the current component, if requested
    if ($text eq '-') {
        # If there is no current component, do nothing
        return unless @_mif_component_offset;

        # Find the component type
        $component_type = $_mif_component_type[$_mif_component_cursor++];

        # Generate the mif
        $offset = pop(@_mif_component_offset);
        @component_data = splice(@outbuffer, $offset + 1);
        @component_data = &_MifFinalise(*component_data, $component_type);

        # Output the mif
        $file = pop(@_mif_component_file);
        unless (open(CHAPTER, ">$file")) {
            &AppMsg("error", "unable to write to component file '$file'");
            return;
        }
        print CHAPTER join("\n", @component_data), "\n";
        close(CHAPTER);
    }

    # Otherwise, save the output filename and the current offset
    # Note: the type is pushed onto @_mif_component_type by MifNewComponent.
    else {
        push(@_mif_component_file, $text);
        push(@_mif_component_offset, $#outbuffer);
    }
}

#
# >>_Description::
# {{Y:_MifHandlerImport}} handles the import directive.
#
sub _MifHandlerImport {
    local(*outbuffer, $filepath, %attr) = @_;
#   local();
    local($ref_id);

    # Add the figure to the internal buffers
    $ref_id = &_MifAddFigure($filepath, *attr);

    # Reference this object in the main flow
    &_MifUpdateMainFlow(*outbuffer, 'figure', $ref_id, $attr{'title'},
      $attr{'wide'});
}

#
# >>_Description::
# {{Y:_MifAddFigure}} adds a figure to the internal buffers.
# The reference id of the figure is returned.
#
sub _MifAddFigure {
    local($filepath, *attr) = @_;
    local($ref_id);
    local($fullname);
    local($ext);

    # Get the complete pathname and the file extension
    $fullname = $attr{'fullname'};
    $ext = (&NameSplit($fullname))[2];

    # Get the reference in the output buffer
    if ($ext eq 'mif') {
        $ref_id = &_MifAdd($fullname, 'figure', 1, *attr);
    }
    elsif ($attr{'mif_figure'}) {
        $ref_id = &_MifAdd($fullname, 'figure', $attr{'mif_figure'}, *attr);
    }
    else {
        $attr{'position'} = 'RunIntoParagraph' if $attr{'wrap_text'};
        $attr{'position'} = 'below' unless $attr{'position'};
        $ref_id = &_MifAddRef($filepath, %attr);
    }

    # Return result
    return $ref_id;
}

#
# >>_Description::
# {{Y:_MifHandlerObject}} handles the 'object' directive.
#
sub _MifHandlerObject {
    local(*outbuffer, $type, %attrs) = @_;
#   local();
    local($fn);
    local($name, $parent);

    # Find the object handler, if any
    $fn = "_MifObjectHandler$type";
    if (defined &$fn) {

        # Get the name and parent
        $name = $attrs{'Name'};
        delete $attrs{'Name'};
        $parent = $attrs{'Parent'};
        delete $attrs{'Parent'};

        # Jump to the routine which handles this object
        &$fn(*outbuffer, $name, $parent, %attrs);
    }
}

#
# >>_Description::
# {{Y:_MifAddToCatalog}} adds an object to a catalog of objects.
# If {{$parent}} and {{$root}} are set and equal, then {{%root_attr}} is
# used instead of retrieving the parent attributes from the catalog.
# This is done to improve performance.
#
sub _MifAddToCatalog {
    local(*catalog, $type, $name, $parent, *attr, *root, *root_attr) = @_;
#   local();
    local(%parent_attr);

    # Get the parent definition, if any
    %parent_attr = ();
    if ($parent ne '') {
        if ($parent eq $root) {
            %parent_attr = %root_attr;
        }
        else {
            %parent_attr = &_MifAttrSplit($catalog{$parent});
        }
        unless (%parent_attr) {
            &AppMsg("warning", "unknown parent '$parent' in definition of mif $type '$name'");
            return;
        }
    }

    # Merge in the new attributes
    @parent_attr{keys %attr} = values %attr;

    # Store the new definition
    $catalog{$name} = &_MifAttrJoin(*parent_attr);

    # Save the root, if necessary
    if ($root eq '' && $parent eq '') {
        $root = $name;
        %root_attr = %attr;
    }
}

#
# >>_Description::
# {{Y:_MifObjectHandlerVariable}} defines 'Variable' objects.
#
sub _MifObjectHandlerVariable {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();
    local($value);

    $name = $_MIF_VAR_MAP{$name} if $_MIF_VAR_MAP{$name};
    $value = $attr{'value'};
    if ($name =~ /^MIF_/) {
        return if $name eq 'MIF_TABLE_UNIT';
        return if $name eq 'MIF_TABLE_UNIT_WIDE';
        if ($name eq 'MIF_COVER') {
            $_mif_cover = $value;
        }
        else {
            $name = 'D' . &MiscUpperToMixed($');
            $_mif_ctrls{$name} = $value;
        }
    }
    else {
        $_mif_vars{$name} = $value;
    }
}

#
# >>_Description::
# {{Y:_MifObjectHandlerXref}} defines 'Xref' objects.
#
sub _MifObjectHandlerXref {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    $_mif_xrefs{$name} = $attr{'value'};
}

#
# >>_Description::
# {{Y:_MifObjectHandlerPara}} defines 'Para' objects,
# i.e. it generates a new paragraph format in the output.
#
sub _MifObjectHandlerPara {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_paras, 'paragraph', $name, $parent, *attr,
      *_mif_pararoot_name, *_mif_pararoot_attr);
}

#
# >>_Description::
# {{Y:_MifObjectHandlerPhrase}} defines 'Phrase' objects,
# i.e. it generates a new font format in the output.
#
sub _MifObjectHandlerPhrase {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_fonts, 'font', $name, $parent, *attr,
      *_mif_fontroot_name, *_mif_fontroot_attr);
}

#
# >>_Description::
# {{Y:_MifObjectHandlerTable}} defines 'Table' objects,
# i.e. it generates a new table format in the output.
# {{tags}} contains the new and base tags separated by a space.
#
sub _MifObjectHandlerTable {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_tbls, 'table', $name, $parent, *attr,
      *_mif_tblroot_name, *_mif_tblroot_attr);
}

#
# >>_Description::
# {{Y:_MifObjectHandlerFrame}} defines 'Frame' objects,
# i.e. it generates a reference frame in the output.
#
sub _MifObjectHandlerFrame {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_frames, 'frame', $name, $parent, *attr,
      *_mif_frameroot_name, *_mif_frameroot_attr);
}

#
# >>_Description::
# {{Y:_MifObjectHandlerList}} defines 'List' objects.
# i.e. it defines the layout of the TOC (Table of Contents), etc.
#
sub _MifObjectHandlerList {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_lists, 'lists', $name, $parent, *attr,
      *_mif_listroot_name, *_mif_listroot_attr);
}

#
# >>_Description::
# {{Y:_MifObjectHandlerIndex}} defines 'Index' objects.
# i.e. it defines the layout of an index
#
sub _MifObjectHandlerIndex {
    local(*outbuffer, $name, $parent, %attr) = @_;
#   local();

    &_MifAddToCatalog(*_mif_indexes, 'indexes', $name, $parent, *attr,
      *_mif_indexroot_name, *_mif_indexroot_attr);
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerChar}} handles the 'char' phrase directive.
#
sub _MifPhraseHandlerChar {
    local(*para, $text, %attr) = @_;
#   local();
    local($char);
    local($hex);

    $char = $_MIF_CHAR{$text};
    if ($text =~ /^\d+/) {
        $hex = sprintf("%x", $text);
        $para .= "  <String `\\x$hex '>\n";
    }
    elsif ($char =~ /^\W/) {
        $para .= "  <String `$char'>\n";
    }
    elsif ($char) {
        $para .= "  <Char $char>\n";
    }
    else {
        &AppMsg("warning", "ignoring unknown character '$text'");
    }

    # Hard returns must be the last thing in a ParaLine
    if ($char eq 'HardReturn' || $text == $_MIF_HARDRETURN_CODE) {
        $para .= " > # end of ParaLine\n" .
            " <ParaLine\n";
    }
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerInline}} handles the 'inline' phrase directive.
#
sub _MifPhraseHandlerInline {
    local(*para, $text, %attr) = @_;
#   local();

    # Build the result
    $para .= $text;
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerImport}} handles the 'import' phrase directive.
#
sub _MifPhraseHandlerImport {
    local(*para, $filepath, %attr) = @_;
#   local();

    # Add the figure to the internal buffers
    $attr{'position'} = 'inline' unless $attr{'position'};
    $ref_id = &_MifAddFigure($filepath, *attr);

    # Add the reference to the paragraph
    $para .= "  <AFrame $ref_id>\n";
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerVariable}} handles the 'variable' phrase directive.
#
sub _MifPhraseHandlerVariable {
    local(*para, $text, %attr) = @_;
#   local();

    $para .= "  <Variable\n" .
             "   <VariableName `$text'>\n" .
             "  > # end of Variable\n";
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerXRef}} handles the 'xref' phrase directive.
#
sub _MifPhraseHandlerXRef {
    local(*para, $para_text, %attr) = @_;
#   local();
    local($format, $text, $file);

    # Get the text and file from the jump attribute.
    $format = $attr{'xref'};
    ($file, $text) = split(/#/, $attr{'jump'}, 2);

    # Assume the Frame file, if any, has a doc extension.
    if ($file ne '') {
        $file = &NameSubExt($file, 'doc');
    }

    # Build the cross-reference
    $para .= "  <XRef\n" .
             "   <XRefName `$format'>\n" .
             "   <XRefSrcText `$text'>\n" .
             "   <XRefSrcFile `$file'>\n" .
             "  > # end of XRef\n";
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerPageNum}} handles the 'pagenum' phrase directive.
#
sub _MifPhraseHandlerPageNum {
    local(*para, $para_text, %attr) = @_;
#   local();

    &_MifPhraseHandlerVariable(*para, 'Current Page #');
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerPageCount}} handles the 'pagecount' phrase directive.
#
sub _MifPhraseHandlerPageCount {
    local(*para, $para_text, %attr) = @_;
#   local();

    &_MifPhraseHandlerVariable(*para, 'Page Count');
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerParaText}} handles the 'paratext' phrase directive.
#
sub _MifPhraseHandlerParaText {
    local(*para, $tags, %attr) = @_;
#   local();

    &_MifRunningHF(*para, 'paratext', $tags);
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerParaNum}} handles the 'paranum' phrase directive.
#
sub _MifPhraseHandlerParaNum {
    local(*para, $tags, %attr) = @_;
#   local();

    &_MifRunningHF(*para, 'paranum', $tags);
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerParaNumOnly}} handles the 'paranumonly' phrase directive.
#
sub _MifPhraseHandlerParaNumOnly {
    local(*para, $tags, %attr) = @_;
#   local();

    &_MifRunningHF(*para, 'paranumonly', $tags);
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerParaShort}} handles the 'parashort' phrase directive.
#
sub _MifPhraseHandlerParaShort {
    local(*para, $tags, %attr) = @_;
#   local();

    &_MifRunningHF(*para, 'marker1');
}

#
# >>_Description::
# {{Y:_MifPhraseHandlerParaLast}} handles the 'paralast' phrase directive.
#
sub _MifPhraseHandlerParaLast {
    local(*para, $tags, %attr) = @_;
#   local();

    &_MifRunningHF(*para, 'paratext', "+,$tags");
}

#
# >>_Description::
# {{Y:_MifRunningHF}} defines and inserts a Running H/F variable.
# {{type}} is one of paratext, paranum, paranumonly,
# marker1 or marker2. For the first 3 of these, {{tags}} is a
# comma separated list of paragraph tags.
#
sub _MifRunningHF {
    local(*para, $type, $tags) = @_;
#   local();
    local($defn);
    local(@tags, $tag, $fmtag);
    local($num);
    local($varname);

    # Build the definition
    if ($tags eq '') {
        $defn = "<\$$type>";
    }
    else {
        # Convert the SDF tag names to Frame ones
        @tags = split(/,/, $tags);
        for $tag (@tags) {
            $fmtag = $SDF_USER'parastyles_to{$tag};
            $tag =  $fmtag if $fmtag ne '';
        }
        $defn = "<\$$type\[" . join(",", @tags) . "]>";
    }

    # If the definition matches an existing running h/f variable, reuse it
    for ($num = 1; $num <= $_mif_runninghf_cnt; $num++) {
        last if $_mif_vars{"Running H/F $num"} eq $defn;
    }

    # If necessary, update the running h/f counter,
    # checking we haven't used them all
    if ($num  > 4) {
        &AppMsg("warning", "ignoring MIF directive '$type' - Running H/F 4 already used");
        return;
    }
    elsif ($num > $_mif_runninghf_cnt) {
        $_mif_runninghf_cnt++;
    }

    # Insert the variable
    $varname = "Running H/F $num";
    $_mif_vars{$varname} = $defn;
    &_MifPhraseHandlerVariable(*para, $varname);
}

#
# >>_Description::
# {{Y:_MifParaAppend}} merges {{para}} into the last paragraph
# in {{@result}}.
# (This is used to workaround Frame putting blank lines between
# each example paragraph when it converts documents to text.)
#
sub _MifParaAppend {
    local(*result, $para) = @_;
#   local();
    local($body_start);

    # find the location of the first ParaLine in the paragraph
    $body_start = index($para, " <ParaLine");

    # append the paragraph
    if ($body_start >= 0) {
        substr($result[$#result], - $_MIF_PARA_END_LEN) = 
          "  <Char HardReturn>\n >\n" . substr($para, $body_start) .
          $_MIF_PARA_END;
    }
    else {
        &AppMsg("failed", "bad paragraph format in '_MifParaAppend'");
    }
}
       
#
# >>_Description::
# {{Y:_MifAdd}} adds an object to the internal lists. These
# lists are merged with the converted text in {{Y:_MifFinalise}}
# to produce the final MIF file. {{id}} can be either:
#
# * a number (the internal object number in the file), or
# * a string (a pattern matched in the text just before the object
# is referenced in the main text flow of the file)
#
sub _MifAdd {
    local($file, $what, $id, *attr) = @_;
    local($ref_id);
    local($ok);
    local(@file);
    local($obj_id, $tag, $tbl_begin, $fmt_id, $fmt_ok);

    # Fetch the file
    ($ok, @file) = &_MifFetchAlbum($file);
    unless ($ok) {
        &AppMsg("warning", "failed to fetch file '$file'");
        return 0;
    }

    # If a pattern is passed as the id, first convert it to an
    # object reference
    if ($id =~ /^\d+$/) {
        $obj_id = $id;
    }
    else {
        $obj_id = &MifIdLookupBySymbol(*file, $_MIF_REF{$what}, $id);
    }

    # Add the object itself & format, if any, onto our stacks
    if ($obj_id) {
        if ($what eq 'table') {
            ($ref_id, $tbl_begin) = &_MifAddObject(*file,
              'Tbl', 'TblID', $obj_id, 'stack', *_mif_table);

            # Assume the table format id is the second attribute
            if ($ref_id) {
                $file[$tbl_begin + 2] =~ /TblTag (.*)\>/;
                $fmt_id = $1;
                $_mif_tblstyle_used{$fmt_id}++;
                ($fmt_ok) = &_MifAddObject(*file, 'TblFormat',
                  'TblTag', $fmt_id, 'byname', *_mif_tbls);
                unless ($fmt_ok) {
                    &AppMsg("warning", "MIF table format clash ($file, $id, $fmt_id)");
                }
            }
        }
        else {
            ($ref_id) = &_MifAddObject(*file,
              'Frame', 'ID', $obj_id, 'stack', *_mif_figure, *attr);
        }
    }

    # Check object was found
    unless ($ref_id) {
        &AppMsg("warning", "mif object '$what $id' not found in file '$file'");
    }

    # Return result
    return $ref_id;
}

#
# >>_Description::
# {{Y:_MifAddRef}} adds a referenced figure to the internal lists. These
# lists are merged with the converted text in {{Y:_MifFinalise}}
# to produce the final MIF file.
# {{%attr}} contains tuning attributes.
#
sub _MifAddRef {
    local($name, %attr) = @_;
    local($ref_id);
    local($fullname);
    local($width, $height);
    local($mif_path);
    local($shape_rect_params);
    local(@frame);

    # Default the height and width, if we can
    $fullname = $attr{'fullname'};
    ($width, $height) = &SdfSizeGraphic($fullname);
    $attr{'width'}  = $width  ? $width  : 20 unless $attr{'width'};
    $attr{'height'} = $height ? $height : 20 unless $attr{'height'};

    # points is the default measurement
    $width = $attr{'width'};
    $height = $attr{'height'};
    $width  .= "pt" if $width  =~ /^[\d\.]+$/;
    $height .= "pt" if $height =~ /^[\d\.]+$/;

    # Convert the name into a MIF device-independent pathname
    #if (! &NameIsAbsolute($name) && $attr{'root'} ne '') {
        #$name = &NameJoin($attr{'root'}, $name);
    #}
    if ($SDF_USER'var{'OPT_TARGET'} eq 'hlp') {
        $mif_path = &_MifPathName($name);
    }
    else {
        $mif_path = &_MifPathName($fullname);
    }

    # Build the Frame body
    $shape_rect_params = "0\" 0\" $width $height";
    @body = (
        "  <ImportObject",
        "   <ImportObFileDI `$mif_path'>",
        "   <ShapeRect $shape_rect_params>",
        "  > # end of ImportObject");

    # Build the frame
    $ref_id = scalar(@_mif_figure) + $_MIF_OBJ_REF_START;
    @frame = &_MifBuildFrame($ref_id, $shape_rect_params, *attr, *body);

    # Add the frame to the internal list
    push(@_mif_figure, join("\n", @frame));

    # Return result
    return $ref_id;
}

#
# >>_Description::
# {{_MifBuildFrame}} build a 'Frame' object, given its id, size, attributes
# and body.
#
sub _MifBuildFrame {
    local($ref_id, $shape_rect_params, *attr, *body) = @_;
    local(@frame);
    local($frametype);

    # Map the attributes to MIF names
    $frametype = $attr{'position'};
    $frametype = 'Below' unless $frametype;
    substr($frametype, 0, 1) =~ tr/a-z/A-Z/;

    # Build the figure frame header
    @frame = (
        " <Frame",
        "  <ID $ref_id>",
        "  <ShapeRect $shape_rect_params>",
        "  <FrameType $frametype>");

    # Add the optional stuff
    if ($attr{'align'}) {
        substr($attr{'align'}, 0, 1) =~ tr/a-z/A-Z/;
        push(@frame, "  <AnchorAlign $attr{'align'}>");
    }
    if ($attr{'clipped'}) {
        push(@frame, "  <Cropped Yes>");
    }
    if ($attr{'floating'}) {
        push(@frame, "  <Float Yes>");
    }
    if ($attr{'bl_offset'}) {
        $attr{'bl_offset'}  .= "pt" if $attr{'bl_offset'}  =~ /^[\d\.]+$/;
        push(@frame, "  <BLOffset $attr{'bl_offset'}>");
    }
    if ($attr{'ns_offset'}) {
        $attr{'ns_offset'}  .= "pt" if $attr{'ns_offset'}  =~ /^[\d\.]+$/;
        push(@frame, "  <NSOffset $attr{'ns_offset'}>");
    }

    # Return result
    return (@frame, @body, " > # end of Frame");
}

#
# >>_Description::
# {{Y:_MifUpdateMainFlow}} adds a reference to the main text flow.
# If {{nested}} is set, the object is nested inside a TectRect
# which is allocated that percentage of the text column height and
# rotated by {{angle}}.
#
sub _MifUpdateMainFlow {
    local(*outbuffer, $what, $ref_id, $title, $wide, $nested, $angle) = @_;
#   local();
    local($tag);
    local(@para);

    # Get the paragraph tag: 'Title' or 'Body'?
    if ($title ne '') {
        $tag = $SDF_USER'parastyles_to{$_MIF_TITLE_TAG{$what}};

        # Escape special characters
        $title =~ s/([\>\\])/\\$1/g;
        $title =~ s/([\t])/\\t/g;
        $title =~ s/(['])/\\q/g;
        $title =~ s/([`])/\\Q/g;
    }
    else {
        $tag = $SDF_USER'parastyles_to{$_MIF_NOTITLE_TAG{$what}};
    }

    # Build the paragraph
    $_mif_parastyle_used{$tag}++;
    @para = (
        "<Para\n" .
        " <PgfTag `$tag'>\n" .
        " <Pgf\n");
    push(@para, "  <PgfPlacementStyle Straddle>\n") if $wide;
    push(@para, "  <PgfAlignment Center>\n") if $angle;
    push(@para,
        " >\n" .
        " <ParaLine\n" .
        "  <String `$title'>\n" .
        "  <$_MIF_REF{$what} $ref_id>\n" .
        " >\n" .
        "> # end of Para");

    # Handle nested objects
    if ($nested ne '') {
        @para = &_MifAddNestedText(*para, $nested, $angle);
    }

    # Add a reference directly into the output
    push(@outbuffer, @para);
}

#
# >>_Description::
# {{Y:_MifAddObject}} adds an object to the nominated internal list.
# {{lookup_id}} is the object reference id (numeric).
# If {{%attr}} is passed and {{how}} is stack, the object
# is assumed to be a figure and the Frame header is built
# from the attributes passed in.
#
sub _MifAddObject {
    local(*source, $type, $id_str, $lookup_id, $how, *where, *attr) = @_;
    local($new_id, $first, $last);
    local(@this_obj, $this_index, $i);
    local($shape_rect_params);
    local(@body);

    # Search for object using lookup_id
    record:
    for ($i = 0; $i < $#source; $i++) {
        if ($first && $source[$i] =~ /\> \# end of $type\s*$/) {
            $last = $i;
            last record;
        }
        if ($source[$i] =~ /\<$type\s*$/ &&
          $source[$i + 1] =~ /\<$id_str $lookup_id\>/) {
            $first = $i;
            $i += 2;
        }
    }
    return 0 unless $last;

    # Save object
    if ($how eq 'stack') {
        $this_index = scalar(@where) + $_MIF_OBJ_REF_START;
        if (%attr) {
            ($shape_rect_params, @body) =
                &_MifRemoveFrameAttrs(@source[$first + 2 .. $last - 1]);
            @this_obj = &_MifBuildFrame($this_index, $shape_rect_params, *attr,
                *body);
        }
        else {
            @this_obj = (" <$type", "  <$id_str $this_index>",
              @source[$first + 2 .. $last]);
        }
        push(@where, join("\n", @this_obj));
    }
    elsif ($how eq 'byname') {
        $this_index = $lookup_id;
        $this_obj = join("\n", @source[$first .. $last]);
        if ($where{$this_index}) {
            if ($where{$this_index} ne $this_obj) {
                $this_index = "";
            }
        }
        else {
            $where{$this_index} = $this_obj;
        }
    }

    # Return result
    return $this_index, $first, $last;
}

#
# >>_Description::
# {{_MifRemoveFrameAttrs}} removes attributes from a Frame object.
#
sub _MifRemoveFrameAttrs {
    local(@frame) = @_;
    local($shape_rect_params, @body);
    local($junk);
    local($index);

    # Copy across the lines except those between ShapeRect and Cropped
    @body = ();
    $junk = 0;
    for ($index = 0; $index <= $#frame; $index++) {
        $line = $frame[$index];
        last if $line =~ /^\s*<Cropped /;

        if ($line =~ /^\s*<ShapeRect\s*([^\>]+)\>/) {
            $shape_rect_params = $1;
            $junk = 1;
        }
        elsif (! $junk) {
            push(@body, $line);
        }
    }
    push(@body, @frame[$index + 1 .. $#frame]);

    # Return result
    return ($shape_rect_params, @body);
}

#
# >>_Description::
# {{Y:_MifAddNestedText}} adds paragraph text as a nested object.
# {{@text}} is the text to add. The new text to add into the main text
# flow is returned. {{nested}} is the percentage of the text column
# to allocate to the object and {{angle}} is the angle.
#
sub _MifAddNestedText {
    local(*text, $nested, $angle) = @_;
    local(@result);
    local($textflow_id);
    local($width, $height, $adj_height);
    local($ref_id);
    local(@frame);
    local($tag);

    # Create the new text flow
    $textflow_id = &_MifAddTextFlow(*text, '', 1);

    # Calculate the width and height of the rotated text column.
    # For simplicity, the width is always the column width.
    # The height is calculated as a percentage of the text column
    # height. We also need an "adjusted height" for the embedded
    # text rectangle, otherwise Frame crops the table border.
    $width = $SDF_USER'var{'DOC_TEXT_WIDTH'};
    if ($nested == 1) {
        $height = $SDF_USER'var{'DOC_TEXT_HEIGHT'};
    }
    elsif ($nested =~ /^(\d+)\%?$/) {
        $height = $SDF_USER'var{'DOC_TEXT_HEIGHT'} * $1 / 100;
    }
    else {
        $height = &SdfPoints($nested);
    }
    $adj_height = $height - 2;

    # Build the rotated text column
    $ref_id = scalar(@_mif_figure) + $_MIF_OBJ_REF_START;
    @frame = (
        " <Frame",
        "  <ID $ref_id>",
        "  <ShapeRect 0\" 0\" ${width}pt ${height}pt>",
        "  <FrameType Below>",
        "  <Float Yes>",
        "  <TextRect",
        "   <ID $textflow_id>");
    push(@frame,
        "   <Angle $angle>") if $angle != 0;
    push(@frame,
        "   <BRect 0\" 0\" ${width}pt ${adj_height}pt>",
        "  > # end of TextRect",
        " > # end of Frame");

    # Add it to the internal buffers
    push(@_mif_figure, join("\n", @frame));

    # Build the new text
    $tag = $SDF_USER'parastyles_to{'N'};
    $_mif_parastyle_used{$tag}++;
    @result = (
        "<Para\n" .
        " <PgfTag `$tag'>\n" .
        " <ParaLine\n" .
        "  <AFrame $ref_id>\n" .
        " >\n" .
        "> # end of Para");

    # return result
    return @result;
}

#
# >>Description::
# {{Y:MifIdLookupBySymbol}} searches in {{@source}}
# for {{symbol}} closely followed by a reference of type {{ref}}.
# If found, the ID of the object is returned. Otherwise, 0 is returned.
#
sub MifIdLookupBySymbol {
    local(*source, $ref, $symbol) = @_;
    local($id);
    local($safe, $i);

    # escape any metacharacters if symbol
    $safe = $symbol;
    $safe =~ s/(\W)/\\\1/g;

    # search
    for ($i = 0; $i < $#source; $i++) {
        if ($source[$i] =~ /\<String\s+\`.*$safe.*\'\>/ &&
          $source[$i + 1] =~ /\<$ref\s+(\d+)\>/) {
            return $1;
        }
    }

    # If reach here, no luck
    return 0;
}

#
# >>_Description::
# {{Y:_MifPathName}} converts a pathname into a MIF device-independent
# pathname.
# (Thanks to Prachin Ranavat for the initial code.)
#
sub _MifPathName {
    local($name) = @_;
    local($mif);
    local($k);
	my ($is_absolute, $os_name);
	my @components = NamePathComponentSplit($name);

    # check if path is absolute
	$is_absolute = &NameIsAbsolute($name);
	$os_name = &NameOS;
	
	SWITCH_OS: {
		$mif = '<r\>', last SWITCH_OS if $os_name eq 'unix' && $is_absolute;
		$mif = '<v\>' . shift @components, last SWITCH_OS if $os_name eq 'mac' && $is_absolute;
		$mif = '<v\>' . shift @components, last SWITCH_OS if $os_name eq 'dos' && $is_absolute;
	}
    foreach $k (@components) {
		next if !$k;
	    if ($k eq '..' || $k eq '::') {
    	    # parent directory in path - replace by <u\>
	        $mif .= "<u\\>";
        }
        else {
	        # directory name in path - replace by <c\>directory_name
	        $mif .= "<c\\>".$k;
    	}
    }

    # Return result
    return ($mif);
}

#
# >>Description::
# {{Y:MifLink}} converts a hypertext link in URL format to a frame one.
# {{mif_ext}} is the extension to use on Frame files in hypertext jumps.
# The default value is {{fvo}}.
#
sub _MifLink {
    local($url, $mif_ext) = @_;
    local($mif_link);
    local($file, $topic);
    local($spec_char);

    # Setup special characters match string
    $spec_char = "\\>\\\\";

    # Default the file extension to fvo, if necessary
    $mif_ext = 'fvo' unless $mif_ext ne '';

    if ($url =~ /^([-\w\/\.]*)#/) {
        ($file, $topic) = ($1, $');
        if ($file ne '') {
            $file =~ s/\.html$//;
            $file .= ".$mif_ext";
        }
        $topic =~ s/([$spec_char])/\\$1/g;
        $mif_link = "gotolink " . ($file ? "$file:$topic" : $topic);
    }
    elsif ($url =~ /^([-\w\/\.]*)$/) {
        $url =~ s/\.html$//;
        $url .= ".$mif_ext";
        $mif_link = "gotopage $url:firstpage";
    }
    else {
        $mif_link = "gotourl $url";
        #$mif_link = "sdf url=$url";    # once TJH fixes fm2html
    }

    # Return result
    return $mif_link;
}

#
# >>_Description::
# {{Y:MifEscapeNewlink}} escapes a newlink.
# (Thanks to Tim Hudson for the code.)
#
sub _MifEscapeNewlink {
    local($link) = @_;
    local($result);
    local($spec_char);

    ##print STDERR "NEWLINK-IN: $link\n";

    # Assign default special characters match string, if necessary
    $spec_char = "\\>\\\\" if $spec_char eq '';

    $link =~ s/([$spec_char])/\\$1/g;

    # escape quotes
    $link =~ s/\'/\\q/g;

    # strip backquotes 
    $link =~ s/\`//g;

    1 while $link =~ s/{{.:([^}]*)}}/$1/e;

    $link =~ s/^\s+//;

    ##print STDERR "NEWLINK-OUT: $link\n";

    return $link;
}

#
# >>Description::
# {{Y:MifNewComponent}} is an event processing routine for
# paragraphs which begin a new component.
#
sub MifNewComponent {
    local($type) = @_;
    local($cname);

    # Save away the component details (so a book can be constructed later)
    $cname = &_MifComponentName($SDF_USER'var{'DOC_BASE'});
    $type = 'chapter' if $type eq '1';
    push(@_mif_component_tbl, "$cname|$type");
    push(@_mif_component_type, "\U$type");

    # Ensure that each component gets placed into its own output file.
    # (stdlib/mif.sdt takes care of the first part, so ignore it)
    if (scalar(@_mif_component_tbl) > 2) {
        &SDF_USER'PrependText(
            "!DOC_COMPONENT_END",
            "!output '-'",
            "!output '$cname'",
            "!define DOC_COMPONENT '$type'",
            "!DOC_COMPONENT_BEGIN");
    }
    else {
        $SDF_USER'var{'DOC_COMPONENT'} = $type;
    }

    # Return the component name (needed for stdlib/mif.sdt)
    return $cname;
}

#
# >>_Description::
# {{Y:_MifComponentName}} generates a name for a component,
# given the basename of the document.
#
sub _MifComponentName {
    local($base) = @_;
    local($cname);

    $_mif_component_cntr++;
    $cname = $SDF_USER'var{'MIF_COMPONENT_PATTERN'};
    $cname = '$b_$n.$o' if $cname eq '';
    $cname =~ s/\$b/$base/g;
    $cname =~ s/\$n/$_mif_component_cntr/;
    $cname =~ s/\$o/$out_ext/;
    return $cname;
}

#
# >>_Description::
# {{Y:_MifBookBuild}} builds a book from information collected
# during the generation of components.
#
sub _MifBookBuild {
    local(*book, $base) = @_;
#   local(@result);
    local(@newbook);
    local(@batch);
    local($added_toc);
    local(@flds, %values, $i);
    local($mif_file, $type);

    # Build a new book table which includes the derived components.
    # As we go, we also:
    # * generate the mif for each derived component
    # * build a set of fmbatch commands which
    #   generate binary documents for each part
    # * collect the set of mif files so we can delete them later.
    @newbook = ($book[0]);
    @batch = ();
    @sdf_book_files = ();
    $added_toc = 0;
    @flds = &TableFields($book[0]);
    for ($i = 1; $i <= $#book; $i++) {
        %values = &TableRecSplit(*flds, $book[$i]);
        $type = $values{'Type'};
        if ($added_toc == 0 && $type ne 'front' && $type ne 'pretoc') {
            if ($SDF_USER'var{'DOC_TOC'}) {
                $mif_file = &_MifBookDerived($base, 'toc', 'Table of Contents');
                &_MifBookAddPart(*newbook, *batch, $mif_file, 'toc');
                push(@sdf_book_files, $mif_file);
            }
            if ($SDF_USER'var{'DOC_LOF'}) {
                $mif_file = &_MifBookDerived($base, 'lof', 'List of Figures');
                &_MifBookAddPart(*newbook, *batch, $mif_file, 'lof');
                push(@sdf_book_files, $mif_file);
            }
            if ($SDF_USER'var{'DOC_LOT'}) {
                $mif_file = &_MifBookDerived($base, 'lot', 'List of Tables');
                &_MifBookAddPart(*newbook, *batch, $mif_file, 'lot');
                push(@sdf_book_files, $mif_file);
            }
            $added_toc = 1;
        }

        # Add this part
        $mif_file = $values{'Part'};
        &_MifBookAddPart(*newbook, *batch, $mif_file, $type);
        push(@sdf_book_files, $mif_file);
    }
    if ($SDF_USER'var{'DOC_IX'}) {
        $mif_file = &_MifBookDerived($base, 'ix', 'Index');
        &_MifBookAddPart(*newbook, *batch, $mif_file, 'ix');
        push(@sdf_book_files, $mif_file);
    }

    # Pass the batch commands to fmbatch
    &_MifRunBatch(*batch, $verbose);

    # Cleanup the MIF for each part
    &SDF_USER'SdfBookClean();

    # Return result
    return &MifBook(*newbook, $SDF_USER'var{'OPT_NUMBER_PER_COMPONENT'});
}

#
# >>_Description::
# {{Y:_MifBookDerived}} creates a derived component for a book.
# {{mainbase}} is the base component (e.g. ug_doc) of the main file.
# {{type}} is the type of derived component (e.g. toc).
# The {{default_title}} is used if the appropriate SDF variable is not set.
# The name of the new file is returned.
#
sub _MifBookDerived {
    local($mainbase, $type, $default_title) = @_;
    local($newfile);
    local($upper_type, $tag, $title);
    local(@sdf_text, @mif_data);

    # Build the sdf
    $upper_type = "\U$type";
    $tag = $upper_type . 'T';
    $title = $SDF_USER'var{"DOC_${upper_type}_TITLE"};
    $title = $default_title if $title eq '';
    @sdf_text = ("$tag:$title");

    # Convert the sdf to mif
    @mif_data = ();
    &_MifAddSection(*mif_data, *sdf_text);
    @mif_data = &_MifFinalise(*mif_data, $upper_type);

    # Output the mif
    $newfile = &NameJoin('', $mainbase . "_$type", $out_ext);
    if (open(DERIVED, ">$newfile")) {
        print DERIVED join("\n", @mif_data), "\n";
    }
    else {
        &AppMsg("warning", "unable to create file '$newfile'");
    }
    close(DERIVED);

    # Return result
    return $newfile;
}

#
# >>_Description::
# {{_MifBookAddPart}} is the common processing required in {{_MifBookBuild}}
# to add a part to a book for each mif file.
#
sub _MifBookAddPart {
    local(*newbook, *batch, $mif_file, $type) = @_;
#   local();
    local($doc_file);

    $doc_file = &NameSubExt($mif_file, 'doc');
    push(@newbook, &TableRecJoin(*flds, 'Part', $doc_file, 'Type', $type));
    push(@batch, "Open \"$mif_file\"");
    push(@batch, "SaveAs d \"$mif_file\" \"$doc_file\"");
    push(@batch, "Quit \"$mif_file\"");
#print STDERR "adding $doc_file,$type.\n";
}

#
# >>_Description::
# {{_MifRunBatch}} executes a set of fmbatch commands ({{@batch}}).
# If {{verbose}} is set, the results are keep in a temporary file.
#
sub _MifRunBatch {
    local(*batch, $verbose) = @_;
#   local();
    local($fmbatch, $tmp_file);

    $fmbatch = "fmbatch -i";
    $tmp_file = "/tmp/sdf$$";
    if (open(FMBATCH, "|$fmbatch > $tmp_file")) {
        printf FMBATCH "%s\n", join("\n", @batch);
    }
    else {
        &AppMsg("error", "failed to pipe data to fmbatch");
    }
    close(FMBATCH);
    unlink($tmp_file) unless $verbose;
}

#
# >>Description::
# {{Y:MifBook}} returns a mif book built from the components given in
# {{@book}}. If {{number_per_component}} is set:
#
# * parts before the first chapter have roman page numbering
# * remaining parts are numbered per section (i.e. 1-1, 1-2, etc.)
#
sub MifBook {
    local(*book, $number_per_component) = @_;
    local(@mif);
    local(@flds, %values, $i);
    local($partfile, $type);
    local($suffix, @tags, %settings);
    local($j);
    local($sectnum, $chapter_cnt, $appendix_cnt);

    # Build the title
    $title = sprintf("<Book 5.0> # Generated by %s %s",
            $app_product_name, $app_product_version);
    @mif = ($title);

    # Add the paragraph catalog so that the PDF table of contents works
    #push(@mif, &_MifParasToText(*_mif_tpl_paras));

    # Process the list of parts
    @flds = &TableFields($book[0]);
    $chapter_cnt = 0;
    $appendix_cnt = 0;
    for ($i = 1; $i <= $#book; $i++) {

        # Get the attributes
        %values = &TableRecSplit(*flds, $book[$i]);
        $partfile = $values{'Part'};
        $type = $values{'Type'};

        # For derived components, get the suffix, list of tags and settings.
        $suffix = '';
        @tags = ();
        %settings = ();
        if ($type eq 'toc') {
            $suffix = 'TOC';
            @tags = ();
            for ($j = 1; $j <= $SDF_USER'var{'DOC_TOC'}; $j++) {
                push(@tags, $SDF_USER'parastyles_to{"H$j"},
                  $SDF_USER'parastyles_to{"A$j"}, $SDF_USER'parastyles_to{"P$j"});
            }
            push(@tags, $SDF_USER'parastyles_to{"LOFT"});
            push(@tags, $SDF_USER'parastyles_to{"LOTT"});
            push(@tags, $SDF_USER'parastyles_to{"IXT"});
        }
        elsif ($type eq 'lof') {
            $suffix = 'LOF';
            @tags = ($SDF_USER'parastyles_to{"FT"});
            $settings{'StartPageSide'} = 'NextAvailableSide';
        }
        elsif ($type eq 'lot') {
            $suffix = 'LOT';
            @tags = ($SDF_USER'parastyles_to{"TT"});
            $settings{'StartPageSide'} = 'NextAvailableSide';
        }
        elsif ($type eq 'ix') {
            $suffix = 'IX';
            @tags = ('Index');
            %settings = ();
        }

        # If requested, number per component once a chapter is found
        if ($number_per_component) {
            if ($type eq 'chapter') {
                $sectnum = ++$chapter_cnt;
            }
            elsif ($type eq 'appendix') {
                $sectnum = sprintf("%c", ord('A') + $appendix_cnt++);
            }
            elsif ($type eq 'ix') {
                $sectnum = "Index";
            }
            elsif (! $_MIF_FRONT_PART{$type}) {
                $sectnum = "\u$type";
            }
            if ($sectnum ne '') {
                # Note: "\\x15 " is FrameMaker's nonbreaking hyphen
                $settings{'PageNumbering'} = 'Restart';
                $settings{'PageNumPrefix'} = "$sectnum\\x15 ";
            }
        }

        # Build result
        push(@mif, &MifBookComponent($partfile, $suffix, *tags, %settings));
    }

    # Add a closing comment (to match output as generated by Frame)
    push(@mif, "# End of Book");

    # Return result
    return @mif;
}

#
# >>Description::
# {{Y:MifBookComponent}} returns a book component object
# for {{file}}. If {{suffix}} is supplied, a derived book
# component is returned which includes derived tags for each
# member in {{@derived}}. The book attributes can be
# changed from the defaults using {{attrs}}. No checking
# is done on {{attrs}} so take care.
#
# >>Limitations::
# {{Y:MifBookComponent}} currently assumes the referenced file will
# be in the same directory as the output. i.e. only the file's
# local name (including extension) is stored.
#
sub MifBookComponent {
    local($file, $suffix, *derived, %attrs) = @_;
    local(@result);
    local(%book_attrs);
    local($dir, $base, $ext, $short_name, $tag);
    local($derive_type);

    # Generate the book attributes to be used
    %book_attrs = %_MIF_DFLT_BOOK_ATTRS;
    @book_attrs{keys %attrs} = values %attrs;

    # Build the header
    ($dir, $base, $ext) = &NameSplit($file);
    $short_name = &NameJoin('', $base, $ext);
    @result = ("<BookComponent ", sprintf(" <FileName `<c\\>%s'>",
      $short_name));

    # Add the derived stuff, if any
    $derive_type = $suffix eq 'IX' ? 'IDX' : $suffix;
    if ($suffix) {
        push(@result, " <FileNameSuffix `$suffix\'>",
          " <DeriveLinks Yes >",
          " <DeriveType $derive_type >");
        for $tag (@derived) {
            push(@result, " <DeriveTag `$tag\'>");
        }
    }

    # Add the attributes
    push(@result,
      sprintf(" <StartPageSide %s >", $book_attrs{'StartPageSide'}),
      sprintf(" <PageNumbering %s >", $book_attrs{'PageNumbering'}),
      sprintf(" <PgfNumbering %s >", $book_attrs{'PgfNumbering'}),
      sprintf(" <PageNumPrefix `%s'>", $book_attrs{'PageNumPrefix'}),
      sprintf(" <PageNumSuffix `%s'>", $book_attrs{'PageNumSuffix'}),
      sprintf(" <DefaultPrint %s >", $book_attrs{'DefaultPrint'}),
      sprintf(" <DefaultApply %s >", $book_attrs{'DefaultApply'}));

    # Add more derived stuff, if applicable.
    # Note that we add this here rather than above
    # to keep the same ordering as Frame uses.
    if ($suffix) {
        push(@result, " <DefaultDerive Yes >");
    }

    # End the object
    push(@result, "> # end of BookComponent");
    
    # Return result
    return @result;
}

# package return value
1;