#!/usr/bin/perl
# This file is part of "osis-converters".
# 
# Copyright 2019 John Austin (gpl.programs.info@gmail.com)
#     
# "osis-converters" is free software: you can redistribute it and/or 
# modify it under the terms of the GNU General Public License as 
# published by the Free Software Foundation, either version 2 of 
# the License, or (at your option) any later version.
# 
# "osis-converters" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with "osis-converters".  If not, see 
# <http://www.gnu.org/licenses/>.

# This script might be loaded on any operating system. So code here
# should be as operating system agnostic as possible and should not 
# rely on non-standard Perl modules. The functions in this file are
# required for bootstrapping osis-converters.

use strict;

our ($CONF, $CONFFILE, $CONFSRC, $DEBUG, $DICTINPD, $DICTMOD, 
    $GO_BIBLE_CREATOR, $INPD, $LOGFILE, $LOGFLAG, $MAININPD, $MAINMOD, 
    $MOD, $MODULETOOLS_BIN, $SCRD, $SCRIPT, $SCRIPT_NAME, $SWORD_BIN, 
    $VAGRANT, @CONV_PUBS, %CONV_BIN_DEPENDENCIES, %SYSTEM_DEFAULT_PATHS,
    %CONV_BIN_TEST, $MOD_OUTDIR, @CONV_PUB_SETS, @CONV_OSIS, 
    %CONV_NOCANDO, %ARGS);
    
require("$SCRD/lib/common/block.pm");

our $WRITELAYER  =  ">:encoding(UTF-8)";
our $APPENDLAYER = ">>:encoding(UTF-8)";
our $READLAYER   =  "<:encoding(UTF-8)";
# crlf read should work with both Windows and Linux, but only use it with Vagrant anyway
if (&runningInVagrant()) {$READLAYER .= ":crlf";}

# Config.conf sections
our @CONFIG_SECTIONS = ('MAINMOD', 'DICTMOD', 'system', @CONV_PUBS);

our @CONFIG_DEPRECATED = ('LangSortOrder', 'SPECIAL_CAPITALS', 
  'PUNC_AS_LETTER', 'SET_MODE_CCTable');

# Config entries that are defined by CrossWire SWORD standard
our @SWORD_CONFIGS = (
  'MATCHES:History_[\d\.]+', 'Abbreviation', 'Description', 'DataPath', 
  'ModDrv', 'SourceType', 'Encoding', 'CompressType', 'BlockType', 
  'BlockCount', 'Versification', 'CipherKey', 'KeyType', 'CaseSensitiveKeys', 
  'GlobalOptionFilter', 'Direction', 'DisplayLevel', 'Font', 'Feature', 
  'GlossaryFrom', 'GlossaryTo', 'PreferredCSSXHTML', 'About', 'SwordVersionDate', 
  'Version', 'MinimumVersion', 'Category', 'LCSH', 'Lang', 'InstallSize', 
  'Obsoletes', 'OSISVersion', 'Companion', 'Copyright', 'CopyrightHolder', 
  'CopyrightDate', 'CopyrightNotes', 'CopyrightContactName', 
  'CopyrightContactNotes', 'CopyrightContactAddress', 'CopyrightContactEmail', 
  'ShortPromo', 'ShortCopyright', 'DistributionLicense', 'DistributionNotes', 
  'TextSource',
);

our @SWORD_NOT_SUPPORTED = ('CipherKey', 'KeyType', 'GlossaryFrom', 
  'GlossaryTo', 'DisplayLevel', 'LCSH');

# Custom SWORD module config entries that are used by osis-converters SWORD modules
our @SWORD_OC_CONFIGS = ('Scope', 'KeySort', 'AudioCode', 'LangSortOrder');

# These are entries which should NOT appear in config.conf, because 
# they are autogenerated only for SWORD config.conf files
our @SWORD_AUTOGEN_CONFIGS = (
  'DataPath', 'Category', 'Companion', 'CompressType', 'BlockType', 
  'BlockCount', 'SourceType', 'Feature', 'OSISVersion', 'ModDrv',
  'PreferredCSSXHTML', 'GlobalOptionFilter', 'Scope', 'Encoding',
  'SwordVersionDate', 'InstallSize', 'MinimumVersion', 
  'CaseSensitiveKeys', 'LangSortOrder',
);

# Valid osis-converters config file entries (in addition to SWORD entries)
our @OC_CONFIGS = (
  'MATCHES:SubPublicationTitle\[\S+\]', 'MATCHES:GlossaryNavmenuLink\[[1-9]\]',
  'MATCHES:MakeSet\[(' . join('|', @CONV_PUB_SETS) . ')\]', 
  'MATCHES:ARG_\w+', 'TOC', 'TitleCase', 'TitleTOC', 'MakeTypes', 
  'FullResourceURL', 'TranslationTitle', 'CombineGlossaries', 
  'CombinedGlossaryTitle', 'MATCHES:BookGroupTitle\[\w+\]', 
  'NormalizeUnicode', 'AddScripRefLinks', 'AddDictLinks', 
  'AddFootnoteLinks' , 'AddCrossRefLinks', 'ReorderGlossaryEntries', 
  'CustomBookOrder', 'IntroductionTitle', 'ProjectType',
);

# Valid [system] section config entries (these end up as Perl global variables)
our @OC_SYSTEM_CONFIGS = (
  'REPOSITORY', 'MODULETOOLS_BIN', 'GO_BIBLE_CREATOR', 'SWORD_BIN', 
  'OUTDIR', 'FONTS', 'COVERS', 'EBOOKS', 'DEBUG', 'NO_OUTPUT_DELETE', 
  'VAGRANT', 'NO_FORKS', 'RAM_GB_EBOOKS', 'RAM_MB_EBOOKS_PERBOOK'
);
our $RAM_GB_EBOOKS_DEF = 1;
our $RAM_MB_EBOOKS_PERBOOK_DEF = 32;

# These are the [system] path variables
our @OC_SYSTEM_PATH_CONFIGS = ('MODULETOOLS_BIN', 'GO_BIBLE_CREATOR', 
  'SWORD_BIN', 'OUTDIR', 'FONTS', 'COVERS', 'REPOSITORY');
  
# These are the [system] path variables
our @OC_URL_CONFIGS = ('EBOOKS', 'FONTS', 'COVERS', 'REPOSITORY', 
  'FullResourceURL');
  
our @OC_DEVEL_CONFIGS = ('GO_BIBLE_CREATOR', 'MODULETOOLS_BIN', 
  'SWORD_BIN', 'NO_OUTPUT_DELETE');

# CrossWire SWORD entries which may be localized by appending _code
our @SWORD_LOCALIZABLE_CONFIGS = (
  'MATCHES:History_[\d\.]+', 'Abbreviation', 'Description', 'About', 
  'Copyright', 'CopyrightHolder', 'CopyrightDate', 'CopyrightNotes', 
  'CopyrightContactName', 'CopyrightContactNotes', 'CopyrightContactAddress', 
  'CopyrightContactEmail', 'ShortPromo', 'ShortCopyright', 'DistributionNotes'
);

# Osis-converters entries which contain localized text
our @OC_LOCALIZABLE_CONFIGS = (
  'MATCHES:SubPublicationTitle\[(?<scope>\S+)\]$', 'MATCHES:ARG_\w+Title\d', 
  'TranslationTitle', 'MATCHES:BookGroupTitle\[\w+\]', 'CombinedGlossaryTitle', 
  'Abbreviation', 'Description', 'About', 'IntroductionTitle',
);

# CrossWire SWORD configs which may be continued line to line by '\'
our @CONTINUABLE_CONFIGS = (
  'About', 'Copyright', 'CopyrightNotes', 'CopyrightContactName', 
  'CopyrightContactNotes', 'CopyrightContactAddress', 'DistributionNotes', 
  'TextSource'
);

# These are config entries which may appear multiple times within a 
# config.conf context and thus its value is rather an array of values. 
# All other config entries throughout osis-converters may only have one 
# value per context of the config file, or an error will be given.
our @MULTIVALUE_CONFIGS = ('GlobalOptionFilter', 'Feature', 'Obsoletes'); 

# Default values for config entries which have a default value. Values
# ending in 'DEF' will trigger an error if used, to let the developer
# know a value is required.
our %CONFIG_DEFAULTS = (
  'ProjectType' => 'bible',
  'AddScripRefLinks' => 'AUTO',
  'AddDictLinks' => 'AUTO',
  'AddFootnoteLinks' => 'AUTO',
  'AddCrossRefLinks' => 'AUTO',
  'Versification' => 'KJV',
  'TOC' => '2',
  'TitleCase' => '0',
  'TitleTOC' => '2',
  'MakeSet[tran]' => 'AUTO',
  'MakeSet[subpub]' => 'AUTO',
  'MakeSet[book]' => 'AUTO',
  'MakeTypes' => 'AUTO',
  'CombineGlossaries' => 'AUTO',
  'FullResourceURL' => 'false',
  'CustomBookOrder' => 'false',
  'ReorderGlossaryEntries' => 'false',
  'CombinedGlossaryTitle' => 'Glossary DEF',
  'BookGroupTitle[OT]' => 'Old Testament DEF',
  'BookGroupTitle[NT]' => 'New Testament DEF',
  'TranslationTitle' => 'English Bible DEF',
  'IntroductionTitle' => 'Introduction DEF',
  'NormalizeUnicode' => 'false',
  'Direction' => 'LtoR',
);

our $UPPERCASE_DICTIONARY_KEYS = 1;

our %SWORD_CONFIG_VALUES = (
  'all' => {
    'SourceType' => 'OSIS',
    'OSISVersion' => '$osisversion',
    'Encoding' => 'UTF-8', 
    'GlobalOptionFilter' => join('<nx/>', 'OSISFootnotes', 'OSISHeadings', 'OSISScripref'),
    'SwordVersionDate' => '$today',
    'PreferredCSSXHTML' => '$modulecss',
    'MinimumVersion' => '1.7.0',
  },
  'bible' => {
    'Companion' => '$DICTMOD',
    # At this time (Jan 2017) JSword does not yet support zText4
    'ModDrv' => 'zText', 
    'DataPath' => './modules/texts/ztext/$mod/', 
    'Category' => 'Biblical Texts', 
    'CompressType' => 'ZIP', 
    'BlockType' => 'BOOK',
    'Scope' => '$scope',
    'GlobalOptionFilter' => 'OSISReferenceLinks|Reference Material Links|Hide or show links to study helps in the Biblical text.|x-glossary||On',
  },
  'childrens_bible' => {
    'Companion' => '$chblCompanion',
    'ModDrv', => 'RawGenBook',
    'DataPath' => './modules/genbook/rawgenbook/$mod/$mod',
  },
  'commentary' => {
    'ModDrv', => 'RawCom4',
    'DataPath' => './modules/comments/rawcom4/$mod/',
    'Scope' => '$scope',
  },
  'dictionary' => {
    'Companion' => '$MAINMOD',
    'ModDrv', => 'RawLD4',
    'DataPath' => './modules/lexdict/rawld4/$mod/$mod',
    'CaseSensitiveKeys' => ($UPPERCASE_DICTIONARY_KEYS ? 'true' : undef),
    'LangSortOrder' => '$keysort',
  },
);

