com.vladsch.flexmark.html.HtmlRenderer Maven / Gradle / Ivy
Show all versions of driver-cql-shaded Show documentation
package com.vladsch.flexmark.html;
import com.vladsch.flexmark.ast.HtmlBlock;
import com.vladsch.flexmark.ast.HtmlInline;
import com.vladsch.flexmark.html.renderer.*;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.ast.IRender;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.builder.BuilderBase;
import com.vladsch.flexmark.util.data.*;
import com.vladsch.flexmark.util.dependency.DependencyResolver;
import com.vladsch.flexmark.util.format.TrackedOffset;
import com.vladsch.flexmark.util.format.TrackedOffsetUtils;
import com.vladsch.flexmark.util.html.Attributes;
import com.vladsch.flexmark.util.misc.Extension;
import com.vladsch.flexmark.util.misc.Pair;
import com.vladsch.flexmark.util.sequence.Escaping;
import com.vladsch.flexmark.util.sequence.LineAppendable;
import com.vladsch.flexmark.util.sequence.TagRange;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Renders a tree of nodes to HTML.
*
* Start with the {@link #builder} method to configure the renderer. Example:
*
* HtmlRenderer renderer = HtmlRenderer.builder().escapeHtml(true).build();
* renderer.render(node);
*
*/
@SuppressWarnings("WeakerAccess")
public class HtmlRenderer implements IRender {
final public static DataKey SOFT_BREAK = new DataKey<>("SOFT_BREAK", "\n");
final public static DataKey HARD_BREAK = new DataKey<>("HARD_BREAK", "
\n");
final public static NullableDataKey STRONG_EMPHASIS_STYLE_HTML_OPEN = new NullableDataKey<>("STRONG_EMPHASIS_STYLE_HTML_OPEN");
final public static NullableDataKey STRONG_EMPHASIS_STYLE_HTML_CLOSE = new NullableDataKey<>("STRONG_EMPHASIS_STYLE_HTML_CLOSE");
final public static NullableDataKey EMPHASIS_STYLE_HTML_OPEN = new NullableDataKey<>("EMPHASIS_STYLE_HTML_OPEN");
final public static NullableDataKey EMPHASIS_STYLE_HTML_CLOSE = new NullableDataKey<>("EMPHASIS_STYLE_HTML_CLOSE");
final public static NullableDataKey CODE_STYLE_HTML_OPEN = new NullableDataKey<>("CODE_STYLE_HTML_OPEN");
final public static NullableDataKey CODE_STYLE_HTML_CLOSE = new NullableDataKey<>("CODE_STYLE_HTML_CLOSE");
final public static NullableDataKey INLINE_CODE_SPLICE_CLASS = new NullableDataKey<>("INLINE_CODE_SPLICE_CLASS");
final public static DataKey PERCENT_ENCODE_URLS = SharedDataKeys.PERCENT_ENCODE_URLS;
final public static DataKey INDENT_SIZE = SharedDataKeys.INDENT_SIZE;
final public static DataKey ESCAPE_HTML = new DataKey<>("ESCAPE_HTML", false);
final public static DataKey ESCAPE_HTML_BLOCKS = new DataKey<>("ESCAPE_HTML_BLOCKS", ESCAPE_HTML);
final public static DataKey ESCAPE_HTML_COMMENT_BLOCKS = new DataKey<>("ESCAPE_HTML_COMMENT_BLOCKS", ESCAPE_HTML_BLOCKS);
final public static DataKey ESCAPE_INLINE_HTML = new DataKey<>("ESCAPE_HTML_BLOCKS", ESCAPE_HTML);
final public static DataKey ESCAPE_INLINE_HTML_COMMENTS = new DataKey<>("ESCAPE_INLINE_HTML_COMMENTS", ESCAPE_INLINE_HTML);
final public static DataKey SUPPRESS_HTML = new DataKey<>("SUPPRESS_HTML", false);
final public static DataKey SUPPRESS_HTML_BLOCKS = new DataKey<>("SUPPRESS_HTML_BLOCKS", SUPPRESS_HTML);
final public static DataKey SUPPRESS_HTML_COMMENT_BLOCKS = new DataKey<>("SUPPRESS_HTML_COMMENT_BLOCKS", SUPPRESS_HTML_BLOCKS);
final public static DataKey SUPPRESS_INLINE_HTML = new DataKey<>("SUPPRESS_INLINE_HTML", SUPPRESS_HTML);
final public static DataKey SUPPRESS_INLINE_HTML_COMMENTS = new DataKey<>("SUPPRESS_INLINE_HTML_COMMENTS", SUPPRESS_INLINE_HTML);
final public static DataKey SOURCE_WRAP_HTML = new DataKey<>("SOURCE_WRAP_HTML", false);
final public static DataKey SOURCE_WRAP_HTML_BLOCKS = new DataKey<>("SOURCE_WRAP_HTML_BLOCKS", SOURCE_WRAP_HTML);
final public static DataKey HEADER_ID_GENERATOR_RESOLVE_DUPES = SharedDataKeys.HEADER_ID_GENERATOR_RESOLVE_DUPES;
final public static DataKey HEADER_ID_GENERATOR_TO_DASH_CHARS = SharedDataKeys.HEADER_ID_GENERATOR_TO_DASH_CHARS;
final public static DataKey HEADER_ID_GENERATOR_NON_DASH_CHARS = SharedDataKeys.HEADER_ID_GENERATOR_NON_DASH_CHARS;
final public static DataKey HEADER_ID_GENERATOR_NO_DUPED_DASHES = SharedDataKeys.HEADER_ID_GENERATOR_NO_DUPED_DASHES;
final public static DataKey HEADER_ID_GENERATOR_NON_ASCII_TO_LOWERCASE = SharedDataKeys.HEADER_ID_GENERATOR_NON_ASCII_TO_LOWERCASE;
final public static DataKey HEADER_ID_REF_TEXT_TRIM_LEADING_SPACES = SharedDataKeys.HEADER_ID_REF_TEXT_TRIM_LEADING_SPACES;
final public static DataKey HEADER_ID_REF_TEXT_TRIM_TRAILING_SPACES = SharedDataKeys.HEADER_ID_REF_TEXT_TRIM_TRAILING_SPACES;
final public static DataKey HEADER_ID_ADD_EMOJI_SHORTCUT = SharedDataKeys.HEADER_ID_ADD_EMOJI_SHORTCUT;
final public static DataKey RENDER_HEADER_ID = SharedDataKeys.RENDER_HEADER_ID;
final public static DataKey GENERATE_HEADER_ID = SharedDataKeys.GENERATE_HEADER_ID;
final public static DataKey DO_NOT_RENDER_LINKS = SharedDataKeys.DO_NOT_RENDER_LINKS;
final public static DataKey FENCED_CODE_LANGUAGE_CLASS_PREFIX = new DataKey<>("FENCED_CODE_LANGUAGE_CLASS_PREFIX", "language-");
final public static DataKey FENCED_CODE_NO_LANGUAGE_CLASS = new DataKey<>("FENCED_CODE_NO_LANGUAGE_CLASS", "");
final public static DataKey SOURCE_POSITION_ATTRIBUTE = new DataKey<>("SOURCE_POSITION_ATTRIBUTE", "");
final public static DataKey SOURCE_POSITION_PARAGRAPH_LINES = new DataKey<>("SOURCE_POSITION_PARAGRAPH_LINES", false);
final public static DataKey TYPE = new DataKey<>("TYPE", "HTML");
final public static DataKey> TAG_RANGES = new DataKey<>("TAG_RANGES", ArrayList::new);
final public static DataKey RECHECK_UNDEFINED_REFERENCES = new DataKey<>("RECHECK_UNDEFINED_REFERENCES", false);
final public static DataKey OBFUSCATE_EMAIL = new DataKey<>("OBFUSCATE_EMAIL", false);
final public static DataKey OBFUSCATE_EMAIL_RANDOM = new DataKey<>("OBFUSCATE_EMAIL_RANDOM", true);
final public static DataKey HTML_BLOCK_OPEN_TAG_EOL = new DataKey<>("HTML_BLOCK_OPEN_TAG_EOL", true);
final public static DataKey HTML_BLOCK_CLOSE_TAG_EOL = new DataKey<>("HTML_BLOCK_CLOSE_TAG_EOL", true);
final public static DataKey UNESCAPE_HTML_ENTITIES = new DataKey<>("UNESCAPE_HTML_ENTITIES", true);
final public static DataKey AUTOLINK_WWW_PREFIX = new DataKey<>("AUTOLINK_WWW_PREFIX", "http://");
// regex for suppressed link prefixes
final public static DataKey SUPPRESSED_LINKS = new DataKey<>("SUPPRESSED_LINKS", "javascript:.*");
final public static DataKey NO_P_TAGS_USE_BR = new DataKey<>("NO_P_TAGS_USE_BR", false);
final public static DataKey EMBEDDED_ATTRIBUTE_PROVIDER = new DataKey<>("EMBEDDED_ATTRIBUTE_PROVIDER", true);
/**
* output control for FormattingAppendable, see {@link LineAppendable#setOptions(int)}
*/
final public static DataKey FORMAT_FLAGS = new DataKey<>("RENDERER_FORMAT_FLAGS", LineAppendable.F_TRIM_LEADING_WHITESPACE);
final public static DataKey MAX_TRAILING_BLANK_LINES = SharedDataKeys.RENDERER_MAX_TRAILING_BLANK_LINES;
final public static DataKey MAX_BLANK_LINES = SharedDataKeys.RENDERER_MAX_BLANK_LINES;
// Use LineFormattingAppendable values instead,
// NOTE: ALLOW_LEADING_WHITESPACE is now inverted and named F_TRIM_LEADING_WHITESPACE
@Deprecated final public static int CONVERT_TABS = LineAppendable.F_CONVERT_TABS;
@Deprecated final public static int COLLAPSE_WHITESPACE = LineAppendable.F_COLLAPSE_WHITESPACE;
@Deprecated final public static int SUPPRESS_TRAILING_WHITESPACE = LineAppendable.F_TRIM_TRAILING_WHITESPACE;
@Deprecated final public static int PASS_THROUGH = LineAppendable.F_PASS_THROUGH;
// @Deprecated final public static int ALLOW_LEADING_WHITESPACE = LineAppendable.F_TRIM_LEADING_WHITESPACE;
@Deprecated final public static int FORMAT_ALL = LineAppendable.F_FORMAT_ALL;
/**
* Stores pairs of equivalent renderer types to allow extensions to resolve types not known to them
*
* Pair contains: rendererType, equivalentType
*/
final public static DataKey>> RENDERER_TYPE_EQUIVALENCE = new DataKey<>("RENDERER_TYPE_EQUIVALENCE", Collections.emptyList());
// Use LineFormattingAppendable values instead
@Deprecated final public static int FORMAT_CONVERT_TABS = LineAppendable.F_CONVERT_TABS;
@Deprecated final public static int FORMAT_COLLAPSE_WHITESPACE = LineAppendable.F_COLLAPSE_WHITESPACE;
@Deprecated final public static int FORMAT_SUPPRESS_TRAILING_WHITESPACE = LineAppendable.F_TRIM_TRAILING_WHITESPACE;
@Deprecated final public static int FORMAT_ALL_OPTIONS = LineAppendable.F_FORMAT_ALL;
// Experimental, not tested
final public static DataKey> TRACKED_OFFSETS = new DataKey<>("TRACKED_OFFSETS", Collections.emptyList());
// now not final only to allow disposal of resources
final List attributeProviderFactories;
final List nodeRendererFactories;
final List linkResolverFactories;
final HeaderIdGeneratorFactory htmlIdGeneratorFactory;
final HtmlRendererOptions htmlOptions;
final DataHolder options;
HtmlRenderer(@NotNull Builder builder) {
this.options = builder.toImmutable();
this.htmlOptions = new HtmlRendererOptions(this.options);
this.htmlIdGeneratorFactory = builder.htmlIdGeneratorFactory;
// resolve renderer dependencies
List nodeRenderers = new ArrayList<>(builder.nodeRendererFactories.size());
for (int i = builder.nodeRendererFactories.size() - 1; i >= 0; i--) {
NodeRendererFactory nodeRendererFactory = builder.nodeRendererFactories.get(i);
nodeRenderers.add(new DelegatingNodeRendererFactoryWrapper(nodeRenderers, nodeRendererFactory));
}
// Add as last. This means clients can override the rendering of core nodes if they want by default
CoreNodeRenderer.Factory nodeRendererFactory = new CoreNodeRenderer.Factory();
nodeRenderers.add(new DelegatingNodeRendererFactoryWrapper(nodeRenderers, nodeRendererFactory));
nodeRendererFactories = DependencyResolver.resolveFlatDependencies(nodeRenderers, null, dependent -> dependent.getFactory().getClass());
// HACK: but for now works
boolean addEmbedded = !builder.attributeProviderFactories.containsKey(EmbeddedAttributeProvider.Factory.getClass());
List values = new ArrayList<>(builder.attributeProviderFactories.values());
if (addEmbedded && EMBEDDED_ATTRIBUTE_PROVIDER.get(options)) {
// add it first so the rest can override it if needed
values.add(0, EmbeddedAttributeProvider.Factory);
}
this.attributeProviderFactories = DependencyResolver.resolveFlatDependencies(values, null, null);
this.linkResolverFactories = DependencyResolver.resolveFlatDependencies(builder.linkResolverFactories, null, null);
}
/**
* Create a new builder for configuring an {@link HtmlRenderer}.
*
* @return a builder
*/
public static @NotNull Builder builder() {
return new Builder();
}
/**
* Create a new builder for configuring an {@link HtmlRenderer}.
*
* @param options initialization options
* @return a builder
*/
public static @NotNull Builder builder(@Nullable DataHolder options) {
return new Builder(options);
}
@NotNull
@Override
public DataHolder getOptions() {
return options;
}
/**
* Render a node to the appendable
*
* @param node node to render
* @param output appendable to use for the output
*/
public void render(@NotNull Node node, @NotNull Appendable output) {
render(node, output, htmlOptions.maxTrailingBlankLines);
}
/**
* Render a node to the appendable
*
* @param node node to render
* @param output appendable to use for the output
*/
public void render(@NotNull Node node, @NotNull Appendable output, int maxTrailingBlankLines) {
HtmlWriter htmlWriter = new HtmlWriter(output, htmlOptions.indentSize, htmlOptions.formatFlags, !htmlOptions.htmlBlockOpenTagEol, !htmlOptions.htmlBlockCloseTagEol);
MainNodeRenderer renderer = new MainNodeRenderer(options, htmlWriter, node.getDocument());
if (renderer.htmlIdGenerator != HtmlIdGenerator.NULL && !(node instanceof Document)) {
renderer.htmlIdGenerator.generateIds(node.getDocument());
}
renderer.render(node);
htmlWriter.appendToSilently(output, htmlOptions.maxBlankLines, maxTrailingBlankLines);
// resolve any unresolved tracked offsets that are outside elements which resolve their own
TrackedOffsetUtils.resolveTrackedOffsets(node.getChars(), htmlWriter, TRACKED_OFFSETS.get(renderer.getDocument()), maxTrailingBlankLines, SharedDataKeys.RUNNING_TESTS.get(options));
renderer.dispose();
}
/**
* Render the tree of nodes to HTML.
*
* @param node the root node
* @return the rendered HTML.
*/
@NotNull
public String render(@NotNull Node node) {
StringBuilder sb = new StringBuilder();
render(node, sb);
return sb.toString();
}
static public boolean isCompatibleRendererType(@NotNull MutableDataHolder options, @NotNull String supportedRendererType) {
String rendererType = HtmlRenderer.TYPE.get(options);
return isCompatibleRendererType(options, rendererType, supportedRendererType);
}
static public boolean isCompatibleRendererType(@NotNull MutableDataHolder options, @NotNull String rendererType, @NotNull String supportedRendererType) {
if (rendererType.equals(supportedRendererType)) {
return true;
}
List> equivalence = RENDERER_TYPE_EQUIVALENCE.get(options);
for (Pair pair : equivalence) {
if (rendererType.equals(pair.getFirst())) {
if (supportedRendererType.equals(pair.getSecond())) {
return true;
}
}
}
return false;
}
@SuppressWarnings("UnusedReturnValue")
static public @NotNull MutableDataHolder addRenderTypeEquivalence(@NotNull MutableDataHolder options, @NotNull String rendererType, @NotNull String supportedRendererType) {
if (!isCompatibleRendererType(options, rendererType, supportedRendererType)) {
// need to add
List> equivalence = RENDERER_TYPE_EQUIVALENCE.get(options);
ArrayList> newEquivalence = new ArrayList<>(equivalence);
newEquivalence.add(new Pair<>(rendererType, supportedRendererType));
options.set(RENDERER_TYPE_EQUIVALENCE, newEquivalence);
}
return options;
}
/**
* Builder for configuring an {@link HtmlRenderer}. See methods for default configuration.
*/
public static class Builder extends BuilderBase implements RendererBuilder {
Map, AttributeProviderFactory> attributeProviderFactories = new LinkedHashMap<>();
List nodeRendererFactories = new ArrayList<>();
List linkResolverFactories = new ArrayList<>();
HeaderIdGeneratorFactory htmlIdGeneratorFactory = null;
public Builder() {
super();
}
public Builder(@Nullable DataHolder options) {
super(options);
loadExtensions();
}
@Override
protected void removeApiPoint(@NotNull Object apiPoint) {
if (apiPoint instanceof AttributeProviderFactory) this.attributeProviderFactories.remove(apiPoint.getClass());
else if (apiPoint instanceof NodeRendererFactory) this.nodeRendererFactories.remove(apiPoint);
else if (apiPoint instanceof LinkResolverFactory) this.linkResolverFactories.remove(apiPoint);
else if (apiPoint instanceof HeaderIdGeneratorFactory) this.htmlIdGeneratorFactory = null;
else {
throw new IllegalStateException("Unknown data point type: " + apiPoint.getClass().getName());
}
}
@Override
protected void preloadExtension(@NotNull Extension extension) {
if (extension instanceof HtmlRendererExtension) {
HtmlRendererExtension htmlRendererExtension = (HtmlRendererExtension) extension;
htmlRendererExtension.rendererOptions(this);
} else if (extension instanceof RendererExtension) {
RendererExtension htmlRendererExtension = (RendererExtension) extension;
htmlRendererExtension.rendererOptions(this);
}
}
@Override
protected boolean loadExtension(@NotNull Extension extension) {
if (extension instanceof HtmlRendererExtension) {
HtmlRendererExtension htmlRendererExtension = (HtmlRendererExtension) extension;
htmlRendererExtension.extend(this, TYPE.get(this));
return true;
} else if (extension instanceof RendererExtension) {
RendererExtension htmlRendererExtension = (RendererExtension) extension;
htmlRendererExtension.extend(this, TYPE.get(this));
return true;
}
return false;
}
/**
* @return the configured {@link HtmlRenderer}
*/
@NotNull
public HtmlRenderer build() {
return new HtmlRenderer(this);
}
/**
* The HTML to use for rendering a softbreak, defaults to {@code "\n"} (meaning the rendered result doesn't have
* a line break).
*
* Set it to {@code "
"} (or {@code "
"} to make them hard breaks.
*
* Set it to {@code " "} to ignore line wrapping in the source.
*
* @param softBreak HTML for softbreak
* @return {@code this}
*/
public @NotNull Builder softBreak(@NotNull String softBreak) {
this.set(SOFT_BREAK, softBreak);
return this;
}
/**
* The size of the indent to use for hierarchical elements, default 0, means no indent, also fastest rendering
*
* @param indentSize number of spaces per indent
* @return {@code this}
*/
public @NotNull Builder indentSize(int indentSize) {
this.set(INDENT_SIZE, indentSize);
return this;
}
/**
* Whether {@link HtmlInline} and {@link HtmlBlock} should be escaped, defaults to {@code false}.
*
* 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 @NotNull Builder escapeHtml(boolean escapeHtml) {
this.set(ESCAPE_HTML, escapeHtml);
return this;
}
public boolean isRendererType(@NotNull String supportedRendererType) {
String rendererType = HtmlRenderer.TYPE.get(this);
return HtmlRenderer.isCompatibleRendererType(this, rendererType, supportedRendererType);
}
/**
* Whether URLs of link or images should be percent-encoded, defaults to {@code false}.
*
* If enabled, the following is done:
*
* - Existing percent-encoded parts are preserved (e.g. "%20" is kept as "%20")
* - Reserved characters such as "/" are preserved, except for "[" and "]" (see encodeURI in JS)
* - Unreserved characters such as "a" are preserved
* - Other characters such umlauts are percent-encoded
*
*
* @param percentEncodeUrls true to percent-encode, false for leaving as-is
* @return {@code this}
*/
public @NotNull Builder percentEncodeUrls(boolean percentEncodeUrls) {
this.set(PERCENT_ENCODE_URLS, percentEncodeUrls);
return this;
}
/**
* Add an attribute provider for adding/changing HTML attributes to the rendered tags.
*
* @param attributeProviderFactory the attribute provider factory to add
* @return {@code this}
*/
public @NotNull Builder attributeProviderFactory(@NotNull AttributeProviderFactory attributeProviderFactory) {
this.attributeProviderFactories.put(attributeProviderFactory.getClass(), attributeProviderFactory);
addExtensionApiPoint(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.
*
* 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 @NotNull Builder nodeRendererFactory(@NotNull NodeRendererFactory nodeRendererFactory) {
this.nodeRendererFactories.add(nodeRendererFactory);
addExtensionApiPoint(nodeRendererFactory);
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.
*
* 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 linkResolverFactory the factory for creating a node renderer
* @return {@code this}
*/
public @NotNull Builder linkResolverFactory(@NotNull LinkResolverFactory linkResolverFactory) {
this.linkResolverFactories.add(linkResolverFactory);
addExtensionApiPoint(linkResolverFactory);
return this;
}
/**
* Add a factory for generating the header id attribute from the header's text
*
* @param htmlIdGeneratorFactory the factory for generating header tag id attributes
* @return {@code this}
*/
@NotNull
public Builder htmlIdGeneratorFactory(@NotNull HeaderIdGeneratorFactory htmlIdGeneratorFactory) {
//noinspection VariableNotUsedInsideIf
if (this.htmlIdGeneratorFactory != null) {
throw new IllegalStateException("custom header id factory is already set to " + htmlIdGeneratorFactory.getClass().getName());
}
this.htmlIdGeneratorFactory = htmlIdGeneratorFactory;
addExtensionApiPoint(htmlIdGeneratorFactory);
return this;
}
}
/**
* Extension for {@link HtmlRenderer}.
*
* This should be implemented by all extensions that have HtmlRenderer extension code.
*
* Each extension will have its {@link HtmlRendererExtension#extend(Builder, String)} method called.
* and should call back on the builder argument to register all extension points
*/
public interface HtmlRendererExtension extends Extension {
/**
* This method is called first on all extensions so that they can adjust the options that must be
* common to all extensions.
*
* @param options option set that will be used for the builder
*/
void rendererOptions(@NotNull MutableDataHolder options);
/**
* Called to give each extension to register extension points that it contains
*
* @param htmlRendererBuilder builder to call back for extension point registration
* @param rendererType type of rendering being performed. For now "HTML", "JIRA" or "YOUTRACK"
* @see Builder#attributeProviderFactory(AttributeProviderFactory)
* @see Builder#nodeRendererFactory(NodeRendererFactory)
* @see Builder#linkResolverFactory(LinkResolverFactory)
* @see Builder#htmlIdGeneratorFactory(HeaderIdGeneratorFactory)
*/
void extend(@NotNull Builder htmlRendererBuilder, @NotNull String rendererType);
}
private class MainNodeRenderer extends NodeRendererSubContext implements NodeRendererContext, Disposable {
private Document document;
private Map, NodeRenderingHandlerWrapper> renderers;
private List phasedRenderers;
private LinkResolver[] myLinkResolvers;
private Set renderingPhases;
private DataHolder options;
private RenderingPhase phase;
HtmlIdGenerator htmlIdGenerator;
private HashMap> resolvedLinkMap = new HashMap<>();
private AttributeProvider[] attributeProviders;
@Override
public void dispose() {
document = null;
renderers = null;
phasedRenderers = null;
for (LinkResolver linkResolver : myLinkResolvers) {
if (linkResolver instanceof Disposable) ((Disposable) linkResolver).dispose();
}
myLinkResolvers = null;
renderingPhases = null;
options = null;
if (htmlIdGenerator instanceof Disposable) ((Disposable) htmlIdGenerator).dispose();
htmlIdGenerator = null;
resolvedLinkMap = null;
for (AttributeProvider attributeProvider : attributeProviders) {
if (attributeProvider instanceof Disposable) ((Disposable) attributeProvider).dispose();
}
attributeProviders = null;
}
MainNodeRenderer(DataHolder options, HtmlWriter htmlWriter, Document document) {
super(htmlWriter);
this.options = new ScopedDataSet(document, options);
this.document = document;
this.renderers = new HashMap<>(32);
this.renderingPhases = new HashSet<>(RenderingPhase.values().length);
this.phasedRenderers = new ArrayList<>(nodeRendererFactories.size());
this.myLinkResolvers = new LinkResolver[linkResolverFactories.size()];
this.doNotRenderLinksNesting = htmlOptions.doNotRenderLinksInDocument ? 0 : 1;
this.htmlIdGenerator = htmlIdGeneratorFactory != null ? htmlIdGeneratorFactory.create(this)
: (!(htmlOptions.renderHeaderId || htmlOptions.generateHeaderIds) ? HtmlIdGenerator.NULL : new HeaderIdGenerator.Factory().create(this));
htmlWriter.setContext(this);
for (int i = nodeRendererFactories.size() - 1; i >= 0; i--) {
NodeRendererFactory nodeRendererFactory = nodeRendererFactories.get(i);
NodeRenderer nodeRenderer = nodeRendererFactory.apply(this.getOptions());
Set> renderingHandlers = nodeRenderer.getNodeRenderingHandlers();
assert (renderingHandlers != null);
for (NodeRenderingHandler> nodeType : renderingHandlers) {
// Overwrite existing renderer
NodeRenderingHandlerWrapper handlerWrapper = new NodeRenderingHandlerWrapper(nodeType, renderers.get(nodeType.getNodeType()));
renderers.put(nodeType.getNodeType(), handlerWrapper);
}
if (nodeRenderer instanceof PhasedNodeRenderer) {
Set renderingPhases = ((PhasedNodeRenderer) nodeRenderer).getRenderingPhases();
assert (renderingPhases != null);
this.renderingPhases.addAll(renderingPhases);
this.phasedRenderers.add((PhasedNodeRenderer) nodeRenderer);
}
}
for (int i = 0; i < linkResolverFactories.size(); i++) {
myLinkResolvers[i] = linkResolverFactories.get(i).apply(this);
}
this.attributeProviders = new AttributeProvider[attributeProviderFactories.size()];
for (int i = 0; i < attributeProviderFactories.size(); i++) {
attributeProviders[i] = attributeProviderFactories.get(i).apply(this);
}
}
@NotNull
@Override
public Node getCurrentNode() {
return renderingNode;
}
@NotNull
@Override
public ResolvedLink resolveLink(@NotNull LinkType linkType, @NotNull CharSequence url, Boolean urlEncode) {
return resolveLink(linkType, url, (Attributes) null, urlEncode);
}
@NotNull
@Override
public ResolvedLink resolveLink(@NotNull LinkType linkType, @NotNull CharSequence url, Attributes attributes, Boolean urlEncode) {
HashMap resolvedLinks = resolvedLinkMap.computeIfAbsent(linkType, k -> new HashMap<>());
String urlSeq = String.valueOf(url);
ResolvedLink resolvedLink = resolvedLinks.get(urlSeq);
if (resolvedLink == null) {
resolvedLink = new ResolvedLink(linkType, urlSeq, attributes);
if (!urlSeq.isEmpty()) {
Node currentNode = getCurrentNode();
for (LinkResolver linkResolver : myLinkResolvers) {
resolvedLink = linkResolver.resolveLink(currentNode, this, resolvedLink);
if (resolvedLink.getStatus() != LinkStatus.UNKNOWN) break;
}
if (urlEncode == null && htmlOptions.percentEncodeUrls || urlEncode != null && urlEncode) {
resolvedLink = resolvedLink.withUrl(Escaping.percentEncodeUrl(resolvedLink.getUrl()));
}
}
// put it in the map
resolvedLinks.put(urlSeq, resolvedLink);
}
return resolvedLink;
}
@Override
public String getNodeId(@NotNull Node node) {
String id = htmlIdGenerator.getId(node);
if (attributeProviderFactories.size() != 0) {
Attributes attributes = new Attributes();
if (id != null) attributes.replaceValue("id", id);
for (AttributeProvider attributeProvider : attributeProviders) {
attributeProvider.setAttributes(this.renderingNode, AttributablePart.ID, attributes);
}
id = attributes.getValue("id");
}
return id;
}
@NotNull
@Override
public DataHolder getOptions() {
return options;
}
@NotNull
@Override
public HtmlRendererOptions getHtmlOptions() {
return htmlOptions;
}
@NotNull
@Override
public Document getDocument() {
return document;
}
@NotNull
@Override
public RenderingPhase getRenderingPhase() {
return phase;
}
@NotNull
@Override
public String encodeUrl(@NotNull CharSequence url) {
if (htmlOptions.percentEncodeUrls) {
return Escaping.percentEncodeUrl(url);
} else {
return String.valueOf(url);
}
}
@NotNull
@Override
public Attributes extendRenderingNodeAttributes(@NotNull AttributablePart part, Attributes attributes) {
Attributes attr = attributes != null ? attributes : new Attributes();
for (AttributeProvider attributeProvider : attributeProviders) {
attributeProvider.setAttributes(this.renderingNode, part, attr);
}
return attr;
}
@NotNull
@Override
public Attributes extendRenderingNodeAttributes(@NotNull Node node, @NotNull AttributablePart part, Attributes attributes) {
Attributes attr = attributes != null ? attributes : new Attributes();
for (AttributeProvider attributeProvider : attributeProviders) {
attributeProvider.setAttributes(node, part, attr);
}
return attr;
}
@Override
public void render(@NotNull Node node) {
renderNode(node, this);
}
@Override
public void delegateRender() {
renderByPreviousHandler(this);
}
void renderByPreviousHandler(NodeRendererSubContext subContext) {
if (subContext.renderingNode != null) {
NodeRenderingHandlerWrapper nodeRenderer = subContext.renderingHandlerWrapper.myPreviousRenderingHandler;
if (nodeRenderer != null) {
Node oldNode = subContext.renderingNode;
int oldDoNotRenderLinksNesting = subContext.doNotRenderLinksNesting;
NodeRenderingHandlerWrapper prevWrapper = subContext.renderingHandlerWrapper;
try {
subContext.renderingHandlerWrapper = nodeRenderer;
nodeRenderer.myRenderingHandler.render(oldNode, subContext, subContext.htmlWriter);
} finally {
subContext.renderingNode = oldNode;
subContext.doNotRenderLinksNesting = oldDoNotRenderLinksNesting;
subContext.renderingHandlerWrapper = prevWrapper;
}
}
} else {
throw new IllegalStateException("renderingByPreviousHandler called outside node rendering code");
}
}
@NotNull
@Override
public NodeRendererContext getSubContext(boolean inheritIndent) {
HtmlWriter htmlWriter = new HtmlWriter(getHtmlWriter(), inheritIndent);
htmlWriter.setContext(this);
//noinspection ReturnOfInnerClass
return new SubNodeRenderer(this, htmlWriter, false);
}
@NotNull
@Override
public NodeRendererContext getDelegatedSubContext(boolean inheritIndent) {
HtmlWriter htmlWriter = new HtmlWriter(getHtmlWriter(), inheritIndent);
htmlWriter.setContext(this);
//noinspection ReturnOfInnerClass
return new SubNodeRenderer(this, htmlWriter, true);
}
void renderNode(Node node, NodeRendererSubContext subContext) {
if (node instanceof Document) {
// here we render multiple phases
int oldDoNotRenderLinksNesting = subContext.getDoNotRenderLinksNesting();
int documentDoNotRenderLinksNesting = getHtmlOptions().doNotRenderLinksInDocument ? 1 : 0;
this.htmlIdGenerator.generateIds(document);
for (RenderingPhase phase : RenderingPhase.values()) {
if (phase != RenderingPhase.BODY && !renderingPhases.contains(phase)) { continue; }
this.phase = phase;
// here we render multiple phases
// go through all renderers that want this phase
for (PhasedNodeRenderer phasedRenderer : phasedRenderers) {
if (Objects.requireNonNull(phasedRenderer.getRenderingPhases()).contains(phase)) {
subContext.doNotRenderLinksNesting = documentDoNotRenderLinksNesting;
subContext.renderingNode = node;
phasedRenderer.renderDocument(subContext, subContext.htmlWriter, (Document) node, phase);
subContext.renderingNode = null;
subContext.doNotRenderLinksNesting = oldDoNotRenderLinksNesting;
}
}
if (getRenderingPhase() == RenderingPhase.BODY) {
NodeRenderingHandlerWrapper nodeRenderer = renderers.get(node.getClass());
if (nodeRenderer != null) {
subContext.doNotRenderLinksNesting = documentDoNotRenderLinksNesting;
NodeRenderingHandlerWrapper prevWrapper = subContext.renderingHandlerWrapper;
try {
subContext.renderingNode = node;
subContext.renderingHandlerWrapper = nodeRenderer;
nodeRenderer.myRenderingHandler.render(node, subContext, subContext.htmlWriter);
} finally {
subContext.renderingHandlerWrapper = prevWrapper;
subContext.renderingNode = null;
subContext.doNotRenderLinksNesting = oldDoNotRenderLinksNesting;
}
}
}
}
} else {
NodeRenderingHandlerWrapper nodeRenderer = renderers.get(node.getClass());
if (nodeRenderer != null) {
Node oldNode = this.renderingNode;
int oldDoNotRenderLinksNesting = subContext.doNotRenderLinksNesting;
NodeRenderingHandlerWrapper prevWrapper = subContext.renderingHandlerWrapper;
try {
subContext.renderingNode = node;
subContext.renderingHandlerWrapper = nodeRenderer;
nodeRenderer.myRenderingHandler.render(node, subContext, subContext.htmlWriter);
} finally {
subContext.renderingNode = oldNode;
subContext.doNotRenderLinksNesting = oldDoNotRenderLinksNesting;
subContext.renderingHandlerWrapper = prevWrapper;
}
}
}
}
public void renderChildren(@NotNull Node parent) {
renderChildrenNode(parent, this);
}
@SuppressWarnings("WeakerAccess")
protected void renderChildrenNode(Node parent, NodeRendererSubContext subContext) {
Node node = parent.getFirstChild();
while (node != null) {
Node next = node.getNext();
renderNode(node, subContext);
node = next;
}
}
@SuppressWarnings("WeakerAccess")
private class SubNodeRenderer extends NodeRendererSubContext implements NodeRendererContext {
final private MainNodeRenderer myMainNodeRenderer;
public SubNodeRenderer(MainNodeRenderer mainNodeRenderer, HtmlWriter htmlWriter, boolean inheritCurrentHandler) {
super(htmlWriter);
myMainNodeRenderer = mainNodeRenderer;
doNotRenderLinksNesting = mainNodeRenderer.getHtmlOptions().doNotRenderLinksInDocument ? 1 : 0;
if (inheritCurrentHandler) {
renderingNode = mainNodeRenderer.renderingNode;
renderingHandlerWrapper = mainNodeRenderer.renderingHandlerWrapper;
}
}
@Override
public String getNodeId(@NotNull Node node) {return myMainNodeRenderer.getNodeId(node);}
@NotNull
@Override
public DataHolder getOptions() {return myMainNodeRenderer.getOptions();}
@NotNull
@Override
public HtmlRendererOptions getHtmlOptions() {return myMainNodeRenderer.getHtmlOptions();}
@NotNull
@Override
public Document getDocument() {return myMainNodeRenderer.getDocument();}
@NotNull
@Override
public RenderingPhase getRenderingPhase() {return myMainNodeRenderer.getRenderingPhase();}
@NotNull
@Override
public String encodeUrl(@NotNull CharSequence url) {return myMainNodeRenderer.encodeUrl(url);}
@NotNull
@Override
public Attributes extendRenderingNodeAttributes(@NotNull AttributablePart part, Attributes attributes) {
return myMainNodeRenderer.extendRenderingNodeAttributes(
part,
attributes
);
}
@NotNull
@Override
public Attributes extendRenderingNodeAttributes(@NotNull Node node, @NotNull AttributablePart part, Attributes attributes) {
return myMainNodeRenderer.extendRenderingNodeAttributes(
node,
part,
attributes
);
}
@Override
public void render(@NotNull Node node) {
myMainNodeRenderer.renderNode(node, this);
}
@Override
public void delegateRender() {
myMainNodeRenderer.renderByPreviousHandler(this);
}
@NotNull
@Override
public Node getCurrentNode() {
return myMainNodeRenderer.getCurrentNode();
}
@NotNull
@Override
public ResolvedLink resolveLink(@NotNull LinkType linkType, @NotNull CharSequence url, Boolean urlEncode) {
return myMainNodeRenderer.resolveLink(linkType, url, urlEncode);
}
@NotNull
@Override
public ResolvedLink resolveLink(@NotNull LinkType linkType, @NotNull CharSequence url, Attributes attributes, Boolean urlEncode) {
return myMainNodeRenderer.resolveLink(linkType, url, attributes, urlEncode);
}
@NotNull
@Override
public NodeRendererContext getSubContext(boolean inheritIndent) {
HtmlWriter htmlWriter = new HtmlWriter(this.htmlWriter, inheritIndent);
htmlWriter.setContext(this);
//noinspection ReturnOfInnerClass
return new SubNodeRenderer(myMainNodeRenderer, htmlWriter, false);
}
@NotNull
@Override
public NodeRendererContext getDelegatedSubContext(boolean inheritIndent) {
HtmlWriter htmlWriter = new HtmlWriter(this.htmlWriter, inheritIndent);
htmlWriter.setContext(this);
//noinspection ReturnOfInnerClass
return new SubNodeRenderer(myMainNodeRenderer, htmlWriter, true);
}
@Override
public void renderChildren(@NotNull Node parent) {
myMainNodeRenderer.renderChildrenNode(parent, this);
}
@NotNull
@Override
public HtmlWriter getHtmlWriter() { return htmlWriter; }
protected int getDoNotRenderLinksNesting() {return super.getDoNotRenderLinksNesting();}
@Override
public boolean isDoNotRenderLinks() {return super.isDoNotRenderLinks();}
@Override
public void doNotRenderLinks(boolean doNotRenderLinks) {super.doNotRenderLinks(doNotRenderLinks);}
@Override
public void doNotRenderLinks() {super.doNotRenderLinks();}
@Override
public void doRenderLinks() {super.doRenderLinks();}
}
}
}