Skip to content

Dynamic extension loading for goldmark markdown parser

License

Notifications You must be signed in to change notification settings

yuin/goldmark-dynamic

Repository files navigation

goldmark-dynamic

GoDev

goldmark-dynamic is an extension for the goldmark that allows loading extensions without re-compilation.

goldmark-dynamic can load extensions written in Lua.

Supported Go versions

>=1.20

Installation

go get github.com/yuin/goldmark-dynamic

Status

This project is an experimental project. API may changes, but almost API is stable enough for most users.

Limitations

  • This extension is not goroutine safe. Please note that a goldmark with this extension can not be used by multiple goroutines.
  • Lua extension is much slower than Go extension. Do not recklessly use goldmark-dynamic! You should use this extension only if you really do not want to recompile applications even get the sacrifice of the performance.
  • TODO: This extension does not export all functionalities of the goldmark for now. Exporting goldmark functionalities is a monotonous work(I got bored of doing the work over and over), so contributions are welcome.

Architecture

  • This extension uses the gopher-lua as a Lua interpreter.
    • gopher-lua is a fast(in comparison with other script languages written in pure Go), easy to integrate pure Go implementation of the Lua language. Importantly, author of the gopher-lua is the identical person of which is the goldmark author(and this extension) :-).
  • Most objects are converted with gopher-luar.

Usage

Go API

import (
    "bytes"
    "fmt"
    "os"

    "github.com/yuin/goldmark"
    "github.com/yuin/goldmark-dynamic"
)

func main() {
    ext, cleanup =  
        dynamic.New(
             dynamic.WithExtensions([]dynamic.Extension{
                   {
                       File: "mention.lua",
                       Options: map[string]string{
                           "class": "user-mention",
                       },
                   },
               }
             }),
             dynamic.WithOnError(func(err error) {
                fmt.Fprintln(os.Stderr, err.Error())
             }),
       )
    defer cleanup()
    markdown := goldmark.New(
        goldmark.WithExtensions(ext),
    )
    // codes use markdown
}

See dynamic_test.go for detailed usage.

Since Lua is a dynamic language, unexpected errors may orccur at a runtime. You can set a function that will be called if such errors occur. Default OnError just panics if errors occur.

Lua API

This extension preloads below modules:

package name
go.bytes exports Go's bytes package functionalities
goldmark.ast exports goldmark/ast functionalities
goldmark.parser exports goldmark/parser package functionalities
goldmark.renderer.html exports goldmark/renderer/htm functionalities
goldmark.renderer exports goldmark/renderer functionalities
goldmark.text.segment exports goldmark/text.Segment functionalities
goldmark.text exports goldmark/text package functionalities
goldmark.uti exports goldmark/util package functionalities

See _examples directory for detailed usage.

Most of Lua API is exported by gopher-luar. So you can access simple properties by obj.propName and access methods by obj:propName.

Dynamic extensions are almost same as extensions written in Go. Basic structure is like the following:

-- loads required packages
local bit32 = require 'bit32'
local bytes = require 'go.bytes'
local gparser = require 'goldmark.parser'
local gutil = require 'goldmark.util'
local gtext = require 'goldmark.text'
local gsegment = require 'goldmark.text.segment'
local gast = require 'goldmark.ast'
local grenderer = require 'goldmark.renderer'
local hrenderer = require 'goldmark.renderer.html'

-- assign properties to local variables for the performance(this is a common technique in Lua scripts)
local format = string.format
local bstring, bsub, btrimspace, bequlstring, bfromstring = bytes.string, bytes.sub, bytes.trimSpace, bytes.equalString, bytes.fromString
local isspace, prioritized = gutil.isSpace, gutil.prioritized
local seglen = gsegment.len
local bitor = bit32.bor
local walkcontinue = gast.walkContinue
local pclose, pnochildren, phaschildren, pcontinue = gparser.close, gparser.noChildren, gparser.hasChildren, gparser.continue

-- define my AST node kind
local kindadmonition = gast.newNodeKind("admonition")

-- a function applies this extension to a goldmark.
-- This function takes a goldmark.Markdown and arbitary options.
return function(m, opts) 
  -- creates a new BlockParser(, Inline Parser, AST Transformers...)
  local admonitionBlockParser = gparser.newBlockParser({
    triggers = ":",
    open = function(self, parent, reader, pc)
      local line, segment = reader:peekLine()
      if not bequlstring(bsub(line, 0, 3), ":::") then
        return nil, pnochildren
      end
      local class = btrimspace(bsub(line, 3, #line))
      if not class or #class == 0 then
        class = bfromstring("admonition")
      end
      local node = gast.newBlockNode({
        kind = kindadmonition,
        props = {
          class = class
        }
      })
      reader:advance(seglen(segment) - 1)
      return node, phaschildren
    end,
    continue = function(self, node, reader ,pc)
      local line, segment = reader:peekLine()
      if bequlstring(bsub(line, 0, 3), ":::") then
        reader:advance(seglen(segment) - 1)
        return pclose
      end
      return bitor(phaschildren, pcontinue)
    end,
    close = function(self, node, reader, pc)
      -- nothing to do
    end
  })
  
  -- creates a new HTML renderer
  local prefix = opts.prefix or ""
  local admonitionHTMLRenderer = hrenderer.newRenderer({
    registerFuncs = function(self, reg)
      reg:register(kindadmonition,  function(w, source, n, entering) 
        if entering then
          w:writeString(format("<div class=\"%s%s\">", prefix, bstring(n:prop("class"))))
        else
          w:writeString("</div>")
        end
        return walkcontinue, nil
      end)
    end
  })


  -- adds our functionalities to a goldmark.Markdown
  m:parser():addOptions(
    gparser.withBlockParsers(
      prioritized(admonitionBlockParser, 999)
    )
  )
  m:renderer():addOptions(
    grenderer.withNodeRenderers(
      prioritized(admonitionHTMLRenderer, 999)
    ),
    hrenderer.withXHTML()
  )
end

Note that goldmark heavily uses []byte. go.bytes package simply exports Go functions by gopher-luar, so these functions use 0-started index unlike Lua functions(Lua has an 1-started index).

For dynamic extension authors

It is recommended that dynamic extensions have a name prefixed with goldmark-dynamic- allow users to distinguish a language in which an extension written. For instance, goldmark-dynamic-admonition(an extension written in Lua) and goldmark-admonition(an extension written in Go).

List of dynamic extensions

Please let me known your dynamic extensions by a pull requst that updates the list.

TODO

Contributions are welcome.

  • Export rest of goldmark functionalities
  • Write tests
  • Add convinience gopher-lua libraries like gluare, glua-lfs etc.

License

MIT

Author

Yusuke Inuzuka

About

Dynamic extension loading for goldmark markdown parser

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published