# Command files and settings
our @CF_FILES = ('config.conf', 'CF_sfm2osis.txt', 'CF_<vsys>.xml', 
    'CF_addScripRefLinks.txt', 'CF_addDictLinks.xml', 
    'CF_addFootnoteLinks.txt');
    
our @CF_OSIS2OSIS = ('SourceProject', 'CC', 'CCOSIS', 
    'Mode[copy]', 'Mode[script]', 'Mode[transcode]', 
    'Mode[cctable]', 'MATCHES:Config\[.+\]', 'SkipNodesMatching', 
    'SkipStringsMatching');

our @CF_SFM2OSIS = ('EVAL_REGEX', 'RUN', 'SPECIAL_CAPITALS', 
    'PUNC_AS_LETTER');
    
our @TOC_INSTRUCTIONS = ('[levelN]', '[no_toc]', '[parent]', '[not_parent]', 
    '[no_inline_toc]', '[only_inline_toc]', '[no_main_inline_toc]', 
    '[inline_toc_first]', '[inline_toc_last]', '[bookSubGroup]');
    
our %ID_DIRECTIVES = (
    'placement' => ['location', '<identifier>', 'x-unknown'], 
    'mark'      => ['scope', 'feature', 'cover', 'conversion', 
                    'not_conversion']);
    
our @VSYS_INSTRUCTIONS = ('VSYS_MOVED', 'VSYS_MISSING', 'VSYS_EXTRA',  
    'VSYS_EMPTY', 'VSYS_MOVED_ALT', 'VSYS_MISSING_FN', 
    'VSYS_CHAPTER_SPLIT_AT', 'VSYS_FROM_TO');
    
our @CF_ADDSCRIPREFLINKS = ('CONTEXT_BOOK', 'WORK_PREFIX', 'SKIP_XPATH', 
    'ONLY_XPATH', 'CHAPTER_TERMS', 'CURRENT_CHAPTER_TERMS', 
    'CURRENT_BOOK_TERMS', 'VERSE_TERMS', 'COMMON_REF_TERMS', 'PREFIXES', 
    'REF_END_TERMS', 'SUFFIXES', 'SEPARATOR_TERMS', '<osis-abbreviation>',
    'CHAPTER_TO_VERSE_TERMS', 'CONTINUATION_TERMS', 'FIX');
    
our @CF_ADDFOOTNOTELINKS = ('ORDINAL_TERMS', 'FIX', 'SKIP_XPATH', 
    'ONLY_XPATH', 'FOOTNOTE_TERMS', 'COMMON_TERMS', 
    'CURRENT_VERSE_TERMS', 'SUFFIXES', 'STOP_REFERENCE');
    
our %CF_ADDDICTLINKS = (
  'elements' => [
    'addDictLinks',
    'div',
    'entry',
    'name',
    'match',
  ],
  'attributes' => {
    'multiple'         => 'boolean|match', 
    'onlyExplicit'     => 'boolean|context', 
    'notExplicit'      => 'boolean|context', 
    'context'          => 'cumulative|context', 
    'notContext'       => 'cumulative|context', 
    'XPATH'            => 'cumulative|xpath', 
    'notXPATH'         => 'cumulative|xpath', 
    'dontLink'         => 'boolean', 
    'onlyOldTestament' => 'boolean', 
    'onlyNewTestament' => 'boolean',
    'osisRef'          => 'osisRef+',
    'noOutboundLinks'  => 'boolean',
  },
);

our @VERSE_SYSTEMS = ('KJV', 'German', 'KJVA', 'Synodal', 'Leningrad', 'NRSVA', 'Luther', 'Vulg', 'SynodalProt', 'Orthodox', 'LXX', 'NRSV', 'MT', 'Catholic', 'Catholic2');

# OSIS book groups. Taken from wiki.crosswire.org/OSIS_Book_Abbreviations on 11/19/20.
our @OSIS_GROUPS = ('OT','NT','Apocrypha','Apostolic_Fathers','Armenian_Orthodox_Canon_Additions','Ethiopian_Orthodox_Canon','Peshitta_Syriac_Orthodox_Canon','Rahlfs_LXX','Rahlfs_variant_books','Vulgate_and_other_later_Latin_mss','Other');
our %OSIS_GROUP = (
   'OT' => ['Gen','Exod','Lev','Num','Deut','Josh','Judg','Ruth','1Sam','2Sam','1Kgs','2Kgs','1Chr','2Chr','Ezra','Neh','Esth','Job','Ps','Prov','Eccl','Song','Isa','Jer','Lam','Ezek','Dan','Hos','Joel','Amos','Obad','Jonah','Mic','Nah','Hab','Zeph','Hag','Zech','Mal'],
   'NT' => ['Matt','Mark','Luke','John','Acts','Rom','1Cor','2Cor','Gal','Eph','Phil','Col','1Thess','2Thess','1Tim','2Tim','Titus','Phlm','Heb','Jas','1Pet','2Pet','1John','2John','3John','Jude','Rev'],
   'Apocrypha' => ['Tob','Jdt','EsthGr','AddEsth','Wis','SirP','Sir','Bar','EpJer','DanGr','AddDan','PrAzar','Sus','Bel','1Macc','2Macc','3Macc','4Macc','PrMan','1Esd','2Esd','AddPs'],
   'Apostolic_Fathers' => ['1Clem','2Clem','IgnEph','IgnMagn','IgnTrall','IgnRom','IgnPhld','IgnSmyrn','IgnPol','PolPhil','MartPol','Did','Barn','Herm','Herm.Mand','Herm.Sim','Herm.Vis','Diogn','AposCreed','PapFrag','RelElders','QuadFrag'],
   'Armenian_Orthodox_Canon_Additions' => ['EpCorPaul','3Cor','WSir','PrEuth','DormJohn','JosAsen','T12Patr','T12Patr.TAsh','T12Patr.TBenj','T12Patr.TDan','T12Patr.TGad','T12Patr.TIss','T12Patr.TJos','T12Patr.TJud','T12Patr.TLevi','T12Patr.TNaph','T12Patr.TReu','T12Patr.TSim','T12Patr.TZeb'],
   'Ethiopian_Orthodox_Canon' => ['1En','Jub','4Bar','1Meq','2Meq','3Meq','Rep','AddJer','PsJos'],
   'Peshitta_Syriac_Orthodox_Canon' => ['2Bar','EpBar','5ApocSyrPss','JosephusJWvi'],
   'Rahlfs_LXX' => ['Odes','PssSol'],
   'Rahlfs_variant_books' => ['JoshA','JudgB','TobS','SusTh','DanTh','BelTh'],
   'Vulgate_and_other_later_Latin_mss' => ['EpLao','5Ezra','4Ezra','6Ezra','PrSol','PrJer'],
   'Other' => ['TatDiat','PsMet']
);

# OSIS book abbreviations => Paratext abbreviations. Taken from 
# wiki.crosswire.org/OSIS_Book_Abbreviations on 11/19/20.
our %OSIS_ABBR = ('1Chr' => '1CH', '1Clem' => '1CL', '1Cor' => '1CO', '1En' => 'ENO', '1Esd' => '1ES', '1John' => '1JN', '1Kgs' => '1KI', '1Macc' => '1MA', '1Meq' => '1MQ', '1Pet' => '1PE', '1Sam' => '1SA', '1Thess' => '1TH', '1Tim' => '1TI', '2Bar' => '2BA', '2Chr' => '2CH', '2Clem' => '2CL', '2Cor' => '2CO', '2Esd' => '2ES', '2John' => '2JN', '2Kgs' => '2KI', '2Macc' => '2MA', '2Meq' => '2MQ', '2Pet' => '2PE', '2Sam' => '2SA', '2Thess' => '2TH', '2Tim' => '2TI', '3Cor' => '3CO', '3John' => '3JN', '3Macc' => '3MA', '3Meq' => '3MQ', '4Bar' => '4BA', '4Ezra' => 'EZA', '4Macc' => '4MA', '5ApocSyrPss' => 'PS3', '5Ezra' => '5EZ', '6Ezra' => '6EZ', 'Acts' => 'ACT', 'AddDan' => '', 'AddEsth' => 'ADE', 'AddJer' => '', 'AddPs' => 'PS2', 'Amos' => 'AMO', 'AposCreed' => '', 'Bar' => 'BAR', 'Barn' => 'LBA', 'Bel' => 'BEL', 'BelTh' => 'BLT', 'Col' => 'COL', 'Dan' => 'DAN', 'DanGr' => 'DAG', 'DanTh' => 'DNT', 'Deut' => 'DEU', 'Did' => 'DID', 'Diogn' => '', 'DormJohn' => 'DOJ', 'Eccl' => 'ECC', 'EpBar' => 'LBA', 'EpCorPaul' => 'COP', 'EpJer' => 'LJE', 'EpLao' => 'LAO', 'Eph' => 'EPH', 'Esth' => 'EST', 'EsthGr' => 'ESG', 'Exod' => 'EXO', 'Ezek' => 'EZK', 'Ezra' => 'EZR', 'Gal' => 'GAL', 'Gen' => 'GEN', 'Hab' => 'HAB', 'Hag' => 'HAG', 'Heb' => 'HEB', 'Herm' => 'SHE', 'Herm.Mand' => '', 'Herm.Sim' => '', 'Herm.Vis' => '', 'Hos' => 'HOS', 'IgnEph' => '', 'IgnMagn' => '', 'IgnPhld' => '', 'IgnPol' => '', 'IgnRom' => '', 'IgnSmyrn' => '', 'IgnTrall' => '', 'Isa' => 'ISA', 'Jas' => 'JAS', 'Jdt' => 'JDT', 'Jer' => 'JER', 'Job' => 'JOB', 'Joel' => 'JOL', 'John' => 'JHN', 'Jonah' => 'JON', 'JosAsen' => '', 'JosephusJWvi' => '', 'Josh' => 'JOS', 'JoshA' => 'JSA', 'Jub' => 'JUB', 'Jude' => 'JUD', 'Judg' => 'JDG', 'JudgB' => 'JDB', 'Lam' => 'LAM', 'Lev' => 'LEV', 'Luke' => 'LUK', 'Mal' => 'MAL', 'Mark' => 'MRK', 'MartPol' => '', 'Matt' => 'MAT', 'Mic' => 'MIC', 'Nah' => 'NAM', 'Neh' => 'NEH', 'Num' => 'NUM', 'Obad' => 'OBA', 'Odes' => 'ODA', 'PapFrag' => '', 'Phil' => 'PHP', 'Phlm' => 'PHM', 'PolPhil' => '', 'PrAzar' => 'S3Y', 'PrEuth' => 'EUT', 'PrJer' => 'PJE', 'PrMan' => 'MAN', 'PrSol' => 'PSO', 'Prov' => 'PRO', 'Ps' => 'PSA', 'PsJos' => '', 'PsMet' => 'PSB', 'PssSol' => 'PSS', 'QuadFrag' => '', 'RelElders' => '', 'Rep' => 'REP', 'Rev' => 'REV', 'Rom' => 'ROM', 'Ruth' => 'RUT', 'Sir' => 'SIR', 'SirP' => '', 'Song' => 'SNG', 'Sus' => 'SUS', 'SusTh' => 'SST', 'T12Patr' => '', 'T12Patr.TAsh' => '', 'T12Patr.TBenj' => '', 'T12Patr.TDan' => '', 'T12Patr.TGad' => '', 'T12Patr.TIss' => '', 'T12Patr.TJos' => '', 'T12Patr.TJud' => '', 'T12Patr.TLevi' => '', 'T12Patr.TNaph' => '', 'T12Patr.TReu' => '', 'T12Patr.TSim' => '', 'T12Patr.TZeb' => '', 'TatDiat' => '', 'Titus' => 'TIT', 'Tob' => 'TOB', 'TobS' => 'TBS', 'WSir' => 'WSI', 'Wis' => 'WIS', 'Zech' => 'ZEC', 'Zeph' => 'ZEP');
our @OSIS_ABBR_OT = @{$OSIS_GROUP{'OT'}};
our @OSIS_ABBR_NT = @{$OSIS_GROUP{'NT'}};
our @OSIS_ABBR_ALL = (map(@{$OSIS_GROUP{$_}}, @OSIS_GROUPS));
our @OSIS_ABBR = (@OSIS_ABBR_OT, @OSIS_ABBR_NT);
our $OSISBOOKSRE = join('|', @OSIS_ABBR_ALL);

