mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-07 20:30:36 -05:00
fbf8bcacc4
The flag `HTML_SMARTYPANTS_ANGLED_QUOTES` was added to Blackfriday on Black Friday. This configures rendering of double quotes as angled left and right quotes (« »). Typical use cases would be either or, or combined, but never in the same document. As an example would be a person from Norway; he has a blog in both English and Norwegian (his native tongue); he would then configure Blackfriday to use angled quotes for the Norwegian section, but keep them as reqular double quotes for the English. This commit adds configuration support for this new flag, configuration that can be set in the site configuration, but overridden in page front matter. Fixes #605
262 lines
7.3 KiB
Go
262 lines
7.3 KiB
Go
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
|
//
|
|
// Licensed under the Simple Public License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://opensource.org/licenses/Simple-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//Package helpers implements general utility functions that work with and on content. The helper functions defined here
|
|
//lay down the foundation of how Hugo works with files, filepaths and does string operations on content.
|
|
package helpers
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"os/exec"
|
|
|
|
"github.com/russross/blackfriday"
|
|
"github.com/spf13/viper"
|
|
|
|
jww "github.com/spf13/jwalterweatherman"
|
|
|
|
"strings"
|
|
)
|
|
|
|
// Length of the summary that Hugo extracts from a content.
|
|
var SummaryLength = 70
|
|
|
|
// Custom divider <!--more--> let's user define where summarization ends.
|
|
var SummaryDivider = []byte("<!--more-->")
|
|
|
|
//StripHTML accepts a string, strips out all HTML tags and returns it.
|
|
func StripHTML(s string) string {
|
|
output := ""
|
|
|
|
// Shortcut strings with no tags in them
|
|
if !strings.ContainsAny(s, "<>") {
|
|
output = s
|
|
} else {
|
|
s = strings.Replace(s, "\n", " ", -1)
|
|
s = strings.Replace(s, "</p>", "\n", -1)
|
|
s = strings.Replace(s, "<br>", "\n", -1)
|
|
s = strings.Replace(s, "<br />", "\n", -1) // <br /> is the xhtml line break tag
|
|
|
|
// Walk through the string removing all tags
|
|
b := new(bytes.Buffer)
|
|
inTag := false
|
|
for _, r := range s {
|
|
switch r {
|
|
case '<':
|
|
inTag = true
|
|
case '>':
|
|
inTag = false
|
|
default:
|
|
if !inTag {
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
}
|
|
output = b.String()
|
|
}
|
|
return output
|
|
}
|
|
|
|
// StripEmptyNav strips out empty <nav> tags from content.
|
|
func StripEmptyNav(in []byte) []byte {
|
|
return bytes.Replace(in, []byte("<nav>\n</nav>\n\n"), []byte(``), -1)
|
|
}
|
|
|
|
//BytesToHTML converts bytes to type template.HTML.
|
|
func BytesToHTML(b []byte) template.HTML {
|
|
return template.HTML(string(b))
|
|
}
|
|
|
|
func GetHtmlRenderer(defaultFlags int, ctx RenderingContext) blackfriday.Renderer {
|
|
renderParameters := blackfriday.HtmlRendererParameters{
|
|
FootnoteAnchorPrefix: viper.GetString("FootnoteAnchorPrefix"),
|
|
FootnoteReturnLinkContents: viper.GetString("FootnoteReturnLinkContents"),
|
|
}
|
|
|
|
if len(ctx.DocumentId) != 0 {
|
|
renderParameters.FootnoteAnchorPrefix = ctx.DocumentId + ":" + renderParameters.FootnoteAnchorPrefix
|
|
renderParameters.HeaderIDSuffix = ":" + ctx.DocumentId
|
|
}
|
|
|
|
htmlFlags := defaultFlags
|
|
htmlFlags |= blackfriday.HTML_USE_XHTML
|
|
htmlFlags |= blackfriday.HTML_USE_SMARTYPANTS
|
|
htmlFlags |= blackfriday.HTML_SMARTYPANTS_FRACTIONS
|
|
htmlFlags |= blackfriday.HTML_SMARTYPANTS_LATEX_DASHES
|
|
htmlFlags |= blackfriday.HTML_FOOTNOTE_RETURN_LINKS
|
|
|
|
var angledQuotes bool
|
|
|
|
if m, ok := ctx.ConfigFlags["angledQuotes"]; ok {
|
|
angledQuotes = m
|
|
}
|
|
|
|
if angledQuotes {
|
|
htmlFlags |= blackfriday.HTML_SMARTYPANTS_ANGLED_QUOTES
|
|
}
|
|
|
|
return blackfriday.HtmlRendererWithParameters(htmlFlags, "", "", renderParameters)
|
|
}
|
|
|
|
func GetMarkdownExtensions() int {
|
|
return 0 | blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
|
|
blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE |
|
|
blackfriday.EXTENSION_AUTOLINK | blackfriday.EXTENSION_STRIKETHROUGH |
|
|
blackfriday.EXTENSION_SPACE_HEADERS | blackfriday.EXTENSION_FOOTNOTES |
|
|
blackfriday.EXTENSION_HEADER_IDS | blackfriday.EXTENSION_AUTO_HEADER_IDS
|
|
}
|
|
|
|
func MarkdownRender(ctx RenderingContext) []byte {
|
|
return blackfriday.Markdown(ctx.Content, GetHtmlRenderer(0, ctx),
|
|
GetMarkdownExtensions())
|
|
}
|
|
|
|
func MarkdownRenderWithTOC(ctx RenderingContext) []byte {
|
|
return blackfriday.Markdown(ctx.Content,
|
|
GetHtmlRenderer(blackfriday.HTML_TOC, ctx),
|
|
GetMarkdownExtensions())
|
|
}
|
|
|
|
//ExtractTOC extracts Table of Contents from content.
|
|
func ExtractTOC(content []byte) (newcontent []byte, toc []byte) {
|
|
origContent := make([]byte, len(content))
|
|
copy(origContent, content)
|
|
first := []byte(`<nav>
|
|
<ul>`)
|
|
|
|
last := []byte(`</ul>
|
|
</nav>`)
|
|
|
|
replacement := []byte(`<nav id="TableOfContents">
|
|
<ul>`)
|
|
|
|
startOfTOC := bytes.Index(content, first)
|
|
|
|
peekEnd := len(content)
|
|
if peekEnd > 70+startOfTOC {
|
|
peekEnd = 70 + startOfTOC
|
|
}
|
|
|
|
if startOfTOC < 0 {
|
|
return StripEmptyNav(content), toc
|
|
}
|
|
// Need to peek ahead to see if this nav element is actually the right one.
|
|
correctNav := bytes.Index(content[startOfTOC:peekEnd], []byte(`<li><a href="#`))
|
|
if correctNav < 0 { // no match found
|
|
return content, toc
|
|
}
|
|
lengthOfTOC := bytes.Index(content[startOfTOC:], last) + len(last)
|
|
endOfTOC := startOfTOC + lengthOfTOC
|
|
|
|
newcontent = append(content[:startOfTOC], content[endOfTOC:]...)
|
|
toc = append(replacement, origContent[startOfTOC+len(first):endOfTOC]...)
|
|
return
|
|
}
|
|
|
|
type RenderingContext struct {
|
|
Content []byte
|
|
PageFmt string
|
|
DocumentId string
|
|
ConfigFlags map[string]bool
|
|
}
|
|
|
|
func RenderBytesWithTOC(ctx RenderingContext) []byte {
|
|
switch ctx.PageFmt {
|
|
default:
|
|
return MarkdownRenderWithTOC(ctx)
|
|
case "markdown":
|
|
return MarkdownRenderWithTOC(ctx)
|
|
case "rst":
|
|
return []byte(GetRstContent(ctx.Content))
|
|
}
|
|
}
|
|
|
|
func RenderBytes(ctx RenderingContext) []byte {
|
|
switch ctx.PageFmt {
|
|
default:
|
|
return MarkdownRender(ctx)
|
|
case "markdown":
|
|
return MarkdownRender(ctx)
|
|
case "rst":
|
|
return []byte(GetRstContent(ctx.Content))
|
|
}
|
|
}
|
|
|
|
// TotalWords returns an int of the total number of words in a given content.
|
|
func TotalWords(s string) int {
|
|
return len(strings.Fields(s))
|
|
}
|
|
|
|
//WordCount takes content and returns a map of words and count of each word.
|
|
func WordCount(s string) map[string]int {
|
|
m := make(map[string]int)
|
|
for _, f := range strings.Fields(s) {
|
|
m[f] += 1
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
//RemoveSummaryDivider removes summary-divider <!--more--> from content.
|
|
func RemoveSummaryDivider(content []byte) []byte {
|
|
return bytes.Replace(content, SummaryDivider, []byte(""), -1)
|
|
}
|
|
|
|
//TruncateWords takes content and na int and shortens down the number of words in the content down to the number of int.
|
|
func TruncateWords(s string, max int) string {
|
|
words := strings.Fields(s)
|
|
if max > len(words) {
|
|
return strings.Join(words, " ")
|
|
}
|
|
|
|
return strings.Join(words[:max], " ")
|
|
}
|
|
|
|
//TruncateWordsToWholeSentence takes content and an int and returns entire sentences from content, delimited by the int.
|
|
func TruncateWordsToWholeSentence(s string, max int) string {
|
|
words := strings.Fields(s)
|
|
if max > len(words) {
|
|
return strings.Join(words, " ")
|
|
}
|
|
|
|
for counter, word := range words[max:] {
|
|
if strings.HasSuffix(word, ".") ||
|
|
strings.HasSuffix(word, "?") ||
|
|
strings.HasSuffix(word, ".\"") ||
|
|
strings.HasSuffix(word, "!") {
|
|
return strings.Join(words[:max+counter+1], " ")
|
|
}
|
|
}
|
|
|
|
return strings.Join(words[:max], " ")
|
|
}
|
|
|
|
func GetRstContent(content []byte) string {
|
|
cleanContent := bytes.Replace(content, SummaryDivider, []byte(""), 1)
|
|
|
|
cmd := exec.Command("rst2html.py", "--leave-comments")
|
|
cmd.Stdin = bytes.NewReader(cleanContent)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
if err := cmd.Run(); err != nil {
|
|
jww.ERROR.Println(err)
|
|
}
|
|
|
|
rstLines := strings.Split(out.String(), "\n")
|
|
for i, line := range rstLines {
|
|
if strings.HasPrefix(line, "<body>") {
|
|
rstLines = (rstLines[i+1 : len(rstLines)-3])
|
|
}
|
|
}
|
|
return strings.Join(rstLines, "\n")
|
|
}
|