private static void ReplaceTableOfContents(MarkdownSpec spec, Body body)
        {
            // We have to find the TOC, if one exists, and replace it...
            if (FindToc(body, out var tocFirst, out var tocLast, out var tocInstr, out var tocSec))
            {
                for (int i = tocLast; i >= tocFirst; i--)
                {
                    body.RemoveChild(body.ChildElements[i]);
                }
                var afterToc = body.ChildElements[tocFirst];
                for (int i = 0; i < spec.Sections.Count; i++)
                {
                    var section = spec.Sections[i];
                    if (section.Level > 2)
                    {
                        continue;
                    }

                    var p = new Paragraph();
                    p.AppendChild(new Hyperlink(new Run(new Text(section.Title)))
                    {
                        Anchor = section.BookmarkName
                    });

                    p.ParagraphProperties = new ParagraphProperties(new ParagraphStyleId {
                        Val = $"TOC{section.Level}"
                    });
                    body.InsertBefore(p, afterToc);
                }
                if (tocSec != null)
                {
                    body.InsertBefore(tocSec, afterToc);
                }
            }
        }
        public static void ConvertToWord(MarkdownSpec spec, string templateFile, string outputFile)
        {
            using (var templateDoc = WordprocessingDocument.Open(templateFile, false))
                using (var resultDoc = WordprocessingDocument.Create(outputFile, WordprocessingDocumentType.Document))
                {
                    foreach (var part in templateDoc.Parts)
                    {
                        resultDoc.AddPart(part.OpenXmlPart, part.RelationshipId);
                    }

                    var body = resultDoc.MainDocumentPart.Document.Body;

                    ReplaceTableOfContents(spec, body);

                    var context = new ConversionContext();
                    context.MaxBookmarkId.Value = 1 + body.Descendants <BookmarkStart>().Max(bookmark => int.Parse(bookmark.Id));

                    foreach (var src in spec.Sources)
                    {
                        var converter = new MarkdownSourceConverter(
                            markdownDocument: src.Item2,
                            wordDocument: resultDoc,
                            spec: spec,
                            context: context,
                            filename: Path.GetFileName(src.Item1));
                        foreach (var p in converter.Paragraphs())
                        {
                            body.AppendChild(p);
                        }
                    }
                }
        }
Beispiel #3
0
    public static MarkdownSpec ReadFiles(IEnumerable <string> files)
    {
        var md = new MarkdownSpec {
            files = files
        };

        md.Init();
        return(md);
    }
Beispiel #4
0
    public static MarkdownSpec ReadString(string s)
    {
        var md = new MarkdownSpec {
            s = s
        };

        md.Init();
        return(md);
    }
Beispiel #5
0
 public MarkdownSourceConverter(
     MarkdownDocument markdownDocument,
     WordprocessingDocument wordDocument,
     MarkdownSpec spec,
     ConversionContext context,
     string filename)
 {
     this.markdownDocument = markdownDocument;
     this.wordDocument     = wordDocument;
     sections      = spec.Sections.ToDictionary(sr => sr.Url);
     productions   = spec.Productions;
     this.context  = context;
     this.filename = filename;
     reporter      = new Reporter(filename);
 }
