github.com/dominikszabo/hugo-ds-clean@v0.47.1/helpers/content.go (about) 1 // Copyright 2015 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package helpers implements general utility functions that work with 15 // and on content. The helper functions defined here lay down the 16 // foundation of how Hugo works with files and filepaths, and perform 17 // string operations on content. 18 package helpers 19 20 import ( 21 "bytes" 22 "fmt" 23 "html/template" 24 "os/exec" 25 "unicode" 26 "unicode/utf8" 27 28 "github.com/gohugoio/hugo/common/maps" 29 30 "github.com/chaseadamsio/goorgeous" 31 bp "github.com/gohugoio/hugo/bufferpool" 32 "github.com/gohugoio/hugo/config" 33 "github.com/miekg/mmark" 34 "github.com/mitchellh/mapstructure" 35 "github.com/russross/blackfriday" 36 jww "github.com/spf13/jwalterweatherman" 37 38 "strings" 39 ) 40 41 // SummaryDivider denotes where content summarization should end. The default is "<!--more-->". 42 var SummaryDivider = []byte("<!--more-->") 43 44 // ContentSpec provides functionality to render markdown content. 45 type ContentSpec struct { 46 BlackFriday *BlackFriday 47 footnoteAnchorPrefix string 48 footnoteReturnLinkContents string 49 // SummaryLength is the length of the summary that Hugo extracts from a content. 50 summaryLength int 51 52 BuildFuture bool 53 BuildExpired bool 54 BuildDrafts bool 55 56 Highlight func(code, lang, optsStr string) (string, error) 57 defatultPygmentsOpts map[string]string 58 59 cfg config.Provider 60 } 61 62 // NewContentSpec returns a ContentSpec initialized 63 // with the appropriate fields from the given config.Provider. 64 func NewContentSpec(cfg config.Provider) (*ContentSpec, error) { 65 bf := newBlackfriday(cfg.GetStringMap("blackfriday")) 66 spec := &ContentSpec{ 67 BlackFriday: bf, 68 footnoteAnchorPrefix: cfg.GetString("footnoteAnchorPrefix"), 69 footnoteReturnLinkContents: cfg.GetString("footnoteReturnLinkContents"), 70 summaryLength: cfg.GetInt("summaryLength"), 71 BuildFuture: cfg.GetBool("buildFuture"), 72 BuildExpired: cfg.GetBool("buildExpired"), 73 BuildDrafts: cfg.GetBool("buildDrafts"), 74 75 cfg: cfg, 76 } 77 78 // Highlighting setup 79 options, err := parseDefaultPygmentsOpts(cfg) 80 if err != nil { 81 return nil, err 82 } 83 spec.defatultPygmentsOpts = options 84 85 // Use the Pygmentize on path if present 86 useClassic := false 87 h := newHiglighters(spec) 88 89 if cfg.GetBool("pygmentsUseClassic") { 90 if !hasPygments() { 91 jww.WARN.Println("Highlighting with pygmentsUseClassic set requires Pygments to be installed and in the path") 92 } else { 93 useClassic = true 94 } 95 } 96 97 if useClassic { 98 spec.Highlight = h.pygmentsHighlight 99 } else { 100 spec.Highlight = h.chromaHighlight 101 } 102 103 return spec, nil 104 } 105 106 // BlackFriday holds configuration values for BlackFriday rendering. 107 type BlackFriday struct { 108 Smartypants bool 109 SmartypantsQuotesNBSP bool 110 AngledQuotes bool 111 Fractions bool 112 HrefTargetBlank bool 113 NofollowLinks bool 114 NoreferrerLinks bool 115 SmartDashes bool 116 LatexDashes bool 117 TaskLists bool 118 PlainIDAnchors bool 119 Extensions []string 120 ExtensionsMask []string 121 } 122 123 // NewBlackfriday creates a new Blackfriday filled with site config or some sane defaults. 124 func newBlackfriday(config map[string]interface{}) *BlackFriday { 125 defaultParam := map[string]interface{}{ 126 "smartypants": true, 127 "angledQuotes": false, 128 "smartypantsQuotesNBSP": false, 129 "fractions": true, 130 "hrefTargetBlank": false, 131 "nofollowLinks": false, 132 "noreferrerLinks": false, 133 "smartDashes": true, 134 "latexDashes": true, 135 "plainIDAnchors": true, 136 "taskLists": true, 137 } 138 139 maps.ToLower(defaultParam) 140 141 siteConfig := make(map[string]interface{}) 142 143 for k, v := range defaultParam { 144 siteConfig[k] = v 145 } 146 147 if config != nil { 148 for k, v := range config { 149 siteConfig[k] = v 150 } 151 } 152 153 combinedConfig := &BlackFriday{} 154 if err := mapstructure.Decode(siteConfig, combinedConfig); err != nil { 155 jww.FATAL.Printf("Failed to get site rendering config\n%s", err.Error()) 156 } 157 158 return combinedConfig 159 } 160 161 var blackfridayExtensionMap = map[string]int{ 162 "noIntraEmphasis": blackfriday.EXTENSION_NO_INTRA_EMPHASIS, 163 "tables": blackfriday.EXTENSION_TABLES, 164 "fencedCode": blackfriday.EXTENSION_FENCED_CODE, 165 "autolink": blackfriday.EXTENSION_AUTOLINK, 166 "strikethrough": blackfriday.EXTENSION_STRIKETHROUGH, 167 "laxHtmlBlocks": blackfriday.EXTENSION_LAX_HTML_BLOCKS, 168 "spaceHeaders": blackfriday.EXTENSION_SPACE_HEADERS, 169 "hardLineBreak": blackfriday.EXTENSION_HARD_LINE_BREAK, 170 "tabSizeEight": blackfriday.EXTENSION_TAB_SIZE_EIGHT, 171 "footnotes": blackfriday.EXTENSION_FOOTNOTES, 172 "noEmptyLineBeforeBlock": blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK, 173 "headerIds": blackfriday.EXTENSION_HEADER_IDS, 174 "titleblock": blackfriday.EXTENSION_TITLEBLOCK, 175 "autoHeaderIds": blackfriday.EXTENSION_AUTO_HEADER_IDS, 176 "backslashLineBreak": blackfriday.EXTENSION_BACKSLASH_LINE_BREAK, 177 "definitionLists": blackfriday.EXTENSION_DEFINITION_LISTS, 178 "joinLines": blackfriday.EXTENSION_JOIN_LINES, 179 } 180 181 var stripHTMLReplacer = strings.NewReplacer("\n", " ", "</p>", "\n", "<br>", "\n", "<br />", "\n") 182 183 var mmarkExtensionMap = map[string]int{ 184 "tables": mmark.EXTENSION_TABLES, 185 "fencedCode": mmark.EXTENSION_FENCED_CODE, 186 "autolink": mmark.EXTENSION_AUTOLINK, 187 "laxHtmlBlocks": mmark.EXTENSION_LAX_HTML_BLOCKS, 188 "spaceHeaders": mmark.EXTENSION_SPACE_HEADERS, 189 "hardLineBreak": mmark.EXTENSION_HARD_LINE_BREAK, 190 "footnotes": mmark.EXTENSION_FOOTNOTES, 191 "noEmptyLineBeforeBlock": mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK, 192 "headerIds": mmark.EXTENSION_HEADER_IDS, 193 "autoHeaderIds": mmark.EXTENSION_AUTO_HEADER_IDS, 194 } 195 196 // StripHTML accepts a string, strips out all HTML tags and returns it. 197 func StripHTML(s string) string { 198 199 // Shortcut strings with no tags in them 200 if !strings.ContainsAny(s, "<>") { 201 return s 202 } 203 s = stripHTMLReplacer.Replace(s) 204 205 // Walk through the string removing all tags 206 b := bp.GetBuffer() 207 defer bp.PutBuffer(b) 208 var inTag, isSpace, wasSpace bool 209 for _, r := range s { 210 if !inTag { 211 isSpace = false 212 } 213 214 switch { 215 case r == '<': 216 inTag = true 217 case r == '>': 218 inTag = false 219 case unicode.IsSpace(r): 220 isSpace = true 221 fallthrough 222 default: 223 if !inTag && (!isSpace || (isSpace && !wasSpace)) { 224 b.WriteRune(r) 225 } 226 } 227 228 wasSpace = isSpace 229 230 } 231 return b.String() 232 } 233 234 // stripEmptyNav strips out empty <nav> tags from content. 235 func stripEmptyNav(in []byte) []byte { 236 return bytes.Replace(in, []byte("<nav>\n</nav>\n\n"), []byte(``), -1) 237 } 238 239 // BytesToHTML converts bytes to type template.HTML. 240 func BytesToHTML(b []byte) template.HTML { 241 return template.HTML(string(b)) 242 } 243 244 // getHTMLRenderer creates a new Blackfriday HTML Renderer with the given configuration. 245 func (c *ContentSpec) getHTMLRenderer(defaultFlags int, ctx *RenderingContext) blackfriday.Renderer { 246 renderParameters := blackfriday.HtmlRendererParameters{ 247 FootnoteAnchorPrefix: c.footnoteAnchorPrefix, 248 FootnoteReturnLinkContents: c.footnoteReturnLinkContents, 249 } 250 251 b := len(ctx.DocumentID) != 0 252 253 if ctx.Config == nil { 254 panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) 255 } 256 257 if b && !ctx.Config.PlainIDAnchors { 258 renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix 259 renderParameters.HeaderIDSuffix = ":" + ctx.DocumentID 260 } 261 262 htmlFlags := defaultFlags 263 htmlFlags |= blackfriday.HTML_USE_XHTML 264 htmlFlags |= blackfriday.HTML_FOOTNOTE_RETURN_LINKS 265 266 if ctx.Config.Smartypants { 267 htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS 268 } 269 270 if ctx.Config.SmartypantsQuotesNBSP { 271 htmlFlags |= blackfriday.HTML_SMARTYPANTS_QUOTES_NBSP 272 } 273 274 if ctx.Config.AngledQuotes { 275 htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES 276 } 277 278 if ctx.Config.Fractions { 279 htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS 280 } 281 282 if ctx.Config.HrefTargetBlank { 283 htmlFlags |= blackfriday.HTML_HREF_TARGET_BLANK 284 } 285 286 if ctx.Config.NofollowLinks { 287 htmlFlags |= blackfriday.HTML_NOFOLLOW_LINKS 288 } 289 290 if ctx.Config.NoreferrerLinks { 291 htmlFlags |= blackfriday.HTML_NOREFERRER_LINKS 292 } 293 294 if ctx.Config.SmartDashes { 295 htmlFlags |= blackfriday.HTML_SMARTYPANTS_DASHES 296 } 297 298 if ctx.Config.LatexDashes { 299 htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES 300 } 301 302 return &HugoHTMLRenderer{ 303 cs: c, 304 RenderingContext: ctx, 305 Renderer: blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters), 306 } 307 } 308 309 func getMarkdownExtensions(ctx *RenderingContext) int { 310 // Default Blackfriday common extensions 311 commonExtensions := 0 | 312 blackfriday.EXTENSION_NO_INTRA_EMPHASIS | 313 blackfriday.EXTENSION_TABLES | 314 blackfriday.EXTENSION_FENCED_CODE | 315 blackfriday.EXTENSION_AUTOLINK | 316 blackfriday.EXTENSION_STRIKETHROUGH | 317 blackfriday.EXTENSION_SPACE_HEADERS | 318 blackfriday.EXTENSION_HEADER_IDS | 319 blackfriday.EXTENSION_BACKSLASH_LINE_BREAK | 320 blackfriday.EXTENSION_DEFINITION_LISTS 321 322 // Extra Blackfriday extensions that Hugo enables by default 323 flags := commonExtensions | 324 blackfriday.EXTENSION_AUTO_HEADER_IDS | 325 blackfriday.EXTENSION_FOOTNOTES 326 327 if ctx.Config == nil { 328 panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) 329 } 330 331 for _, extension := range ctx.Config.Extensions { 332 if flag, ok := blackfridayExtensionMap[extension]; ok { 333 flags |= flag 334 } 335 } 336 for _, extension := range ctx.Config.ExtensionsMask { 337 if flag, ok := blackfridayExtensionMap[extension]; ok { 338 flags &= ^flag 339 } 340 } 341 return flags 342 } 343 344 func (c ContentSpec) markdownRender(ctx *RenderingContext) []byte { 345 if ctx.RenderTOC { 346 return blackfriday.Markdown(ctx.Content, 347 c.getHTMLRenderer(blackfriday.HTML_TOC, ctx), 348 getMarkdownExtensions(ctx)) 349 } 350 return blackfriday.Markdown(ctx.Content, c.getHTMLRenderer(0, ctx), 351 getMarkdownExtensions(ctx)) 352 } 353 354 // getMmarkHTMLRenderer creates a new mmark HTML Renderer with the given configuration. 355 func (c *ContentSpec) getMmarkHTMLRenderer(defaultFlags int, ctx *RenderingContext) mmark.Renderer { 356 renderParameters := mmark.HtmlRendererParameters{ 357 FootnoteAnchorPrefix: c.footnoteAnchorPrefix, 358 FootnoteReturnLinkContents: c.footnoteReturnLinkContents, 359 } 360 361 b := len(ctx.DocumentID) != 0 362 363 if ctx.Config == nil { 364 panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) 365 } 366 367 if b && !ctx.Config.PlainIDAnchors { 368 renderParameters.FootnoteAnchorPrefix = ctx.DocumentID + ":" + renderParameters.FootnoteAnchorPrefix 369 // renderParameters.HeaderIDSuffix = ":" + ctx.DocumentId 370 } 371 372 htmlFlags := defaultFlags 373 htmlFlags |= mmark.HTML_FOOTNOTE_RETURN_LINKS 374 375 return &HugoMmarkHTMLRenderer{ 376 cs: c, 377 Renderer: mmark.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters), 378 Cfg: c.cfg, 379 } 380 } 381 382 func getMmarkExtensions(ctx *RenderingContext) int { 383 flags := 0 384 flags |= mmark.EXTENSION_TABLES 385 flags |= mmark.EXTENSION_FENCED_CODE 386 flags |= mmark.EXTENSION_AUTOLINK 387 flags |= mmark.EXTENSION_SPACE_HEADERS 388 flags |= mmark.EXTENSION_CITATION 389 flags |= mmark.EXTENSION_TITLEBLOCK_TOML 390 flags |= mmark.EXTENSION_HEADER_IDS 391 flags |= mmark.EXTENSION_AUTO_HEADER_IDS 392 flags |= mmark.EXTENSION_UNIQUE_HEADER_IDS 393 flags |= mmark.EXTENSION_FOOTNOTES 394 flags |= mmark.EXTENSION_SHORT_REF 395 flags |= mmark.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK 396 flags |= mmark.EXTENSION_INCLUDE 397 398 if ctx.Config == nil { 399 panic(fmt.Sprintf("RenderingContext of %q doesn't have a config", ctx.DocumentID)) 400 } 401 402 for _, extension := range ctx.Config.Extensions { 403 if flag, ok := mmarkExtensionMap[extension]; ok { 404 flags |= flag 405 } 406 } 407 return flags 408 } 409 410 func (c ContentSpec) mmarkRender(ctx *RenderingContext) []byte { 411 return mmark.Parse(ctx.Content, c.getMmarkHTMLRenderer(0, ctx), 412 getMmarkExtensions(ctx)).Bytes() 413 } 414 415 // ExtractTOC extracts Table of Contents from content. 416 func ExtractTOC(content []byte) (newcontent []byte, toc []byte) { 417 if !bytes.Contains(content, []byte("<nav>")) { 418 return content, nil 419 } 420 origContent := make([]byte, len(content)) 421 copy(origContent, content) 422 first := []byte(`<nav> 423 <ul>`) 424 425 last := []byte(`</ul> 426 </nav>`) 427 428 replacement := []byte(`<nav id="TableOfContents"> 429 <ul>`) 430 431 startOfTOC := bytes.Index(content, first) 432 433 peekEnd := len(content) 434 if peekEnd > 70+startOfTOC { 435 peekEnd = 70 + startOfTOC 436 } 437 438 if startOfTOC < 0 { 439 return stripEmptyNav(content), toc 440 } 441 // Need to peek ahead to see if this nav element is actually the right one. 442 correctNav := bytes.Index(content[startOfTOC:peekEnd], []byte(`<li><a href="#`)) 443 if correctNav < 0 { // no match found 444 return content, toc 445 } 446 lengthOfTOC := bytes.Index(content[startOfTOC:], last) + len(last) 447 endOfTOC := startOfTOC + lengthOfTOC 448 449 newcontent = append(content[:startOfTOC], content[endOfTOC:]...) 450 toc = append(replacement, origContent[startOfTOC+len(first):endOfTOC]...) 451 return 452 } 453 454 // RenderingContext holds contextual information, like content and configuration, 455 // for a given content rendering. 456 // By creating you must set the Config, otherwise it will panic. 457 type RenderingContext struct { 458 Content []byte 459 PageFmt string 460 DocumentID string 461 DocumentName string 462 Config *BlackFriday 463 RenderTOC bool 464 Cfg config.Provider 465 } 466 467 // RenderBytes renders a []byte. 468 func (c ContentSpec) RenderBytes(ctx *RenderingContext) []byte { 469 switch ctx.PageFmt { 470 default: 471 return c.markdownRender(ctx) 472 case "markdown": 473 return c.markdownRender(ctx) 474 case "asciidoc": 475 return getAsciidocContent(ctx) 476 case "mmark": 477 return c.mmarkRender(ctx) 478 case "rst": 479 return getRstContent(ctx) 480 case "org": 481 return orgRender(ctx, c) 482 case "pandoc": 483 return getPandocContent(ctx) 484 } 485 } 486 487 // TotalWords counts instance of one or more consecutive white space 488 // characters, as defined by unicode.IsSpace, in s. 489 // This is a cheaper way of word counting than the obvious len(strings.Fields(s)). 490 func TotalWords(s string) int { 491 n := 0 492 inWord := false 493 for _, r := range s { 494 wasInWord := inWord 495 inWord = !unicode.IsSpace(r) 496 if inWord && !wasInWord { 497 n++ 498 } 499 } 500 return n 501 } 502 503 // Old implementation only kept for benchmark comparison. 504 // TODO(bep) remove 505 func totalWordsOld(s string) int { 506 return len(strings.Fields(s)) 507 } 508 509 // TruncateWordsByRune truncates words by runes. 510 func (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) { 511 words := make([]string, len(in)) 512 copy(words, in) 513 514 count := 0 515 for index, word := range words { 516 if count >= c.summaryLength { 517 return strings.Join(words[:index], " "), true 518 } 519 runeCount := utf8.RuneCountInString(word) 520 if len(word) == runeCount { 521 count++ 522 } else if count+runeCount < c.summaryLength { 523 count += runeCount 524 } else { 525 for ri := range word { 526 if count >= c.summaryLength { 527 truncatedWords := append(words[:index], word[:ri]) 528 return strings.Join(truncatedWords, " "), true 529 } 530 count++ 531 } 532 } 533 } 534 535 return strings.Join(words, " "), false 536 } 537 538 // TruncateWordsToWholeSentence takes content and truncates to whole sentence 539 // limited by max number of words. It also returns whether it is truncated. 540 func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) { 541 var ( 542 wordCount = 0 543 lastWordIndex = -1 544 ) 545 546 for i, r := range s { 547 if unicode.IsSpace(r) { 548 wordCount++ 549 lastWordIndex = i 550 551 if wordCount >= c.summaryLength { 552 break 553 } 554 555 } 556 } 557 558 if lastWordIndex == -1 { 559 return s, false 560 } 561 562 endIndex := -1 563 564 for j, r := range s[lastWordIndex:] { 565 if isEndOfSentence(r) { 566 endIndex = j + lastWordIndex + utf8.RuneLen(r) 567 break 568 } 569 } 570 571 if endIndex == -1 { 572 return s, false 573 } 574 575 return strings.TrimSpace(s[:endIndex]), endIndex < len(s) 576 } 577 578 func isEndOfSentence(r rune) bool { 579 return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n' 580 } 581 582 // Kept only for benchmark. 583 func (c *ContentSpec) truncateWordsToWholeSentenceOld(content string) (string, bool) { 584 words := strings.Fields(content) 585 586 if c.summaryLength >= len(words) { 587 return strings.Join(words, " "), false 588 } 589 590 for counter, word := range words[c.summaryLength:] { 591 if strings.HasSuffix(word, ".") || 592 strings.HasSuffix(word, "?") || 593 strings.HasSuffix(word, ".\"") || 594 strings.HasSuffix(word, "!") { 595 upper := c.summaryLength + counter + 1 596 return strings.Join(words[:upper], " "), (upper < len(words)) 597 } 598 } 599 600 return strings.Join(words[:c.summaryLength], " "), true 601 } 602 603 func getAsciidocExecPath() string { 604 path, err := exec.LookPath("asciidoc") 605 if err != nil { 606 return "" 607 } 608 return path 609 } 610 611 func getAsciidoctorExecPath() string { 612 path, err := exec.LookPath("asciidoctor") 613 if err != nil { 614 return "" 615 } 616 return path 617 } 618 619 // HasAsciidoc returns whether Asciidoc or Asciidoctor is installed on this computer. 620 func HasAsciidoc() bool { 621 return (getAsciidoctorExecPath() != "" || 622 getAsciidocExecPath() != "") 623 } 624 625 // getAsciidocContent calls asciidoctor or asciidoc as an external helper 626 // to convert AsciiDoc content to HTML. 627 func getAsciidocContent(ctx *RenderingContext) []byte { 628 var isAsciidoctor bool 629 path := getAsciidoctorExecPath() 630 if path == "" { 631 path = getAsciidocExecPath() 632 if path == "" { 633 jww.ERROR.Println("asciidoctor / asciidoc not found in $PATH: Please install.\n", 634 " Leaving AsciiDoc content unrendered.") 635 return ctx.Content 636 } 637 } else { 638 isAsciidoctor = true 639 } 640 641 jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...") 642 args := []string{"--no-header-footer", "--safe"} 643 if isAsciidoctor { 644 // asciidoctor-specific arg to show stack traces on errors 645 args = append(args, "--trace") 646 } 647 args = append(args, "-") 648 return externallyRenderContent(ctx, path, args) 649 } 650 651 // HasRst returns whether rst2html is installed on this computer. 652 func HasRst() bool { 653 return getRstExecPath() != "" 654 } 655 656 func getRstExecPath() string { 657 path, err := exec.LookPath("rst2html") 658 if err != nil { 659 path, err = exec.LookPath("rst2html.py") 660 if err != nil { 661 return "" 662 } 663 } 664 return path 665 } 666 667 func getPythonExecPath() string { 668 path, err := exec.LookPath("python") 669 if err != nil { 670 path, err = exec.LookPath("python.exe") 671 if err != nil { 672 return "" 673 } 674 } 675 return path 676 } 677 678 // getRstContent calls the Python script rst2html as an external helper 679 // to convert reStructuredText content to HTML. 680 func getRstContent(ctx *RenderingContext) []byte { 681 python := getPythonExecPath() 682 path := getRstExecPath() 683 684 if path == "" { 685 jww.ERROR.Println("rst2html / rst2html.py not found in $PATH: Please install.\n", 686 " Leaving reStructuredText content unrendered.") 687 return ctx.Content 688 689 } 690 jww.INFO.Println("Rendering", ctx.DocumentName, "with", path, "...") 691 args := []string{path, "--leave-comments", "--initial-header-level=2"} 692 result := externallyRenderContent(ctx, python, args) 693 // TODO(bep) check if rst2html has a body only option. 694 bodyStart := bytes.Index(result, []byte("<body>\n")) 695 if bodyStart < 0 { 696 bodyStart = -7 //compensate for length 697 } 698 699 bodyEnd := bytes.Index(result, []byte("\n</body>")) 700 if bodyEnd < 0 || bodyEnd >= len(result) { 701 bodyEnd = len(result) - 1 702 if bodyEnd < 0 { 703 bodyEnd = 0 704 } 705 } 706 707 return result[bodyStart+7 : bodyEnd] 708 } 709 710 // getPandocContent calls pandoc as an external helper to convert pandoc markdown to HTML. 711 func getPandocContent(ctx *RenderingContext) []byte { 712 path, err := exec.LookPath("pandoc") 713 if err != nil { 714 jww.ERROR.Println("pandoc not found in $PATH: Please install.\n", 715 " Leaving pandoc content unrendered.") 716 return ctx.Content 717 } 718 args := []string{"--mathjax"} 719 return externallyRenderContent(ctx, path, args) 720 } 721 722 func orgRender(ctx *RenderingContext, c ContentSpec) []byte { 723 content := ctx.Content 724 cleanContent := bytes.Replace(content, []byte("# more"), []byte(""), 1) 725 return goorgeous.Org(cleanContent, 726 c.getHTMLRenderer(blackfriday.HTML_TOC, ctx)) 727 } 728 729 func externallyRenderContent(ctx *RenderingContext, path string, args []string) []byte { 730 content := ctx.Content 731 cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1) 732 733 cmd := exec.Command(path, args...) 734 cmd.Stdin = bytes.NewReader(cleanContent) 735 var out, cmderr bytes.Buffer 736 cmd.Stdout = &out 737 cmd.Stderr = &cmderr 738 err := cmd.Run() 739 // Most external helpers exit w/ non-zero exit code only if severe, i.e. 740 // halting errors occurred. -> log stderr output regardless of state of err 741 for _, item := range strings.Split(string(cmderr.Bytes()), "\n") { 742 item := strings.TrimSpace(item) 743 if item != "" { 744 jww.ERROR.Printf("%s: %s", ctx.DocumentName, item) 745 } 746 } 747 if err != nil { 748 jww.ERROR.Printf("%s rendering %s: %v", path, ctx.DocumentName, err) 749 } 750 751 return normalizeExternalHelperLineFeeds(out.Bytes()) 752 }