render_hooks.go - hugo - [fork] hugo port for 9front
 (HTM) git clone https://git.drkhsh.at/hugo.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) Submodules
 (DIR) README
 (DIR) LICENSE
       ---
       render_hooks.go (14689B)
       ---
            1 // Copyright 2019 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 goldmark
           15 
           16 import (
           17         "bytes"
           18         "strings"
           19 
           20         "github.com/gohugoio/hugo/common/types/hstring"
           21         "github.com/gohugoio/hugo/markup/converter/hooks"
           22         "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
           23         "github.com/gohugoio/hugo/markup/goldmark/images"
           24         "github.com/gohugoio/hugo/markup/goldmark/internal/render"
           25         "github.com/gohugoio/hugo/markup/internal/attributes"
           26 
           27         "github.com/yuin/goldmark"
           28         "github.com/yuin/goldmark/ast"
           29         "github.com/yuin/goldmark/renderer"
           30         "github.com/yuin/goldmark/renderer/html"
           31         "github.com/yuin/goldmark/util"
           32 )
           33 
           34 var _ renderer.SetOptioner = (*hookedRenderer)(nil)
           35 
           36 func newLinkRenderer(cfg goldmark_config.Config) renderer.NodeRenderer {
           37         r := &hookedRenderer{
           38                 linkifyProtocol: []byte(cfg.Extensions.LinkifyProtocol),
           39                 Config: html.Config{
           40                         Writer: html.DefaultWriter,
           41                 },
           42         }
           43         return r
           44 }
           45 
           46 func newLinks(cfg goldmark_config.Config) goldmark.Extender {
           47         return &links{cfg: cfg}
           48 }
           49 
           50 type linkContext struct {
           51         page        any
           52         pageInner   any
           53         destination string
           54         title       string
           55         text        hstring.HTML
           56         plainText   string
           57         *attributes.AttributesHolder
           58 }
           59 
           60 func (ctx linkContext) Destination() string {
           61         return ctx.destination
           62 }
           63 
           64 func (ctx linkContext) Page() any {
           65         return ctx.page
           66 }
           67 
           68 func (ctx linkContext) PageInner() any {
           69         return ctx.pageInner
           70 }
           71 
           72 func (ctx linkContext) Text() hstring.HTML {
           73         return ctx.text
           74 }
           75 
           76 func (ctx linkContext) PlainText() string {
           77         return ctx.plainText
           78 }
           79 
           80 func (ctx linkContext) Title() string {
           81         return ctx.title
           82 }
           83 
           84 type imageLinkContext struct {
           85         linkContext
           86         ordinal int
           87         isBlock bool
           88 }
           89 
           90 func (ctx imageLinkContext) IsBlock() bool {
           91         return ctx.isBlock
           92 }
           93 
           94 func (ctx imageLinkContext) Ordinal() int {
           95         return ctx.ordinal
           96 }
           97 
           98 type headingContext struct {
           99         page      any
          100         pageInner any
          101         level     int
          102         anchor    string
          103         text      hstring.HTML
          104         plainText string
          105         *attributes.AttributesHolder
          106 }
          107 
          108 func (ctx headingContext) Page() any {
          109         return ctx.page
          110 }
          111 
          112 func (ctx headingContext) PageInner() any {
          113         return ctx.pageInner
          114 }
          115 
          116 func (ctx headingContext) Level() int {
          117         return ctx.level
          118 }
          119 
          120 func (ctx headingContext) Anchor() string {
          121         return ctx.anchor
          122 }
          123 
          124 func (ctx headingContext) Text() hstring.HTML {
          125         return ctx.text
          126 }
          127 
          128 func (ctx headingContext) PlainText() string {
          129         return ctx.plainText
          130 }
          131 
          132 type hookedRenderer struct {
          133         linkifyProtocol []byte
          134         html.Config
          135 }
          136 
          137 func (r *hookedRenderer) SetOption(name renderer.OptionName, value any) {
          138         r.Config.SetOption(name, value)
          139 }
          140 
          141 // RegisterFuncs implements NodeRenderer.RegisterFuncs.
          142 func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
          143         reg.Register(ast.KindLink, r.renderLink)
          144         reg.Register(ast.KindAutoLink, r.renderAutoLink)
          145         reg.Register(ast.KindImage, r.renderImage)
          146         reg.Register(ast.KindHeading, r.renderHeading)
          147 }
          148 
          149 func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          150         n := node.(*ast.Image)
          151         var lr hooks.LinkRenderer
          152 
          153         ctx, ok := w.(*render.Context)
          154         if ok {
          155                 h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
          156                 ok = h != nil
          157                 if ok {
          158                         lr = h.(hooks.LinkRenderer)
          159                 }
          160         }
          161 
          162         if !ok {
          163                 return r.renderImageDefault(w, source, node, entering)
          164         }
          165 
          166         if entering {
          167                 // Store the current pos so we can capture the rendered text.
          168                 ctx.PushPos(ctx.Buffer.Len())
          169                 return ast.WalkContinue, nil
          170         }
          171 
          172         text := ctx.PopRenderedString()
          173 
          174         var (
          175                 isBlock bool
          176                 ordinal int
          177         )
          178         if b, ok := n.AttributeString(images.AttrIsBlock); ok && b.(bool) {
          179                 isBlock = true
          180         }
          181         if n, ok := n.AttributeString(images.AttrOrdinal); ok {
          182                 ordinal = n.(int)
          183         }
          184 
          185         // We use the attributes to signal from the parser whether the image is in
          186         // a block context or not.
          187         // We may find a better way to do that, but for now, we'll need to remove any
          188         // internal attributes before rendering.
          189         attrs := r.filterInternalAttributes(n.Attributes())
          190 
          191         page, pageInner := render.GetPageAndPageInner(ctx)
          192 
          193         err := lr.RenderLink(
          194                 ctx.RenderContext().Ctx,
          195                 w,
          196                 imageLinkContext{
          197                         linkContext: linkContext{
          198                                 page:             page,
          199                                 pageInner:        pageInner,
          200                                 destination:      string(n.Destination),
          201                                 title:            string(n.Title),
          202                                 text:             hstring.HTML(text),
          203                                 plainText:        render.TextPlain(n, source),
          204                                 AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
          205                         },
          206                         ordinal: ordinal,
          207                         isBlock: isBlock,
          208                 },
          209         )
          210 
          211         return ast.WalkContinue, err
          212 }
          213 
          214 func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
          215         n := 0
          216         for _, x := range attrs {
          217                 if !bytes.HasPrefix(x.Name, []byte(internalAttrPrefix)) {
          218                         attrs[n] = x
          219                         n++
          220                 }
          221         }
          222         return attrs[:n]
          223 }
          224 
          225 // Fall back to the default Goldmark render funcs. Method below borrowed from:
          226 // https://github.com/yuin/goldmark
          227 func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          228         if !entering {
          229                 return ast.WalkContinue, nil
          230         }
          231         n := node.(*ast.Image)
          232         _, _ = w.WriteString("<img src=\"")
          233         if r.Unsafe || !html.IsDangerousURL(n.Destination) {
          234                 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
          235         }
          236         _, _ = w.WriteString(`" alt="`)
          237         r.renderTexts(w, source, n)
          238         _ = w.WriteByte('"')
          239         if n.Title != nil {
          240                 _, _ = w.WriteString(` title="`)
          241                 r.Writer.Write(w, n.Title)
          242                 _ = w.WriteByte('"')
          243         }
          244         if n.Attributes() != nil {
          245                 html.RenderAttributes(w, n, html.ImageAttributeFilter)
          246         }
          247         if r.XHTML {
          248                 _, _ = w.WriteString(" />")
          249         } else {
          250                 _, _ = w.WriteString(">")
          251         }
          252         return ast.WalkSkipChildren, nil
          253 }
          254 
          255 func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          256         n := node.(*ast.Link)
          257         var lr hooks.LinkRenderer
          258 
          259         ctx, ok := w.(*render.Context)
          260         if ok {
          261                 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
          262                 ok = h != nil
          263                 if ok {
          264                         lr = h.(hooks.LinkRenderer)
          265                 }
          266         }
          267 
          268         if !ok {
          269                 return r.renderLinkDefault(w, source, node, entering)
          270         }
          271 
          272         if entering {
          273                 // Store the current pos so we can capture the rendered text.
          274                 ctx.PushPos(ctx.Buffer.Len())
          275                 return ast.WalkContinue, nil
          276         }
          277 
          278         text := ctx.PopRenderedString()
          279 
          280         page, pageInner := render.GetPageAndPageInner(ctx)
          281 
          282         err := lr.RenderLink(
          283                 ctx.RenderContext().Ctx,
          284                 w,
          285                 linkContext{
          286                         page:             page,
          287                         pageInner:        pageInner,
          288                         destination:      string(n.Destination),
          289                         title:            string(n.Title),
          290                         text:             hstring.HTML(text),
          291                         plainText:        render.TextPlain(n, source),
          292                         AttributesHolder: attributes.Empty,
          293                 },
          294         )
          295 
          296         return ast.WalkContinue, err
          297 }
          298 
          299 // Borrowed from Goldmark's HTML renderer.
          300 func (r *hookedRenderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
          301         for c := n.FirstChild(); c != nil; c = c.NextSibling() {
          302                 if s, ok := c.(*ast.String); ok {
          303                         _, _ = r.renderString(w, source, s, true)
          304                 } else if t, ok := c.(*ast.Text); ok {
          305                         _, _ = r.renderText(w, source, t, true)
          306                 } else {
          307                         r.renderTexts(w, source, c)
          308                 }
          309         }
          310 }
          311 
          312 // Borrowed from Goldmark's HTML renderer.
          313 func (r *hookedRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          314         if !entering {
          315                 return ast.WalkContinue, nil
          316         }
          317         n := node.(*ast.String)
          318         if n.IsCode() {
          319                 _, _ = w.Write(n.Value)
          320         } else {
          321                 if n.IsRaw() {
          322                         r.Writer.RawWrite(w, n.Value)
          323                 } else {
          324                         r.Writer.Write(w, n.Value)
          325                 }
          326         }
          327         return ast.WalkContinue, nil
          328 }
          329 
          330 // Borrowed from Goldmark's HTML renderer.
          331 func (r *hookedRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          332         if !entering {
          333                 return ast.WalkContinue, nil
          334         }
          335         n := node.(*ast.Text)
          336         segment := n.Segment
          337         if n.IsRaw() {
          338                 r.Writer.RawWrite(w, segment.Value(source))
          339         } else {
          340                 value := segment.Value(source)
          341                 r.Writer.Write(w, value)
          342                 if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
          343                         if r.XHTML {
          344                                 _, _ = w.WriteString("<br />\n")
          345                         } else {
          346                                 _, _ = w.WriteString("<br>\n")
          347                         }
          348                 } else if n.SoftLineBreak() {
          349                         // TODO(bep) we use these methods a fallback to default rendering when no image/link hooks are defined.
          350                         // I don't think the below is relevant in these situations, but if so, we need to create a PR
          351                         // upstream to export softLineBreak.
          352                         /*if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
          353                                 sibling := node.NextSibling()
          354                                 if sibling != nil && sibling.Kind() == ast.KindText {
          355                                         if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
          356                                                 thisLastRune := util.ToRune(value, len(value)-1)
          357                                                 siblingFirstRune, _ := utf8.DecodeRune(siblingText)
          358                                                 if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
          359                                                         _ = w.WriteByte('\n')
          360                                                 }
          361                                         }
          362                                 }
          363                         } else {
          364                                 _ = w.WriteByte('\n')
          365                         }*/
          366                         _ = w.WriteByte('\n')
          367                 }
          368         }
          369         return ast.WalkContinue, nil
          370 }
          371 
          372 // Fall back to the default Goldmark render funcs. Method below borrowed from:
          373 // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
          374 func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          375         n := node.(*ast.Link)
          376         if entering {
          377                 _, _ = w.WriteString("<a href=\"")
          378                 if r.Unsafe || !html.IsDangerousURL(n.Destination) {
          379                         _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
          380                 }
          381                 _ = w.WriteByte('"')
          382                 if n.Title != nil {
          383                         _, _ = w.WriteString(` title="`)
          384                         r.Writer.Write(w, n.Title)
          385                         _ = w.WriteByte('"')
          386                 }
          387                 _ = w.WriteByte('>')
          388         } else {
          389                 _, _ = w.WriteString("</a>")
          390         }
          391         return ast.WalkContinue, nil
          392 }
          393 
          394 func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          395         if !entering {
          396                 return ast.WalkContinue, nil
          397         }
          398 
          399         n := node.(*ast.AutoLink)
          400         var lr hooks.LinkRenderer
          401 
          402         ctx, ok := w.(*render.Context)
          403         if ok {
          404                 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
          405                 ok = h != nil
          406                 if ok {
          407                         lr = h.(hooks.LinkRenderer)
          408                 }
          409         }
          410 
          411         if !ok {
          412                 return r.renderAutoLinkDefault(w, source, node, entering)
          413         }
          414 
          415         url := string(r.autoLinkURL(n, source))
          416         label := string(n.Label(source))
          417         if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
          418                 url = "mailto:" + url
          419         }
          420 
          421         page, pageInner := render.GetPageAndPageInner(ctx)
          422 
          423         err := lr.RenderLink(
          424                 ctx.RenderContext().Ctx,
          425                 w,
          426                 linkContext{
          427                         page:             page,
          428                         pageInner:        pageInner,
          429                         destination:      url,
          430                         text:             hstring.HTML(label),
          431                         plainText:        label,
          432                         AttributesHolder: attributes.Empty,
          433                 },
          434         )
          435 
          436         return ast.WalkContinue, err
          437 }
          438 
          439 // Fall back to the default Goldmark render funcs. Method below borrowed from:
          440 // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439
          441 func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          442         n := node.(*ast.AutoLink)
          443         if !entering {
          444                 return ast.WalkContinue, nil
          445         }
          446 
          447         _, _ = w.WriteString(`<a href="`)
          448         url := r.autoLinkURL(n, source)
          449         label := n.Label(source)
          450         if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
          451                 _, _ = w.WriteString("mailto:")
          452         }
          453         _, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
          454         if n.Attributes() != nil {
          455                 _ = w.WriteByte('"')
          456                 html.RenderAttributes(w, n, html.LinkAttributeFilter)
          457                 _ = w.WriteByte('>')
          458         } else {
          459                 _, _ = w.WriteString(`">`)
          460         }
          461         _, _ = w.Write(util.EscapeHTML(label))
          462         _, _ = w.WriteString(`</a>`)
          463         return ast.WalkContinue, nil
          464 }
          465 
          466 func (r *hookedRenderer) autoLinkURL(n *ast.AutoLink, source []byte) []byte {
          467         url := n.URL(source)
          468         if len(n.Protocol) > 0 && !bytes.Equal(n.Protocol, r.linkifyProtocol) {
          469                 // The CommonMark spec says "http" is the correct protocol for links,
          470                 // but this doesn't make much sense (the fact that they should care about the rendered output).
          471                 // Note that n.Protocol is not set if protocol is provided by user.
          472                 url = append(r.linkifyProtocol, url[len(n.Protocol):]...)
          473         }
          474         return url
          475 }
          476 
          477 func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          478         n := node.(*ast.Heading)
          479         var hr hooks.HeadingRenderer
          480 
          481         ctx, ok := w.(*render.Context)
          482         if ok {
          483                 h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
          484                 ok = h != nil
          485                 if ok {
          486                         hr = h.(hooks.HeadingRenderer)
          487                 }
          488         }
          489 
          490         if !ok {
          491                 return r.renderHeadingDefault(w, source, node, entering)
          492         }
          493 
          494         if entering {
          495                 // Store the current pos so we can capture the rendered text.
          496                 ctx.PushPos(ctx.Buffer.Len())
          497                 return ast.WalkContinue, nil
          498         }
          499 
          500         text := ctx.PopRenderedString()
          501 
          502         var anchor []byte
          503         if anchori, ok := n.AttributeString("id"); ok {
          504                 anchor, _ = anchori.([]byte)
          505         }
          506 
          507         page, pageInner := render.GetPageAndPageInner(ctx)
          508 
          509         err := hr.RenderHeading(
          510                 ctx.RenderContext().Ctx,
          511                 w,
          512                 headingContext{
          513                         page:             page,
          514                         pageInner:        pageInner,
          515                         level:            n.Level,
          516                         anchor:           string(anchor),
          517                         text:             hstring.HTML(text),
          518                         plainText:        render.TextPlain(n, source),
          519                         AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
          520                 },
          521         )
          522 
          523         return ast.WalkContinue, err
          524 }
          525 
          526 func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
          527         n := node.(*ast.Heading)
          528         if entering {
          529                 _, _ = w.WriteString("<h")
          530                 _ = w.WriteByte("0123456"[n.Level])
          531                 if n.Attributes() != nil {
          532                         attributes.RenderASTAttributes(w, node.Attributes()...)
          533                 }
          534                 _ = w.WriteByte('>')
          535         } else {
          536                 _, _ = w.WriteString("</h")
          537                 _ = w.WriteByte("0123456"[n.Level])
          538                 _, _ = w.WriteString(">\n")
          539         }
          540         return ast.WalkContinue, nil
          541 }
          542 
          543 type links struct {
          544         cfg goldmark_config.Config
          545 }
          546 
          547 // Extend implements goldmark.Extender.
          548 func (e *links) Extend(m goldmark.Markdown) {
          549         m.Renderer().AddOptions(renderer.WithNodeRenderers(
          550                 util.Prioritized(newLinkRenderer(e.cfg), 100),
          551         ))
          552 }