Beispiel #6
0
        static int Main(string[] args)
        {
            // mdspec2docx *.md csharp.g4 template.docx -o spec.docx
            var    ifiles    = new List <string>();
            var    ofiles    = new List <string>();
            string argserror = "";

            for (int i = 0; i < args.Length; i++)
            {
                var arg = args[i];
                if (arg.StartsWith("-"))
                {
                    if (arg == "-o" && i < args.Length - 1)
                    {
                        i++; ofiles.Add(args[i]);
                    }
                    else
                    {
                        argserror += $"Unrecognized '{arg}'\n";
                    }
                }
                else if (!arg.Contains("*") && !arg.Contains("?"))
                {
                    if (!File.Exists(arg))
                    {
                        Console.Error.WriteLine($"Not found - {arg}"); return(1);
                    }
                    ifiles.Add(arg);
                }
                else
                {
                    // Windows command-shell doesn't do globbing, so we have to do it ourselves
                    string dir = Path.GetDirectoryName(arg), filename = Path.GetFileName(arg);
                    if (dir.Contains("*") || dir.Contains("?"))
                    {
                        Console.Error.WriteLine("Can't match wildcard directory names");
                        return(1);
                    }
                    if (dir == "")
                    {
                        dir = Directory.GetCurrentDirectory();
                    }

                    if (!Directory.Exists(dir))
                    {
                        Console.Error.WriteLine($"Not found - \"{dir}\""); return(1);
                    }
                    var fns2 = Directory.GetFiles(dir, filename);
                    if (fns2.Length == 0)
                    {
                        Console.Error.WriteLine($"Not found - \"{arg}\""); return(1);
                    }
                    ifiles.AddRange(fns2);
                }
            }

            var    imdfiles = new List <string>();
            string ireadmefile = null, iantlrfile = null, idocxfile = null, odocfile = null;

            foreach (var ifile in ifiles)
            {
                var name = Path.GetFileName(ifile);
                var ext  = Path.GetExtension(ifile).ToLower();
                if (ext == ".g4")
                {
                    if (iantlrfile != null)
                    {
                        argserror += "Multiple input .g4 files\n";
                    }
                    iantlrfile = ifile;
                }
                else if (ext == ".docx")
                {
                    if (idocxfile != null)
                    {
                        argserror += "Multiple input .docx files\n";
                    }
                    idocxfile = ifile;
                }
                else if (ext != ".md")
                {
                    argserror += $"Not .g4 or .docx or .md '{ifile}'\n"; continue;
                }
                else if (String.Equals(name, "readme.md", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (ireadmefile != null)
                    {
                        argserror += "Multiple readme.md files\n";
                    }
                    ireadmefile = ifile;
                }
                else
                {
                    imdfiles.Add(ifile);
                }
            }
            foreach (var ofile in ofiles)
            {
                var ext = Path.GetExtension(ofile).ToLower();
                if (ext == ".docx")
                {
                    if (odocfile != null)
                    {
                        argserror += "Multiple output docx files\n";
                    }
                    odocfile = ofile;
                }
                else
                {
                    argserror += $"Unknown output file extension: {ext}\n";
                }
            }

            if (odocfile == null)
            {
                argserror += "No output .docx file specified\n";
            }

            if (ireadmefile == null && ifiles.Count == 0)
            {
                argserror += "No .md files supplied\n";
            }

            if (idocxfile == null)
            {
                argserror += "No template.docx supplied\n";
            }

            if (argserror != "")
            {
                Console.Error.WriteLine(argserror);
                Console.Error.WriteLine("mdspec2docx *.md grammar.g4 template.docx -o spec.docx");
                Console.Error.WriteLine();
                Console.Error.WriteLine("Turns the markdown files into a word document based on the template.");
                Console.Error.WriteLine("If readme.md and other files are given, then readme is used solely to");
                Console.Error.WriteLine("   sort the docx based on its list of `* [Link](subfile.md)`.");
                Console.Error.WriteLine("If a .g4 is given, it verifies 1:1 correspondence with ```antlr blocks.");
                Console.Error.WriteLine("The -td temp directory will enable incremental (faster) runs in future.");
                return(1);
            }

            Console.WriteLine("mdspec2docx");
            Console.WriteLine("Reading markdown files");

            // Read input file. If it contains a load of linked filenames, then read them instead.
            List <string> ifiles_in_order = new List <string>();
            List <Tuple <int, string, string, SourceLocation> > urls = null;

            if (ireadmefile == null)
            {
                ifiles_in_order.AddRange(ifiles);
            }
            else if (ireadmefile != null && ifiles.Count == 0)
            {
                ifiles_in_order.Add(ireadmefile);
            }
            else
            {
                var readme = FSharp.Markdown.Markdown.Parse(File.ReadAllText(ireadmefile));
                urls = new List <Tuple <int, string, string, SourceLocation> >();
                // is there a nicer way to get the URLs of all depth-1 and depth-2 URLs in this list? ...
                foreach (var list in readme.Paragraphs.OfType <FSharp.Markdown.MarkdownParagraph.ListBlock>())
                {
                    var pp = new List <Tuple <int, FSharp.Markdown.MarkdownParagraph> >();
                    foreach (var pars in list.items)
                    {
                        foreach (var par in pars)
                        {
                            pp.Add(Tuple.Create(1, par));
                            var sublist = par as FSharp.Markdown.MarkdownParagraph.ListBlock;
                            if (sublist != null)
                            {
                                pp.AddRange(from subpars in sublist.items
                                            from subpar in subpars
                                            select Tuple.Create(2, subpar));
                            }
                        }
                    }
                    foreach (var tpp in pp)
                    {
                        var level   = tpp.Item1;
                        var spanpar = tpp.Item2 as FSharp.Markdown.MarkdownParagraph.Span;
                        if (spanpar == null)
                        {
                            continue;
                        }

                        var links = spanpar.body.OfType <FSharp.Markdown.MarkdownSpan.DirectLink>();
                        urls.AddRange(from link in links
                                      let title                     = string.Join("", link.body.OfType <FSharp.Markdown.MarkdownSpan.Literal>().Select(l => l.text))
                                                            let url = link.link
                                                                      where url.ToLower().EndsWith(".md") || url.ToLower().Contains(".md#")
                                                                      let loc = new SourceLocation(ireadmefile, null, list, link)
                                                                                select Tuple.Create(level, title, url, loc));
                    }
                }
                var filelinks = (from turl in urls
                                 let url = turl.Item3
                                           let i = url.IndexOf('#')
                                                   let url2 = (i == -1 ? url : url.Substring(0, i))
                                                              select url2).ToList().Distinct();
                foreach (var link in filelinks)
                {
                    var ifile = ifiles.FirstOrDefault(f => Path.GetFileName(f) == link);
                    if (ifile == null)
                    {
                        Console.Error.WriteLine($"readme.md link '{link}' wasn't one of the files passed on the command line"); return(1);
                    }
                    ifiles_in_order.Add(ifile);
                }
            }
            var md = MarkdownSpec.ReadFiles(ifiles_in_order, urls);


            // Now md.Gramar contains the grammar as extracted out of the *.md files, and moreover has
            // correct references to within the spec. We'll check that it has the same productions as
            // in the corresponding ANTLR file
            if (iantlrfile != null)
            {
                Console.WriteLine($"Reading {Path.GetFileName(iantlrfile)}");
                var grammar = Antlr.ReadFile(iantlrfile);
                foreach (var diff in CompareGrammars(grammar, md.Grammar))
                {
                    if (diff.authority == null)
                    {
                        Report("MD21", "error", $"markdown has superfluous production '{diff.productionName}'", "mdspec2docx");
                    }
                    else if (diff.copy == null)
                    {
                        Report("MD22", "error", $"markdown lacks production '{diff.productionName}'", "mdspec2docx");
                    }
                    else
                    {
                        Report("MD23", "error", $"production '{diff.productionName}' differs between markdown and antlr.g4", "mdspec2docx");
                        Report("MD23b", "error", "antlr.g4 says " + diff.authority.Replace("\r", "\\r").Replace("\n", "\\n"), "mdspec2docx");
                        Report("MD23c", "error", "markdown says " + diff.copy.Replace("\r", "\\r").Replace("\n", "\\n"), "mdspec2docx");
                    }
                }
                foreach (var p in grammar.Productions)
                {
                    p.Link     = md.Grammar.Productions.FirstOrDefault(mdp => mdp?.Name == p.Name)?.Link;
                    p.LinkName = md.Grammar.Productions.FirstOrDefault(mdp => mdp?.Name == p.Name)?.LinkName;
                }
            }

            // Generate the Specification.docx file
            if (odocfile != null)
            {
                var odocfile2 = odocfile;
                if (odocfile2 != odocfile)
                {
                    Report("MD26", "error", $"File '{odocfile}' was in use", "mdspec2docx");
                }

                Console.WriteLine($"Writing '{Path.GetFileName(odocfile2)}'");
                try
                {
                    MarkdownSpecConverter.ConvertToWord(md, idocxfile, odocfile2);
                }
                catch (Exception ex)
                {
                    Report("MD27", "error", ex.Message, "mdspec2docx");
                    return(1);
                }
                if (odocfile2 != odocfile)
                {
                    return(1);
                }
            }
            return(0);
        }
Beispiel #7
0
 private static IEnumerable <string> Span2Words(MarkdownSpan md)
 {
     if (md.IsLiteral)
     {
         var mdl = md as MarkdownSpan.Literal;
         foreach (var s in String2Words(MarkdownSpec.mdunescape(mdl)))
         {
             yield return(s);
         }
     }
     else if (md.IsStrong)
     {
         var mds = md as MarkdownSpan.Strong;
         foreach (var s in Spans2Words(mds.body))
         {
             yield return(s);
         }
     }
     else if (md.IsEmphasis)
     {
         var mde = md as MarkdownSpan.Emphasis;
         foreach (var s in Spans2Words(mde.body))
         {
             yield return(s);
         }
     }
     else if (md.IsInlineCode)
     {
         var mdi = md as MarkdownSpan.InlineCode;
         foreach (var s in String2Words(mdi.code))
         {
             yield return(s);
         }
     }
     else if (md.IsDirectLink)
     {
         var mdl = md as MarkdownSpan.DirectLink;
         foreach (var s in Spans2Words(mdl.body))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdl.title.Option()))
         {
             yield return(s);
         }
     }
     else if (md.IsIndirectLink)
     {
         var mdl = md as MarkdownSpan.DirectLink;
         foreach (var s in Spans2Words(mdl.body))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdl.link))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdl.title.Option()))
         {
             yield return(s);
         }
     }
     else if (md.IsAnchorLink)
     {
         var mdl = md as MarkdownSpan.AnchorLink;
         foreach (var s in String2Words(mdl.link))
         {
             yield return(s);
         }
     }
     else if (md.IsDirectImage)
     {
         var mdi = md as MarkdownSpan.DirectImage;
         foreach (var s in String2Words(mdi.body))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdi.link))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdi.title.Option()))
         {
             yield return(s);
         }
     }
     else if (md.IsIndirectImage)
     {
         var mdi = md as MarkdownSpan.IndirectImage;
         foreach (var s in String2Words(mdi.body))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdi.link))
         {
             yield return(s);
         }
         foreach (var s in String2Words(mdi.key))
         {
             yield return(s);
         }
     }
     else if (md.IsEmbedSpans)
     {
     }
     else if (md.IsHardLineBreak)
     {
     }
     else if (md.IsLatexDisplayMath)
     {
         var mdl = md as MarkdownSpan.LatexDisplayMath;
         foreach (var s in String2Words(mdl.code))
         {
             yield return(s);
         }
     }
     else if (md.IsLatexInlineMath)
     {
         var mdl = md as MarkdownSpan.LatexInlineMath;
         foreach (var s in String2Words(mdl.code))
         {
             yield return(s);
         }
     }
 }
