p1: /v2/scrape, /v1/fetch, auth, CLI, contract tests
Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification is ignored. No 502. keys add writes the secret only to --out. Release binary has no -test-allow-prefix.
This commit is contained in:
+142
@@ -0,0 +1,142 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type convertOption struct {
|
||||
domain string
|
||||
context context.Context
|
||||
}
|
||||
type ConvertOptionFunc func(o *convertOption)
|
||||
|
||||
func WithContext(ctx context.Context) ConvertOptionFunc {
|
||||
return func(o *convertOption) {
|
||||
o.context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
// WithDomain provides a base `domain` to the converter and
|
||||
// to the `AssembleAbsoluteURL` function.
|
||||
//
|
||||
// If a *relative* url is encountered (in an image or link) then the `domain` is used
|
||||
// to convert it to a *absolute* url.
|
||||
func WithDomain(domain string) ConvertOptionFunc {
|
||||
return func(o *convertOption) {
|
||||
o.domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
func (conv *Converter) setError(err error) {
|
||||
conv.m.Lock()
|
||||
defer conv.m.Unlock()
|
||||
|
||||
conv.err = err
|
||||
}
|
||||
func (conv *Converter) getError() error {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
return conv.err
|
||||
}
|
||||
|
||||
var errNoRenderHandlers = errors.New(`no render handlers are registered. did you forget to register the "commonmark" and "base" plugins?`)
|
||||
var errBasePluginMissing = errors.New(`you registered the "commonmark" plugin but the "base" plugin is also required`)
|
||||
|
||||
// ConvertNode converts a `*html.Node` to a markdown byte slice.
|
||||
//
|
||||
// If you have already parsed an HTML page using the `html.Parse()` function
|
||||
// from the "golang.org/x/net/html" package then you can pass this node
|
||||
// directly to the converter.
|
||||
func (conv *Converter) ConvertNode(doc *html.Node, opts ...ConvertOptionFunc) ([]byte, error) {
|
||||
|
||||
if err := conv.getError(); err != nil {
|
||||
// There can be errors while calling `Init` on the plugins (e.g. validation errors).
|
||||
// Now is the first opportunity where we can return that error.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conv.m.Lock()
|
||||
option := &convertOption{}
|
||||
for _, fn := range opts {
|
||||
fn(option)
|
||||
}
|
||||
conv.m.Unlock()
|
||||
|
||||
// If there are no render handlers registered this is
|
||||
// usually a user error - since people want the Commonmark Plugin in 99% of cases.
|
||||
if len(conv.getRenderHandlers()) == 0 {
|
||||
return nil, errNoRenderHandlers
|
||||
}
|
||||
|
||||
containsCommonmark := slices.Contains(conv.registeredPlugins, "commonmark")
|
||||
containsBase := slices.Contains(conv.registeredPlugins, "base")
|
||||
if containsCommonmark && !containsBase {
|
||||
return nil, errBasePluginMissing
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - //
|
||||
|
||||
state := newGlobalState()
|
||||
|
||||
if option.context == nil {
|
||||
option.context = context.Background()
|
||||
}
|
||||
ctx := option.context
|
||||
ctx = provideDomain(ctx, option.domain)
|
||||
ctx = provideAssembleAbsoluteURL(ctx, defaultAssembleAbsoluteURL)
|
||||
ctx = state.provideGlobalState(ctx)
|
||||
|
||||
customCtx := newConverterContext(ctx, conv)
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - //
|
||||
|
||||
// Pre-Render
|
||||
for _, handler := range conv.getPreRenderHandlers() {
|
||||
handler.Value(customCtx, doc)
|
||||
}
|
||||
|
||||
// Render
|
||||
var buf bytes.Buffer
|
||||
conv.handleRenderNode(customCtx, &buf, doc)
|
||||
|
||||
// Post-Render
|
||||
result := buf.Bytes()
|
||||
for _, handler := range conv.getPostRenderHandlers() {
|
||||
result = handler.Value(customCtx, result)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ConvertReader converts the html from the reader to markdown.
|
||||
//
|
||||
// Under the hood `html.Parse()` is used to parse the HTML.
|
||||
func (conv *Converter) ConvertReader(r io.Reader, opts ...ConvertOptionFunc) ([]byte, error) {
|
||||
doc, err := html.Parse(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conv.ConvertNode(doc, opts...)
|
||||
}
|
||||
|
||||
// ConvertString converts a html-string to a markdown-string.
|
||||
//
|
||||
// Under the hood `html.Parse()` is used to parse the HTML.
|
||||
func (conv *Converter) ConvertString(htmlInput string, opts ...ConvertOptionFunc) (string, error) {
|
||||
r := strings.NewReader(htmlInput)
|
||||
output, err := conv.ConvertReader(r, opts...)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package converter
|
||||
|
||||
import "sync"
|
||||
|
||||
type Converter struct {
|
||||
m sync.RWMutex
|
||||
|
||||
err error
|
||||
|
||||
registeredPlugins []string
|
||||
|
||||
preRenderHandlers prioritizedSlice[HandlePreRenderFunc]
|
||||
renderHandlers prioritizedSlice[HandleRenderFunc]
|
||||
postRenderHandlers prioritizedSlice[HandlePostRenderFunc]
|
||||
|
||||
textTransformHandlers prioritizedSlice[HandleTextTransformFunc]
|
||||
|
||||
markdownChars map[rune]interface{}
|
||||
unEscapeHandlers prioritizedSlice[HandleUnEscapeFunc]
|
||||
|
||||
tagTypes map[string]prioritizedSlice[tagType]
|
||||
|
||||
escapeMode escapeMode
|
||||
|
||||
Register register
|
||||
}
|
||||
|
||||
type converterOption = func(c *Converter) error
|
||||
|
||||
func NewConverter(opts ...converterOption) *Converter {
|
||||
conv := &Converter{
|
||||
markdownChars: make(map[rune]interface{}),
|
||||
tagTypes: make(map[string]prioritizedSlice[tagType]),
|
||||
}
|
||||
conv.Register = register{conv}
|
||||
|
||||
for _, opt := range opts {
|
||||
err := opt(conv)
|
||||
if err != nil {
|
||||
conv.setError(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return conv
|
||||
}
|
||||
|
||||
type escapeMode string
|
||||
|
||||
const (
|
||||
EscapeModeDisabled escapeMode = "disabled"
|
||||
EscapeModeSmart escapeMode = "smart"
|
||||
)
|
||||
|
||||
// WithEscapeMode changes the strictness of the "escaping".
|
||||
//
|
||||
// Some characters have a special meaning in markdown.
|
||||
// For example, the character "*" can be used for lists, emphasis and dividers.
|
||||
// By placing a backlash before that character (e.g. "\*") you can "escape" it.
|
||||
// Then the character will render as a raw "*" without the "markdown meaning" applied.
|
||||
//
|
||||
// Learn more in the documentation
|
||||
//
|
||||
// "disabled" or "smart"
|
||||
//
|
||||
// default: "smart"
|
||||
func WithEscapeMode(mode escapeMode) converterOption {
|
||||
return func(c *Converter) error {
|
||||
c.escapeMode = mode
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/JohannesKaufmann/dom"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// func GetValue[K string, V any](ctx context.Context, key K) V {
|
||||
// val, _ := ctx.Value(key).(V)
|
||||
// return val
|
||||
// }
|
||||
// func SetValue[K string, V any](ctx context.Context, key K, val V) context.Context {
|
||||
// return context.WithValue(ctx, key, val)
|
||||
// }
|
||||
|
||||
type ctxKey string
|
||||
|
||||
const (
|
||||
ctxKeyAssembleAbsoluteURL ctxKey = "AssembleAbsoluteURL"
|
||||
ctxKeyDomain ctxKey = "Domain"
|
||||
|
||||
ctxKeySetState ctxKey = "SetState"
|
||||
ctxKeyUpdateState ctxKey = "UpdateState"
|
||||
ctxKeyGetState ctxKey = "GetState"
|
||||
)
|
||||
|
||||
func provideDomain(ctx context.Context, domain string) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyDomain, domain)
|
||||
}
|
||||
func GetDomain(ctx context.Context) string {
|
||||
domain, ok := ctx.Value(ctxKeyDomain).(string)
|
||||
if !ok {
|
||||
fmt.Println("[warning] value ctxKeyDomain is different")
|
||||
return ""
|
||||
}
|
||||
|
||||
return domain
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - //
|
||||
|
||||
type AssembleAbsoluteURLFunc func(tagName string, rawURL string, domain string) string
|
||||
|
||||
func assembleAbsoluteURL(ctx context.Context, tagName string, rawURL string) string {
|
||||
domain := GetDomain(ctx)
|
||||
|
||||
// TODO: since this gets passed down from the converter, it doesn't have to provided from the ctx anymore
|
||||
fn, ok := ctx.Value(ctxKeyAssembleAbsoluteURL).(AssembleAbsoluteURLFunc)
|
||||
if !ok {
|
||||
fmt.Println("[warning] func ctxKeyAssembleAbsoluteURL is different")
|
||||
return ""
|
||||
}
|
||||
|
||||
return fn(tagName, rawURL, domain)
|
||||
}
|
||||
|
||||
func provideAssembleAbsoluteURL(ctx context.Context, fn AssembleAbsoluteURLFunc) context.Context {
|
||||
return context.WithValue(ctx, ctxKeyAssembleAbsoluteURL, fn)
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - //
|
||||
|
||||
type SetStateFunc func(key string, val any)
|
||||
type UpdateStateFunc func(key string, fn func(any) any)
|
||||
type GetStateFunc func(key string) any
|
||||
|
||||
type globalState struct {
|
||||
data map[string]any
|
||||
}
|
||||
|
||||
func newGlobalState() *globalState {
|
||||
|
||||
return &globalState{
|
||||
data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *globalState) setState(key string, val any) {
|
||||
s.data[key] = val
|
||||
}
|
||||
func (s *globalState) updateState(key string, fn func(any) any) {
|
||||
s.data[key] = fn(s.data[key])
|
||||
}
|
||||
func (s *globalState) getState(key string) any {
|
||||
return s.data[key]
|
||||
}
|
||||
|
||||
func (s *globalState) provideGlobalState(ctx context.Context) context.Context {
|
||||
|
||||
var setState SetStateFunc = s.setState
|
||||
var updateState UpdateStateFunc = s.updateState
|
||||
var getState GetStateFunc = s.getState
|
||||
|
||||
ctx = context.WithValue(ctx, ctxKeySetState, setState)
|
||||
ctx = context.WithValue(ctx, ctxKeyUpdateState, updateState)
|
||||
ctx = context.WithValue(ctx, ctxKeyGetState, getState)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func GetState[V any](ctx context.Context, key string) V {
|
||||
fn := ctx.Value(ctxKeyGetState).(GetStateFunc)
|
||||
|
||||
val, _ := fn(key).(V)
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
func SetState[V any](ctx context.Context, key string, val V) {
|
||||
fn := ctx.Value(ctxKeySetState).(SetStateFunc)
|
||||
|
||||
fn(key, val)
|
||||
}
|
||||
|
||||
func UpdateState[V any](ctx context.Context, key string, fn func(V) V) {
|
||||
updater := ctx.Value(ctxKeyUpdateState).(UpdateStateFunc)
|
||||
|
||||
updater(key, func(val any) any {
|
||||
value, ok := val.(V)
|
||||
if !ok && val != nil {
|
||||
// TODO: slog?
|
||||
fmt.Println("[warning] val is different than V in UpdateState")
|
||||
}
|
||||
|
||||
return fn(value)
|
||||
})
|
||||
}
|
||||
|
||||
// - - - - - - //
|
||||
|
||||
// Context extends the normal context.Context with some additional
|
||||
// methods useful for the process of converting.
|
||||
type Context interface {
|
||||
context.Context
|
||||
|
||||
AssembleAbsoluteURL(ctx Context, tagName string, rawURL string) string
|
||||
|
||||
GetTagType(tagName string) (tagType, bool)
|
||||
|
||||
RenderNodes(ctx Context, w Writer, nodes ...*html.Node)
|
||||
RenderChildNodes(ctx Context, w Writer, n *html.Node)
|
||||
|
||||
EscapeContent(content []byte) []byte
|
||||
UnEscapeContent(content []byte) []byte
|
||||
|
||||
WithValue(key any, val any) Context
|
||||
}
|
||||
|
||||
type converterContext struct {
|
||||
context.Context
|
||||
conv *Converter
|
||||
}
|
||||
|
||||
func newConverterContext(ctx context.Context, conv *Converter) Context {
|
||||
return &converterContext{
|
||||
Context: ctx,
|
||||
conv: conv,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *converterContext) AssembleAbsoluteURL(ctx Context, tagName string, rawURL string) string {
|
||||
return assembleAbsoluteURL(ctx, tagName, rawURL)
|
||||
}
|
||||
|
||||
func (c *converterContext) RenderNodes(ctx Context, w Writer, nodes ...*html.Node) {
|
||||
c.conv.handleRenderNodes(ctx, w, nodes...)
|
||||
}
|
||||
func (c *converterContext) RenderChildNodes(ctx Context, w Writer, n *html.Node) {
|
||||
c.conv.handleRenderNodes(ctx, w, dom.AllChildNodes(n)...)
|
||||
}
|
||||
|
||||
func (c *converterContext) GetTagType(tagName string) (tagType, bool) {
|
||||
return c.conv.getTagType(tagName)
|
||||
}
|
||||
func (c *converterContext) EscapeContent(content []byte) []byte {
|
||||
return c.conv.escapeContent(content)
|
||||
}
|
||||
func (c *converterContext) UnEscapeContent(content []byte) []byte {
|
||||
return c.conv.unEscapeContent(content)
|
||||
}
|
||||
|
||||
func (c *converterContext) WithValue(key any, val any) Context {
|
||||
return &converterContext{
|
||||
Context: context.WithValue(c.Context, key, val),
|
||||
conv: c.conv,
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/JohannesKaufmann/html-to-markdown/v2/marker"
|
||||
)
|
||||
|
||||
const (
|
||||
actionKeep = iota
|
||||
actionEscape = iota
|
||||
)
|
||||
|
||||
// IMPORTANT: Only internally we assume it is only byte
|
||||
var placeholderByte byte = marker.BytesMarkerEscaping[0]
|
||||
|
||||
func (conv *Converter) escapeContent(chars []byte) []byte {
|
||||
if conv.escapeMode == EscapeModeDisabled {
|
||||
return chars
|
||||
}
|
||||
|
||||
newChars := make([]byte, 0, len(chars))
|
||||
for index := 0; index < len(chars); index++ {
|
||||
if chars[index] == '\u0000' {
|
||||
// For security reasons, the Unicode character U+0000 must be replaced with the REPLACEMENT CHARACTER (U+FFFD).
|
||||
newChars = append(newChars, []byte(string('\ufffd'))...)
|
||||
continue
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeRune(chars[index:])
|
||||
|
||||
isMarkdownChar := conv.checkIsEscapedChar(r)
|
||||
if isMarkdownChar {
|
||||
newChars = append(newChars, placeholderByte, chars[index])
|
||||
} else {
|
||||
newChars = append(newChars, chars[index])
|
||||
}
|
||||
}
|
||||
|
||||
return newChars
|
||||
}
|
||||
|
||||
func (conv *Converter) unEscapeContent(chars []byte) []byte {
|
||||
if conv.escapeMode == EscapeModeDisabled {
|
||||
return chars
|
||||
}
|
||||
|
||||
checkElements := func(index int) int {
|
||||
for _, handler := range conv.getUnEscapeHandlers() {
|
||||
if skip := handler.Value(chars, index); skip != -1 {
|
||||
return skip
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
changes := make([]uint8, len(chars))
|
||||
for index := 0; index < len(chars); index++ {
|
||||
|
||||
if chars[index] != placeholderByte {
|
||||
continue
|
||||
}
|
||||
if index+1 >= len(chars) {
|
||||
break
|
||||
}
|
||||
|
||||
skip := checkElements(index + 1)
|
||||
if skip == -1 {
|
||||
continue
|
||||
}
|
||||
changes[index] = actionEscape
|
||||
index += skip - 1
|
||||
}
|
||||
|
||||
newChars := make([]byte, 0, len(chars))
|
||||
for index, char := range chars {
|
||||
if char != placeholderByte {
|
||||
newChars = append(newChars, char)
|
||||
continue
|
||||
}
|
||||
|
||||
// What to do with this placeholder? Should we escape or not?
|
||||
if changes[index] == actionEscape {
|
||||
newChars = append(newChars, '\\')
|
||||
}
|
||||
}
|
||||
return newChars
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package converter
|
||||
|
||||
// Plugin can be used to extends functionality beyond what
|
||||
// is offered by commonmark.
|
||||
type Plugin interface {
|
||||
// The public name of the plugin, e.g. "strikethrough"
|
||||
Name() string
|
||||
|
||||
// Init is called to initialize the plugin. It can be used to
|
||||
// *validate* the arguments and *register* the rules.
|
||||
Init(conv *Converter) error
|
||||
}
|
||||
|
||||
// WithPlugins can be used to add additional functionality to the converter.
|
||||
func WithPlugins(plugins ...Plugin) converterOption {
|
||||
return func(c *Converter) error {
|
||||
for _, plugin := range plugins {
|
||||
c.Register.Plugin(plugin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package converter
|
||||
|
||||
import "sort"
|
||||
|
||||
const (
|
||||
// PriorityEarly means that the handler will be run **early** in the process.
|
||||
// To run it even earlier you need to subtract from this number.
|
||||
PriorityEarly = 100
|
||||
|
||||
// PriorityStandard is for handlers that don't need to be run in a particular order.
|
||||
PriorityStandard = 500
|
||||
|
||||
// PriorityLate means that the handler will be run **late** in the process.
|
||||
// To run it even later you need to add to this number.
|
||||
PriorityLate = 1000
|
||||
)
|
||||
|
||||
type prioritizedValue[V any] struct {
|
||||
Value V
|
||||
Priority int
|
||||
}
|
||||
|
||||
type prioritizedSlice[V any] []prioritizedValue[V]
|
||||
|
||||
func (s prioritizedSlice[V]) Sort() {
|
||||
sort.Slice(s, func(i, j int) bool {
|
||||
return s[i].Priority < s[j].Priority
|
||||
})
|
||||
}
|
||||
|
||||
func prioritized[V any](v V, priority int) prioritizedValue[V] {
|
||||
return prioritizedValue[V]{v, priority}
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/JohannesKaufmann/dom"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
type register struct {
|
||||
conv *Converter
|
||||
}
|
||||
|
||||
func (r *register) Plugin(plugin Plugin) {
|
||||
pluginName := plugin.Name()
|
||||
if pluginName == "" {
|
||||
r.conv.setError(errors.New("the plugin has no name"))
|
||||
return
|
||||
}
|
||||
|
||||
r.conv.m.Lock()
|
||||
r.conv.registeredPlugins = append(r.conv.registeredPlugins, pluginName)
|
||||
r.conv.m.Unlock()
|
||||
|
||||
err := plugin.Init(r.conv)
|
||||
if err != nil {
|
||||
r.conv.setError(fmt.Errorf("error while initializing %q plugin: %w", pluginName, err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Pre-Render - - - - - - - - - - - - - //
|
||||
|
||||
type HandlePreRenderFunc func(ctx Context, doc *html.Node)
|
||||
|
||||
func (r *register) PreRenderer(fn HandlePreRenderFunc, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
handler := prioritized(fn, priority)
|
||||
r.conv.preRenderHandlers = append(r.conv.preRenderHandlers, handler)
|
||||
}
|
||||
func (conv *Converter) getPreRenderHandlers() prioritizedSlice[HandlePreRenderFunc] {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
handlers := make(prioritizedSlice[HandlePreRenderFunc], len(conv.preRenderHandlers))
|
||||
copy(handlers, conv.preRenderHandlers)
|
||||
handlers.Sort()
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Render - - - - - - - - - - - - - //
|
||||
|
||||
// Writer is an interface that only conforms to the Write* methods of bytes.Buffer
|
||||
type Writer interface {
|
||||
Write(p []byte) (n int, err error)
|
||||
WriteByte(c byte) error
|
||||
WriteRune(r rune) (n int, err error)
|
||||
WriteString(s string) (n int, err error)
|
||||
}
|
||||
|
||||
type HandleRenderFunc func(ctx Context, w Writer, n *html.Node) RenderStatus
|
||||
|
||||
func (r *register) Renderer(fn HandleRenderFunc, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
handler := prioritized(fn, priority)
|
||||
r.conv.renderHandlers = append(r.conv.renderHandlers, handler)
|
||||
}
|
||||
|
||||
// RendererFor registers a renderer for a specific tag (e.g. "div").
|
||||
// It is a small wrapper around `TagType()` and `Renderer()`.
|
||||
func (r *register) RendererFor(tagName string, tagType tagType, renderFn HandleRenderFunc, priority int) {
|
||||
|
||||
// 1. we add the "tagType" to the map
|
||||
r.TagType(tagName, tagType, priority)
|
||||
|
||||
// 2. we register the render function
|
||||
r.Renderer(func(ctx Context, w Writer, n *html.Node) RenderStatus {
|
||||
if dom.NodeName(n) == tagName {
|
||||
return renderFn(ctx, w, n)
|
||||
}
|
||||
return RenderTryNext
|
||||
}, priority)
|
||||
}
|
||||
|
||||
func (conv *Converter) getRenderHandlers() prioritizedSlice[HandleRenderFunc] {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
handlers := make(prioritizedSlice[HandleRenderFunc], len(conv.renderHandlers))
|
||||
copy(handlers, conv.renderHandlers)
|
||||
handlers.Sort()
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Post Render - - - - - - - - - - - - - //
|
||||
|
||||
type HandlePostRenderFunc func(ctx Context, content []byte) []byte
|
||||
|
||||
func (r *register) PostRenderer(fn HandlePostRenderFunc, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
handler := prioritized(fn, priority)
|
||||
r.conv.postRenderHandlers = append(r.conv.postRenderHandlers, handler)
|
||||
}
|
||||
func (conv *Converter) getPostRenderHandlers() prioritizedSlice[HandlePostRenderFunc] {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
handlers := make(prioritizedSlice[HandlePostRenderFunc], len(conv.postRenderHandlers))
|
||||
copy(handlers, conv.postRenderHandlers)
|
||||
handlers.Sort()
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Text - - - - - - - - - - - - - //
|
||||
|
||||
type HandleTextTransformFunc func(ctx Context, content string) string
|
||||
|
||||
func (r *register) TextTransformer(fn HandleTextTransformFunc, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
handler := prioritized(fn, priority)
|
||||
r.conv.textTransformHandlers = append(r.conv.textTransformHandlers, handler)
|
||||
}
|
||||
func (conv *Converter) getTextTransformHandlers() prioritizedSlice[HandleTextTransformFunc] {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
handlers := make(prioritizedSlice[HandleTextTransformFunc], len(conv.textTransformHandlers))
|
||||
copy(handlers, conv.textTransformHandlers)
|
||||
handlers.Sort()
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Escaping - - - - - - - - - - - - - //
|
||||
|
||||
func (r *register) EscapedChar(chars ...rune) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
for _, char := range chars {
|
||||
r.conv.markdownChars[char] = struct{}{}
|
||||
}
|
||||
}
|
||||
func (conv *Converter) checkIsEscapedChar(r rune) bool {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
_, ok := conv.markdownChars[r]
|
||||
return ok
|
||||
}
|
||||
|
||||
type HandleUnEscapeFunc func(chars []byte, index int) int
|
||||
|
||||
func (r *register) UnEscaper(fn HandleUnEscapeFunc, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
handler := prioritized(fn, priority)
|
||||
r.conv.unEscapeHandlers = append(r.conv.unEscapeHandlers, handler)
|
||||
}
|
||||
func (conv *Converter) getUnEscapeHandlers() prioritizedSlice[HandleUnEscapeFunc] {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
handlers := make(prioritizedSlice[HandleUnEscapeFunc], len(conv.unEscapeHandlers))
|
||||
copy(handlers, conv.unEscapeHandlers)
|
||||
handlers.Sort()
|
||||
|
||||
return handlers
|
||||
}
|
||||
|
||||
// - - - - - - - - - - - - - Tag Type - - - - - - - - - - - - - //
|
||||
|
||||
type tagType string
|
||||
|
||||
const (
|
||||
TagTypeBlock tagType = "block"
|
||||
TagTypeInline tagType = "inline"
|
||||
|
||||
// TagTypeRemove will remove that node in the _PreRender_ phase with a high priority.
|
||||
TagTypeRemove tagType = "remove"
|
||||
)
|
||||
|
||||
func (r *register) TagType(tagName string, tagType tagType, priority int) {
|
||||
r.conv.m.Lock()
|
||||
defer r.conv.m.Unlock()
|
||||
|
||||
val := prioritized(tagType, priority)
|
||||
r.conv.tagTypes[tagName] = append(r.conv.tagTypes[tagName], val)
|
||||
}
|
||||
func (conv *Converter) getTagType(tagName string) (tagType, bool) {
|
||||
conv.m.RLock()
|
||||
defer conv.m.RUnlock()
|
||||
|
||||
types, ok := conv.tagTypes[tagName]
|
||||
if !ok || len(types) == 0 {
|
||||
|
||||
if dom.NameIsBlockNode(tagName) {
|
||||
return TagTypeBlock, true
|
||||
} else if dom.NameIsInlineNode(tagName) {
|
||||
return TagTypeInline, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
types.Sort()
|
||||
firstType := types[0].Value
|
||||
|
||||
return firstType, true
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/JohannesKaufmann/dom"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func (conv *Converter) handleRenderNodes(ctx Context, w Writer, nodes ...*html.Node) {
|
||||
for _, node := range nodes {
|
||||
conv.handleRenderNode(ctx, w, node)
|
||||
}
|
||||
}
|
||||
|
||||
func (conv *Converter) handleRenderNode(ctx Context, w Writer, node *html.Node) RenderStatus {
|
||||
name := dom.NodeName(node)
|
||||
|
||||
// - - A: the #text node - - //
|
||||
if name == "#text" {
|
||||
return conv.handleRenderText(ctx, w, node)
|
||||
}
|
||||
|
||||
// - - B: the render handlers - - //
|
||||
for _, handler := range conv.getRenderHandlers() {
|
||||
status := handler.Value(ctx, w, node)
|
||||
if status == RenderSuccess {
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
// - - C: the fallback - - //
|
||||
// If nothing works we fallback to this:
|
||||
return conv.handleRenderFallback(ctx, w, node)
|
||||
}
|
||||
|
||||
func (conv *Converter) handleRenderFallback(ctx Context, w Writer, node *html.Node) RenderStatus {
|
||||
|
||||
tagName := dom.NodeName(node)
|
||||
tagType, _ := ctx.GetTagType(tagName)
|
||||
|
||||
if tagType == TagTypeBlock {
|
||||
w.WriteRune('\n')
|
||||
w.WriteRune('\n')
|
||||
}
|
||||
ctx.RenderChildNodes(ctx, w, node)
|
||||
if tagType == TagTypeBlock {
|
||||
w.WriteRune('\n')
|
||||
w.WriteRune('\n')
|
||||
}
|
||||
|
||||
return RenderSuccess
|
||||
}
|
||||
func (conv *Converter) handleRenderText(ctx Context, w Writer, node *html.Node) RenderStatus {
|
||||
content := node.Data
|
||||
|
||||
for _, handler := range conv.getTextTransformHandlers() {
|
||||
content = handler.Value(ctx, content)
|
||||
}
|
||||
|
||||
w.WriteString(content)
|
||||
return RenderSuccess
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package converter
|
||||
|
||||
type RenderStatus int
|
||||
|
||||
const (
|
||||
RenderTryNext RenderStatus = iota
|
||||
RenderSuccess
|
||||
)
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package converter
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var percentEncodingReplacer = strings.NewReplacer(
|
||||
" ", "%20",
|
||||
"[", "%5B",
|
||||
"]", "%5D",
|
||||
"(", "%28",
|
||||
")", "%29",
|
||||
"<", "%3C",
|
||||
">", "%3E",
|
||||
)
|
||||
|
||||
func parseBaseDomain(rawDomain string) *url.URL {
|
||||
if rawDomain == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
u1, err := url.Parse(rawDomain)
|
||||
if err == nil && u1.Host != "" {
|
||||
// Yes, we got valid domain (probably with a http/https scheme)
|
||||
return u1
|
||||
}
|
||||
|
||||
u2, err := url.Parse("http://" + rawDomain)
|
||||
if err == nil && u2.Host != "" {
|
||||
// Yes, we got a valid domain (by choosing a fallback scheme)
|
||||
return u2
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func defaultAssembleAbsoluteURL(tagName string, rawURL string, domain string) string {
|
||||
rawURL = strings.TrimSpace(rawURL)
|
||||
|
||||
if rawURL == "#" {
|
||||
// Golangs url.Parse does not seem to distinguish between
|
||||
// no fragment and an empty fragment.
|
||||
return rawURL
|
||||
}
|
||||
|
||||
// Increase the chance that the url will be parsed
|
||||
rawURL = strings.ReplaceAll(rawURL, "\n", "%0A")
|
||||
rawURL = strings.ReplaceAll(rawURL, "\t", "%09")
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
// We can't do anything with this url because it is invalid
|
||||
return percentEncodingReplacer.Replace(rawURL)
|
||||
}
|
||||
|
||||
if u.Scheme == "data" {
|
||||
// This is a data uri (for example an inline base64 image)
|
||||
return percentEncodingReplacer.Replace(rawURL)
|
||||
}
|
||||
|
||||
// The default Query().Encode() encodes the query parameters "sorted by key".
|
||||
// Instead we want to keep the original order, but still encode the parameters.
|
||||
u.RawQuery = ParseAndEncodeQuery(u.RawQuery)
|
||||
|
||||
// For better compatibility (especially in regards to mailto links),
|
||||
// instead of encoding a space with a "+" we use ""%20" to prevent
|
||||
// e.g. the email reading "Hi+Johannes" instead of "Hi Johannes"
|
||||
u.RawQuery = strings.ReplaceAll(u.RawQuery, "+", "%20")
|
||||
|
||||
if base := parseBaseDomain(domain); base != nil {
|
||||
// If a "domain" is provided, we use that to convert relative links
|
||||
// to absolute links.
|
||||
u = base.ResolveReference(u)
|
||||
}
|
||||
|
||||
return percentEncodingReplacer.Replace(u.String())
|
||||
}
|
||||
|
||||
// - - - - //
|
||||
|
||||
func decodeAndEncode(original string) string {
|
||||
s, err := url.QueryUnescape(original)
|
||||
if err != nil {
|
||||
return original
|
||||
}
|
||||
|
||||
return url.QueryEscape(s)
|
||||
}
|
||||
|
||||
func ParseAndEncodeQuery(rawQuery string) string {
|
||||
if rawQuery == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
rawParts := strings.Split(rawQuery, "&")
|
||||
encodedParts := make([]string, len(rawParts))
|
||||
|
||||
for i, part := range rawParts {
|
||||
splitted := strings.SplitN(part, "=", 2)
|
||||
|
||||
if len(splitted) == 1 {
|
||||
// A: Just the key
|
||||
encodedParts[i] = decodeAndEncode(splitted[0])
|
||||
} else if splitted[1] == "" {
|
||||
// B: The key and the equal sign
|
||||
encodedParts[i] = decodeAndEncode(splitted[0]) + "="
|
||||
} else {
|
||||
// C: The key and the equal sign and the value
|
||||
encodedParts[i] = decodeAndEncode(splitted[0]) + "=" + decodeAndEncode(splitted[1])
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(encodedParts, "&")
|
||||
}
|
||||
Reference in New Issue
Block a user