//
// Copyright (c) 2024, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 01 Nov 2024 Matthew Giannini Creation
//
**
** Extension for adding attributes to image nodes.
**
@Js
const class ImgAttrsExt : MarkdownExt
{
override Void extendParser(ParserBuilder builder)
{
builder.customDelimiterProcessor(ImgAttrsDelimiterProcessor())
}
override Void extendHtml(HtmlRendererBuilder builder)
{
builder.attrProviderFactory |HtmlContext cx->AttrProvider| { ImgAttrsAttrProvider() }
}
override Void extendMarkdown(MarkdownRendererBuilder builder)
{
builder.nodeRendererFactory(|cx->NodeRenderer| { MarkdownImgAttrsRenderer(cx) })
}
}
**************************************************************************
** ImgAttrs
**************************************************************************
@Js
class ImgAttrs : CustomNode, Delimited
{
new make([Str:Str] attrs) { this.attrs = attrs }
const [Str:Str] attrs
override const Str openingDelim := "{"
override const Str closingDelim := "}"
override protected Str toStrAttributes() { "imgAttrs=${attrs}" }
}
**************************************************************************
** ImgAttrsDelimiterProcessor
**************************************************************************
@Js
internal const class ImgAttrsDelimiterProcessor : DelimiterProcessor
{
private static const Str[] supported_attrs := ["width", "height"]
override const Int openingChar := '{'
override const Int closingChar := '}'
override const Int minLen := 1
override Int process(Delimiter openingRun, Delimiter closingRun)
{
if (openingRun.size != 1) return 0
// check if the attributes can be applied - if the previous node is an image,
// and if all the attributes are in the set of supported_attrs
opener := openingRun.opener
nodeToStyle := opener.prev
if (nodeToStyle isnot Image) return 0
toUnlink := Node[,]
content := StrBuf()
unsupported := false
Node.eachBetween(opener, closingRun.closer) |node|
{
if (unsupported) return
// only text nodes can be used for attributes
if (node is Text)
{
content.add(((Text)node).literal)
toUnlink.add(node)
}
else unsupported = true
}
if (unsupported) return 0
attrs := [Str:Str][:] { ordered = true }
res := content.toStr.split.eachWhile |s|
{
attr := s.split('=')
if (attr.size > 1 && supported_attrs.contains(attr[0].lower))
{
attrs[attr[0]] = attr[1]
return null
}
else
{
// attribute is unsupported
return 0
}
}
if (res != null) return 0
// unlink the temp nodes
toUnlink.each { it.unlink }
if (!attrs.isEmpty)
{
nodeToStyle.appendChild(ImgAttrs(attrs))
}
return 1
}
}
**************************************************************************
** ImgAttrsAttrProvider
**************************************************************************
@Js
internal class ImgAttrsAttrProvider : AttrProvider
{
override Void setAttrs(Node node, Str tagName, [Str:Str?] attrs)
{
if (node is Image)
{
Node.eachChild(node) |c|
{
imgAttrs := c as ImgAttrs
if (imgAttrs == null) return
imgAttrs.attrs.each |v, k| { attrs[k] = v }
// NOTE: the java implementation removes the node, but then the same
// doc cannot be used for html and markdown rendering. not sure why
// they do that. Gonna leave this commented out for now.
// now that we have used the image attributes we remove the node
// imgAttrs.unlink
}
}
}
}
**************************************************************************
** MarkdownImgAttrsRenderer
**************************************************************************
@Js
internal class MarkdownImgAttrsRenderer : NodeRenderer, Visitor
{
new make(MarkdownContext cx)
{
this.writer = cx.writer
}
private MarkdownWriter writer
override const Type[] nodeTypes := [ImgAttrs#]
override Void beforeRoot(Node root)
{
root.walk(this)
}
override Void afterRoot(Node root)
{
root.walk(this)
}
Void visitImgAttrs(ImgAttrs attrs)
{
if (attrs.parent is Image)
{
// if parent is an image, then this is pre-processing and we need to make
// it the next sibling of the image so the attributes render correctly back
// to markdown after the image is rendered
img := attrs.parent
attrs.unlink
img.insertAfter(attrs)
}
else if (attrs.prev is Image)
{
// we are post-processing and want to put the node back as a child of the image
// to try and preserve the original parsed AST
img := attrs.prev
attrs.unlink
img.appendChild(attrs)
}
}
override Void render(Node node)
{
attrs := node as ImgAttrs
writer.raw(attrs.openingDelim)
i := 0
attrs.attrs.each |v,k|
{
if (i > 0) writer.raw(' ')
writer.raw("${k}=${v}")
++i
}
writer.raw(attrs.closingDelim)
}
}