/*
* Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
*/
package commonmark4cj.commonmark
/**
* Renders a tree of nodes to HTML.
* <p>
* Start with the {@link #builder} method to configure the renderer. Example:
* <pre><code>
* HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build()
* renderer.render(node)
* </code></pre>
*/
public class HtmlRenderer <: Renderer {
let softbreak: String
let escapeHtml: Bool
let percentEncodeUrls: Bool
let attributeProviderFactories: ArrayList<AttributeProviderFactory>
let nodeRendererFactories: ArrayList<HtmlNodeRendererFactory>
HtmlRenderer(builder: HtmlRendererBuilder) {
this.softbreak = builder._softbreak
this.escapeHtml = builder._escapeHtml
this.percentEncodeUrls = builder._percentEncodeUrls
this.attributeProviderFactories = ArrayList<AttributeProviderFactory>(builder.attributeProviderFactories)
this.nodeRendererFactories = ArrayList<HtmlNodeRendererFactory>(builder.nodeRendererFactories.size + 1)
this.nodeRendererFactories.add(all: builder.nodeRendererFactories)
// Add as last. This means clients can override the rendering of core nodes if they want.
this.nodeRendererFactories.add({context: HtmlNodeRendererContext => CoreHtmlNodeRenderer(context)})
}
/**
* Create a new builder for configuring an {@link HtmlRenderer}.
*
* @return a builder
*/
public static func builder(): HtmlRendererBuilder {
return HtmlRendererBuilder()
}
public override func render(node: Node, output: StringBuilder): Unit {
let context: HtmlRendererContextImpl = HtmlRendererContextImpl(HtmlWriter(output), this)
for (attributeProviderFactory in attributeProviderFactories) {
context.attributeProviders.add(attributeProviderFactory(context))
}
// The first node renderer for a node type "wins".
for (i in nodeRendererFactories.size - 1..=0 : -1) {
let nodeRendererFactory: HtmlNodeRendererFactory = nodeRendererFactories[i]
let nodeRenderer: NodeRenderer = nodeRendererFactory(context)
context.nodeRendererMap.add(nodeRenderer)
}
context.render(node)
}
public override func render(node: Node): String {
let sb: StringBuilder = StringBuilder()
render(node, sb)
return sb.toString()
}
}
/**
* Builder for configuring an {@link HtmlRenderer}. See methods for default configuration.
*/
public class HtmlRendererBuilder {
var _softbreak: String = "\n"
var _escapeHtml: Bool = false
var _percentEncodeUrls: Bool = false
let attributeProviderFactories: ArrayList<AttributeProviderFactory> = ArrayList<AttributeProviderFactory>()
let nodeRendererFactories: ArrayList<HtmlNodeRendererFactory> = ArrayList<HtmlNodeRendererFactory>()
/**
* @return the configured {@link HtmlRenderer}
*/
public func build(): HtmlRenderer {
return HtmlRenderer(this)
}
/**
* The HTML to use for rendering a softbreak, defaults to {@code "\n"} (meaning the rendered result doesn't have
* a line break).
* <p>
* HashSet it to {@code "<br>"} (or {@code "<br />"} to make them hard breaks.
* <p>
* HashSet it to {@code " "} to ignore line wrapping in the source.
*
* @param softbreak HTML for softbreak
* @return {@code this}
*/
public func softbreak(softbreak: String): HtmlRendererBuilder {
this._softbreak = softbreak
return this
}
/**
* Whether {@link HtmlInline} and {@link HtmlBlock} should be escaped, defaults to {@code false}.
* <p>
* Note that {@link HtmlInline} is only a tag itself, not the text between an opening tag and a closing tag. So
* markup in the text will be parsed as normal and is not affected by this option.
*
* @param escapeHtml true for escaping, false for preserving raw HTML
* @return {@code this}
*/
public func escapeHtml(escapeHtml: Bool): HtmlRendererBuilder {
this._escapeHtml = escapeHtml
return this
}
/**
* Whether URLs of link or images should be percent-encoded, defaults to {@code false}.
* <p>
* If enabled, the following is done:
* <ul>
* <li>Existing percent-encoded parts are preserved (e.g. "%20" is kept as "%20")</li>
* <li>Reserved characters such as "/" are preserved, except for "[" and "]" (see encodeURI in JS)</li>
* <li>Unreserved characters such as "a" are preserved</li>
* <li>Other characters such umlauts are percent-encoded</li>
* </ul>
*
* @param percentEncodeUrls true to percent-encode, false for leaving as-is
* @return {@code this}
*/
public func percentEncodeUrls(percentEncodeUrls: Bool): HtmlRendererBuilder {
this._percentEncodeUrls = percentEncodeUrls
return this
}
/**
* Add a factory for an attribute provider for adding/changing HTML attributes to the rendered tags.
*
* @param attributeProviderFactory the attribute provider factory to add
* @return {@code this}
*/
public func attributeProviderFactory(attributeProviderFactory: AttributeProviderFactory): HtmlRendererBuilder {
this.attributeProviderFactories.add(attributeProviderFactory)
return this
}
/**
* Add a factory for instantiating a node renderer (done when rendering). This allows to override the rendering
* of node types or define rendering for custom node types.
* <p>
* If multiple node renderers for the same node type are created, the one from the factory that was added first
* "wins". (This is how the rendering for core node types can be overridden; the default rendering comes last.)
*
* @param nodeRendererFactory the factory for creating a node renderer
* @return {@code this}
*/
public func nodeRendererFactory(nodeRendererFactory: HtmlNodeRendererFactory): HtmlRendererBuilder {
this.nodeRendererFactories.add(nodeRendererFactory)
return this
}
/**
* @param extensions extensions to use on this HTML renderer
* @return {@code this}
*/
public func extensions(extensions: Iterable<Extension>): HtmlRendererBuilder {
for (extension in extensions) {
if (extension is HtmlRendererExtension) {
let htmlRendererExtension: HtmlRendererExtension = (extension as HtmlRendererExtension)()
htmlRendererExtension.ext(this)
}
}
return this
}
}
/**
* Extension for {@link HtmlRenderer}.
*/
public interface HtmlRendererExtension <: Extension {
func ext(rendererBuilder: HtmlRendererBuilder): Unit
}
class HtmlRendererContextImpl <: HtmlNodeRendererContext & AttributeProviderContext {
let htmlWriter: HtmlWriter
let attributeProviders: ArrayList<AttributeProvider>
let nodeRendererMap: NodeRendererMap = NodeRendererMap()
let htmlRenderer: HtmlRenderer
init(htmlWriter: HtmlWriter, htmlRenderer: HtmlRenderer) {
this.htmlWriter = htmlWriter
this.htmlRenderer = htmlRenderer
this.attributeProviders = ArrayList<AttributeProvider>(htmlRenderer.attributeProviderFactories.size)
}
public override func shouldEscapeHtml(): Bool {
return htmlRenderer.escapeHtml
}
public override func encodeUrl(url: String): String {
if (htmlRenderer.percentEncodeUrls) {
return Escaping.percentEncodeUrl(url)
} else {
return url
}
}
public override func extendAttributes(
node: Node,
tagName: String,
attributes: HashMap<String, String>
): HashMap<String, String> {
let attrs: HashMap<String, String> = attributes.clone()
setCustomAttributes(node, tagName, attrs)
return attrs
}
public override func getWriter(): HtmlWriter {
return htmlWriter
}
public override func getSoftbreak(): String {
return htmlRenderer.softbreak
}
public override func render(node: Node): Unit {
nodeRendererMap.render(node)
}
private func setCustomAttributes(node: Node, tagName: String, attrs: HashMap<String, String>): Unit {
for (attributeProvider in attributeProviders) {
attributeProvider.setAttributes(node, tagName, attrs)
}
}
}
public class HtmlWriter {
private static let NO_ATTRIBUTES: HashMap<String, String> = EMPTY_MAP
private let buffer: StringBuilder
private var lastChar: Byte = 0 // 只关注[\x0\n]所以用Byte类型
public init(out: StringBuilder) {
this.buffer = out
}
public func raw(s: String): Unit {
append(s)
}
public func text(text: String): Unit {
append(Escaping.escapeHtml(text))
}
public func tag(name: String): Unit {
tag(name, NO_ATTRIBUTES)
}
public func tag(name: String, attrs: HashMap<String, String>): Unit {
tag(name, attrs, false)
}
public func tag(name: String, attrs: ?HashMap<String, String>, voidElement: Bool): Unit {
append("<")
append(name)
if (attrs.isSome() && attrs().size > 0) {
for ((k, v) in attrs()) {
append(" ")
append(Escaping.escapeHtml(k))
append("=\"")
append(Escaping.escapeHtml(v))
append("\"")
}
}
if (voidElement) {
append(" /")
}
append(">")
}
public func line(): Unit {
if (lastChar != 0 && lastChar != b'\n') {
append("\n")
}
}
protected func append(s: String): Unit {
buffer.append(s)
let length = s.size
if (length != 0) {
lastChar = s[length - 1]
}
}
}
/**
* Extension point for adding/changing attributes on HTML tags for a node.
*/
public interface AttributeProvider {
/**
* HashSet the attributes for a HTML tag of the specified node by modifying the provided map.
* <p>
* This allows to change or even remove default attributes. With great power comes great responsibility.
* <p>
* The attribute key and values will be escaped (preserving character entities), so don't escape them here,
* otherwise they will be double-escaped.
* <p>
* This method may be called multiple times for the same node, if the node is rendered using multiple nested
* tags (e.g. code blocks).
*
* @param node the node to set attributes for
* @param tagName the HTML tag name that these attributes are for (e.g. {@code h1}, {@code pre}, {@code code}).
* @param attributes the attributes, with any default attributes already set in the map
*/
func setAttributes(node: Node, tagName: String, attributes: HashMap<String, String>): Unit
}
/**
* The context for attribute providers.
* <p>Note: There are currently no methods here, this is for future extensibility.</p>
* <p><em>This interface is not intended to be implemented by clients.</em></p>
*/
public interface AttributeProviderContext {}
/**
* Factory for instantiating new attribute providers when rendering is done.
* Create a new attribute provider.
*
* @param context for this attribute provider
* @return an AttributeProvider
*/
public type AttributeProviderFactory = (context: AttributeProviderContext) -> AttributeProvider
/**
* The node renderer that renders all the core nodes (comes last in the order of node renderers).
*/
class CoreHtmlNodeRenderer <: AbstractVisitor & NodeRenderer {
protected let context: HtmlNodeRendererContext
private let html: HtmlWriter
public init(context: HtmlNodeRendererContext) {
this.context = context
this.html = context.getWriter()
}
public override func getNodeTypes(): HashSet<NodeType> {
return HashSet<NodeType>(
[
DocumentType,
HeadingType,
ParagraphType,
BlockQuoteType,
BulletListType,
FencedCodeBlockType,
HtmlBlockType,
ThematicBreakType,
IndentedCodeBlockType,
LinkType,
ListItemType,
OrderedListType,
ImageType,
EmphasisType,
StrongEmphasisType,
TextType,
CodeType,
HtmlInlineType,
SoftLineBreakType,
HardLineBreakType
]
)
}
public override func render(node: Node): Unit {
node.accept(this)
}
public override func visit(document: Document): Unit {
// No rendering itself
visitChildren(document)
}
public override func visit(heading: Heading): Unit {
let htag: String = "h${heading.getLevel()}"
html.line()
html.tag(htag, getAttrs(heading, htag))
visitChildren(heading)
html.tag("/${htag}")
html.line()
}
public override func visit(paragraph: Paragraph): Unit {
let inTightList: Bool = isInTightList(paragraph)
if (!inTightList) {
html.line()
html.tag("p", getAttrs(paragraph, "p"))
}
visitChildren(paragraph)
if (!inTightList) {
html.tag("/p")
html.line()
}
}
public override func visit(blockQuote: BlockQuote): Unit {
html.line()
html.tag("blockquote", getAttrs(blockQuote, "blockquote"))
html.line()
visitChildren(blockQuote)
html.line()
html.tag("/blockquote")
html.line()
}
public override func visit(bulletList: BulletList): Unit {
renderListBlock(bulletList, "ul", getAttrs(bulletList, "ul"))
}
public override func visit(fencedCodeBlock: FencedCodeBlock): Unit {
let literal: String = fencedCodeBlock.getLiteral()
let attributes: HashMap<String, String> = HashMap<String, String>()
let info: String = fencedCodeBlock.getInfo()
if (info.size > 0) {
let language: String = if (let Some(space) <- info.indexOf(" ")) {
info[..space]
} else {
info
}
attributes.add("class", "language-${language}")
}
renderCodeBlock(literal, fencedCodeBlock, attributes)
}
public override func visit(htmlBlock: HtmlBlock): Unit {
html.line()
if (context.shouldEscapeHtml()) {
html.tag("p", getAttrs(htmlBlock, "p"))
html.text(htmlBlock.getLiteral())
html.tag("/p")
} else {
html.raw(htmlBlock.getLiteral())
}
html.line()
}
public override func visit(thematicBreak: ThematicBreak): Unit {
html.line()
html.tag("hr", getAttrs(thematicBreak, "hr"), true)
html.line()
}
public override func visit(indentedCodeBlock: IndentedCodeBlock): Unit {
renderCodeBlock(indentedCodeBlock.getLiteral(), indentedCodeBlock, EMPTY_MAP)
}
public override func visit(link: Link): Unit {
let attrs: HashMap<String, String> = HashMap<String, String>()
let url: String = context.encodeUrl(link.getDestination())
attrs.add("href", url)
if (let Some(v) <- link.getTitle()) {
attrs.add("title", v)
}
html.tag("a", getAttrs(link, "a", attrs))
visitChildren(link)
html.tag("/a")
}
public override func visit(listItem: ListItem): Unit {
html.tag("li", getAttrs(listItem, "li"))
visitChildren(listItem)
html.tag("/li")
html.line()
}
public override func visit(orderedList: OrderedList): Unit {
let start: Int = orderedList.getStartNumber()
let attrs: HashMap<String, String> = HashMap<String, String>()
if (start != 1) {
attrs.add("start", start.toString())
}
renderListBlock(orderedList, "ol", getAttrs(orderedList, "ol", attrs))
}
public override func visit(image: Image): Unit {
let url: String = context.encodeUrl(image.getDestination())
let altTextVisitor: AltTextVisitor = AltTextVisitor()
image.accept(altTextVisitor)
let altText: String = altTextVisitor.getAltText()
let attrs: HashMap<String, String> = HashMap<String, String>()
attrs.add("src", url)
attrs.add("alt", altText)
if (let Some(v) <- image.getTitle()) {
attrs.add("title", v)
}
html.tag("img", getAttrs(image, "img", attrs), true)
}
public override func visit(emphasis: Emphasis): Unit {
html.tag("em", getAttrs(emphasis, "em"))
visitChildren(emphasis)
html.tag("/em")
}
public override func visit(strongEmphasis: StrongEmphasis): Unit {
html.tag("strong", getAttrs(strongEmphasis, "strong"))
visitChildren(strongEmphasis)
html.tag("/strong")
}
public override func visit(text: Text): Unit {
html.text(text.getLiteral())
}
public override func visit(code: Code): Unit {
html.tag("code", getAttrs(code, "code"))
html.text(code.getLiteral())
html.tag("/code")
}
public override func visit(htmlInline: HtmlInline): Unit {
if (context.shouldEscapeHtml()) {
html.text(htmlInline.getLiteral())
} else {
html.raw(htmlInline.getLiteral())
}
}
public override func visit(_: SoftLineBreak): Unit {
html.raw(context.getSoftbreak())
}
public override func visit(hardLineBreak: HardLineBreak): Unit {
html.tag("br", getAttrs(hardLineBreak, "br"), true)
html.line()
}
protected override func visitChildren(parent: Node): Unit {
var node: ?Node = parent.getFirstChild()
while (let Some(v) <- node) {
node = v.getNext()
context.render(v)
}
}
private func renderCodeBlock(literal: String, node: Node, attributes: HashMap<String, String>): Unit {
html.line()
html.tag("pre", getAttrs(node, "pre"))
html.tag("code", getAttrs(node, "code", attributes))
html.text(literal)
html.tag("/code")
html.tag("/pre")
html.line()
}
private func renderListBlock(listBlock: ListBlock, tagName: String, attributes: HashMap<String, String>): Unit {
html.line()
html.tag(tagName, attributes)
html.line()
visitChildren(listBlock)
html.line()
html.tag("/${tagName}")
html.line()
}
private func isInTightList(paragraph: Paragraph): Bool {
if (let Some(parent) <- paragraph.getParent()) {
match (parent.getParent()) {
case Some(list: ListBlock) => return list.isTight()
case _ => ()
}
}
return false
}
private func getAttrs(node: Node, tagName: String): HashMap<String, String> {
return getAttrs(node, tagName, EMPTY_MAP)
}
private func getAttrs(node: Node, tagName: String, defaultAttributes: HashMap<String, String>): HashMap<String,
String> {
return context.extendAttributes(node, tagName, defaultAttributes)
}
}
class AltTextVisitor <: AbstractVisitor {
private let sb: StringBuilder = StringBuilder()
func getAltText(): String {
return sb.toString()
}
public override func visit(text: Text): Unit {
sb.append(text.getLiteral())
}
public override func visit(_: SoftLineBreak): Unit {
sb.append('\n')
}
public override func visit(_: HardLineBreak): Unit {
sb.append('\n')
}
}
public interface HtmlNodeRendererContext {
/**
* @param url to be encoded
* @return an encoded URL (depending on the configuration)
*/
func encodeUrl(url: String): String
/**
* Let extensions modify the HTML tag attributes.
*
* @param node the node for which the attributes are applied
* @param tagName the HTML tag name that these attributes are for (e.g. {@code h1}, {@code pre}, {@code code}).
* @param attributes the attributes that were calculated by the renderer
* @return the extended attributes with added/updated/removed entries
*/
func extendAttributes(node: Node, tagName: String, attributes: HashMap<String, String>): HashMap<String, String>
/**
* @return the HTML writer to use
*/
func getWriter(): HtmlWriter
/**
* @return HTML that should be rendered for a soft line break
*/
func getSoftbreak(): String
/**
* Render the specified node and its children using the configured renderers. This should be used to render child
* nodes; be careful not to pass the node that is being rendered, that would result in an endless loop.
*
* @param node the node to render
*/
func render(node: Node): Unit
/**
* @return whether HTML blocks and tags should be escaped or not
*/
func shouldEscapeHtml(): Bool
}
/**
* Factory for instantiating new node renderers when rendering is done.
* Create a new node renderer for the specified rendering context.
*
* @param context the context for rendering (normally passed on to the node renderer)
* @return a node renderer
*/
public type HtmlNodeRendererFactory = (context: HtmlNodeRendererContext) -> NodeRenderer