Beispiel #8
0
        static int Main(string[] args)
        {
            // mdspec2docx *.md csharp.g4 template.docx -o spec.docx
            var    ifiles    = new List <string>();
            var    ofiles    = new List <string>();
            string argserror = "";

            for (int i = 0; i < args.Length; i++)
            {
                var arg = args[i];
                if (arg.StartsWith("-"))
                {
                    if (arg == "-o" && i < args.Length - 1)
                    {
                        i++; ofiles.Add(args[i]);
                    }
                    else
                    {
                        argserror += $"Unrecognized '{arg}'\n";
                    }
                }
                else if (!arg.Contains("*") && !arg.Contains("?"))
                {
                    if (!File.Exists(arg))
                    {
                        Console.Error.WriteLine($"Not found - {arg}"); return(1);
                    }
                    ifiles.Add(arg);
                }
                else
                {
                    // Windows command-shell doesn't do globbing, so we have to do it ourselves
                    string dir = Path.GetDirectoryName(arg), filename = Path.GetFileName(arg);
                    if (dir.Contains("*") || dir.Contains("?"))
                    {
                        Console.Error.WriteLine("Can't match wildcard directory names");
                        return(1);
                    }
                    if (dir == "")
                    {
                        dir = Directory.GetCurrentDirectory();
                    }

                    if (!Directory.Exists(dir))
                    {
                        Console.Error.WriteLine($"Not found - \"{dir}\""); return(1);
                    }
                    var fns2 = Directory.GetFiles(dir, filename);
                    if (fns2.Length == 0)
                    {
                        Console.Error.WriteLine($"Not found - \"{arg}\""); return(1);
                    }
                    ifiles.AddRange(fns2);
                }
            }

            var    imdfiles = new List <string>();
            string idocxfile = null, odocfile = null;

            foreach (var ifile in ifiles)
            {
                var name = Path.GetFileName(ifile);
                // Ignore the README, which is maintained separately and not part of the Standard.
                if (name.Equals("README.md", StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }
                var ext = Path.GetExtension(ifile).ToLower();
                if (ext == ".docx")
                {
                    if (idocxfile != null)
                    {
                        argserror += "Multiple input .docx files\n";
                    }
                    idocxfile = ifile;
                }
                else if (ext != ".md")
                {
                    argserror += $"Not .g4 or .docx or .md '{ifile}'\n"; continue;
                }
                else
                {
                    imdfiles.Add(ifile);
                }
            }
            foreach (var ofile in ofiles)
            {
                var ext = Path.GetExtension(ofile).ToLower();
                if (ext == ".docx")
                {
                    if (odocfile != null)
                    {
                        argserror += "Multiple output docx files\n";
                    }
                    odocfile = ofile;
                }
                else
                {
                    argserror += $"Unknown output file extension: {ext}\n";
                }
            }

            if (odocfile == null)
            {
                argserror += "No output .docx file specified\n";
            }

            if (idocxfile == null)
            {
                argserror += "No template.docx supplied\n";
            }

            if (argserror != "")
            {
                Console.Error.WriteLine(argserror);
                Console.Error.WriteLine("mdspec2docx *.md grammar.g4 template.docx -o spec.docx");
                Console.Error.WriteLine();
                Console.Error.WriteLine("Turns the markdown files into a word document based on the template.");
                Console.Error.WriteLine("If readme.md and other files are given, then readme is used solely to");
                Console.Error.WriteLine("   sort the docx based on its list of `* [Link](subfile.md)`.");
                return(1);
            }

            Console.WriteLine("Reading markdown files");

            // Read input file. If it contains a load of linked filenames, then read them instead.
            var md = MarkdownSpec.ReadFiles(imdfiles);

            // Generate the Specification.docx file
            if (odocfile != null)
            {
                var odocfile2 = odocfile;
                if (odocfile2 != odocfile)
                {
                    Report("MD26", error: true, $"File '{odocfile}' was in use", "mdspec2docx");
                }

                Console.WriteLine($"Writing '{Path.GetFileName(odocfile2)}'");
                try
                {
                    MarkdownSpecConverter.ConvertToWord(md, idocxfile, odocfile2);
                }
                catch (Exception ex)
                {
                    Report("MD27", error: true, ex.Message, ex.StackTrace);
                    return(1);
                }
                if (odocfile2 != odocfile)
                {
                    return(1);
                }
            }
            Console.WriteLine($"Errors: {errors}");
            Console.WriteLine($"Warnings: {warnings}");
            return(errors == 0 ? 0 : 1);
        }
Beispiel #9
0
    static int Main(string[] args)
    {
        var ifn = (args.Length >= 2 ? args[1] : "readme.md");

        if (!File.Exists(ifn) || !File.Exists("template.docx") || Directory.GetFiles(".", "*.g4").Length > 1)
        {
            Console.Error.WriteLine("md2docx <filename>.md -- converts it to '<filename>.docx', based on 'template.docx'");
            Console.Error.WriteLine();
            Console.Error.WriteLine("If no file is specified:");
            Console.Error.WriteLine("    it looks for readme.md instead");
            Console.Error.WriteLine("If input file has a list with links of the form `* [Link](subfile.md)`:");
            Console.Error.WriteLine("   it converts the listed subfiles instead of <filename>.md");
            Console.Error.WriteLine("If the current directory contains one <grammar>.g4 file:");
            Console.Error.WriteLine("   it verifies all ```antlr blocks correspond, and also generates <grammar>.html");
            Console.Error.WriteLine("If 'template.docx' contains a Table of Contents:");
            Console.Error.WriteLine("   it replaces it with one based on the markdown (but page numbers aren't supported)");
            return(1);
        }

        // Read input file. If it contains a load of linked filenames, then read them instead.
        var readme = FSharp.Markdown.Markdown.Parse(File.ReadAllText(ifn));
        var files  = (from list in readme.Paragraphs.OfType <FSharp.Markdown.MarkdownParagraph.ListBlock>()
                      let items = list.Item2
                                  from par in items
                                  from spanpar in par.OfType <FSharp.Markdown.MarkdownParagraph.Span>()
                                  let spans = spanpar.Item
                                              from link in spans.OfType <FSharp.Markdown.MarkdownSpan.DirectLink>()
                                              let url = link.Item2.Item1
                                                        where url.EndsWith(".md", StringComparison.InvariantCultureIgnoreCase)
                                                        select url).ToList().Distinct();

        if (files.Count() == 0)
        {
            files = new[] { ifn }
        }
        ;
        var md = MarkdownSpec.ReadFiles(files);


        // Now md.Gramar contains the grammar as extracted out of the *.md files, and moreover has
        // correct references to within the spec. We'll check that it has the same productions as
        // in the corresponding ANTLR file
        var antlrfn = Directory.GetFiles(".", "*.g4").FirstOrDefault();

        if (antlrfn != null)
        {
            var htmlfn  = Path.ChangeExtension(antlrfn, ".html");
            var grammar = Antlr.ReadFile(antlrfn);
            if (!AreProductionsSame(grammar, md.Grammar))
            {
                throw new Exception("Grammar mismatch");
            }
            foreach (var p in grammar.Productions)
            {
                p.Link     = md.Grammar.Productions.FirstOrDefault(mdp => mdp?.ProductionName == p.ProductionName)?.Link;
                p.LinkName = md.Grammar.Productions.FirstOrDefault(mdp => mdp?.ProductionName == p.ProductionName)?.LinkName;
            }

            File.WriteAllText(htmlfn, grammar.ToHtml(), Encoding.UTF8);
            Process.Start(htmlfn);
        }

        // Generate the Specification.docx file
        var fn = PickUniqueFilename(Path.ChangeExtension(ifn, ".docx"));

        md.WriteFile("template.docx", fn);
        Process.Start(fn);
        return(0);
    }
Beispiel #10
0
        static int Main(string[] args)
        {
            // mdspec2docx *.md csharp.g4 template.docx -o spec.docx
            var    ifiles    = new List <string>();
            var    ofiles    = new List <string>();
            string argserror = "";

            for (int i = 0; i < args.Length; i++)
            {
                var arg = args[i];
                if (arg.StartsWith("-"))
                {
                    if (arg == "-o" && i < args.Length - 1)
                    {
                        i++; ofiles.Add(args[i]);
                    }
                    else
                    {
                        argserror += $"Unrecognized '{arg}'\n";
                    }
                }
                else if (!arg.Contains("*") && !arg.Contains("?"))
                {
                    if (!File.Exists(arg))
                    {
                        Console.Error.WriteLine($"Not found - {arg}"); return(1);
                    }
                    ifiles.Add(arg);
                }
                else
                {
                    // Windows command-shell doesn't do globbing, so we have to do it ourselves
                    string dir = Path.GetDirectoryName(arg), filename = Path.GetFileName(arg);
                    if (dir.Contains("*") || dir.Contains("?"))
                    {
                        Console.Error.WriteLine("Can't match wildcard directory names");
                        return(1);
                    }
                    if (dir == "")
                    {
                        dir = Directory.GetCurrentDirectory();
                    }

                    if (!Directory.Exists(dir))
                    {
                        Console.Error.WriteLine($"Not found - \"{dir}\""); return(1);
                    }
                    var fns2 = Directory.GetFiles(dir, filename);
                    if (fns2.Length == 0)
                    {
                        Console.Error.WriteLine($"Not found - \"{arg}\""); return(1);
                    }
                    ifiles.AddRange(fns2);
                }
            }

            var    imdfiles = new List <string>();
            string ireadmefile = null, idocxfile = null, odocfile = null;

            foreach (var ifile in ifiles)
            {
                var name = Path.GetFileName(ifile);
                var ext  = Path.GetExtension(ifile).ToLower();
                if (ext == ".docx")
                {
                    if (idocxfile != null)
                    {
                        argserror += "Multiple input .docx files\n";
                    }
                    idocxfile = ifile;
                }
                else if (ext != ".md")
                {
                    argserror += $"Not .g4 or .docx or .md '{ifile}'\n"; continue;
                }
                else if (String.Equals(name, "readme.md", StringComparison.InvariantCultureIgnoreCase))
                {
                    if (ireadmefile != null)
                    {
                        argserror += "Multiple readme.md files\n";
                    }
                    ireadmefile = ifile;
                }
                else
                {
                    imdfiles.Add(ifile);
                }
            }
            foreach (var ofile in ofiles)
            {
                var ext = Path.GetExtension(ofile).ToLower();
                if (ext == ".docx")
                {
                    if (odocfile != null)
                    {
                        argserror += "Multiple output docx files\n";
                    }
                    odocfile = ofile;
                }
                else
                {
                    argserror += $"Unknown output file extension: {ext}\n";
                }
            }

            if (odocfile == null)
            {
                argserror += "No output .docx file specified\n";
            }

            if (ireadmefile == null && ifiles.Count == 0)
            {
                argserror += "No .md files supplied\n";
            }

            if (idocxfile == null)
            {
                argserror += "No template.docx supplied\n";
            }

            if (argserror != "")
            {
                Console.Error.WriteLine(argserror);
                Console.Error.WriteLine("mdspec2docx *.md grammar.g4 template.docx -o spec.docx");
                Console.Error.WriteLine();
                Console.Error.WriteLine("Turns the markdown files into a word document based on the template.");
                Console.Error.WriteLine("If readme.md and other files are given, then readme is used solely to");
                Console.Error.WriteLine("   sort the docx based on its list of `* [Link](subfile.md)`.");
                return(1);
            }

            Console.WriteLine("Reading markdown files");

            // Read input file. If it contains a load of linked filenames, then read them instead.
            List <string> ifiles_in_order = new List <string>();
            List <Tuple <int, string, string, SourceLocation> > urls = null;

            if (ireadmefile == null)
            {
                ifiles_in_order.AddRange(imdfiles);
            }
            else if (ireadmefile != null && ifiles.Count == 0)
            {
                ifiles_in_order.Add(ireadmefile);
            }
            else
            {
                var readme = FSharp.Markdown.Markdown.Parse(File.ReadAllText(ireadmefile));
                urls = new List <Tuple <int, string, string, SourceLocation> >();
                // is there a nicer way to get the URLs of all depth-1 and depth-2 URLs in this list? ...
                foreach (var list in readme.Paragraphs.OfType <FSharp.Markdown.MarkdownParagraph.ListBlock>())
                {
                    var pp = new List <Tuple <int, FSharp.Markdown.MarkdownParagraph> >();
                    foreach (var pars in list.items)
                    {
                        foreach (var par in pars)
                        {
                            pp.Add(Tuple.Create(1, par));
                            var sublist = par as FSharp.Markdown.MarkdownParagraph.ListBlock;
                            if (sublist != null)
                            {
                                pp.AddRange(from subpars in sublist.items
                                            from subpar in subpars
                                            select Tuple.Create(2, subpar));
                            }
                        }
                    }
                    foreach (var tpp in pp)
                    {
                        var level   = tpp.Item1;
                        var spanpar = tpp.Item2 as FSharp.Markdown.MarkdownParagraph.Span;
                        if (spanpar == null)
                        {
                            continue;
                        }

                        var links = spanpar.body.OfType <FSharp.Markdown.MarkdownSpan.DirectLink>();
                        urls.AddRange(from link in links
                                      let title                     = string.Join("", link.body.OfType <FSharp.Markdown.MarkdownSpan.Literal>().Select(l => l.text))
                                                            let url = link.link
                                                                      where url.ToLower().EndsWith(".md") || url.ToLower().Contains(".md#")
                                                                      let loc = new SourceLocation(ireadmefile, null, list, link)
                                                                                select Tuple.Create(level, title, url, loc));
                    }
                }
                var filelinks = (from turl in urls
                                 let url = turl.Item3
                                           let i = url.IndexOf('#')
                                                   let url2 = (i == -1 ? url : url.Substring(0, i))
                                                              select url2).ToList().Distinct();
                foreach (var link in filelinks)
                {
                    var ifile = ifiles.FirstOrDefault(f => Path.GetFileName(f) == link);
                    if (ifile == null)
                    {
                        Console.Error.WriteLine($"readme.md link '{link}' wasn't one of the files passed on the command line"); return(1);
                    }
                    ifiles_in_order.Add(ifile);
                }
            }
            var md = MarkdownSpec.ReadFiles(ifiles_in_order, urls);

            // Generate the Specification.docx file
            if (odocfile != null)
            {
                var odocfile2 = odocfile;
                if (odocfile2 != odocfile)
                {
                    Report("MD26", error: true, $"File '{odocfile}' was in use", "mdspec2docx");
                }

                Console.WriteLine($"Writing '{Path.GetFileName(odocfile2)}'");
                try
                {
                    MarkdownSpecConverter.ConvertToWord(md, idocxfile, odocfile2);
                }
                catch (Exception ex)
                {
                    Report("MD27", error: true, ex.Message, ex.StackTrace);
                    return(1);
                }
                if (odocfile2 != odocfile)
                {
                    return(1);
                }
            }
            Console.WriteLine($"Errors: {errors}");
            Console.WriteLine($"Warnings: {warnings}");
            return(errors == 0 ? 0 : 1);
        }