# The following MAPs were taken from usfm2osis.py and apply to USFM 2.4
our %ID_TYPE_MAP = (
  # File ID code => <div> type attribute value
  'FRT' => 'front',
  'INT' => 'introduction',
  'BAK' => 'back',
  'CNC' => 'concordance',
  'GLO' => 'glossary',
  'TDX' => 'index',
  'NDX' => 'gazetteer',
  'OTH' => 'x-other'
);
our %ID_TYPE_MAP_R = reverse %ID_TYPE_MAP;

our %PERIPH_TYPE_MAP = (
  # Text following \periph => <div> type attribute value
  'Title Page' => 'titlePage', 
  'Half Title Page' => 'x-halfTitlePage', 
  'Promotional Page' => 'x-promotionalPage',
  'Imprimatur' => 'imprimatur', 
  'Publication Data' => 'publicationData', 
  'Foreword' => 'x-foreword', 
  'Preface' => 'preface',
  'Table of Contents' => 'tableofContents', 
  'Alphabetical Contents' => 'x-alphabeticalContents',
  'Table of Abbreviations' => 'x-tableofAbbreviations', 
  'Chronology' => 'x-chronology',
  'Weights and Measures' => 'x-weightsandMeasures', 
  'Map Index' => 'x-mapIndex',
  'NT Quotes from LXX' => 'x-ntQuotesfromLXX', 
  'Cover' => 'coverPage', 
  'Spine' => 'x-spine', 
  'Tables' => 'x-tables', 
  'Verses for Daily Living' => 'x-dailyVerses',
  'Bible Introduction' => 'introduction', 
  'Old Testament Introduction' => 'introduction',
  'Pentateuch Introduction' => 'introduction', 
  'History Introduction' => 'introduction', 
  'Poetry Introduction' => 'introduction',
  'Prophecy Introduction' => 'introduction', 
  'New Testament Introduction' => 'introduction',
  'Gospels Introduction' => 'introduction', 
  'Acts Introduction' => 'introduction', 
  'Epistles Introduction' => 'introduction',
  'Letters Introduction' => 'introduction', 
  'Deuterocanon Introduction' => 'introduction'
);
our %PERIPH_TYPE_MAP_R = reverse %PERIPH_TYPE_MAP;

our %PERIPH_SUBTYPE_MAP = (
  # Text following \periph => <div type=introduction"> subType attribute value
  'Bible Introduction' => 'x-bible', 
  'Old Testament Introduction' => 'x-oldTestament',
  'Pentateuch Introduction' => 'x-pentateuch', 
  'History Introduction' => 'x-history', 
  'Poetry Introduction' => 'x-poetry',
  'Prophecy Introduction' => 'x-prophecy', 
  'New Testament Introduction' => 'x-newTestament',
  'Gospels Introduction' => 'x-gospels', 
  'Acts Introduction' => 'x-acts', 
  'Epistles Introduction' => 'x-epistles',
  'Letters Introduction' => 'x-letters', 
  'Deuterocanon Introduction' => 'x-deuterocanon'
);
our %PERIPH_SUBTYPE_MAP_R = reverse %PERIPH_SUBTYPE_MAP;

our %USFM_DEFAULT_PERIPH_TARGET = (
  'Cover|Title Page|Half Title Page|Promotional Page|Imprimatur|Publication Data|Table of Contents|Table of Abbreviations|Bible Introduction|Foreword|Preface|Chronology|Weights and Measures|Map Index' => 'place-according-to-scope',
  'Old Testament Introduction' => 'osis:div[@type="bookGroup"][1]/node()[1]',
  'NT Quotes from LXX' => 'osis:div[@type="bookGroup"][last()]/node()[1]',
  'Pentateuch Introduction' => 'osis:div[@type="book"][@osisID="Gen" or @osisID="Exod" or @osisID="Lev" or @osisID="Num" or @osisID="Deut"]/node()[1]',
  'History Introduction' => 'osis:div[@type="book"][@osisID="Josh" or @osisID="Judg" or @osisID="Ruth" or @osisID="1Sam" or @osisID="2Sam" or @osisID="1Kgs" or @osisID="2Kgs" or @osisID="1Chr" or @osisID="2Chr" or @osisID="Ezra" or @osisID="Neh" or @osisID="Esth"]/node()[1]',
  'Poetry Introduction' => 'osis:div[@type="book"][@osisID="Job" or @osisID="Ps" or @osisID="Prov" or @osisID="Eccl" or @osisID="Song"]/node()[1]',
  'Prophecy Introduction' => 'osis:div[@type="book"][@osisID="Rev" or @osisID="Isa" or @osisID="Jer" or @osisID="Lam" or @osisID="Ezek" or @osisID="Dan" or @osisID="Hos" or @osisID="Joel" or @osisID="Amos" or @osisID="Obad" or @osisID="Jonah" or @osisID="Mic" or @osisID="Nah" or @osisID="Hab" or @osisID="Zeph" or @osisID="Hag" or @osisID="Zech" or @osisID="Mal"]/node()[1]',
  'New Testament Introduction' => 'osis:div[@type="bookGroup"][last()]/node()[1]',
  'Gospels Introduction' => 'osis:div[@type="book"][@osisID="Matt" or @osisID="Mark" or @osisID="Luke" or @osisID="John"]/node()[1]',
  'Acts Introduction' => 'osis:div[@type="book"][@osisID="Acts"]/node()[1]',
  'Letters Introduction' => 'osis:div[@type="book"][@osisID="Acts" or @osisID="Rom" or @osisID="1Cor" or @osisID="2Cor" or @osisID="Gal" or @osisID="Eph" or @osisID="Phil" or @osisID="Col" or @osisID="1Thess" or @osisID="2Thess" or @osisID="1Tim" or @osisID="2Tim" or @osisID="Titus" or @osisID="Phlm" or @osisID="Heb" or @osisID="Jas" or @osisID="1Pet" or @osisID="2Pet" or @osisID="1John" or @osisID="2John" or @osisID="3John" or @osisID="Jude"]/node()[1]',
  'Deuterocanon Introduction' => 'osis:div[@type="book"][ancestor::osis:div[@osisID="Apocrypha"]]/node()[1]'
);

# The attribute types and values below are hardwired into the xsl files
# to allow them to be more portable. But in Perl, variables are used.
our %RESP; # Values for the OSIS resp attribute
$RESP{'oc'}   = 'x-oc';   # means osis-converters is responsible for adding the element
$RESP{'vsys'} = 'x-vsys'; # means fitToVerseSystem is responsible for adding this element

our $ROC = $RESP{'oc'}; # a convenience variable name

# Verse System element type attribute values
our %VSYS;
$VSYS{'prefix_vs'}     = 'x-vsys';
$VSYS{'missing_vs'}    = $VSYS{'prefix_vs'}.'-missing';
$VSYS{'movedto_vs'}    = $VSYS{'prefix_vs'}.'-movedto';
$VSYS{'extra_vs'}      = $VSYS{'prefix_vs'}.'-extra';
$VSYS{'fitted_vs'}     = $VSYS{'prefix_vs'}.'-fitted';
$VSYS{'start_vs'}      = '-start';
$VSYS{'end_vs'}        = '-end';
$VSYS{'fixed_altvs'}   = 'x-alternate-'; # Versification should be appended
$VSYS{'moved_type'}    = $VSYS{'prefix_vs'}.'-moved';

# All annotateType attribute values
our %ANNOTATE_TYPE;
$ANNOTATE_TYPE{'Source'} = $VSYS{'prefix_vs'}.'-source'; # annotateRef is osisRef to source (custom) verse system
$ANNOTATE_TYPE{'Universal'} = $VSYS{'prefix_vs'}.'-universal'; # annotateRef is osisRef to an external (fixed) verse system
$ANNOTATE_TYPE{'conversion'} = 'x-conversion'; # annotateRef listing conversions where an element should be output
$ANNOTATE_TYPE{'not_conversion'} = 'x-notConversion'; # annotateRef listing conversions where an element should not be output
$ANNOTATE_TYPE{'cover'} = 'x-coverInsertion'; # annotateRef gives cover insertion info
$ANNOTATE_TYPE{'Feature'} = 'x-feature'; # annotateRef listing special features to which an element applies

our $OSISSCHEMA = "http://localhost/~dmsmith/osis/osisCore.2.1.1-cw-latest.xsd"; # Original is at www.crosswire.org, but it's copied locally for speedup/networkless functionality
our $OSIS_NAMESPACE = 'http://www.bibletechnologies.net/2003/OSIS/namespace';
our $TEI_NAMESPACE = 'http://www.crosswire.org/2013/TEIOSIS/namespace';
our $ADDDICTLINKS_NAMESPACE= "http://github.com/JohnAustinDev/osis-converters";
our $ONS = "xmlns='$OSIS_NAMESPACE'";
our $TNS = "xmlns='$TEI_NAMESPACE'";

