private static int BugWorkaroundIndent(ref MarkdownParagraph mdp, int level) { if (!mdp.IsParagraph) { return(level); } var p = mdp as MarkdownParagraph.Paragraph; var spans = p.Item; if (spans.Count() == 0 || !spans[0].IsLiteral) { return(level); } var literal = spans[0] as MarkdownSpan.Literal; if (!literal.Item.StartsWith("ceci-n'est-pas-une-indent")) { return(level); } // var literal2 = MarkdownSpan.NewLiteral(literal.Item.Substring(25)); var spans2 = Microsoft.FSharp.Collections.FSharpList <MarkdownSpan> .Cons(literal2, spans.Tail); var p2 = MarkdownParagraph.NewParagraph(spans2); mdp = p2; return(0); }
IEnumerable <OpenXmlCompositeElement> Paragraph2Paragraphs(MarkdownParagraph md) { if (md.IsHeading) { var mdh = md as MarkdownParagraph.Heading; var level = mdh.Item1; var spans = mdh.Item2; var sr = sections[new SectionRef(mdh, filename).Url]; var props = new ParagraphProperties(new ParagraphStyleId() { Val = $"Heading{level}" }); var p = new Paragraph { ParagraphProperties = props }; maxBookmarkId.Value += 1; p.AppendChild(new BookmarkStart { Name = sr.BookmarkName, Id = maxBookmarkId.Value.ToString() }); p.Append(Spans2Elements(spans)); p.AppendChild(new BookmarkEnd { Id = maxBookmarkId.Value.ToString() }); yield return(p); Console.WriteLine(new string(' ', level * 4 - 4) + sr.Number + " " + sr.Title); yield break; } else if (md.IsParagraph) { var mdp = md as MarkdownParagraph.Paragraph; var spans = mdp.Item; yield return(new Paragraph(Spans2Elements(spans))); yield break; } else if (md.IsListBlock) { var mdl = md as MarkdownParagraph.ListBlock; var flat = FlattenList(mdl); // Let's figure out what kind of list it is - ordered or unordered? nested? var format0 = new[] { "1", "1", "1", "1" }; foreach (var item in flat) { format0[item.Level] = (item.IsBulletOrdered ? "1" : "o"); } var format = string.Join("", format0); var numberingPart = wdoc.MainDocumentPart.NumberingDefinitionsPart ?? wdoc.MainDocumentPart.AddNewPart <NumberingDefinitionsPart>("NumberingDefinitionsPart001"); if (numberingPart.Numbering == null) { numberingPart.Numbering = new Numbering(); } Func <int, bool, Level> createLevel; createLevel = (level, isOrdered) => { var numformat = NumberFormatValues.Bullet; var levelText = new[] { "·", "o", "·", "o" }[level]; if (isOrdered && level == 0) { numformat = NumberFormatValues.Decimal; levelText = "%1."; } if (isOrdered && level == 1) { numformat = NumberFormatValues.LowerLetter; levelText = "%2."; } if (isOrdered && level == 2) { numformat = NumberFormatValues.LowerRoman; levelText = "%3."; } if (isOrdered && level == 3) { numformat = NumberFormatValues.LowerRoman; levelText = "%4."; } var r = new Level { LevelIndex = level }; r.Append(new StartNumberingValue { Val = 1 }); r.Append(new NumberingFormat { Val = numformat }); r.Append(new LevelText { Val = levelText }); r.Append(new ParagraphProperties(new Indentation { Left = (540 + 360 * level).ToString(), Hanging = "360" })); if (levelText == "·") { r.Append(new NumberingSymbolRunProperties(new RunFonts { Hint = FontTypeHintValues.Default, Ascii = "Symbol", HighAnsi = "Symbol", EastAsia = "Times new Roman", ComplexScript = "Times new Roman" })); } if (levelText == "o") { r.Append(new NumberingSymbolRunProperties(new RunFonts { Hint = FontTypeHintValues.Default, Ascii = "Courier New", HighAnsi = "Courier New", ComplexScript = "Courier New" })); } return(r); }; var level0 = createLevel(0, format[0] == '1'); var level1 = createLevel(1, format[1] == '1'); var level2 = createLevel(2, format[2] == '1'); var level3 = createLevel(3, format[3] == '1'); var abstracts = numberingPart.Numbering.OfType <AbstractNum>().Select(an => an.AbstractNumberId.Value).ToList(); var aid = (abstracts.Count == 0 ? 1 : abstracts.Max() + 1); var aabstract = new AbstractNum(new MultiLevelType() { Val = MultiLevelValues.Multilevel }, level0, level1, level2, level3) { AbstractNumberId = aid }; numberingPart.Numbering.InsertAt(aabstract, 0); var instances = numberingPart.Numbering.OfType <NumberingInstance>().Select(ni => ni.NumberID.Value); var nid = (instances.Count() == 0 ? 1 : instances.Max() + 1); var numInstance = new NumberingInstance(new AbstractNumId { Val = aid }) { NumberID = nid }; numberingPart.Numbering.AppendChild(numInstance); // We'll also figure out the indentation(for the benefit of those paragraphs that should be // indendent with the list but aren't numbered). I'm not sure what the indent comes from. // in the docx, each AbstractNum that I created has an indent for each of its levels, // defaulted at 900, 1260, 1620, ... but I can't see where in the above code that's created? Func <int, string> calcIndent = level => (540 + level * 360).ToString(); foreach (var item in flat) { var content = item.Paragraph; if (content.IsParagraph || content.IsSpan) { var spans = (content.IsParagraph ? (content as MarkdownParagraph.Paragraph).Item : (content as MarkdownParagraph.Span).Item); if (item.HasBullet) { yield return new Paragraph(Spans2Elements(spans)) { ParagraphProperties = new ParagraphProperties(new NumberingProperties(new ParagraphStyleId { Val = "ListParagraph" }, new NumberingLevelReference { Val = item.Level }, new NumberingId { Val = nid })) } } ; else { yield return new Paragraph(Spans2Elements(spans)) { ParagraphProperties = new ParagraphProperties(new Indentation { Left = calcIndent(item.Level) }) } }; } else if (content.IsQuotedBlock || content.IsCodeBlock) { foreach (var p in Paragraph2Paragraphs(content)) { var props = p.GetFirstChild <ParagraphProperties>(); if (props == null) { props = new ParagraphProperties(); p.InsertAt(props, 0); } var indent = props?.GetFirstChild <Indentation>(); if (indent == null) { indent = new Indentation(); props.Append(indent); } indent.Left = calcIndent(item.Level); yield return(p); } } else if (content.IsTableBlock) { foreach (var p in Paragraph2Paragraphs(content)) { var table = p as Table; if (table == null) { yield return(p); continue; } var tprops = table.GetFirstChild <TableProperties>(); var tindent = tprops?.GetFirstChild <TableIndentation>(); if (tindent == null) { throw new Exception("Ooops! Table is missing indentation"); } tindent.Width = int.Parse(calcIndent(item.Level)); yield return(table); } } else { throw new Exception("Unexpected item in list"); } } } else if (md.IsCodeBlock) { var mdc = md as MarkdownParagraph.CodeBlock; var code = mdc.Item1; var lang = mdc.Item2; code = BugWorkaroundDecode(code); var runs = new List <Run>(); var onFirstLine = true; IEnumerable <ColorizedLine> lines; if (lang == "csharp" || lang == "c#" || lang == "cs") { lines = Colorize.CSharp(code); } else if (lang == "vb" || lang == "vbnet" || lang == "vb.net") { lines = Colorize.VB(code); } else if (lang == "" || lang == "xml") { lines = Colorize.PlainText(code); } else if (lang == "antlr") { lines = Antlr.ColorizeAntlr(code); } else { throw new NotSupportedException($"unrecognized language {lang}"); } foreach (var line in lines) { if (onFirstLine) { onFirstLine = false; } else { runs.Add(new Run(new Break())); } foreach (var word in line.Words) { var run = new Run(); var props = new RunProperties(); if (word.Red != 0 || word.Green != 0 || word.Blue != 0) { props.Append(new Color { Val = $"{word.Red:X2}{word.Green:X2}{word.Blue:X2}" }); } if (word.IsItalic) { props.Append(new Italic()); } if (props.HasChildren) { run.Append(props); } run.Append(new Text(word.Text) { Space = SpaceProcessingModeValues.Preserve }); runs.Add(run); } } var style = new ParagraphStyleId { Val = (lang == "antlr" ? "Grammar" : "Code") }; yield return(new Paragraph(runs) { ParagraphProperties = new ParagraphProperties(style) }); } else if (md.IsQuotedBlock) { var mdq = md as MarkdownParagraph.QuotedBlock; var quoteds = mdq.Item; var kind = ""; foreach (var quoted0 in quoteds) { var quoted = quoted0; if (quoted.IsParagraph) { var p = quoted as MarkdownParagraph.Paragraph; var spans = p.Item; if (spans.Any() && spans.First().IsStrong) { var strong = (spans.First() as MarkdownSpan.Strong).Item; if (strong.Any() && strong.First().IsLiteral) { var literal = mdunescape(strong.First() as MarkdownSpan.Literal); if (literal == "Note") { kind = "AlertText"; } else if (literal == "Annotation") { kind = "Annotation"; yield return(new Paragraph(Span2Elements(spans.Head)) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = kind }) }); if (spans.Tail.Any() && spans.Tail.First().IsLiteral) { quoted = MarkdownParagraph.NewParagraph(new Microsoft.FSharp.Collections.FSharpList <MarkdownSpan>(MarkdownSpan.NewLiteral(mdunescape(spans.Tail.First() as MarkdownSpan.Literal).TrimStart()), spans.Tail.Tail)); } else { quoted = MarkdownParagraph.NewParagraph(spans.Tail); } } } } // foreach (var qp in Paragraph2Paragraphs(quoted)) { var qpp = qp as Paragraph; if (qpp != null) { var props = new ParagraphProperties(new ParagraphStyleId() { Val = kind }); qpp.ParagraphProperties = props; } yield return(qp); } } else if (quoted.IsCodeBlock) { var mdc = quoted as MarkdownParagraph.CodeBlock; var code = mdc.Item1; var lang = mdc.Item2; var ignoredAfterLang = mdc.Item3; var lines = code.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None).ToList(); if (string.IsNullOrWhiteSpace(lines.Last())) { lines.RemoveAt(lines.Count - 1); } var run = new Run() { RunProperties = new RunProperties(new RunStyle { Val = "CodeEmbedded" }) }; foreach (var line in lines) { if (run.ChildElements.Count() > 1) { run.Append(new Break()); } run.Append(new Text(" " + line) { Space = SpaceProcessingModeValues.Preserve }); } yield return(new Paragraph(run) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = kind }) }); } else if (quoted.IsListBlock) { if (!(quoted as MarkdownParagraph.ListBlock).Item1.IsOrdered) { throw new NotImplementedException("unordered list inside annotation"); } var count = 1; foreach (var qp in Paragraph2Paragraphs(quoted)) { var qpp = qp as Paragraph; if (qpp == null) { yield return(qp); continue; } qp.InsertAt(new Run(new Text($"{count}. ") { Space = SpaceProcessingModeValues.Preserve }), 0); count += 1; var props = new ParagraphProperties(new ParagraphStyleId() { Val = kind }); qpp.ParagraphProperties = props; yield return(qp); } } } } else if (md.IsTableBlock) { var mdt = md as MarkdownParagraph.TableBlock; var header = mdt.Item1.Option(); var align = mdt.Item2; var rows = mdt.Item3; var table = new Table(); var tstyle = new TableStyle { Val = "TableGrid" }; var tindent = new TableIndentation { Width = 360, Type = TableWidthUnitValues.Dxa }; var tborders = new TableBorders(); tborders.TopBorder = new TopBorder { Val = BorderValues.Single }; tborders.BottomBorder = new BottomBorder { Val = BorderValues.Single }; tborders.LeftBorder = new LeftBorder { Val = BorderValues.Single }; tborders.RightBorder = new RightBorder { Val = BorderValues.Single }; tborders.InsideHorizontalBorder = new InsideHorizontalBorder { Val = BorderValues.Single }; tborders.InsideVerticalBorder = new InsideVerticalBorder { Val = BorderValues.Single }; var tcellmar = new TableCellMarginDefault(); tcellmar.Append(); table.Append(new TableProperties(tstyle, tindent, tborders)); var ncols = align.Length; for (int irow = -1; irow < rows.Length; irow++) { if (irow == -1 && header == null) { continue; } var mdrow = (irow == -1 ? header : rows[irow]); var row = new TableRow(); for (int icol = 0; icol < Math.Min(ncols, mdrow.Length); icol++) { var mdcell = mdrow[icol]; var cell = new TableCell(); var pars = Paragraphs2Paragraphs(mdcell).ToList(); for (int ip = 0; ip < pars.Count; ip++) { var p = pars[ip] as Paragraph; if (p == null) { cell.Append(pars[ip]); continue; } var props = new ParagraphProperties(new ParagraphStyleId { Val = "TableCellNormal" }); if (align[icol].IsAlignCenter) { props.Append(new Justification { Val = JustificationValues.Center }); } if (align[icol].IsAlignRight) { props.Append(new Justification { Val = JustificationValues.Right }); } p.InsertAt(props, 0); cell.Append(pars[ip]); } if (pars.Count == 0) { cell.Append(new Paragraph(new ParagraphProperties(new SpacingBetweenLines { After = "0" }), new Run(new Text("")))); } row.Append(cell); } table.Append(row); } yield return(new Paragraph(new Run(new Text(""))) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "TableLineBefore" }) }); yield return(table); yield return(new Paragraph(new Run(new Text(""))) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "TableLineAfter" }) }); } else { yield return(new Paragraph(new Run(new Text($"[{md.GetType().Name}]")))); } }
IEnumerable <OpenXmlElement> Span2Elements(MarkdownSpan md, bool nestedSpan = false) { reporter.CurrentSpan = md; if (md.IsLiteral) { var mdl = md as MarkdownSpan.Literal; var s = MarkdownUtilities.UnescapeLiteral(mdl); foreach (var r in Literal2Elements(s, nestedSpan)) { yield return(r); } } else if (md.IsStrong || md.IsEmphasis) { IEnumerable <MarkdownSpan> spans = (md.IsStrong ? (md as MarkdownSpan.Strong).body : (md as MarkdownSpan.Emphasis).body); // Workaround for https://github.com/tpetricek/FSharp.formatting/issues/389 - the markdown parser // turns *this_is_it* into a nested Emphasis["this", Emphasis["is"], "it"] instead of Emphasis["this_is_it"] // What we'll do is preprocess it into Emphasis["this_is_it"] if (md.IsEmphasis) { var spans2 = spans.Select(s => { var _ = ""; if (s.IsEmphasis) { s = (s as MarkdownSpan.Emphasis).body.Single(); _ = "_"; } if (s.IsLiteral) { return(_ + (s as MarkdownSpan.Literal).text + _); } reporter.Error("MD15", $"something odd inside emphasis '{s.GetType().Name}' - only allowed emphasis and literal"); return(""); }); spans = new List <MarkdownSpan>() { MarkdownSpan.NewLiteral(string.Join("", spans2), FSharpOption <MarkdownRange> .None) }; } // Convention is that ***term*** is used to define a term. // That's parsed as Strong, which contains Emphasis, which contains one Literal string literal = null; TermRef termdef = null; if (!nestedSpan && md.IsStrong && spans.Count() == 1 && spans.First().IsEmphasis) { var spans2 = (spans.First() as MarkdownSpan.Emphasis).body; if (spans2.Count() == 1 && spans2.First().IsLiteral) { literal = (spans2.First() as MarkdownSpan.Literal).text; termdef = new TermRef(literal, reporter.Location); if (context.Terms.ContainsKey(literal)) { var def = context.Terms[literal]; reporter.Warning("MD16", $"Term '{literal}' defined a second time"); reporter.Warning("MD16b", $"Here was the previous definition of term '{literal}'", def.Loc); } else { context.Terms.Add(literal, termdef); context.TermKeys.Clear(); } } } // Convention inside our specs is that emphasis only ever contains literals, // either to emphasis some human-text or to refer to an ANTLR-production ProductionRef prodref = null; if (!nestedSpan && md.IsEmphasis && (spans.Count() != 1 || !spans.First().IsLiteral)) { reporter.Error("MD17", $"something odd inside emphasis"); } if (!nestedSpan && md.IsEmphasis && spans.Count() == 1 && spans.First().IsLiteral) { literal = (spans.First() as MarkdownSpan.Literal).text; prodref = productions.FirstOrDefault(pr => pr.Names.Contains(literal)); context.Italics.Add(new ItalicUse(literal, prodref != null ? ItalicUse.ItalicUseKind.Production : ItalicUse.ItalicUseKind.Italic, reporter.Location)); } if (prodref != null) { var props = new RunProperties(new Color { Val = "6A5ACD" }, new Underline { Val = UnderlineValues.Single }); var run = new Run(new Text(literal) { Space = SpaceProcessingModeValues.Preserve }) { RunProperties = props }; var link = new Hyperlink(run) { Anchor = prodref.BookmarkName }; yield return(link); } else if (termdef != null) { context.MaxBookmarkId.Value += 1; yield return(new BookmarkStart { Name = termdef.BookmarkName, Id = context.MaxBookmarkId.Value.ToString() }); var props = new RunProperties(new Italic(), new Bold()); yield return(new Run(new Text(literal) { Space = SpaceProcessingModeValues.Preserve }) { RunProperties = props }); yield return(new BookmarkEnd { Id = context.MaxBookmarkId.Value.ToString() }); } else { foreach (var e in Spans2Elements(spans, true)) { var style = (md.IsStrong ? new Bold() as OpenXmlElement : new Italic()); var run = e as Run; if (run != null) { run.InsertAt(new RunProperties(style), 0); } yield return(e); } } } else if (md.IsInlineCode) { var mdi = md as MarkdownSpan.InlineCode; var code = mdi.code; var txt = new Text(BugWorkaroundDecode(code)) { Space = SpaceProcessingModeValues.Preserve }; var props = new RunProperties(new RunStyle { Val = "CodeEmbedded" }); var run = new Run(txt) { RunProperties = props }; yield return(run); } else if (md.IsLatexInlineMath) { var latex = md as MarkdownSpan.LatexInlineMath; var code = latex.code; // TODO: Make this look nice - if we actually need it. It's possible that it's only present // before subscripts are replaced. var txt = new Text(BugWorkaroundDecode(code)) { Space = SpaceProcessingModeValues.Preserve }; var props = new RunProperties(new RunStyle { Val = "CodeEmbedded" }); var run = new Run(txt) { RunProperties = props }; yield return(run); } else if (md.IsDirectLink || md.IsIndirectLink) { IEnumerable <MarkdownSpan> spans; string url = "", alt = ""; if (md.IsDirectLink) { var mddl = md as MarkdownSpan.DirectLink; spans = mddl.body; url = mddl.link; alt = mddl.title.Option(); } else { var mdil = md as MarkdownSpan.IndirectLink; var original = mdil.original; var id = mdil.key; spans = mdil.body; if (markdownDocument.DefinedLinks.ContainsKey(id)) { url = markdownDocument.DefinedLinks[id].Item1; alt = markdownDocument.DefinedLinks[id].Item2.Option(); } } var anchor = ""; if (spans.Count() == 1 && spans.First().IsLiteral) { anchor = MarkdownUtilities.UnescapeLiteral(spans.First() as MarkdownSpan.Literal); } else if (spans.Count() == 1 && spans.First().IsInlineCode) { anchor = (spans.First() as MarkdownSpan.InlineCode).code; } else { reporter.Error("MD18", $"Link anchor must be Literal or InlineCode, not '{md.GetType().Name}'"); yield break; } if (sections.ContainsKey(url)) { var section = sections[url]; // If we're linking to something with a section number, we know what the link text should be. // (There are a few links that aren't to numbered sections, e.g. to "Annex C".) if (section.Number is object) { var expectedAnchor = "§" + section.Number; if (anchor != expectedAnchor) { reporter.Warning("MD19", $"Mismatch: link anchor is '{anchor}', should be '{expectedAnchor}'"); } } var txt = new Text(anchor) { Space = SpaceProcessingModeValues.Preserve }; var run = new Hyperlink(new Run(txt)) { Anchor = section.BookmarkName }; yield return(run); } else if (url.StartsWith("http:") || url.StartsWith("https:")) { var style = new RunStyle { Val = "Hyperlink" }; var hyperlink = new Hyperlink { DocLocation = url, Tooltip = alt }; foreach (var element in Spans2Elements(spans)) { var run = element as Run; if (run != null) { run.InsertAt(new RunProperties(style), 0); } hyperlink.AppendChild(run); } yield return(hyperlink); } else { // TODO: Make this report an error unconditionally once the subscript "latex-like" Markdown is removed. if (url != "") { reporter.Error("MD28", $"Hyperlink url '{url}' unrecognized - not a recognized heading, and not http"); } } } else if (md.IsHardLineBreak) { // I've only ever seen this arise from dodgy markdown parsing, so I'll ignore it... } else { reporter.Error("MD20", $"Unrecognized markdown element {md.GetType().Name}"); yield return(new Run(new Text($"[{md.GetType().Name}]"))); } }
IEnumerable <OpenXmlElement> Span2Elements(MarkdownSpan md) { if (md.IsLiteral) { var mdl = md as MarkdownSpan.Literal; var s = mdunescape(mdl); yield return(new Run(new Text(s) { Space = SpaceProcessingModeValues.Preserve })); } else if (md.IsStrong || md.IsEmphasis) { IEnumerable <MarkdownSpan> spans = (md.IsStrong ? (md as MarkdownSpan.Strong).Item : (md as MarkdownSpan.Emphasis).Item); // Workaround for https://github.com/tpetricek/FSharp.formatting/issues/389 - the markdown parser // turns *this_is_it* into a nested Emphasis["this", Emphasis["is"], "it"] instead of Emphasis["this_is_it"] // What we'll do is preprocess it into Emphasis["this", "_", "is" "_", "it"] if (md.IsEmphasis) { var spans2 = new List <MarkdownSpan>(); foreach (var s in spans) { if (!s.IsEmphasis) { spans2.Add(s); continue; } spans2.Add(MarkdownSpan.NewLiteral("_")); foreach (var ss in (s as MarkdownSpan.Emphasis).Item) { spans2.Add(ss); } spans2.Add(MarkdownSpan.NewLiteral("_")); } spans = spans2; } foreach (var e in Spans2Elements(spans)) { var style = (md.IsStrong ? new Bold() as OpenXmlElement : new Italic()); var run = e as Run; if (run != null) { run.InsertAt(new RunProperties(style), 0); } yield return(e); } } else if (md.IsInlineCode) { var mdi = md as MarkdownSpan.InlineCode; var code = mdi.Item; var txt = new Text(BugWorkaroundDecode(code)) { Space = SpaceProcessingModeValues.Preserve }; var props = new RunProperties(new RunStyle { Val = "CodeEmbedded" }); var run = new Run(txt) { RunProperties = props }; yield return(run); } else if (md.IsDirectLink || md.IsIndirectLink) { IEnumerable <MarkdownSpan> spans; string url = "", alt = ""; if (md.IsDirectLink) { var mddl = md as MarkdownSpan.DirectLink; spans = mddl.Item1; url = mddl.Item2.Item1; alt = mddl.Item2.Item2.Option(); } else { var mdil = md as MarkdownSpan.IndirectLink; var original = mdil.Item2; var id = mdil.Item3; spans = mdil.Item1; if (mddoc.DefinedLinks.ContainsKey(id)) { url = mddoc.DefinedLinks[id].Item1; alt = mddoc.DefinedLinks[id].Item2.Option(); } } var anchor = ""; if (spans.Count() == 1 && spans.First().IsLiteral) { anchor = mdunescape(spans.First() as MarkdownSpan.Literal); } else if (spans.Count() == 1 && spans.First().IsInlineCode) { anchor = (spans.First() as MarkdownSpan.InlineCode).Item; } else { throw new NotImplementedException("Link anchor must be Literal or InlineCode, not " + md.ToString()); } if (sections.ContainsKey(url)) { var section = sections[url]; if (anchor != section.Title) { throw new Exception($"Mismatch: link anchor is '{anchor}', should be '{section.Title}'"); } var txt = new Text("§" + section.Number) { Space = SpaceProcessingModeValues.Preserve }; var run = new Hyperlink(new Run(txt)) { Anchor = section.BookmarkName }; yield return(run); } else if (url.StartsWith("http:") || url.StartsWith("https:")) { var style = new RunStyle { Val = "Hyperlink" }; var hyperlink = new Hyperlink { DocLocation = url, Tooltip = alt }; foreach (var element in Spans2Elements(spans)) { var run = element as Run; if (run != null) { run.InsertAt(new RunProperties(style), 0); } hyperlink.AppendChild(run); } yield return(hyperlink); } else { throw new Exception("Absent hyperlink in " + md.ToString()); } } else if (md.IsHardLineBreak) { // I've only ever seen this arise from dodgy markdown parsing, so I'll ignore it... } else { yield return(new Run(new Text($"[{md.GetType().Name}]"))); } }
IEnumerable <OpenXmlCompositeElement> Paragraph2Paragraphs(MarkdownParagraph md) { reporter.CurrentParagraph = md; if (md.IsHeading) { var mdh = md as MarkdownParagraph.Heading; var level = mdh.size; var spans = mdh.body; var sr = sections[new SectionRef(mdh, filename).Url]; reporter.CurrentSection = sr; var properties = new List <OpenXmlElement> { new ParagraphStyleId { Val = $"Heading{level}" } }; if (sr.Number is null) { properties.Add(new NumberingProperties(new NumberingLevelReference { Val = 0 }, new NumberingId { Val = 0 })); } var props = new ParagraphProperties(properties); var p = new Paragraph { ParagraphProperties = props }; context.MaxBookmarkId.Value += 1; p.AppendChild(new BookmarkStart { Name = sr.BookmarkName, Id = context.MaxBookmarkId.Value.ToString() }); p.Append(Span2Elements(MarkdownSpan.NewLiteral(sr.TitleWithoutNumber, FSharpOption <MarkdownRange> .None))); p.AppendChild(new BookmarkEnd { Id = context.MaxBookmarkId.Value.ToString() }); yield return(p); var i = sr.Url.IndexOf("#"); string currentSection = $"{sr.Url.Substring(0, i)} {new string('#', level)} {sr.Title} [{sr.Number}]"; reporter.Log(currentSection); yield break; } else if (md.IsParagraph) { var mdp = md as MarkdownParagraph.Paragraph; var spans = mdp.body; yield return(new Paragraph(Spans2Elements(spans))); yield break; } else if (md.IsQuotedBlock) { var mdq = md as MarkdownParagraph.QuotedBlock; // TODO: Actually make this a block quote. // See https://github.com/ECMA-TC49-TG2/conversion-to-markdown/issues/123 foreach (var paragraph in mdq.paragraphs.SelectMany(Paragraph2Paragraphs)) { yield return(paragraph); } yield break; } else if (md.IsListBlock) { var mdl = md as MarkdownParagraph.ListBlock; var flat = FlattenList(mdl); // Let's figure out what kind of list it is - ordered or unordered? nested? var format0 = new[] { "1", "1", "1", "1" }; foreach (var item in flat) { format0[item.Level] = (item.IsBulletOrdered ? "1" : "o"); } var format = string.Join("", format0); var numberingPart = wordDocument.MainDocumentPart.NumberingDefinitionsPart ?? wordDocument.MainDocumentPart.AddNewPart <NumberingDefinitionsPart>("NumberingDefinitionsPart001"); if (numberingPart.Numbering == null) { numberingPart.Numbering = new Numbering(); } Func <int, bool, Level> createLevel; createLevel = (level, isOrdered) => { var numformat = NumberFormatValues.Bullet; var levelText = new[] { "·", "o", "·", "o" }[level]; if (isOrdered && level == 0) { numformat = NumberFormatValues.Decimal; levelText = "%1."; } if (isOrdered && level == 1) { numformat = NumberFormatValues.LowerLetter; levelText = "%2."; } if (isOrdered && level == 2) { numformat = NumberFormatValues.LowerRoman; levelText = "%3."; } if (isOrdered && level == 3) { numformat = NumberFormatValues.LowerRoman; levelText = "%4."; } var r = new Level { LevelIndex = level }; r.Append(new StartNumberingValue { Val = 1 }); r.Append(new NumberingFormat { Val = numformat }); r.Append(new LevelText { Val = levelText }); r.Append(new ParagraphProperties(new Indentation { Left = (540 + 360 * level).ToString(), Hanging = "360" })); if (levelText == "·") { r.Append(new NumberingSymbolRunProperties(new RunFonts { Hint = FontTypeHintValues.Default, Ascii = "Symbol", HighAnsi = "Symbol", EastAsia = "Times new Roman", ComplexScript = "Times new Roman" })); } if (levelText == "o") { r.Append(new NumberingSymbolRunProperties(new RunFonts { Hint = FontTypeHintValues.Default, Ascii = "Courier New", HighAnsi = "Courier New", ComplexScript = "Courier New" })); } return(r); }; var level0 = createLevel(0, format[0] == '1'); var level1 = createLevel(1, format[1] == '1'); var level2 = createLevel(2, format[2] == '1'); var level3 = createLevel(3, format[3] == '1'); var abstracts = numberingPart.Numbering.OfType <AbstractNum>().Select(an => an.AbstractNumberId.Value).ToList(); var aid = (abstracts.Count == 0 ? 1 : abstracts.Max() + 1); var aabstract = new AbstractNum(new MultiLevelType() { Val = MultiLevelValues.Multilevel }, level0, level1, level2, level3) { AbstractNumberId = aid }; numberingPart.Numbering.InsertAt(aabstract, 0); var instances = numberingPart.Numbering.OfType <NumberingInstance>().Select(ni => ni.NumberID.Value); var nid = (instances.Count() == 0 ? 1 : instances.Max() + 1); var numInstance = new NumberingInstance(new AbstractNumId { Val = aid }) { NumberID = nid }; numberingPart.Numbering.AppendChild(numInstance); // We'll also figure out the indentation(for the benefit of those paragraphs that should be // indendent with the list but aren't numbered). I'm not sure what the indent comes from. // in the docx, each AbstractNum that I created has an indent for each of its levels, // defaulted at 900, 1260, 1620, ... but I can't see where in the above code that's created? Func <int, string> calcIndent = level => (540 + level * 360).ToString(); foreach (var item in flat) { var content = item.Paragraph; if (content.IsParagraph || content.IsSpan) { var spans = (content.IsParagraph ? (content as MarkdownParagraph.Paragraph).body : (content as MarkdownParagraph.Span).body); if (item.HasBullet) { yield return(new Paragraph(Spans2Elements(spans)) { ParagraphProperties = new ParagraphProperties(new NumberingProperties(new ParagraphStyleId { Val = "ListParagraph" }, new NumberingLevelReference { Val = item.Level }, new NumberingId { Val = nid })) }); } else { yield return(new Paragraph(Spans2Elements(spans)) { ParagraphProperties = new ParagraphProperties(new Indentation { Left = calcIndent(item.Level) }) }); } } else if (content.IsQuotedBlock || content.IsCodeBlock) { foreach (var p in Paragraph2Paragraphs(content)) { var props = p.GetFirstChild <ParagraphProperties>(); if (props == null) { props = new ParagraphProperties(); p.InsertAt(props, 0); } var indent = props?.GetFirstChild <Indentation>(); if (indent == null) { indent = new Indentation(); props.Append(indent); } indent.Left = calcIndent(item.Level); yield return(p); } } else if (content.IsTableBlock) { foreach (var p in Paragraph2Paragraphs(content)) { var table = p as Table; if (table == null) { yield return(p); continue; } var tprops = table.GetFirstChild <TableProperties>(); var tindent = tprops?.GetFirstChild <TableIndentation>(); if (tindent == null) { throw new Exception("Ooops! Table is missing indentation"); } tindent.Width = int.Parse(calcIndent(item.Level)); yield return(table); } } else { reporter.Error("MD08", $"Unexpected item in list '{content.GetType().Name}'"); } } } else if (md.IsCodeBlock) { var mdc = md as MarkdownParagraph.CodeBlock; var code = mdc.code; var lang = mdc.language; code = BugWorkaroundDecode(code); var runs = new List <Run>(); var onFirstLine = true; IEnumerable <ColorizedLine> lines; switch (lang) { case "csharp": case "c#": case "cs": lines = Colorize.CSharp(code); break; case "vb": case "vbnet": case "vb.net": lines = Colorize.VB(code); break; case "": case "console": case "xml": lines = Colorize.PlainText(code); break; case "ANTLR": case "antlr": lines = Antlr.ColorizeAntlr(code); break; default: reporter.Error("MD09", $"unrecognized language {lang}"); lines = Colorize.PlainText(code); break; } foreach (var line in lines) { if (onFirstLine) { onFirstLine = false; } else { runs.Add(new Run(new Break())); } foreach (var word in line.Words) { var run = new Run(); var props = new RunProperties(); if (word.Red != 0 || word.Green != 0 || word.Blue != 0) { props.Append(new Color { Val = $"{word.Red:X2}{word.Green:X2}{word.Blue:X2}" }); } if (word.IsItalic) { props.Append(new Italic()); } if (props.HasChildren) { run.Append(props); } run.Append(new Text(word.Text) { Space = SpaceProcessingModeValues.Preserve }); runs.Add(run); } } if (lang == "antlr") { var p = new Paragraph() { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "Grammar" }) }; var prodref = productions.Single(prod => prod.Code == code); context.MaxBookmarkId.Value += 1; p.AppendChild(new BookmarkStart { Name = prodref.BookmarkName, Id = context.MaxBookmarkId.Value.ToString() }); p.Append(runs); p.AppendChild(new BookmarkEnd { Id = context.MaxBookmarkId.Value.ToString() }); yield return(p); } else { var p = new Paragraph() { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "Code" }) }; p.Append(runs); yield return(p); } } else if (md.IsTableBlock) { var mdt = md as MarkdownParagraph.TableBlock; var header = mdt.headers.Option(); var align = mdt.alignments; var rows = mdt.rows; var table = new Table(); if (header == null) { reporter.Error("MD10", "Github requires all tables to have header rows"); } if (!header.Any(cell => cell.Length > 0)) { header = null; // even if Github requires an empty header, we can at least cull it from Docx } var tstyle = new TableStyle { Val = "TableGrid" }; var tindent = new TableIndentation { Width = 360, Type = TableWidthUnitValues.Dxa }; var tborders = new TableBorders(); tborders.TopBorder = new TopBorder { Val = BorderValues.Single }; tborders.BottomBorder = new BottomBorder { Val = BorderValues.Single }; tborders.LeftBorder = new LeftBorder { Val = BorderValues.Single }; tborders.RightBorder = new RightBorder { Val = BorderValues.Single }; tborders.InsideHorizontalBorder = new InsideHorizontalBorder { Val = BorderValues.Single }; tborders.InsideVerticalBorder = new InsideVerticalBorder { Val = BorderValues.Single }; var tcellmar = new TableCellMarginDefault(); tcellmar.Append(); table.Append(new TableProperties(tstyle, tindent, tborders)); var ncols = align.Length; for (int irow = -1; irow < rows.Length; irow++) { if (irow == -1 && header == null) { continue; } var mdrow = (irow == -1 ? header : rows[irow]); var row = new TableRow(); for (int icol = 0; icol < Math.Min(ncols, mdrow.Length); icol++) { var mdcell = mdrow[icol]; var cell = new TableCell(); var pars = Paragraphs2Paragraphs(mdcell).ToList(); for (int ip = 0; ip < pars.Count; ip++) { var p = pars[ip] as Paragraph; if (p == null) { cell.Append(pars[ip]); continue; } var props = new ParagraphProperties(new ParagraphStyleId { Val = "TableCellNormal" }); if (align[icol].IsAlignCenter) { props.Append(new Justification { Val = JustificationValues.Center }); } if (align[icol].IsAlignRight) { props.Append(new Justification { Val = JustificationValues.Right }); } p.InsertAt(props, 0); cell.Append(pars[ip]); } if (pars.Count == 0) { cell.Append(new Paragraph(new ParagraphProperties(new SpacingBetweenLines { After = "0" }), new Run(new Text("")))); } row.Append(cell); } table.Append(row); } yield return(new Paragraph(new Run(new Text(""))) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "TableLineBefore" }) }); yield return(table); yield return(new Paragraph(new Run(new Text(""))) { ParagraphProperties = new ParagraphProperties(new ParagraphStyleId { Val = "TableLineAfter" }) }); } else { reporter.Error("MD11", $"Unrecognized markdown element {md.GetType().Name}"); yield return(new Paragraph(new Run(new Text($"[{md.GetType().Name}]")))); } }