# Initializes [system] global variables, checks operating system and 
# dependencies, and returns if all is well, otherwise restarts on a 
# Vagrant VM (and then exits) or bails with an error message, if Vagrant 
# is not installed.
sub init_opsys {

  chdir($INPD);
  
  # Mint is like Ubuntu but with totally different release info! 
  # $isCompatibleLinux = ($isCompatibleLinux =~ /Release\:\s*(14|16|18)\./ms);
  my $isCompatibleLinux = ($^O =~ /linux/i ? &shell("lsb_release -a", 3):'');
  my $haveAllDependencies = ($isCompatibleLinux && &checkDependencies($SCRIPT_NAME, $SCRD, $INPD) ? 1:0);
  
  # Continue the script if we're already running on a VM and have 
  # dependencies met and $VAGRANT is not set.
  if (&runningInVagrant() || ($haveAllDependencies && !$VAGRANT)) {
    if ($haveAllDependencies) {return;}
    elsif (&runningInVagrant()) {
      &Error(
"The Vagrant virtual machine does not have the necessary dependancies installed.",
"You may rebuild the virtual machine by running the command: 'Vagrant destroy'.", 1);
      # EXIT...
    }
  }
  
  my $vagrantInstallMessage = "
    Install Vagrant and VirtualBox and then re-run osis-converters:
    Vagrant (https://www.vagrantup.com/downloads.html)
    Virtualbox (https://www.virtualbox.org/wiki/Downloads)";
  
  # If the user is forcing the use of Vagrant, then start Vagrant
  if ($VAGRANT) {
    if (&vagrantInstalled()) {
      &Note("Vagrant will be used because \$VAGRANT is set.\n");
      &restartWithVagrantAndExit();
    }
    else {
      &Error("You have VAGRANT=1 in config.conf but Vagrant is not installed.", 
      $vagrantInstallMessage, 1);
      # EXIT...
    }
  }
  
  # OKAY then, to meet dependancies check if we may use Vagrant and report
  if ($isCompatibleLinux) {
    &Error("Dependancies are not met.", "
You are running a compatible version of Linux, so you have two options:
1) Install the necessary dependancies by running: 
osis-converters\$ sudo provision.sh

2) Run with Vagrant by adding 'VAGRANT=1' to the [system] section 
of config.conf. Vagrant and VirtualBox must be installed.
$vagrantInstallMessage", 1);
    # EXIT...
  }
  
  # Then we must use Vagrant, if it's installed
  if (&vagrantInstalled()) {
    &restartWithVagrantAndExit();
  }
  
  &Error(
"You are not running osis-converters on compatible Linux and do not have Vagrant/VirtualBox installed.", 
$vagrantInstallMessage, 1);
  # EXIT...
}

# Apply the config file's [system] section directly to Perl globals. 
# NOTE: This must be run before init_opsys() so the .vm.conf can be 
# written prior to any Vagrant restart. The .vm.conf file is used to 
# set @OC_SYSTEM_PATH_CONFIGS while running in Vagrant.
sub set_system_globals {
  if (!$MAINMOD) {&ErrorBug("MAINMOD not set.", 1);}

  no strict "refs";
  
  # Write OC_SYSTEM_CONFIGS to Perl globals
  foreach my $e (@OC_SYSTEM_CONFIGS) {
    my $v = &conf($e, undef, undef, undef, 1);
    if (!defined($v)) {next;}
    $$e = $v;
  }
  
  if (!&runningInVagrant()) {
    # Clean OC_SYSTEM_PATH_CONFIGS paths
    foreach my $e (@OC_SYSTEM_PATH_CONFIGS) {
      if (!defined($$e) || $$e =~ /^(https?|ftp)\:/) {next;}
      if ($^O =~ /linux/i) {$$e = &expandLinuxPath($$e);}
      if ($$e =~ /^\./) {$$e = File::Spec->rel2abs($$e, $SCRD);}
    }
    # Write OC_SYSTEM_PATH_CONFIGS Vagrant paths to .vm.conf
    my $cP = &readConfFile("$SCRD/.vm.conf");
    foreach my $k (keys %{$cP}) {
      if ($k =~ /^$MAINMOD\+/) {delete($cP->{$k});}
    }
    foreach my $e (@OC_SYSTEM_PATH_CONFIGS) {
      if (!defined($$e) || $$e =~ /^(https?|ftp)\:/) {next;}
      $cP->{"$MAINMOD+$e"} = ($$e ? &vagrantPath($$e) : $$e);
    }
    $cP->{"all+HOST_SCRD"} = $SCRD;
    $cP->{"all+HOST_SHARE"} = &vagrantHostShare();
    # To prevent collisions between different osis-converters threads, 
    # a BlockFile is used to insure a read doesn't occur during writing.
    my $blockFile = BlockFile->new("$SCRD/.vm.conf-blocked.txt");
    &writeConf("$SCRD/.vm.conf", $cP);
    if (!-e "$SCRD/.vm.conf") {&Error(
"Could not write $SCRD/.vm.conf meaning Vagrant will not work properly.",
"Check that you have write permission on directory $SCRD.");}
  }
  else {
    # Write .vm.conf settings to Perl globals
    my $cP = &readConfFile("$SCRD/.vm.conf");
    foreach my $e (keys %{$cP}) {
      if ($e !~ /^$MAINMOD\+(.*)$/) {next;}
      $$1 = $cP->{$e};
    }
  }
}

sub argPath {
  my $p = shift;
  
  # 'none' is a special value for LOGFILE
  if ($p eq 'none') {return $p;}
  
  # undef is default for LOGFILE
  elsif (!$p) {return;}
  
  if ($p !~ /^[\/\.]/) {$p = "./$p";}
  if ($p =~ /^\./) {$p = File::Spec->rel2abs($p);}
  $p =~ s/\\/\//g;
  
  return &shortPath($p);
}

sub set_project_globals {
  if (!$INPD)    {&ErrorBug("INPD not set.", 1);}

  # Allow using a project subdirectory as $INPD argument
  { my $subs = join('|', 'sfm', 'images', 'output', &PUB_TYPES());
    $INPD =~ s/\/($subs)(\/.*?$|$)//;
  }
  # This works even for MS-Windows because of '\' replacement done above
  $INPD = &shortPath($INPD);
  if (!-e $INPD) {&ErrorBug( 
"Project directory \"$INPD\" does not exist. Check your command line.", 1);
  }

  # Set MOD, MAININPD, MAINMOD, DICTINPD and DICTMOD (DICTMOD is updated  
  # after defaults() in case a new dictionary is discovered in the USFM).
  our $MOD = $INPD; $MOD =~ s/^.*\///;
  our ($MAINMOD, $DICTMOD, $MAININPD, $DICTINPD); 
  if ($INPD =~ /^(.*)\/[^\/]+DICT$/) {
    $MAININPD = $1; 
    $DICTINPD = $INPD;
    $MAINMOD = $MAININPD; $MAINMOD =~ s/^.*\///;
  }
  else {
    $MAININPD = $INPD;
    $MAINMOD = $MAININPD; $MAINMOD =~ s/^.*\///;
    $DICTINPD = "$MAININPD/${MAINMOD}DICT";
  }

  # Before testing the project configuration, run bootstrap.pl if it 
  # exists in the project, to prepare any control files that need it.
  if ($MOD eq $MAINMOD && -e "$MAININPD/bootstrap.pl" && 
      &hasSame([$SCRIPT_NAME], \@CONV_OSIS)) {
    &shell("$MAININPD/bootstrap.pl");
  }

  our $CONF; our $CONFFILE = "$MAININPD/config.conf";
  &readSetCONF();
  
  # $DICTMOD will be empty if there is no dictionary module for the 
  # project, but $DICTINPD always has a value
  $DICTMOD = ( -d $DICTINPD ? $MAINMOD . 'DICT' : '' );

  # Allow running MAININPD-only scripts from a DICT sub-project
  { my $dictncd = join('|', @{$CONV_NOCANDO{'dictionary'}});
    if ($INPD eq $DICTINPD && 
      $SCRIPT =~ /(\/defaults|$dictncd)$/) {
      $INPD = $MAININPD;
      $MOD = $MAINMOD;
    }
  }

  our @SUB_PUBLICATIONS = &getSubPublications("$MAININPD/sfm");

  if (@SUB_PUBLICATIONS == 1) {
    &Error("There is only one sub-publication directory: ".@SUB_PUBLICATIONS[0], 
  "When there is a single publication, all source USFM files should be
  located directly under the sfm directory, without any sub-publication 
  directories.", 1);
  }

  if ($INPD eq $DICTINPD && -e "$INPD/CF_osis2osis.txt") {
    &Error("CF_osis2osis.txt in DICT sub-modules are not processed.", 
  "To run osis2osis on a DICT sub-module, the CF_osis2osis.txt file 
  should still be placed in the main module directory. If you want to  
  run sfm2osis on the main module, then ALSO include a CF_sfm2osis.txt 
  file in the main module directory.", 1);
  }
  
  if (our $NO_OUTPUT_DELETE) {our $DEBUG = 1;}
}

# Set system default paths if not specified in config.conf.
sub set_system_default_paths {

  no strict "refs";
  
  # The following are installed to certain locations by provision.sh
  if ($^O =~ /linux/i) {
    foreach my $v (sort keys %SYSTEM_DEFAULT_PATHS) {
      if ($$v) {next;}
      $$v = &expandLinuxPath($SYSTEM_DEFAULT_PATHS{$v});
    }
  }
  
  # All executable directory paths should end in / or else be empty.
  foreach my $v (sort keys %SYSTEM_DEFAULT_PATHS) {
    if (!$$v) {next;}
    $$v =~ s/([^\/])$/$1\//;
  }
}

# Read the subdirectories of $dir as sub-publication scopes and return
# a sorted array of scopes. Sorting is either by numerical order if the 
# scopes are prepended with a number, or else by KJV order of the first
# book listed in each scope.
sub getSubPublications {
  my $dir = shift;
  
  my $subPubMessage = 
  "The directory name must be a valid scope code, using underscores
  in place of spaces.";
  
  my @scopes = ();
  if (opendir(DIR, $dir)) {
    my %subPubs;
    my @subs = readdir(DIR);
    close(DIR);
subpub:
    foreach my $sub (@subs) {
      if ($sub =~ /^\./) {next;}
      if (!-d "$dir/$sub") {next;}
      if ($sub =~ /\s/) {
        &Error("Sub-publication directory name cannot contain spaces:\n$dir/$sub", $subPubMessage);
        next subpub;
      }
      if ($sub !~ /^((\d\d)_)?([\w\d\-]+)$/) {
        &Error("Could not parse sub-publication name:\n$dir/$sub", $subPubMessage);
        next subpub;
      }
      my $order = $2; my $scope = $3; $scope =~ s/_/ /g;
      my @books = split(/[\-\s]/, $scope);
      foreach my $bk (@books) {
        if (!defined($OSIS_ABBR{$bk})) {
          &Warn(
"Skipping sub-publication directory: '$bk' is not an OSIS Bible book abbreviation or scope.", 
"If this subdirectory is intended to be a sub-publication:\n$subPubMessage");
          next subpub;
        }
      }
      if (!$order) {$order = sprintf("%02i", &defaultOsisIndex(@books[0]));}
      while (defined($subPubs{$order})) {$order .= "00";}
      $subPubs{$order} = $scope;
    }
    foreach my $s (sort keys %subPubs) {push(@scopes, $subPubs{$s});}
  }
  
  return @scopes;
}

# Load current project config. 
sub readSetCONF {

  # NOTE: Perl variables from the [system] section of config.conf are  
  # only set by set_system_globals().

  $CONF = &readProjectConf($CONFFILE, \$CONFSRC);
  
  # Apply config Defaults
  foreach my $e (keys %CONFIG_DEFAULTS) {
    if (!exists($CONF->{"$MAINMOD+$e"})) {
      $CONF->{"$MAINMOD+$e"} = $CONFIG_DEFAULTS{$e};
    }
  }
  
  # Check for configs that need a default added
  foreach my $e (@OC_CONFIGS) {
    if (!exists($CONFIG_DEFAULTS{$e}) && $e !~ /^MATCHES\:/) {
      &ErrorBug("OC_CONFIGS $e should have a default value.");
    }
  }
  
  #use Data::Dumper; &Log(Dumper($CONF)."\n", 1);
}

# Read the configuration files for a project. Returns a hash pointer
# with configuration key => value pairs. The configuration file(s) are
# read in the following order:
#   1) &getDefaultFile('defaults.conf') (if it exists)
#   2) $INPD/config.conf (required)
# Also any 'Include:<config>' statements are read to load other config
# files in the order encountered.
sub readProjectConf {
  my $projconf = shift;
  my $confsrcP = shift;
 
  my (%c, %f);
  
  # Default conf files may use MAINMOD and DICTMOD sections, so the 
  # 'MAINMOD' key must be set before reading any default file(s).
  my $tmpP = &readConfFile($projconf);
  $c{'MAINMOD'} = $tmpP->{'MAINMOD'};

  my $oc = &getDefaultFile('defaults.conf', -1, $projconf, 1);
  if (-e $oc) {
    &readConfFile($oc, \%f, \%c);
  }
  
  &readConfFile($projconf, \%f, \%c, 1);

  #use Data::Dumper; &Log("readConf ".\%c.":\n".Dumper(\%c)."\n", 1);
  
  if ($confsrcP) {$$confsrcP = \%f;}
  return \%c;
}

# Read a config.conf file. Return a hash pointer to encoded config data.
#
# The encoded config data is a hash whose keys are section+entryname
# and whose values are entry VALUES (see below). Sections are denoted
# by square brackets. Allowed sections are [system], [<conversion>] and
# [<module-name>]. Entries in a project's MAINMOD section apply to the
# whole project, but any entries in a DICTMOD section only apply to the
# DICTMOD and will override any same-name entries in the MAINMOD 
# section. Likewise [<conversion>] entries only apply to a particular
# conversion and override any same-name entries in MAINMOD. The [system]
# section defines Perl globals used througout the conversion process
# (see set_system_globals()).
#
# The default module to use as MAINMOD for context interpretation is 
# recorded in a special 'MAINMOD' hash key. This key value will not be 
# overwritten if previously set. The keywords MAINMOD or DICTMOD will
# cause the value of $MAINDMOD to be written. Otherwise, the first 
# unknown section name not ending in DICT will become the MAINMOD key 
# value, and once written it will not be changed.
# 
# VALUES:
# There may by multiple entries for @MULTIVALUE_CONFIGS entries and each 
# of the values will be joined together using <nx/> as separator.
#
# Values of @CONTINUABLE_CONFIGS entries may continue from one line to 
# the next when their line(s) end with '\'.
#
# $confsrcP:
# If hash pointer $confsrcP is provided, it will be filled with key
# source-file data, so the source file of a particular conf value can
# later be determined by findConf().
sub readConfFile {
  my $file = shift;     # config file path
  my $confsrcP = shift; # used for reverse lookup of values
  my $confP = shift;    # optional data hash pointer
  my $warn = shift;     # warn when overwritting existing values
  
  my $sectRE = &configRE(@CONFIG_SECTIONS);
  my $contRE = &configRE(@CONTINUABLE_CONFIGS);
  my $multRE = &configRE(@MULTIVALUE_CONFIGS);
  
  my $continuingEntry = '';
  my $section = '';
  if (!$confP) {my %conf; $confP = \%conf;}
  
  my @lines;
  if (open(XCONF, $READLAYER, $file)) {
    while(<XCONF>) {push(@lines, $_);}
    close(XCONF);
  }
  foreach (@lines) {
    # ignore comment lines
    if ($_ =~ /^#/) {next;}
    
    # blank lines just end any continuing entry
    elsif ($_ =~ /^\s*$/) {
      $continuingEntry = '';
      next;
    }
    
    # handle Include conf files
    elsif ($_ =~ /^Include:\s*(.*?)\s*$/) {
      my $confFile = $1;
      if ($confFile =~ /^\.+\//) {
        my $root = $file; $root =~ s/\/[^\/]+$//;
        $confFile = "$root/$confFile";
      }
      else {$confFile = &expandLinuxPath($confFile);}
      if (-e $confFile) {
        if ($confFile !~ /\.defaults\.conf$/) {
          &Note("Including config file: $confFile.");
        }
        &readConfFile($confFile, $confsrcP, $confP, $warn);
      }
      elsif ($confFile !~ /\.defaults\.conf$/) {
        &Error(
"Include file: '$confFile' not found (at $file line $.)",
"The Include value must be a full or a relative path to an existing config file.");
      }
    }
    
    # handle section headings
    elsif ($_ =~ /^\s*\[(.*?)\]\s*$/) {
      $section = $1;
      $continuingEntry = '';
      
      if ($section =~ /$sectRE/) {
        if ($section eq 'MAINMOD') {
          if (!$confP->{'MAINMOD'}) {$confP->{'MAINMOD'} = $MAINMOD;}
          $section = $confP->{'MAINMOD'};
        }
        elsif ($section eq 'DICTMOD') {
          if (!$confP->{'MAINMOD'}) {$confP->{'MAINMOD'} = $MAINMOD;}
          $section = $confP->{'MAINMOD'}.'DICT';
        }
        if (!$confP->{'MAINMOD'}) {&ErrorBug("MAINMOD is not set.", 1);}
      }
      elsif (!$confP->{'MAINMOD'}) {
        $confP->{'MAINMOD'} = ($section =~ /^(\w+)DICT$/ ? $1 : $section);
      }
    }
    
    # handle config entries
    elsif ($_ =~ /^([^=\s]+)\s*=\s*(.*?)\s*$/) {
      my $e = $1; my $v = $2;
      
      if (!$section) {
        &Error(
"Config entry in '$file' needs a section heading: $_",
"Section headings are enclosed in square brackets like this: '[$MAINMOD]'", 1);
      }
      
      my $fullEntry = "$section+$e";
      $continuingEntry = '';
      
      if ($v eq 'undef') {
        if (exists($confP->{$fullEntry})) {
          delete($confP->{$fullEntry});
        }
        next;
      }
      
      if (!exists($confP->{$fullEntry})) {
        $confP->{$fullEntry} = $v;
        if ($confsrcP) {$confsrcP->{$fullEntry} = $file;}
      }
      else {
        # if this entry supports multiple values, then append another value
        if ($e =~ /$multRE/) {
          $confP->{$fullEntry} .= "<nx/>$v";
        }
        # otherwise overwrite previous value
        else {
          if ($warn) {
            &Warn(
"config.conf '$fullEntry' changed in: '$file':
    was: " . $confP->{$fullEntry} . "
    is : $v");
          }
          $confP->{$fullEntry} = $v;
          if ($confsrcP) {$confsrcP->{$fullEntry} = $file;}
        }
      }
      
      # is this entry going to continue to next line?
      $continuingEntry = ($confP->{$fullEntry} =~ s/\\$/\\\n/ ? $fullEntry:'');
      if ($continuingEntry && $e !~ /$contRE/) {
        &Error("Config file: '$file' config entry '$e' must take only a single line.", 
        "Remove all newline characters from this entry's value.");
      }
    }
    
    # is this line part of the previous line?
    elsif ($continuingEntry) {
      $_ =~ s/[\r\n]+$//;
      $confP->{$continuingEntry} .= $_;
      $continuingEntry = ($confP->{$continuingEntry} =~ s/\\$/\\\n/ ? $continuingEntry:'');
    }
    else {
      &Error("Config file: '$file' unhandled config.conf line: $_");
    }
  }

  #use Data::Dumper; &Log($file.":\n".Dumper($confP)."\n", 1);
  return $confP;
}

# Write a config file having entries of $confP. NOTE: Config 
# entries of the defaults.conf will be filtered out if $filterDefaults 
# is set, and a [system] section will be included, signaling an 
# osis-converters 1.0+ config file.
sub writeConf {
  my $file = shift;
  my $confP = shift;
  my $filterDefaults = shift;

  my %defaults;
  if ($filterDefaults) {
    my $oc = &getDefaultFile('defaults.conf', -1, undef, 1);
    if (-e $oc) {
      $defaults{'MAINMOD'} = $confP->{'MAINMOD'};
      &readConfFile($oc, undef, \%defaults);
    }
  }
  
  my $confdir = $file; $confdir =~ s/([\\\/][^\\\/]+){1}$//;
  if (!-e "$confdir") {make_path("$confdir");}
  
  if (open(XCONF, $WRITELAYER, $file)) {
    my $section = '';
    
    my $hasSystemSection;
    foreach my $fullName (
        sort { &confEntrySort($a, $b, $confP); } 
        keys %{$confP} ) {
      if ($fullName eq 'MAINMOD') {next;}
      elsif ($filterDefaults && defined($defaults{$fullName})) {next;}
      else {
        my $e = $fullName; 
        my $s = ($e =~ s/^([^\+]+)\+// ? $1:'');
        if (!$e) {
          &Error("Config entry is empty string: $fullName", 1);
          next;
        }
        if (!$s) {
          &Error("Config entry has no section: $fullName", 1);
          next;
        }
        if ($s ne $section) {
          print XCONF ($section ? "\n":'')."[$s]\n";
          $section = $s;
          if ($s eq 'system') {$hasSystemSection++;}
        }
        
        if ($confP->{$fullName} =~ /<nx\/>/) {
          foreach my $val (split(/<nx\/>/, $confP->{$fullName})) {
            print XCONF $e."=".$val."\n";
          }
        }
        else {print XCONF $e."=".$confP->{$fullName}."\n";}
      }
    }
    if ($filterDefaults && !$hasSystemSection) {
      print XCONF "\n[system]\n#DEBUG=1\n";
    }
    close(XCONF);
    
  }
  else {
    &Error("Could not open config.conf file: $file.");
    return;
  }

  #&Log(Dumper($confP)."\n", 1);

  return &readConfFile($file);
}

sub confEntrySort {
  my $a = shift;
  my $b = shift;
  my $confP = shift;
  
  my $ae = $a; my $be = $b;
  my $as = ($ae =~ s/([^\+]+)\+// ? $1:'');
  my $bs = ($be =~ s/([^\+]+)\+// ? $1:'');
  if    ($as eq $confP->{'MAINMOD'}) {$as = 'MAINMOD';}
  elsif ($as eq $confP->{'MAINMOD'}.'DICT') {$as = 'DICTMOD';}
  if    ($bs eq $confP->{'MAINMOD'}) {$bs = 'MAINMOD';}
  elsif ($bs eq $confP->{'MAINMOD'}.'DICT') {$bs = 'DICTMOD';}
    
  # First by section
  my $ax = @CONFIG_SECTIONS,
  my $bx = @CONFIG_SECTIONS;
  for (my $i=0; $i < @CONFIG_SECTIONS; $i++) {
    if ($as eq @CONFIG_SECTIONS[$i]) {$ax = $i;}
    if ($bs eq @CONFIG_SECTIONS[$i]) {$bx = $i;}
  }
  if ($ax != @CONFIG_SECTIONS || $bx != @CONFIG_SECTIONS) {
    my $res = ($ax <=> $bx);
    if ($res) {return $res;}
  }
  else {
    my $res = $as cmp $bs;
    if ($res) {return $res;}
  }
  
  # Then by entry
  return $ae cmp $be;
}

# Return the config source file path which specified the requested entry.
sub findConf {
  my $entry = shift;
  my $mod = shift;         # optional, default is $MOD
  my $script_name = shift; # optional, default is $SCRIPT_NAME
  
  my $key = &conf($entry, $mod, $script_name, undef, 1, 1);
  if (!$key) {return;}
  
  return $CONFSRC->{$key};
}

# $CONF contains encoded data from the config.conf file. This function, 
# when used with only one argument, returns the proper value of a config 
# parameter, taking into account the context of $MOD and $SCRIPT_NAME. 
# Also, when passing explicit values for $mod and/or $script_name, the 
# config value for any other context can also be read. If $quiet is set,
# return value checking is disabled, and system values are also retriev-
# able (normally system values should only be retrieved from the Perl
# global variable which shares the system entry's name).
sub conf {
  my $entry = shift;
  my $mod = shift;         # optional, default is $MOD
  my $script_name = shift; # optional, default is $SCRIPT_NAME
  my $autoContext = shift; # optional, context for AUTO values
  my $quiet = shift;       # optional, set to disable value checking for reading system entries or other reasons
  my $getKey = shift;      # return the key, not the value
  
  $mod = ($mod ? $mod:$MOD);
  $script_name = ($script_name ? $script_name:$SCRIPT_NAME);
 
  my $key;
  my $isConf = &isValidConfig("$mod+$entry");
  if (!$isConf) {
    &ErrorBug("Unrecognized config request: $entry");
  }
  elsif (exists($CONF->{$script_name.'+'.$entry})) {
    $key = $script_name.'+'.$entry;
  }
  elsif ($mod eq $CONF->{'MAINMOD'}.'DICT' && exists($CONF->{$mod.'+'.$entry})) {
    $key = $mod.'+'.$entry;
  }
  elsif (exists($CONF->{$CONF->{'MAINMOD'}.'+'.$entry})) {
    $key = $CONF->{'MAINMOD'}.'+'.$entry;
  }
  elsif ($isConf eq 'system' && $quiet && exists($CONF->{'system+'.$entry})) {
    $key = 'system+'.$entry;
  }
  elsif ($isConf eq 'system' && !$quiet) {
    &ErrorBug("Config request $entry is in the [system] section; use " . 
    "\$$entry rather than &conf('$entry') to access [system] section values.");
  }
  if ($getKey) {return $key;}

  #&Debug("entry=$entry, config-key=$key, value=".$CONF->{$key}."\n");
  
  if (!$quiet) {&isValidConfigValue($key, $CONF);}
  
  my $value = (defined($key) ? $CONF->{$key}:undef);
  
  if ($value eq 'AUTO') {
    $value = &confAuto($entry, $mod, $script_name, $autoContext, $quiet);
  }
  
  if ($value =~ /^false$/i) {$value = '';}

  return $value;
}

# Return the actual value of config.conf entries that support the value 'AUTO'.
sub confAuto {
  my $entry = shift;
  my $mod = shift;
  my $script_name = shift;
  my $autoContext = shift;
  my $quiet = shift;
  
  if ($entry eq 'AddScripRefLinks') {
    return (-e "$INPD/CF_addScripRefLinks.txt" ? 'true':'');
  }
  elsif ($entry eq 'AddFootnoteLinks') {
    return (-e "$INPD/CF_addFootnoteLinks.txt" ? 'true':'');
  }
  elsif ($entry eq 'AddFootnoteLinks') {
    return (-e "$INPD/CF_addFootnoteLinks.txt" ? 'true':'');
  }
  elsif ($entry eq 'AddCrossRefLinks') {
    return ($script_name !~ /osis2osis/ && 
            $mod eq $MAINMOD ? 'true':'');
  }
  elsif ($entry eq 'AddDictLinks') {
    return ($script_name !~ /osis2osis/ && 
              ( $mod eq $MAINMOD &&
                -e "$MAININPD/CF_addDictLinks.xml" ||
                $mod eq $DICTMOD &&
                -e "$DICTINPD/CF_addDictLinks.xml" ) ? 'true':'');
  }
  elsif ($autoContext eq 'ebooks') {
    if ($entry eq 'MakeSet[tran]')   {return 'true';}
    if ($entry eq 'MakeSet[subpub]') {return 'true';}
    if ($entry eq 'MakeSet[book]')   {return 'true';}
    if ($entry eq 'MakeTypes')       {
      return join(' ', &CONV_OUTPUT_FILES('osis2ebooks'));
    }
  }
  
  elsif ($autoContext eq 'html') {
    if ($entry eq 'MakeSet[tran]')   {return 'true';}
    if ($entry eq 'MakeSet[subpub]') {return '';}
    if ($entry eq 'MakeSet[book]')   {return '';}
    if ($entry eq 'MakeTypes')       {
      return join(' ', &CONV_OUTPUT_FILES('osis2html'));
    }
  }
  
  if (!$quiet) {
    &ErrorBug("Unhandled AUTO value: entry=$entry, mod=$mod, script_name=$script_name, autoContext=$autoContext");
  }
  
  return 'AUTO';
}

# Checks if the config entry name is valid (see isValidConfigValue() 
# for value checking).
# Returns 0 if $e is not a valid config entry.
# Returns 'sword-autogen' if it is a SWORD auto-generated entry.
# Returns 'sword' if it is an otherwise valid SWORD config.conf entry.
# Returns 'system' if it is a valid [system] config.conf entry.
# Returns 1 otherwise (valid, but nothing special).
sub isValidConfig {
  my $fullEntry = shift;
 
  if ($fullEntry eq 'MAINMOD') {
    return 1;
  }
  
  my $e = $fullEntry;
  my $s = ($e =~ s/^(.*?)\+// ? $1:'');
  if (!$s) {return 0;}
  
  # check for System
  my $systemRE = &configRE(@OC_SYSTEM_CONFIGS);
  if ($e =~ /$systemRE/) {return 'system';}

  # check for SWORD autogen
  my $swordAutoRE = &configRE(@SWORD_AUTOGEN_CONFIGS);
  if ($e =~ /$swordAutoRE/) {return 'sword-autogen';}
  
  # check for other SWORD
  my $swordRE = &configRE(@SWORD_CONFIGS, @SWORD_OC_CONFIGS);
  if ($e =~ /$swordRE/) {return 'sword';}
  
  # check for deprecated
  my $depRE = &configRE(@CONFIG_DEPRECATED);
  if ($e =~ /$depRE/) {
    &Warn("deprecated entry in config.conf: $e");
    return 1;
  }
  
  # check for other valid
  my $valid = &configRE(@OC_CONFIGS);
  if ($e =~ /$valid/) {return 1;}
  
  return 0;
}

sub isValidConfigValue {
  my $fullEntry = shift;
  my $confP = shift;
  
  my $e = $fullEntry; $e =~ s/^[^\+]+\+//;
  
  if ($e =~ /Title/ && $confP->{$fullEntry} =~ / DEF$/) {
    &Error("Using default value for $fullEntry: '".$confP->{$fullEntry}."'", 
    "Add $e=<localized-title> to the config.conf file.");
    return 0;
  }
  elsif ($confP->{$fullEntry} eq 'DEF') {
    &Error("Using default value for $fullEntry: '".$confP->{$fullEntry}."'", 
    "Add $e=Updated-value to the config.conf file.");
    return 0;
  }
  
  my $multRE = &configRE(@MULTIVALUE_CONFIGS);
  if ($confP->{$fullEntry} =~ /<nx\/>/ && $e !~ /$multRE/) {
    &Error("It is not allowed to have multiple '$e' entries in config.conf: ".$confP->{$fullEntry},
      "Remove all but one '$e' entries from config.conf.");
    return 0;
  }
  
  return 1;
}

# Builds a single regex from any number of entries of any of the global 
# config entry lists (such as @SWORD_CONFIGS) which can then be used to 
# match against any config entry to test for membership.
sub configRE {
  my @arr = @_;
  
  my @entryRE;
  foreach my $e (@arr) {
    # skip doc entries
    if ($e =~ /^doc:/) {next;}
    
    # handle special case SWORD_LOCALIZABLE_CONFIGS
    my $a = '';
    foreach my $slc (@SWORD_LOCALIZABLE_CONFIGS) {
      if ($e eq $slc) {$a = '(_\w+)?';}
    }
    
    # remove MATCHES and treat the rest as regex
    $e =~ s/^MATCHES://;
    push(@entryRE, "$e$a");
  }
  
  return (@entryRE ? '^('.join('|', @entryRE).')$':'');
}

# Look for an osis-converters default file. If a default file is not 
# found, return either '' or throw a stop error if priority was 0 (or 
# undef). If priority 1, 2 or 3 is specified, only the location with 
# that priority will be checked:
#
# priority  location(s)
#   -1      check 1, 2 then 3 and fail quietly if not found
#    0      check 1, 2 then 3 and stop-error if not found
#    1      $projdir (or $MAININPD if $projdir is not set)
#    2      $projdir/(../)*defaults directory
#    3      osis-converters/defaults directory
#
# $projdir may be either $MAININPD or $MAININPD/../
sub getDefaultFile {
  my $file = shift;
  my $priority = shift;
  my $projdir = shift; $projdir = ($projdir ? $projdir : $MAININPD);
  my $quiet = shift;
  
  # For project dir, replace DICTMOD with the actual module name
  my $mod = $projdir; $mod =~ s/^.*?([^\/]+)\/?$/$1/;
  my $lfile = $file; $lfile =~ s/^DICTMOD\//${mod}DICT\//;
  my $ret = "$projdir/$lfile";
  if (!$priority || $priority == -1 || $priority == 1) {
    if (-e $ret) {
      if (!$quiet) {&Note("(1) Found $file at $ret");}
      return $ret;
    }
  }
  
  if (!$priority || $priority == -1 || $priority == 2) {
    my @dirs = split(/[\/\\]/, $projdir);
    do {
      my $pdir = join('/', @dirs);
      if ($pdir eq $SCRD) {last;}
      $ret =  "$pdir/defaults/$file";
      pop(@dirs);
    }
    while (@dirs && ! -e $ret);
    if (-e $ret) {
      if (!$quiet) {&Note("(2) Found $file at $ret");}
      return $ret;
    }
  }
  
  if (!$priority || $priority == -1 || $priority == 3) {
    $ret = "$SCRD/defaults/$file";
    if (-e $ret) {
      if (!$quiet) {&Note("(3) Found $file at $ret");}
      return $ret;
    }
  }
  
  if (!$priority) {
    &ErrorBug("Failed to find '$file'", 1);
  }
  
  return '';
}

# Return 1 if dependencies are met for $script and 0 if not
sub checkDependencies {
  my $script = shift;
  my $scrd = shift;
  my $inpd = shift;
  my $quiet = shift;
  
  my $logflag = ($quiet ? ($DEBUG ? 2:3):1);

  my @deps = @{$CONV_BIN_DEPENDENCIES{'all'}};
  if (ref($CONV_BIN_DEPENDENCIES{$script})) {
    push(@deps, @{$CONV_BIN_DEPENDENCIES{$script}});
  }

  my $fail;
  foreach my $p (@deps) {
    if (!exists($CONV_BIN_TEST{$p})) {
      &ErrorBug("No test for \"$p\".");
      return 0;
    }
    
    my $cmd = $CONV_BIN_TEST{$p}[0];
    foreach my $var (keys %SYSTEM_DEFAULT_PATHS) {
      no strict 'refs';
      my $val = $$var; $val =~ s/\/$//;
      $cmd =~ s/\b$var\b/$val/g;
    }
    
    my $result = &shell($cmd, 3, 1);
    
    my $need = $CONV_BIN_TEST{$p}[1];
    if (!$CONV_BIN_TEST{$p}[2] && $result !~ /\Q$need\E/im) {
      &Error("Dependency $p failed:\n\tRan: \"".$CONV_BIN_TEST{$p}[0] .
        "\"\n\tLooking for: \"$need\"\n\tGot:\n$result\n");
      $fail++;
    }
    elsif ($CONV_BIN_TEST{$p}[2] && $result =~ /\Q$need\E/im) {
      &Error("Dependency $p failed:\n\tRan: \"".$CONV_BIN_TEST{$p}[0] .
        "\"\n\tCannot have: \"$need\"\n\tGot:\n$result\n");
      $fail++;
    }
    #&Note("Dependency $p:\n\tRan: \"".$CONV_BIN_TEST{$p}[0]."\"\n\tGot:\n$result");
  }
  
  if ($fail) {
    if (!&runningInVagrant()) {
      &Log("
      SOLUTION: On Linux systems you can try installing dependencies by running:
      $scrd/provision.sh\n\n", 1);
    }
    return 0;
  }
  
  return 1;
}

# Return one of the position values of any OSIS book, bookGroup, or both
# as bookGroup:book in the  default (%OSIS_GROUP) verse system:
# - !$which  return book index (starting at 0) within entire collection
# - $which=1 return book index (starting at 0) within parent bookGroup
# - $which=2 return bookGroup index (starting at 0)
# - $which=3 return bookGroup osisID
# If a corresponding position value is not found, undef is returned.
sub defaultOsisIndex {
  my $name = shift;
  my $which = shift;
  
  if (!$name) {return;}
  
  my $bgName = ($name =~ s/^(.*?):(.*)$/$2/ ? $1:'');
  
  my $t0 = 0;
  my $t2 = 0;
  foreach my $g (@OSIS_GROUPS) {
    my $t1 = 0;
    if ($which == 2 && ($g eq $name || $g eq $bgName)) {return $t2;}
    foreach my $a (@{$OSIS_GROUP{$g}}) {
      if ((!$bgName || $g eq $bgName) && $name eq $a) {
        if    (!$which)     {return $t0;}
        elsif ($which == 1) {return $t1;}
        elsif ($which == 2) {return $t2;}
        return $g;
      }
      $t0++; $t1++;
    }
    $t2++;
  }
}

# Returns $abbr if it is a valid OSIS book abbreviation. If $abbr is a 
# valid Paratext abbreviation, then its corresponding OSIS abbreviation
# is returned. If no OSIS abbreviation is found for $abbr, then undef is
# returned.
sub bookOsisAbbr {
  my $abbr = shift;
  
  if ($abbr) {
    if (defined($OSIS_ABBR{$abbr})) {return $abbr;}
    foreach (keys %OSIS_ABBR) {
      if ($OSIS_ABBR{$_} eq $abbr) {return $_;}
    }
  }
}


########################################################################
# Vagrant related functions
########################################################################

# The host share directory cannot be just a Windows drive letter (native 
# or emulated) because Vagrant cannot create a share to the root of a 
# window's drive.
sub vagrantHostShare {

  if (!$INPD) {
    &ErrorBug("Cannot determine vagrantHostShare(). \$INPD is not set.", 1);
  }
  elsif ($INPD !~ /^((?:\w\:|\/\w)?\/[^\/]+)/) {
    &ErrorBug("Cannot parse vagrantHostShare(). \$INPD=$INPD", 1);
  }
  return $1;
}

sub vagrantInstalled {

  my $pass = &shell("vagrant -v");
  if ($pass =~ /vagrant/i) {return 1;}

  return 0;
}

# Start the current script on a Vagrant VM, wait until it finishes, and 
# then return.
sub initialize_vagrant {

  if (!-e "$SCRD/Vagrantcustom" && open(VAGC, $WRITELAYER, "$SCRD/Vagrantcustom")) {
    print VAGC "# NOTE: You must halt your VM for changes to take effect\n
  config.vm.provider \"virtualbox\" do |vb|
    # Set the RAM for your Vagrant VM
    vb.memory = 2560
  end\n";
    close(VAGC);
  }
  
  chdir $SCRD; # Required for the following vagrant commands to work

  # Make sure Vagrant is up, and with the right share(s)
  my @shares;
  push(@shares, &vagrantShare(&vagrantHostShare(), "/home/vagrant/INDIR_ROOT"));
  my $status = (-e "./.vagrant" ? &shell("vagrant status", 3):'');
  if ($status !~ /\Qrunning (virtualbox)\E/i) {
    &vagrantUp(\@shares);
  }
  elsif (!&matchingShares(\@shares)) {
    &shell("vagrant halt", 3);
    &vagrantUp(\@shares);
  }
}

# Return a vagrant path when running on a host system.
sub vagrantPath {
  my $path = shift; # host system path
  
  if (&runningInVagrant()) {
    &ErrorBug("vagrantPath should only be called from the host system.");
  }
  
  my $vhost = $SCRD;
  my $vvim = '/vagrant';
  my $hhost = &vagrantHostShare();
  my $hvim = "/home/vagrant/INDIR_ROOT";
 
  my $vagrantPath;
  if ($path =~ /^\Q$vhost/) {
    my $rel = &shortPath(File::Spec->abs2rel($path, $vhost));
    if ($rel !~ /^\.\./) {
      $vagrantPath = &shortPath("$vvim/$rel");
    }
  }
  
  if (!$vagrantPath && $path =~ /^\Q$hhost/) {
    my $rel = &shortPath(File::Spec->abs2rel($path, $hhost));
    $vagrantPath = &shortPath("$hvim/$rel");
  }
  
  if (!$vagrantPath) {&ErrorBug("Failed to find vagrantPath('$path')");}

  return $vagrantPath;
}

# Return a host path when running on a Vagrant virtual machine.
sub hostPath {
  my $path = shift; # Vagrant vm path
  
  if (!&runningInVagrant()) {
    &ErrorBug("hostPath should only be called from a Vagrant VM.");
  }
  
  my $cP = &readConfFile("$SCRD/.vm.conf");
  
  my $vhost = $cP->{"all+HOST_SCRD"};
  my $vvim = '/vagrant';
  my $hhost = $cP->{"all+HOST_SHARE"};
  my $hvim = "/home/vagrant/INDIR_ROOT";

  my $hostPath;
  if ($path =~ /^\Q$vvim/) {
    my $rel = &shortPath(File::Spec->abs2rel($path, $vvim));
    if ($rel !~ /^\.\./) {
      $hostPath = "$vhost/$rel";
    }
  }
  
  if (!$hostPath && $path =~ /^\Q$hvim/) {
    my $rel = &shortPath(File::Spec->abs2rel($path, $hvim));
    if ($rel !~ /^\.\./) {
      $hostPath = "$hhost/$rel";
    }
  }
  
  if (!$hostPath) {&ErrorBug("Failed to find hostPath('$path')");}

  return $hostPath;
}

sub restartWithVagrantAndExit {

  &initialize_vagrant();
  
  $SCRIPT = &vagrantPath($SCRIPT);
  
  $ARGS{'first'} = &vagrantPath($INPD);
  
  if ($LOGFILE && $LOGFILE ne 'none') {
    $ARGS{'second'} = &vagrantPath($LOGFILE);
  }

  my $cmd = "vagrant ssh -c \" '$SCRIPT' " . &writeArgs(\%ARGS) . "\"";

  print "\nStarting Vagrant with...\n$cmd\n";
  
  # Continue printing to console while Vagrant ssh remains open
  open(VUP, "$cmd |");
  while(<VUP>) {print $_;}
  close(VUP);
  
  exit $?;
}

sub runningInVagrant {

  return (-e "/vagrant/Vagrantfile" ? 1:0);
}

sub vagrantShare {
  my $host = shift;
  my $client = shift;

  # If the host is Windows, $host must be a native path!
  $host =~ s/^((\w)\:|\/(\w))\//uc($+).":\/"/e;
  $host =~ s/\\/\\\\/g; $client =~ s/\\/\\\\/g; # escape "\"s for use as Vagrantfile quoted strings
  return "config.vm.synced_folder \"$host\", \"$client\"";
}

sub vagrantUp {
  my $sharesP = shift; # \@
  
  if (!-e "./.vagrant") {mkdir("./.vagrant");}
  
  # Create input/output filesystem shares
  open(VAG, $WRITELAYER, "./Vagrantshares") || die "\nError: Cannot open \"./Vagrantshares\"\n";
  foreach my $share (@$sharesP) {print VAG "$share\n";}
  close(VAG);
  print "
Starting Vagrant...
The first use of Vagrant will automatically download and build a virtual
machine having osis-converters fully installed. This build will take some
time. Subsequent use of Vagrant will run much faster.\n\n";
  open(VUP, "vagrant up |");
  while(<VUP>) {print $_;}
  close(VUP);
}

# returns 1 if all shares match, 0 otherwise
sub matchingShares {
  my $sharesP = shift; # \@
  
  my %shares; foreach my $sh (@$sharesP) {$shares{$sh}++;}
  open(CSH, $READLAYER, "./Vagrantshares") || return 0;
  while(<CSH>) {
    if ($_ =~ /^(\Qconfig.vm.synced_folder\E\s.*)$/) {$shares{$1}++;}
    foreach my $share (@$sharesP) {if ($_ =~ /^\Q$share\E$/) {delete($shares{$share});}}
  }
  return (keys(%shares) == 0 ? 1:0);
}

# Replace constant names with their values. An exit error will be thrown
# if a referenced constant has not been set, unless $errorP holds a 
# pointer, where instead the error message will be written and the 
# original text will be returned with default constant values when 
# available.
sub const {
  my $t0 = shift;
  my $errorP = shift;
  
  my %default = (
    'MAINMOD'  => '<main-module>', 
    'MAININPD' => '<main-module>', 
    'DICTMOD'  => '<dictionary-module>', 
    'DICTINPD' => '<dictionary-module>', 
    'MOD'      => '<module>', 
    'SCRD'     => '<osis-converters>'
  );
  
  my $t = $t0;
  foreach my $v ('MAINMOD', 'DICTMOD', 'MOD', 'SCRD', 'SCRIPT', 
                'SCRIPT_NAME') {
    if ($t !~ /\b$v\b/) {next;}
    
    no strict "refs";
    if (!defined($$v)) {
      my $err = "Constant has not been initialized: $v";
      if (!ref($errorP)) {&ErrorBug($err, 1);}
      else {
        $$errorP = $err;
        foreach my $mv (keys %default) {
          my $d = $default{$mv};
          $t =~ s/\b$mv\b/$d/g;
        }
        return $t;
      }
    }
    $t =~ s/\b$v\b/$$v/g;
  }
  return $t;
}

# Try to return the number of CPU cores, or else 2 as the default.
sub numCPUs {
  my $lscpu = &shell('which lscpu', 3, 1);
  if (!$lscpu || $lscpu =~ /no lscpu/i) {return 2;}
  $lscpu = &shell('lscpu | egrep "^CPU\\(s\\)\\:"', 3, 1); 
  $lscpu =~ s/^.*?\s+(\d+)\s*$/$1/;
  return $lscpu;
}

########################################################################
# Logging functions
########################################################################

# Report errors that users need to fix
my %ERR_CHECK;
sub Error {
  my $errmsg = shift;
  my $solmsg = shift;
  my $doDie = shift;
  
  if ($CONF) {
    my $ne = &conf('ARG_noErr');
    if ($ne && $errmsg =~ /$ne/) {return &Warn($errmsg, $solmsg, 1);}
  }
  
  # Solution msgs beginning with <> will only be output once
  if ($solmsg =~ s/^<>//) {
    if ($ERR_CHECK{$solmsg}) {$solmsg='';} 
    else {$ERR_CHECK{$solmsg}++;}
  }
  
  # Terms beginning with <- will not have a leading line-break
  my $n1 = ($errmsg =~ s/^<\-// ? '':"\n");

  &Log($n1."ERROR: $errmsg\n", 1);
  if ($solmsg) {&Log("SOLUTION: $solmsg\n\n", 1);}
  
  if ($doDie) {&Log("Exiting...\n", 1); exit;}
}

# Report errors that are unexpected or need to be seen by osis-converters maintainer
sub ErrorBug {
  my $errmsg = shift;
  my $doDie = shift;
  
  &Log("\nERROR (UNEXPECTED): $errmsg\n", 1);
  
  &Log(&longmess());
  
  &Log("Report the above unexpected error to osis-converters maintainer.\n\n");
  
  if ($doDie) {&Log("Exiting...\n", 1); exit;}
}

my (%WARN_MSG, %WARN_CHECK);
sub Warn {
  my $warnmsg = shift;
  my $checkmsg = shift;
  my $flag = shift;
  
  if ($CONF) {
    my $nw = &conf('ARG_noWarning');
    if ($nw && $warnmsg =~ /$nw/) {return &Note($warnmsg, 1);}
  }
  
  # Terms beginning with <- will not have a leading line-break
  my $n1 = ($warnmsg =~ s/^<\-// ? '':"\n");
  my $n2 = ($checkmsg =~ s/^<\-// ? '':"\n");
  
  # If either term begins with -> there will be no ending line-break
  my $endbreak = ($warnmsg =~ s/^\->// ? '':"\n");
  $endbreak = ($checkmsg =~ s/^\->// || !$endbreak ? '':"\n");
  
  # Messages beginning with <> will only be output once
  if ($warnmsg  =~ s/^<>//) {if ($WARN_MSG{$warnmsg})    {$warnmsg='';}  else {$WARN_MSG{$warnmsg}++;}}
  if ($checkmsg =~ s/^<>//) {if ($WARN_CHECK{$checkmsg}) {$checkmsg='';} else {$WARN_CHECK{$checkmsg}++;}}

  if ($warnmsg) {
    &Log($n1."WARNING: $warnmsg", $flag);
  }
  if ($checkmsg) {
    &Log($n2."CHECK: $checkmsg", $flag);
  }
  if ($endbreak && ($warnmsg || $checkmsg)) {&Log("\n");}
}

my %NOTE_MSG;
sub Note {
  my $notemsg = shift;
  my $flag = shift;
  
  # If message begins with -> there will be no ending line-break
  my $endbreak = ($notemsg =~ s/^\->// ? '':"\n");
  
  # Messages beginning with <> will only be output once
  if ($notemsg  =~ s/^<>//) {if ($NOTE_MSG{$notemsg}) {$notemsg='';} else {$NOTE_MSG{$notemsg}++;}}
  if (!$notemsg) {return;}
  
  &Log("NOTE: $notemsg$endbreak", $flag);
}

sub Debug {
  my $dbgmsg = shift;
  my $flag = shift;
  
  if ($DEBUG) {&Log("DEBUG: $dbgmsg", ($flag ? $flag:1));}
}

sub DebugListVars {
  my $msg = shift;
  
  my $where = (&runningInVagrant() ? "on virtual machine":"on host");
  
  $msg =~ s/WHERE/$where/;
  $msg .= "\n";
  
  no strict "refs";
  foreach my $v (sort @_) {$msg .= "\t$v=$$v\n";}
  &Debug($msg, 1);
}

sub Report {
  my $rptmsg = shift;
  my $flag = shift;
  
  # Terms beginning with <- will not have a leading line-break
  my $n1 = ($rptmsg =~ s/^<\-// ? '':"\n");
  
  &Log("$n1$MOD REPORT: $rptmsg\n", $flag);
}

# Log to console and logfile. $flag can have these values:
#  0   = log-file
#  1   = log file + console
#  2   = only console
#  3   = don't log anything - used by shell($cmd, $flag)
#
# If $LOGFLAG is defined, its value will be used for $flag.
#
# If $LOGFILE is undef then a new log file named $SCRIPT_NAME will be 
# started by init_linux_script().
# If $LOGFILE is 'none' then no log file will be created but log info 
# will be printed to the console.
# If $LOGFILE is a file path then that file will be appended to.
my $LOGFILE_BUFFER;
sub Log {
  my $p = shift; # log message
  my $flag = shift;
  
  if (defined($LOGFLAG)) {$flag = $LOGFLAG;}
  
  if ($flag == 3) {return;}
  
  $p =~ s/&lt;/</g; $p =~ s/&gt;/>/g; $p =~ s/&amp;/&/g;
  $p =~ s/&#(\d+);/my $r = chr($1);/eg;
  
  if ($flag >= 1 || $p =~ /(ERROR|DEBUG)/ || $LOGFILE eq 'none') {
    print encode("utf8", $p);
  }
  
  if ($flag == 2 || $LOGFILE eq 'none') {return;}
  
  if ($p !~ /ERROR/ && !$DEBUG) {$p = &encodePrintPaths($p);}
  
  if (!$LOGFILE) {$LOGFILE_BUFFER .= $p; return;}

  open(LOGF, $APPENDLAYER, $LOGFILE) || die "Could not open log file \"$LOGFILE\"\n";
  if ($LOGFILE_BUFFER) {print LOGF $LOGFILE_BUFFER; $LOGFILE_BUFFER = '';}
  print LOGF $p;
  close(LOGF);
}

END {
  $LOGFLAG = undef;
  my $p = $LOGFILE_BUFFER; $LOGFILE_BUFFER = undef;
  if ($p) {print "Dumping log buffer. LOGFILE=$LOGFILE\n";}
}

my $LOCAL;
sub encodePrintPaths {
  my $t = shift;
  
  if ($DEBUG) {return $t;}
  
  no strict "refs";
  
  # $LOCAL needs to be a global, but it cannot be changed by config.conf
  if ($^O =~ /linux/i) {$LOCAL = &expandLinuxPath('~/.osis-converters'); }
  
  # encode these local file paths, from longest to shortest
  my @paths = ('SCRD', 'MAININPD', 'MOD_OUTDIR', 'LOCAL', 'SWORD_BIN', 'XMLLINT', 'MODULETOOLS_BIN', 'XSLT2', 'GO_BIBLE_CREATOR', 'CALIBRE');
  foreach my $path (sort { length $$b <=> length $$a } @paths) {
    if (!$$path) {next;}
    my $rp = $$path;
    $rp =~ s/[\/\\]+$//;
    $t =~ s/\Q$rp\E/\$$path/g;
  }
  
  # since log files are written to MOD_OUTDIR, simplify this further
  $t =~ s/\$MOD_OUTDIR\//.\//g;

  return $t;
}


########################################################################
# Utility functions
########################################################################

sub expandLinuxPath {
  my $path = shift;
  
  # Allow these global path variables to be used (without initial $ sign) 
  # and expanded in $path.
  my @replacements = ('INPD', 'SCRD', 'OUTPUT', 'MAININPD', 'DICTINPD');
  foreach my $r (@replacements) {
    no strict "refs";
    $path =~ s/$r/$$r/g;
  }

  if ($^O !~ /linux/i) {
    &ErrorBug("expandLinuxPath() should only be run on Linux, but opsys is: $^O", 1);
  }
  my $r = &shell("echo $path", 3);
  chomp($r);
  return $r;
}

sub shortPath {
  my $path = shift;
  
  my @parts = split(/[\\\/]/, $path);
  
  for (my $i=0; $i < @parts; $i++) {
    if (@parts[$i] !~ /^\.+$/ && ($i+1) < @parts && @parts[$i+1] eq '..') {
      splice(@parts, $i, 2);
      $i = -1;
    }
    elsif ($i && @parts[$i] eq '.') {
      splice(@parts, $i, 1);
      $i = -1;
    }
  }

  return join('/', @parts);
}

# Perl manual: Note that glob splits its arguments on whitespace and 
# treats each segment as separate pattern. If you want to glob filenames
# that might contain whitespace, you'll have to use extra quotes around
# the spacey filename to protect it.
sub escglob {
  my $glob = shift;
  
  my $esclin = quotemeta(' ');
  $glob =~ s/(?<!\\)([$esclin])/\\$1/g;
  return $glob;
}

# Escape a linux file path for use as a non-quoted argument.
sub escfile {
  my $f = shift;
  
  my $esclin = quotemeta(' ()');
  $f =~ s/(?<!\\)([$esclin])/\\$1/g;
  return $f;
}

# Escape a linux argument using double quotes
sub escarg {
  my $n = shift;
  
  $n =~ s/(?<!\\)(["])/\\$1/g;
  return '"'.$n.'"';
}

# Encode a file path for writing to a Windows or Linux file system.
our $ESCWIN = '<>:|"'; # ? and * are not included, to allow globbing
sub encfile {
  my $f = shift;
  
  my $escwin = quotemeta($ESCWIN);
  $f =~ s/([$escwin])/my $c=ord($1); my $r="%$c";/ge;
  return $f;
}

# Decode a file path that was encoded with encfile.
sub decfile {
  my $f = shift;
  
  my $ords = join('|', map(ord($_), split(//, $ESCWIN)));
  $f =~ s/%($ords)/my $c=chr($1)/ge;
  return $f;
}

# Encode a string for use in a URL
sub urlencode {
  my $s = shift;
  $s =~ s/([\Q "<>`#?{}\E])/sprintf("%%%02X", ord($1))/seg;
  return $s;
}

sub escAttribute {
  my $v = shift;
  
  $v =~ s/(['"<>])/my $r = '&#'.ord($1).';'/eg;
  
  return $v;
}

sub isFolderEmpty { 
  my $dirname = shift;

  opendir(my $dh, $dirname) or die "Not a directory"; 
  return scalar(grep { $_ ne "." && $_ ne ".." } readdir($dh)) == 0;
}

# Takes two arrays and returns 1 if at least one value is the same in
# both arrays, otherwise returns 0. Array order is insignificant.
sub hasSame {
  my $aP = shift;
  my $bP = shift;
  
  foreach my $a (@{$aP}) {
    foreach my $b (@{$bP}) {
      if ($a eq $b) {return 1;}
    }
  }
  
  return 0;
}

sub printInt {
  my $in = shift; # a number

  my $b = int(0.5 + $in); # rounded to nearest int
  while($b =~ s/(\d+)(\d\d\d)/$1\,$2/){};
  return $b; # rounded with commas: 45,567,234
}

# Run a Linux shell script. $flag can have these values:
#  0   = log-file
#  1   = log file + console
#  2   = only console
#  3   = don't log anything
sub shell {
  my $cmd = shift;
  my $flag = shift; # same as Log flag
  my $allowNonZeroExit = shift;
  
  # Log the command itself using $flag
  &Log("\n$cmd\n", ($DEBUG ? 1:$flag));
 
  # Run and save result of stdout+stderr
  my $result = `$cmd 2>&1`;
  
  # Check for errors. If $allowNonZeroExit, there are no errors.
  my $error = $?; $error = ($allowNonZeroExit ? 0:$error);
  $result = decode('utf8', $result);

  # Errors are always logged to file + console
  if ($DEBUG || $error != 0) {$flag = 1;}

  # Log the result using $flag
  &Log($result."\n", $flag);

  if ($error != 0) {&ErrorBug("Shell command error code $error");}

  return $result;
}

1;
