plugins) {
return new MarkwonBuilderImpl(context, plugins);
}
/**
* Factory method to obtain an instance of {@link Builder} without {@link CorePlugin}.
*
* @since 4.0.0
*/
@NonNull
public static Builder builderNoCore(@NonNull Context context) {
return new MarkwonBuilderImpl(context);
}
/**
* Method to parse markdown (without rendering)
*
* @param input markdown input to parse
* @return parsed via commonmark-java org.commonmark.node.Node
* @see #render(Node)
* @since 3.0.0
*/
@NonNull
public abstract Node parse(@NonNull String input);
/**
* Create Spanned markdown from parsed Node (via {@link #parse(String)} call).
*
* Please note that returned Spanned has few limitations. For example, images, tables
* and ordered lists require TextView to be properly displayed. This is why images and tables
* most likely won\'t work in this case. Ordered lists might have mis-measurements. Whenever
* possible use {@link #setMarkdown(TextView, String)} or {@link #setParsedMarkdown(TextView, Spanned)}
* as these methods will additionally call specific {@link MarkwonPlugin} methods to prepare
* proper display.
*
* @since 3.0.0
*/
@NonNull
public abstract Spanned render(@NonNull Node node);
/**
* This method will {@link #parse(String)} and {@link #render(Node)} supplied markdown. Returned
* Spanned has the same limitations as from {@link #render(Node)} method.
*
* @param input markdown input
* @see #parse(String)
* @see #render(Node)
* @since 3.0.0
*/
@NonNull
public abstract Spanned toMarkdown(@NonNull String input);
public abstract void setMarkdown(@NonNull TextView textView, @NonNull String markdown);
public abstract void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown);
/**
* Requests information if certain plugin has been registered. Please note that this
* method will check for super classes also, so if supplied with {@code markwon.hasPlugin(MarkwonPlugin.class)}
* this method (if has at least one plugin) will return true. If for example a custom
* (subclassed) version of a {@link CorePlugin} has been registered and given name
* {@code CorePlugin2}, then both {@code markwon.hasPlugin(CorePlugin2.class)} and
* {@code markwon.hasPlugin(CorePlugin.class)} will return true.
*
* @param plugin type to query
* @return true if a plugin is used when configuring this {@link Markwon} instance
*/
public abstract boolean hasPlugin(@NonNull Class extends MarkwonPlugin> plugin);
@Nullable
public abstract
P getPlugin(@NonNull Class
type);
/**
* @since 4.1.0
*/
@NonNull
public abstract
P requirePlugin(@NonNull Class
type);
/**
* @return a list of registered {@link MarkwonPlugin}
* @since 4.1.0
*/
@NonNull
public abstract List extends MarkwonPlugin> getPlugins();
@NonNull
public abstract MarkwonConfiguration configuration();
/**
* Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText
* functionality
*
* @see PrecomputedTextSetterCompat
* @since 4.1.0
*/
public interface TextSetter {
/**
* @param textView TextView
* @param markdown prepared markdown
* @param bufferType BufferType specified when building {@link Markwon} instance
* via {@link Builder#bufferType(TextView.BufferType)}
* @param onComplete action to run when set-text is finished (required to call in order
* to execute {@link MarkwonPlugin#afterSetText(TextView)})
*/
void setText(
@NonNull TextView textView,
@NonNull Spanned markdown,
@NonNull TextView.BufferType bufferType,
@NonNull Runnable onComplete);
}
/**
* Builder for {@link Markwon}.
*
* Please note that the order in which plugins are supplied is important as this order will be
* used through the whole usage of built Markwon instance
*
* @since 3.0.0
*/
public interface Builder {
/**
* Specify bufferType when applying text to a TextView {@code textView.setText(CharSequence,BufferType)}.
* By default `BufferType.SPANNABLE` is used
*
* @param bufferType BufferType
*/
@NonNull
Builder bufferType(@NonNull TextView.BufferType bufferType);
/**
* @param textSetter {@link TextSetter} to apply text to a TextView
* @since 4.1.0
*/
@NonNull
Builder textSetter(@NonNull TextSetter textSetter);
@NonNull
Builder usePlugin(@NonNull MarkwonPlugin plugin);
@NonNull
Builder usePlugins(@NonNull Iterable extends MarkwonPlugin> plugins);
/**
* Control if small chunks of non-finished markdown sentences (for example, a single `*` character)
* should be displayed/rendered as raw input instead of an empty string.
*
* Since 4.4.0 {@code true} by default, versions prior - {@code false}
*
* @since 4.4.0
*/
@NonNull
Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty);
Builder setMarkdownTheme(MarkwonTheme theme);
@NonNull
Markwon build();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java
================================================
package io.noties.markwon;
import android.content.Context;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import io.noties.markwon.core.MarkwonTheme;
/**
* @since 3.0.0
*/
class MarkwonBuilderImpl implements Markwon.Builder {
private final Context context;
private final CopyOnWriteArrayList plugins;
private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE;
private Markwon.TextSetter textSetter;
// @since 4.4.0
private boolean fallbackToRawInputWhenEmpty = true;
private MarkwonTheme theme;
MarkwonBuilderImpl(@NonNull Context context) {
this.context = context;
plugins = new CopyOnWriteArrayList<>();
}
MarkwonBuilderImpl(@NonNull Context context, List plugins) {
this.context = context;
if (plugins != null) {
this.plugins = new CopyOnWriteArrayList<>(plugins);
} else {
this.plugins = new CopyOnWriteArrayList<>();
}
}
@NonNull
@Override
public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) {
this.bufferType = bufferType;
return this;
}
@NonNull
@Override
public Markwon.Builder textSetter(@NonNull Markwon.TextSetter textSetter) {
this.textSetter = textSetter;
return this;
}
@NonNull
@Override
public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) {
plugins.add(plugin);
return this;
}
@NonNull
@Override
public Markwon.Builder usePlugins(@NonNull Iterable extends MarkwonPlugin> plugins) {
final Iterator extends MarkwonPlugin> iterator = plugins.iterator();
MarkwonPlugin plugin;
while (iterator.hasNext()) {
plugin = iterator.next();
if (plugin == null) {
throw new NullPointerException();
}
this.plugins.add(plugin);
}
return this;
}
@NonNull
@Override
public Markwon.Builder fallbackToRawInputWhenEmpty(boolean fallbackToRawInputWhenEmpty) {
this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty;
return this;
}
@Override
public Markwon.Builder setMarkdownTheme(MarkwonTheme theme) {
this.theme = theme;
return this;
}
@NonNull
@Override
public Markwon build() {
if (plugins.isEmpty()) {
throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " +
"method to add them");
}
// please note that this method must not modify supplied collection
// if nothing should be done -> the same collection can be returned
final List plugins = preparePlugins(this.plugins);
final Parser.Builder parserBuilder = new Parser.Builder();
final MarkwonConfiguration.Builder configurationBuilder = new MarkwonConfiguration.Builder();
final MarkwonVisitor.Builder visitorBuilder = new MarkwonVisitorImpl.BuilderImpl();
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
for (MarkwonPlugin plugin : plugins) {
plugin.configureParser(parserBuilder);
plugin.configureConfiguration(configurationBuilder);
plugin.configureVisitor(visitorBuilder);
plugin.configureSpansFactory(spanFactoryBuilder);
}
final MarkwonConfiguration configuration = configurationBuilder.build(theme, spanFactoryBuilder.build());
// @since 4.1.1
// @since 4.1.2 - do not reuse render-props (each render call should have own render-props)
final MarkwonVisitorFactory visitorFactory = MarkwonVisitorFactory.create(
visitorBuilder,
configuration);
return new MarkwonImpl(
bufferType,
textSetter,
parserBuilder.build(),
visitorFactory,
configuration,
Collections.unmodifiableList(plugins),
fallbackToRawInputWhenEmpty
);
}
@NonNull
private static List preparePlugins(@NonNull List plugins) {
return new RegistryImpl(plugins).process();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonConfiguration.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.ImageSizeResolver;
import io.noties.markwon.image.ImageSizeResolverDef;
import io.noties.markwon.image.destination.ImageDestinationProcessor;
import io.noties.markwon.syntax.SyntaxHighlight;
import io.noties.markwon.syntax.SyntaxHighlightNoOp;
/**
* since 3.0.0 renamed `SpannableConfiguration` -> `MarkwonConfiguration`
*/
public class MarkwonConfiguration {
@NonNull
public static Builder builder() {
return new Builder();
}
private final MarkwonTheme theme;
private final AsyncDrawableLoader asyncDrawableLoader;
private final SyntaxHighlight syntaxHighlight;
private final LinkResolver linkResolver;
// @since 4.4.0
private final ImageDestinationProcessor imageDestinationProcessor;
private final ImageSizeResolver imageSizeResolver;
// @since 3.0.0
private final MarkwonSpansFactory spansFactory;
private MarkwonConfiguration(@NonNull Builder builder) {
this.theme = builder.theme;
this.asyncDrawableLoader = builder.asyncDrawableLoader;
this.syntaxHighlight = builder.syntaxHighlight;
this.linkResolver = builder.linkResolver;
this.imageDestinationProcessor = builder.imageDestinationProcessor;
this.imageSizeResolver = builder.imageSizeResolver;
this.spansFactory = builder.spansFactory;
}
@NonNull
public MarkwonTheme theme() {
return theme;
}
@NonNull
public AsyncDrawableLoader asyncDrawableLoader() {
return asyncDrawableLoader;
}
@NonNull
public SyntaxHighlight syntaxHighlight() {
return syntaxHighlight;
}
@NonNull
public LinkResolver linkResolver() {
return linkResolver;
}
/**
* @since 4.4.0
*/
@NonNull
public ImageDestinationProcessor imageDestinationProcessor() {
return imageDestinationProcessor;
}
@NonNull
public ImageSizeResolver imageSizeResolver() {
return imageSizeResolver;
}
/**
* @since 3.0.0
*/
@NonNull
public MarkwonSpansFactory spansFactory() {
return spansFactory;
}
@SuppressWarnings({"unused", "UnusedReturnValue"})
public static class Builder {
private MarkwonTheme theme;
private AsyncDrawableLoader asyncDrawableLoader;
private SyntaxHighlight syntaxHighlight;
private LinkResolver linkResolver;
// @since 4.4.0
private ImageDestinationProcessor imageDestinationProcessor;
private ImageSizeResolver imageSizeResolver;
private MarkwonSpansFactory spansFactory;
Builder() {
}
/**
* @since 4.0.0
*/
@NonNull
public Builder asyncDrawableLoader(@NonNull AsyncDrawableLoader asyncDrawableLoader) {
this.asyncDrawableLoader = asyncDrawableLoader;
return this;
}
@NonNull
public Builder syntaxHighlight(@NonNull SyntaxHighlight syntaxHighlight) {
this.syntaxHighlight = syntaxHighlight;
return this;
}
@NonNull
public Builder linkResolver(@NonNull LinkResolver linkResolver) {
this.linkResolver = linkResolver;
return this;
}
/**
* @since 4.4.0
*/
@NonNull
public Builder imageDestinationProcessor(@NonNull ImageDestinationProcessor imageDestinationProcessor) {
this.imageDestinationProcessor = imageDestinationProcessor;
return this;
}
/**
* @since 1.0.1
*/
@NonNull
public Builder imageSizeResolver(@NonNull ImageSizeResolver imageSizeResolver) {
this.imageSizeResolver = imageSizeResolver;
return this;
}
@NonNull
public MarkwonConfiguration build(
@NonNull MarkwonTheme theme,
@NonNull MarkwonSpansFactory spansFactory) {
this.theme = theme;
this.spansFactory = spansFactory;
// @since 4.0.0
if (asyncDrawableLoader == null) {
asyncDrawableLoader = AsyncDrawableLoader.noOp();
}
if (syntaxHighlight == null) {
syntaxHighlight = new SyntaxHighlightNoOp();
}
if (linkResolver == null) {
linkResolver = new LinkResolverDef();
}
// @since 4.4.0
if (imageDestinationProcessor == null) {
imageDestinationProcessor = ImageDestinationProcessor.noOp();
}
if (imageSizeResolver == null) {
imageSizeResolver = new ImageSizeResolverDef();
}
return new MarkwonConfiguration(this);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java
================================================
package io.noties.markwon;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* @since 3.0.0
*/
class MarkwonImpl extends Markwon {
private final TextView.BufferType bufferType;
private final Parser parser;
private final MarkwonVisitorFactory visitorFactory; // @since 4.1.1
private final MarkwonConfiguration configuration;
private final List plugins;
// @since 4.1.0
@Nullable
private final TextSetter textSetter;
// @since 4.4.0
private final boolean fallbackToRawInputWhenEmpty;
MarkwonImpl(
@NonNull TextView.BufferType bufferType,
@Nullable TextSetter textSetter,
@NonNull Parser parser,
@NonNull MarkwonVisitorFactory visitorFactory,
@NonNull MarkwonConfiguration configuration,
@NonNull List plugins,
boolean fallbackToRawInputWhenEmpty
) {
this.bufferType = bufferType;
this.textSetter = textSetter;
this.parser = parser;
this.visitorFactory = visitorFactory;
this.configuration = configuration;
this.plugins = plugins;
this.fallbackToRawInputWhenEmpty = fallbackToRawInputWhenEmpty;
}
@NonNull
@Override
public Node parse(@NonNull String input) {
// make sure that all plugins are called `processMarkdown` before parsing
for (MarkwonPlugin plugin : plugins) {
input = plugin.processMarkdown(input);
}
return parser.parse(input);
}
@NonNull
@Override
public Spanned render(@NonNull Node node) {
for (MarkwonPlugin plugin : plugins) {
plugin.beforeRender(node);
}
// @since 4.1.1 obtain visitor via factory
final MarkwonVisitor visitor = visitorFactory.create();
node.accept(visitor);
for (MarkwonPlugin plugin : plugins) {
plugin.afterRender(node, visitor);
}
//noinspection UnnecessaryLocalVariable
final Spanned spanned = visitor.builder().spannableStringBuilder();
// clear render props and builder after rendering
// @since 4.1.1 as we no longer reuse visitor - there is no need to clean it
// we might still do it if we introduce a thread-local storage though
// visitor.clear();
return spanned;
}
@NonNull
@Override
public Spanned toMarkdown(@NonNull String input) {
final Spanned spanned = render(parse(input));
// @since 4.4.0
// if spanned is empty, we are configured to use raw input and input is not empty
if (TextUtils.isEmpty(spanned)
&& fallbackToRawInputWhenEmpty
&& !TextUtils.isEmpty(input)) {
// let's use SpannableStringBuilder in order to keep backward-compatibility
return new SpannableStringBuilder(input);
}
return spanned;
}
@Override
public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) {
setParsedMarkdown(textView, toMarkdown(markdown));
}
@Override
public void setParsedMarkdown(@NonNull final TextView textView, @NonNull Spanned markdown) {
for (MarkwonPlugin plugin : plugins) {
plugin.beforeSetText(textView, markdown);
}
// @since 4.1.0
if (textSetter != null) {
textSetter.setText(textView, markdown, bufferType, new Runnable() {
@Override
public void run() {
// on-complete we just must call `afterSetText` on all plugins
for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView);
}
}
});
} else {
// if no text-setter is specified -> just a regular sync operation
textView.setText(markdown, bufferType);
for (MarkwonPlugin plugin : plugins) {
plugin.afterSetText(textView);
}
}
}
@Override
public boolean hasPlugin(@NonNull Class extends MarkwonPlugin> type) {
return getPlugin(type) != null;
}
@Nullable
@Override
public P getPlugin(@NonNull Class
type) {
MarkwonPlugin out = null;
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
out = plugin;
}
}
//noinspection unchecked
return (P) out;
}
@NonNull
@Override
public
P requirePlugin(@NonNull Class
type) {
final P plugin = getPlugin(type);
if (plugin == null) {
throw new IllegalStateException(String.format(Locale.US, "Requested plugin `%s` is not " +
"registered with this Markwon instance", type.getName()));
}
return plugin;
}
@NonNull
@Override
public List extends MarkwonPlugin> getPlugins() {
return Collections.unmodifiableList(plugins);
}
@NonNull
@Override
public MarkwonConfiguration configuration() {
return configuration;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonPlugin.java
================================================
package io.noties.markwon;
import android.text.Spanned;
import android.widget.TextView;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import io.noties.markwon.core.CorePlugin;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawableSpan;
import io.noties.markwon.movement.MovementMethodPlugin;
/**
* Class represents a plugin (extension) to Markwon to configure how parsing and rendering
* of markdown is carried on.
*
* @see AbstractMarkwonPlugin
* @see CorePlugin
* @see MovementMethodPlugin
* @since 3.0.0
*/
public interface MarkwonPlugin {
/**
* @see Registry#require(Class, Action)
* @since 4.0.0
*/
interface Action
{
void apply(@NonNull P p);
}
/**
* @see #configure(Registry)
* @since 4.0.0
*/
interface Registry {
@NonNull
P require(@NonNull Class
plugin);
void require(
@NonNull Class
plugin,
@NonNull Action super P> action);
}
/**
* This method will be called before any other during {@link Markwon} instance construction.
*
* @since 4.0.0
*/
void configure(@NonNull Registry registry);
/**
* Method to configure org.commonmark.parser.Parser (for example register custom
* extension, etc).
*/
void configureParser(@NonNull Parser.Builder builder);
/**
* Modify {@link MarkwonTheme} that is used for rendering of markdown.
*
* @see MarkwonTheme
* @see MarkwonTheme.Builder
*/
void configureTheme(@NonNull MarkwonTheme.Builder builder);
/**
* Configure {@link MarkwonConfiguration}
*
* @see MarkwonConfiguration
* @see MarkwonConfiguration.Builder
*/
void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder);
/**
* Configure {@link MarkwonVisitor} to accept new node types or override already registered nodes.
*
* @see MarkwonVisitor
* @see MarkwonVisitor.Builder
*/
void configureVisitor(@NonNull MarkwonVisitor.Builder builder);
/**
* Configure {@link MarkwonSpansFactory} to change what spans are used for certain node types.
*
* @see MarkwonSpansFactory
* @see MarkwonSpansFactory.Builder
*/
void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder);
/**
* Process input markdown and return new string to be used in parsing stage further.
* Can be described as pre-processing of markdown String.
*
* @param markdown String to process
* @return processed markdown String
*/
@NonNull
String processMarkdown(@NonNull String markdown);
/**
* This method will be called before rendering will occur thus making possible
* to post-process parsed node (make changes for example).
*
* @param node root parsed org.commonmark.node.Node
*/
void beforeRender(@NonNull Node node);
/**
* This method will be called after rendering (but before applying markdown to a
* TextView, if such action will happen). It can be used to clean some
* internal state, or trigger certain action. Please note that modifying node won\'t
* have any effect as it has been already visited at this stage.
*
* @param node root parsed org.commonmark.node.Node
* @param visitor {@link MarkwonVisitor} instance used to render markdown
*/
void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor);
/**
* This method will be called before calling TextView#setText.
*
* It can be useful to prepare a TextView for markdown. For example {@code ru.noties.markwon.image.ImagesPlugin}
* uses this method to unregister previously registered {@link AsyncDrawableSpan}
* (if there are such spans in this TextView at this point). Or {@link CorePlugin}
* which measures ordered list numbers
*
* @param textView TextView to which markdown will be applied
* @param markdown Parsed markdown
*/
void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown);
/**
* This method will be called after markdown was applied.
*
* It can be useful to trigger certain action on spans/textView. For example {@code ru.noties.markwon.image.ImagesPlugin}
* uses this method to register {@link AsyncDrawableSpan} and start
* asynchronously loading images.
*
* Unlike {@link #beforeSetText(TextView, Spanned)} this method does not receive parsed markdown
* as at this point spans must be queried by calling TextView#getText#getSpans.
*
* @param textView TextView to which markdown was applied
*/
void afterSetText(@NonNull TextView textView);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonReducer.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import org.commonmark.node.LinkReferenceDefinition;
import org.commonmark.node.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @since 3.0.0
*/
public abstract class MarkwonReducer {
/**
* @return direct children of supplied Node. In the most usual case
* will return all BlockNodes of a Document
*/
@NonNull
public static MarkwonReducer directChildren() {
return new DirectChildren();
}
@NonNull
public abstract List reduce(@NonNull Node node);
static class DirectChildren extends MarkwonReducer {
@NonNull
@Override
public List reduce(@NonNull Node root) {
final List list;
// we will extract all blocks that are direct children of Document
Node node = root.getFirstChild();
// please note, that if there are no children -> we will return a list with
// single element (which was supplied)
if (node == null) {
list = Collections.singletonList(root);
} else {
list = new ArrayList<>();
Node temp;
while (node != null) {
// @since 4.5.0 do not include LinkReferenceDefinition node (would result
// in empty textView if rendered in recycler-view)
if (!(node instanceof LinkReferenceDefinition)) {
list.add(node);
}
temp = node.getNext();
node.unlink();
node = temp;
}
}
return list;
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactory.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
/**
* Class that controls what spans are used for certain Nodes.
*
* @see SpanFactory
* @since 3.0.0
*/
public interface MarkwonSpansFactory {
/**
* Returns registered {@link SpanFactory} or null if a factory for this node type
* is not registered. There is {@link #require(Class)} method that will throw an exception
* if required {@link SpanFactory} is not registered, thus making return type non-null
*
* @param node type of the node
* @return registered {@link SpanFactory} or null if it\'s not registered
* @see #require(Class)
*/
@Nullable
SpanFactory get(@NonNull Class node);
@NonNull
SpanFactory require(@NonNull Class node);
interface Builder {
@NonNull
Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory);
/**
* Helper method to add a {@link SpanFactory} for a Node. This method will merge existing
* {@link SpanFactory} with the specified one.
*
* @since 3.0.1
* @deprecated 4.2.2 consider using {@link #appendFactory(Class, SpanFactory)} or
* {@link #prependFactory(Class, SpanFactory)} methods for more explicit factory ordering.
* `addFactory` behaved like {@link #prependFactory(Class, SpanFactory)}, so
* this method call can be replaced with it
*/
@NonNull
@Deprecated
Builder addFactory(@NonNull Class node, @NonNull SpanFactory factory);
/**
* Append a factory to existing one (or make the first one for specified node). Specified factory
* will be called after original (if present) factory. Can be used to
* change behavior or original span factory.
*
* @since 4.2.2
*/
@NonNull
Builder appendFactory(@NonNull Class node, @NonNull SpanFactory factory);
/**
* Prepend a factory to existing one (or make the first one for specified node). Specified factory
* will be called before original (if present) factory.
*
* @since 4.2.2
*/
@NonNull
Builder prependFactory(@NonNull Class node, @NonNull SpanFactory factory);
/**
* Can be useful when enhancing an already defined SpanFactory with another one.
*/
@Nullable
SpanFactory getFactory(@NonNull Class node);
/**
* To obtain current {@link SpanFactory} associated with specified node. Can be used
* when SpanFactory must be present for node. If it\'s not added/registered a runtime
* exception will be thrown
*
* @see #getFactory(Class)
* @since 3.0.1
*/
@NonNull
SpanFactory requireFactory(@NonNull Class node);
@NonNull
MarkwonSpansFactory build();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonSpansFactoryImpl.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @since 3.0.0
*/
class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
private final Map, SpanFactory> factories;
MarkwonSpansFactoryImpl(@NonNull Map, SpanFactory> factories) {
this.factories = factories;
}
@Nullable
@Override
public SpanFactory get(@NonNull Class node) {
return factories.get(node);
}
@NonNull
@Override
public SpanFactory require(@NonNull Class node) {
final SpanFactory f = get(node);
if (f == null) {
throw new NullPointerException(node.getName());
}
return f;
}
static class BuilderImpl implements Builder {
private final Map, SpanFactory> factories =
new HashMap<>(3);
@NonNull
@Override
public Builder setFactory(@NonNull Class node, @Nullable SpanFactory factory) {
if (factory == null) {
factories.remove(node);
} else {
factories.put(node, factory);
}
return this;
}
@NonNull
@Override
@Deprecated
public Builder addFactory(@NonNull Class node, @NonNull SpanFactory factory) {
return prependFactory(node, factory);
}
@NonNull
@Override
public Builder appendFactory(@NonNull Class node, @NonNull SpanFactory factory) {
final SpanFactory existing = factories.get(node);
if (existing == null) {
factories.put(node, factory);
} else {
if (existing instanceof CompositeSpanFactory) {
((CompositeSpanFactory) existing).factories.add(0, factory);
} else {
final CompositeSpanFactory compositeSpanFactory =
new CompositeSpanFactory(factory, existing);
factories.put(node, compositeSpanFactory);
}
}
return this;
}
@NonNull
@Override
public Builder prependFactory(@NonNull Class node, @NonNull SpanFactory factory) {
// if there is no factory registered for this node -> just add it
final SpanFactory existing = factories.get(node);
if (existing == null) {
factories.put(node, factory);
} else {
// existing span factory can be of CompositeSpanFactory at this point -> append to it
if (existing instanceof CompositeSpanFactory) {
((CompositeSpanFactory) existing).factories.add(factory);
} else {
// if it's not composite at this point -> make it
final CompositeSpanFactory compositeSpanFactory =
new CompositeSpanFactory(existing, factory);
factories.put(node, compositeSpanFactory);
}
}
return this;
}
@Nullable
@Override
public SpanFactory getFactory(@NonNull Class node) {
return factories.get(node);
}
@NonNull
@Override
public SpanFactory requireFactory(@NonNull Class node) {
final SpanFactory factory = getFactory(node);
if (factory == null) {
throw new NullPointerException(node.getName());
}
return factory;
}
@NonNull
@Override
public MarkwonSpansFactory build() {
return new MarkwonSpansFactoryImpl(Collections.unmodifiableMap(factories));
}
}
static class CompositeSpanFactory implements SpanFactory {
final List factories;
CompositeSpanFactory(@NonNull SpanFactory first, @NonNull SpanFactory second) {
this.factories = new ArrayList<>(3);
this.factories.add(first);
this.factories.add(second);
}
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
// please note that we do not check it factory itself returns an array of spans,
// as this behaviour is supported now (previously we supported only a single-level array)
final int length = factories.size();
final Object[] out = new Object[length];
for (int i = 0; i < length; i++) {
out[i] = factories.get(i).getSpans(configuration, props);
}
return out;
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitor.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import org.commonmark.node.Visitor;
/**
* Configurable visitor of parsed markdown. Allows visiting certain (registered) nodes without
* need to create own instance of this class.
*
* @see Builder#on(Class, NodeVisitor)
* @see MarkwonPlugin#configureVisitor(Builder)
* @since 3.0.0
*/
public interface MarkwonVisitor extends Visitor {
/**
* @see Builder#on(Class, NodeVisitor)
*/
interface NodeVisitor {
void visit(@NonNull MarkwonVisitor visitor, @NonNull N n);
}
/**
* Primary purpose is to control the spacing applied before/after certain blocks, which
* visitors are created elsewhere
*
* @since 4.3.0
*/
interface BlockHandler {
void blockStart(@NonNull MarkwonVisitor visitor, @NonNull Node node);
void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node);
}
interface Builder {
/**
* @param node to register
* @param nodeVisitor {@link NodeVisitor} to be used or null to ignore previously registered
* visitor for this node
*/
@NonNull
Builder on(@NonNull Class node, @Nullable NodeVisitor super N> nodeVisitor);
/**
* @param blockHandler to handle block start/end
* @see BlockHandler
* @see BlockHandlerDef
* @since 4.3.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
Builder blockHandler(@NonNull BlockHandler blockHandler);
@NonNull
MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps);
}
@NonNull
MarkwonConfiguration configuration();
@NonNull
RenderProps renderProps();
@NonNull
SpannableBuilder builder();
/**
* Visits all children of supplied node.
*
* @param node to visit
*/
void visitChildren(@NonNull Node node);
/**
* Executes a check if there is further content available.
*
* @param node to check
* @return boolean indicating if there are more nodes after supplied one
*/
boolean hasNext(@NonNull Node node);
/**
* This method ensures that further content will start at a new line. If current
* last character is already a new line, then it won\'t do anything.
*/
void ensureNewLine();
/**
* This method inserts a new line without any condition checking (unlike {@link #ensureNewLine()}).
*/
void forceNewLine();
/**
* Helper method to call builder().length()
*
* @return current length of underlying {@link SpannableBuilder}
*/
int length();
/**
* Clears state of visitor (both {@link RenderProps} and {@link SpannableBuilder} will be cleared
*/
void clear();
/**
* Sets spans to underlying {@link SpannableBuilder} from start
* to {@link SpannableBuilder#length()}.
*
* @param start start position of spans
* @param spans to apply
*/
void setSpans(int start, @Nullable Object spans);
/**
* Helper method to obtain and apply spans for supplied Node. Internally queries {@link SpanFactory}
* for the node (via {@link MarkwonSpansFactory#require(Class)} thus throwing an exception
* if there is no {@link SpanFactory} registered for the node).
*
* @param node to retrieve {@link SpanFactory} for
* @param start start position for further {@link #setSpans(int, Object)} call
* @see #setSpansForNodeOptional(Node, int)
*/
void setSpansForNode(@NonNull N node, int start);
/**
* The same as {@link #setSpansForNode(Node, int)} but can be used in situations when there is
* no access to a Node instance (for example in HTML rendering which doesn\'t have markdown Nodes).
*
* @see #setSpansForNode(Node, int)
*/
void setSpansForNode(@NonNull Class node, int start);
// does not throw if there is no SpanFactory registered for this node
/**
* Helper method to apply spans from a {@link SpanFactory} if it\'s registered in
* {@link MarkwonSpansFactory} instance. Otherwise ignores this call (no spans will be applied).
* If there is a need to ensure that specified node has a {@link SpanFactory} registered,
* then {@link #setSpansForNode(Node, int)} can be used. {@link #setSpansForNode(Node, int)} internally
* uses {@link MarkwonSpansFactory#require(Class)}. This method uses {@link MarkwonSpansFactory#get(Class)}.
*
* @see #setSpansForNode(Node, int)
*/
void setSpansForNodeOptional(@NonNull N node, int start);
/**
* The same as {@link #setSpansForNodeOptional(Node, int)} but can be used in situations when
* there is no access to a Node instance (for example in HTML rendering).
*
* @see #setSpansForNodeOptional(Node, int)
*/
@SuppressWarnings("unused")
void setSpansForNodeOptional(@NonNull Class node, int start);
/**
* @since 4.3.0
*/
void blockStart(@NonNull Node node);
/**
* @since 4.3.0
*/
void blockEnd(@NonNull Node node);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorFactory.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
/**
* @since 4.1.1
*/
abstract class MarkwonVisitorFactory {
@NonNull
abstract MarkwonVisitor create();
@NonNull
static MarkwonVisitorFactory create(
@NonNull final MarkwonVisitorImpl.Builder builder,
@NonNull final MarkwonConfiguration configuration) {
return new MarkwonVisitorFactory() {
@NonNull
@Override
MarkwonVisitor create() {
return builder.build(configuration, new RenderPropsImpl());
}
};
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/MarkwonVisitorImpl.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList;
import org.commonmark.node.Code;
import org.commonmark.node.CustomBlock;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Document;
import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link;
import org.commonmark.node.LinkReferenceDefinition;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @since 3.0.0
*/
class MarkwonVisitorImpl implements MarkwonVisitor {
private final MarkwonConfiguration configuration;
private final RenderProps renderProps;
private final SpannableBuilder builder;
private final Map, NodeVisitor extends Node>> nodes;
// @since 4.3.0
private final BlockHandler blockHandler;
MarkwonVisitorImpl(
@NonNull MarkwonConfiguration configuration,
@NonNull RenderProps renderProps,
@NonNull SpannableBuilder builder,
@NonNull Map, NodeVisitor extends Node>> nodes,
@NonNull BlockHandler blockHandler) {
this.configuration = configuration;
this.renderProps = renderProps;
this.builder = builder;
this.nodes = nodes;
this.blockHandler = blockHandler;
}
@Override
public void visit(BlockQuote blockQuote) {
visit((Node) blockQuote);
}
@Override
public void visit(BulletList bulletList) {
visit((Node) bulletList);
}
@Override
public void visit(Code code) {
visit((Node) code);
}
@Override
public void visit(Document document) {
visit((Node) document);
}
@Override
public void visit(Emphasis emphasis) {
visit((Node) emphasis);
}
@Override
public void visit(FencedCodeBlock fencedCodeBlock) {
visit((Node) fencedCodeBlock);
}
@Override
public void visit(HardLineBreak hardLineBreak) {
visit((Node) hardLineBreak);
}
@Override
public void visit(Heading heading) {
visit((Node) heading);
}
@Override
public void visit(ThematicBreak thematicBreak) {
visit((Node) thematicBreak);
}
@Override
public void visit(HtmlInline htmlInline) {
visit((Node) htmlInline);
}
@Override
public void visit(HtmlBlock htmlBlock) {
visit((Node) htmlBlock);
}
@Override
public void visit(Image image) {
visit((Node) image);
}
@Override
public void visit(IndentedCodeBlock indentedCodeBlock) {
visit((Node) indentedCodeBlock);
}
@Override
public void visit(Link link) {
visit((Node) link);
}
@Override
public void visit(ListItem listItem) {
visit((Node) listItem);
}
@Override
public void visit(OrderedList orderedList) {
visit((Node) orderedList);
}
@Override
public void visit(Paragraph paragraph) {
visit((Node) paragraph);
}
@Override
public void visit(SoftLineBreak softLineBreak) {
visit((Node) softLineBreak);
}
@Override
public void visit(StrongEmphasis strongEmphasis) {
visit((Node) strongEmphasis);
}
@Override
public void visit(Text text) {
visit((Node) text);
}
@Override
public void visit(LinkReferenceDefinition linkReferenceDefinition) {
visit((Node) linkReferenceDefinition);
}
@Override
public void visit(CustomBlock customBlock) {
visit((Node) customBlock);
}
@Override
public void visit(CustomNode customNode) {
visit((Node) customNode);
}
private void visit(@NonNull Node node) {
//noinspection unchecked
final NodeVisitor nodeVisitor = (NodeVisitor) nodes.get(node.getClass());
if (nodeVisitor != null) {
nodeVisitor.visit(this, node);
} else {
visitChildren(node);
}
}
@NonNull
@Override
public MarkwonConfiguration configuration() {
return configuration;
}
@NonNull
@Override
public RenderProps renderProps() {
return renderProps;
}
@NonNull
@Override
public SpannableBuilder builder() {
return builder;
}
@Override
public void visitChildren(@NonNull Node parent) {
Node node = parent.getFirstChild();
while (node != null) {
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
// node after visiting it. So get the next node before visiting.
Node next = node.getNext();
node.accept(this);
node = next;
}
}
@Override
public boolean hasNext(@NonNull Node node) {
return node.getNext() != null;
}
@Override
public void ensureNewLine() {
if (builder.length() > 0
&& '\n' != builder.lastChar()) {
builder.append('\n');
}
}
@Override
public void forceNewLine() {
builder.append('\n');
}
@Override
public int length() {
return builder.length();
}
@Override
public void setSpans(int start, @Nullable Object spans) {
SpannableBuilder.setSpans(builder, spans, start, builder.length());
}
@Override
public void clear() {
renderProps.clearAll();
builder.clear();
}
@Override
public void setSpansForNode(@NonNull N node, int start) {
setSpansForNode(node.getClass(), start);
}
@Override
public void setSpansForNode(@NonNull Class node, int start) {
setSpans(start, configuration.spansFactory().require(node).getSpans(configuration, renderProps));
}
@Override
public void setSpansForNodeOptional(@NonNull N node, int start) {
setSpansForNodeOptional(node.getClass(), start);
}
@Override
public void setSpansForNodeOptional(@NonNull Class node, int start) {
final SpanFactory factory = configuration.spansFactory().get(node);
if (factory != null) {
setSpans(start, factory.getSpans(configuration, renderProps));
}
}
@Override
public void blockStart(@NonNull Node node) {
blockHandler.blockStart(this, node);
}
@Override
public void blockEnd(@NonNull Node node) {
blockHandler.blockEnd(this, node);
}
static class BuilderImpl implements Builder {
private final Map, NodeVisitor extends Node>> nodes = new HashMap<>();
private BlockHandler blockHandler;
@NonNull
@Override
public Builder on(@NonNull Class node, @Nullable NodeVisitor super N> nodeVisitor) {
// @since 4.1.1 we might actually introduce a local flag to check if it's been built
// and throw an exception here if some modification is requested
// NB, as we might be built from different threads this flag must be synchronized
// we should allow `null` to exclude node from being visited (for example to disable
// some functionality)
if (nodeVisitor == null) {
nodes.remove(node);
} else {
nodes.put(node, nodeVisitor);
}
return this;
}
@NonNull
@Override
public Builder blockHandler(@NonNull BlockHandler blockHandler) {
this.blockHandler = blockHandler;
return this;
}
@NonNull
@Override
public MarkwonVisitor build(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps) {
// @since 4.3.0
BlockHandler blockHandler = this.blockHandler;
if (blockHandler == null) {
blockHandler = new BlockHandlerDef();
}
return new MarkwonVisitorImpl(
configuration,
renderProps,
new SpannableBuilder(),
Collections.unmodifiableMap(nodes),
blockHandler);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/PrecomputedFutureTextSetterCompat.java
================================================
package io.noties.markwon;
import android.text.Spanned;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.text.PrecomputedTextCompat;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
/**
* Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies.
* This is intended to be used in a RecyclerView.
*
* @see Markwon.TextSetter
* @since 4.3.1
*/
public class PrecomputedFutureTextSetterCompat implements Markwon.TextSetter {
/**
* @param executor for background execution of text pre-computation,
* if not provided the standard, single threaded one will be used.
*/
@NonNull
public static PrecomputedFutureTextSetterCompat create(@Nullable Executor executor) {
return new PrecomputedFutureTextSetterCompat(executor);
}
@NonNull
public static PrecomputedFutureTextSetterCompat create() {
return new PrecomputedFutureTextSetterCompat(null);
}
@Nullable
private final Executor executor;
@SuppressWarnings("WeakerAccess")
PrecomputedFutureTextSetterCompat(@Nullable Executor executor) {
this.executor = executor;
}
@Override
public void setText(
@NonNull TextView textView,
@NonNull Spanned markdown,
@NonNull TextView.BufferType bufferType,
@NonNull Runnable onComplete) {
if (textView instanceof AppCompatTextView) {
final AppCompatTextView appCompatTextView = (AppCompatTextView) textView;
final Future future = PrecomputedTextCompat.getTextFuture(
markdown,
appCompatTextView.getTextMetricsParamsCompat(),
executor);
appCompatTextView.setTextFuture(future);
// `setTextFuture` is actually a synchronous call, so we should call onComplete now
onComplete.run();
} else {
throw new IllegalStateException("TextView provided is not an instance of AppCompatTextView, " +
"cannot call setTextFuture(), textView: " + textView);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java
================================================
package io.noties.markwon;
import android.os.Build;
import android.text.Spanned;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.text.PrecomputedTextCompat;
import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
/**
* Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies.
* Please do not use with `markwon-recycler` as it will lead to bad item rendering (due to async nature)
*
* @see io.noties.markwon.Markwon.TextSetter
* @since 4.1.0
*/
public class PrecomputedTextSetterCompat implements Markwon.TextSetter {
/**
* @param executor for background execution of text pre-computation
*/
@NonNull
public static PrecomputedTextSetterCompat create(@NonNull Executor executor) {
return new PrecomputedTextSetterCompat(executor);
}
private final Executor executor;
@SuppressWarnings("WeakerAccess")
PrecomputedTextSetterCompat(@NonNull Executor executor) {
this.executor = executor;
}
@Override
public void setText(
@NonNull TextView textView,
@NonNull final Spanned markdown,
@NonNull final TextView.BufferType bufferType,
@NonNull final Runnable onComplete) {
// insert version check and do not execute on a device < 21
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// it's still no-op, so there is no need to start background execution
applyText(textView, markdown, bufferType, onComplete);
return;
}
final WeakReference reference = new WeakReference<>(textView);
executor.execute(new Runnable() {
@Override
public void run() {
try {
final PrecomputedTextCompat precomputedTextCompat = precomputedText(reference.get(), markdown);
if (precomputedTextCompat != null) {
applyText(reference.get(), precomputedTextCompat, bufferType, onComplete);
}
} catch (Throwable t) {
Log.e("PrecomputdTxtSetterCmpt", "Exception during pre-computing text", t);
// apply initial markdown
applyText(reference.get(), markdown, bufferType, onComplete);
}
}
});
}
@Nullable
private static PrecomputedTextCompat precomputedText(@Nullable TextView textView, @NonNull Spanned spanned) {
if (textView == null) {
return null;
}
final PrecomputedTextCompat.Params params;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// use native parameters on P
params = new PrecomputedTextCompat.Params(textView.getTextMetricsParams());
} else {
final PrecomputedTextCompat.Params.Builder builder =
new PrecomputedTextCompat.Params.Builder(textView.getPaint());
// please note that text-direction initialization is omitted
// by default it will be determined by the first locale-specific character
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// another miss on API surface, this can easily be done by the compat class itself
builder
.setBreakStrategy(textView.getBreakStrategy())
.setHyphenationFrequency(textView.getHyphenationFrequency());
}
params = builder.build();
}
return PrecomputedTextCompat.create(spanned, params);
}
private static void applyText(
@Nullable final TextView textView,
@NonNull final Spanned text,
@NonNull final TextView.BufferType bufferType,
@NonNull final Runnable onComplete) {
if (textView != null) {
textView.post(new Runnable() {
@Override
public void run() {
textView.setText(text, bufferType);
onComplete.run();
}
});
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/Prop.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Class to hold data in {@link RenderProps}. Represents a certain property.
*
* @param represents the type that this instance holds
* @see #of(String)
* @see #of(Class, String)
* @since 3.0.0
*/
public class Prop {
@SuppressWarnings("unused")
@NonNull
public static Prop of(@NonNull Class type, @NonNull String name) {
return new Prop<>(name);
}
@NonNull
public static Prop of(@NonNull String name) {
return new Prop<>(name);
}
private final String name;
Prop(@NonNull String name) {
this.name = name;
}
@NonNull
public String name() {
return name;
}
@Nullable
public T get(@NonNull RenderProps props) {
return props.get(this);
}
@NonNull
public T get(@NonNull RenderProps props, @NonNull T defValue) {
return props.get(this, defValue);
}
@NonNull
public T require(@NonNull RenderProps props) {
final T t = get(props);
if (t == null) {
throw new NullPointerException(name);
}
return t;
}
public void set(@NonNull RenderProps props, @Nullable T value) {
props.set(this, value);
}
public void clear(@NonNull RenderProps props) {
props.clear(this);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Prop> prop = (Prop>) o;
return name.equals(prop.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "Prop{" +
"name='" + name + '\'' +
'}';
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/RegistryImpl.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import io.noties.markwon.core.CorePlugin;
// @since 4.0.0
class RegistryImpl implements MarkwonPlugin.Registry {
private final List origin;
private final List plugins;
private final Set pending;
RegistryImpl(@NonNull List origin) {
this.origin = origin;
this.plugins = new ArrayList<>(origin.size());
this.pending = new HashSet<>(3);
}
@NonNull
@Override
public P require(@NonNull Class
plugin) {
return get(plugin);
}
@Override
public
void require(
@NonNull Class
plugin,
@NonNull MarkwonPlugin.Action super P> action) {
action.apply(get(plugin));
}
@NonNull
List process() {
for (MarkwonPlugin plugin : origin) {
configure(plugin);
}
return plugins;
}
private void configure(@NonNull MarkwonPlugin plugin) {
// important -> check if it's in plugins
// if it is -> no need to configure (already configured)
if (!plugins.contains(plugin)) {
if (pending.contains(plugin)) {
throw new IllegalStateException("Cyclic dependency chain found: " + pending);
}
// start tracking plugins that are pending for configuration
pending.add(plugin);
plugin.configure(this);
// stop pending tracking
pending.remove(plugin);
// check again if it's included (a child might've configured it already)
// add to out-collection if not already present
// this is a bit different from `find` method as it does check for exact instance
// and not a sub-type
if (!plugins.contains(plugin)) {
// core-plugin must always be the first one (if it's present)
if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
plugins.add(0, plugin);
} else {
plugins.add(plugin);
}
}
}
}
@NonNull
private P get(@NonNull Class
type) {
// check if present already in plugins
// find in origin, if not found -> throw, else add to out-plugins
P plugin = find(plugins, type);
if (plugin == null) {
plugin = find(origin, type);
if (plugin == null) {
throw new IllegalStateException("Requested plugin is not added: " +
"" + type.getName() + ", plugins: " + origin);
}
configure(plugin);
}
return plugin;
}
@Nullable
private static
P find(
@NonNull List plugins,
@NonNull Class type) {
for (MarkwonPlugin plugin : plugins) {
if (type.isAssignableFrom(plugin.getClass())) {
//noinspection unchecked
return (P) plugin;
}
}
return null;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/RenderProps.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* @since 3.0.0
*/
public interface RenderProps {
@Nullable
T get(@NonNull Prop prop);
@NonNull
T get(@NonNull Prop prop, @NonNull T defValue);
void set(@NonNull Prop prop, @Nullable T value);
void clear(@NonNull Prop prop);
void clearAll();
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/RenderPropsImpl.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
/**
* @since 3.0.0
*/
class RenderPropsImpl implements RenderProps {
private final Map values = new HashMap<>(3);
@Nullable
@Override
public T get(@NonNull Prop prop) {
//noinspection unchecked
return (T) values.get(prop);
}
@NonNull
@Override
public T get(@NonNull Prop prop, @NonNull T defValue) {
Object value = values.get(prop);
if (value != null) {
//noinspection unchecked
return (T) value;
}
return defValue;
}
@Override
public void set(@NonNull Prop prop, @Nullable T value) {
if (value == null) {
values.remove(prop);
} else {
values.put(prop, value);
}
}
@Override
public void clear(@NonNull Prop prop) {
values.remove(prop);
}
@Override
public void clearAll() {
values.clear();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/SoftBreakAddsNewLinePlugin.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import org.commonmark.node.SoftLineBreak;
/**
* @since 4.3.0
*/
public class SoftBreakAddsNewLinePlugin extends AbstractMarkwonPlugin {
@NonNull
public static SoftBreakAddsNewLinePlugin create() {
return new SoftBreakAddsNewLinePlugin();
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(SoftLineBreak.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull SoftLineBreak softLineBreak) {
visitor.ensureNewLine();
}
});
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/SpanFactory.java
================================================
package io.noties.markwon;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* @since 3.0.0
*/
public interface SpanFactory {
@Nullable
Object getSpans(
@NonNull MarkwonConfiguration configuration,
@NonNull RenderProps props);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/SpannableBuilder.java
================================================
package io.noties.markwon;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.fluid.afm.utils.MDLogger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import com.fluid.afm.span.AntUnderlineSupportMulLinesSpan;
/**
* This class is used to _revert_ order of applied spans. Original SpannableStringBuilder
* is using an array to store all the information about spans. So, a span that is added first
* will be drawn first, which leads to subtle bugs (spans receive wrong `x` values when
* requested to draw itself)
*
* since 2.0.0 implements Appendable and CharSequence
*
* @since 1.0.1
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class SpannableBuilder implements Appendable, CharSequence {
/**
* @since 2.0.0
*/
public static void setSpans(@NonNull SpannableBuilder builder, @Nullable Object spans, int start, int end) {
if (spans != null) {
// setting a span for an invalid position can lead to silent fail (no exception,
// but execution is stopped)
if (!isPositionValid(builder.length(), start, end)) {
return;
}
// @since 3.0.1 we introduce another method that recursively applies spans
// allowing array of arrays (and more)
setSpansInternal(builder, spans, start, end);
}
}
// @since 2.0.1 package-private visibility for testing
@VisibleForTesting
static boolean isPositionValid(int length, int start, int end) {
return end > start
&& start >= 0
&& end <= length;
}
private final StringBuilder builder;
// actually we might be just using ArrayList
private final Deque spans = new ArrayDeque<>(8);
public SpannableBuilder() {
this("");
}
public SpannableBuilder(@NonNull CharSequence cs) {
this.builder = new StringBuilder(cs);
copySpans(0, cs);
}
/**
* Additional method that takes a String, which is proven to NOT contain any spans
*
* @param text String to append
* @return this instance
*/
@NonNull
public SpannableBuilder append(@NonNull String text) {
builder.append(text);
return this;
}
@NonNull
@Override
public SpannableBuilder append(char c) {
builder.append(c);
return this;
}
@NonNull
@Override
public SpannableBuilder append(@NonNull CharSequence cs) {
copySpans(length(), cs);
builder.append(cs);
return this;
}
/**
* @since 2.0.0 to follow Appendable interface
*/
@NonNull
@Override
public SpannableBuilder append(CharSequence csq, int start, int end) {
final CharSequence cs = csq.subSequence(start, end);
copySpans(length(), cs);
builder.append(cs);
return this;
}
@NonNull
public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span) {
final int length = length();
append(cs);
setSpan(span, length);
return this;
}
@NonNull
public SpannableBuilder append(@NonNull CharSequence cs, @NonNull Object span, int flags) {
final int length = length();
append(cs);
setSpan(span, length, length(), flags);
return this;
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start) {
return setSpan(span, start, length());
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start, int end) {
return setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
@NonNull
public SpannableBuilder setSpan(@NonNull Object span, int start, int end, int flags) {
spans.push(new Span(span, start, end, flags));
return this;
}
@Override
public int length() {
return builder.length();
}
@Override
public char charAt(int index) {
return builder.charAt(index);
}
/**
* @since 2.0.0 to follow CharSequence interface
*/
@Override
public CharSequence subSequence(int start, int end) {
final CharSequence out;
// @since 2.0.1 we copy spans to resulting subSequence
final List spans = getSpans(start, end);
if (spans.isEmpty()) {
out = builder.subSequence(start, end);
} else {
// we should not be SpannableStringBuilderReversed here
final SpannableStringBuilder builder = new SpannableStringBuilder(this.builder.subSequence(start, end));
final int length = builder.length();
int s;
int e;
for (Span span : spans) {
// we should limit start/end to resulting subSequence length
//
// for example, originally it was 5-7 and range 5-7 requested
// span should have 0-2
//
// if a span was fully including resulting subSequence it's start and
// end must be within 0..length bounds
s = Math.max(0, span.start - start);
e = Math.min(length, s + (span.end - span.start));
builder.setSpan(
span.what,
s,
e,
span.flags
);
}
out = builder;
}
return out;
}
/**
* This method will return all {@link Span} spans that overlap specified range,
* so if for example a 1..9 range is specified some spans might have 0..6 or 0..10 start/end ranges.
* <<<<<<< HEAD:markwon-core/src/main/java/ru/noties/markwon/SpannableBuilder.java
* NB spans are returned in reversed order (not in order that we store them internally)
* =======
* NB spans are returned in reversed order (no in order that we store them internally)
* >>>>>>> master:markwon/src/main/java/ru/noties/markwon/SpannableBuilder.java
*
* @since 2.0.1
*/
@NonNull
public List getSpans(int start, int end) {
final int length = length();
if (!isPositionValid(length, start, end)) {
// we might as well throw here
return Collections.emptyList();
}
// all requested
if (start == 0
&& length == end) {
// but also copy (do not allow external modification)
final List list = new ArrayList<>(spans);
Collections.reverse(list);
return Collections.unmodifiableList(list);
}
final List list = new ArrayList<>(0);
final Iterator iterator = spans.descendingIterator();
Span span;
while (iterator.hasNext()) {
span = iterator.next();
// we must execute 2 checks: if overlap with specified range or fully include it
// if span.start is >= range.start -> check if it's before range.end
// if span.end is <= end -> check if it's after range.start
if (
(span.start >= start && span.start < end)
|| (span.end <= end && span.end > start)
|| (span.start < start && span.end > end)) {
list.add(span);
}
}
return Collections.unmodifiableList(list);
}
public char lastChar() {
return builder.charAt(length() - 1);
}
@NonNull
public CharSequence removeFromEnd(int start) {
// this method is not intended to be used by clients
// it's a workaround to support tables
final int end = length();
// as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String
final SpannableStringBuilderReversed impl = new SpannableStringBuilderReversed(builder.subSequence(start, end));
final Iterator iterator = spans.iterator();
Span span;
while (iterator.hasNext() && ((span = iterator.next())) != null) {
if (span.start >= start && span.end <= end) {
impl.setSpan(span.what, span.start - start, span.end - start, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
iterator.remove();
}
}
builder.replace(start, end, "");
return impl;
}
@Override
@NonNull
public String toString() {
return builder.toString();
}
@NonNull
public CharSequence text() {
// @since 2.0.0 redirects this call to `#spannableStringBuilder()`
return spannableStringBuilder();
}
/**
* Simple method to create a SpannableStringBuilder, which is created anyway. Unlike {@link #text()}
* method which returns the same SpannableStringBuilder there is no need to cast the resulting
* CharSequence and makes the thing more explicit
*
* @since 2.0.0
*/
@NonNull
public SpannableStringBuilder spannableStringBuilder() {
// okay, in order to not allow external modification and keep our spans order
// we should not return our builder
//
// plus, if this method was called -> all spans would be applied, which potentially
// breaks the order that we intend to use
// so, we will defensively copy builder
// as we do not expose builder and do no apply spans to it, we are safe to NOT to convert to String
final SpannableStringBuilderReversed reversed = new SpannableStringBuilderReversed(builder);
// NB, as e are using Deque -> iteration will be started with last element
// so, spans will be appearing in the for loop in reverse order
for (Span span : spans) {
// 每个字设置span 为了增加span的换行能力
if (span.what instanceof AntUnderlineSupportMulLinesSpan) {
for (int i = span.start; i < span.end; i++) {
try {
if (reversed.charAt(i) == '[' && reversed.charAt(i + 1) == '^') {
reversed.setSpan(span.what, i, i+4, span.flags);
i = i + 3;
} else {
AntUnderlineSupportMulLinesSpan antUnderlineSupportMulLinesSpan = (AntUnderlineSupportMulLinesSpan) span.what;
reversed.setSpan(new AntUnderlineSupportMulLinesSpan(antUnderlineSupportMulLinesSpan.getColor(),
antUnderlineSupportMulLinesSpan.getThickness()),
i, i + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} catch (Exception e) {
MDLogger.e("SpannableBuilder", "setSpan error", e);
}
}
} else {
reversed.setSpan(span.what, span.start, span.end, span.flags);
}
}
return reversed;
}
/**
* @since 3.0.0
*/
public void clear() {
builder.setLength(0);
spans.clear();
}
private void copySpans(final int index, @Nullable CharSequence cs) {
// we must identify already reversed Spanned...
// and (!) iterate backwards when adding (to preserve order)
if (cs instanceof Spanned) {
final Spanned spanned = (Spanned) cs;
final boolean reversed = spanned instanceof SpannableStringBuilderReversed;
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
final int length = spans != null
? spans.length
: 0;
if (length > 0) {
if (reversed) {
Object o;
for (int i = length - 1; i >= 0; i--) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
} else {
Object o;
for (int i = 0; i < length; i++) {
o = spans[i];
setSpan(
o,
index + spanned.getSpanStart(o),
index + spanned.getSpanEnd(o),
spanned.getSpanFlags(o)
);
}
}
}
}
}
/**
* @since 2.0.1 made public in order to be returned from `getSpans` method, initially added in 1.0.1
*/
public static class Span {
public final Object what;
public int start;
public int end;
public final int flags;
Span(@NonNull Object what, int start, int end, int flags) {
this.what = what;
this.start = start;
this.end = end;
this.flags = flags;
}
}
/**
* @since 2.0.1 made inner class of {@link SpannableBuilder}, initially added in 1.0.1
*/
static class SpannableStringBuilderReversed extends SpannableStringBuilder {
SpannableStringBuilderReversed(CharSequence text) {
super(text);
}
}
/**
* @since 3.0.1
*/
private static void setSpansInternal(
@NonNull SpannableBuilder builder,
@Nullable Object spans,
int start,
int end) {
if (spans != null) {
if (spans.getClass().isArray()) {
for (Object o : ((Object[]) spans)) {
// @since 3.0.1 recursively apply spans (allow array of arrays)
setSpansInternal(builder, o, start, end);
}
} else {
builder.setSpan(spans, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/CorePlugin.java
================================================
package io.noties.markwon.core;
import android.content.Context;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.text.style.ForegroundColorSpan;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.fluid.afm.styles.TitleStyle;
import com.fluid.afm.utils.MDLogger;
import org.commonmark.node.Block;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.BulletList;
import org.commonmark.node.Code;
import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.HtmlBlock;
import org.commonmark.node.HtmlInline;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link;
import org.commonmark.node.ListBlock;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import org.commonmark.node.Paragraph;
import org.commonmark.node.SoftLineBreak;
import org.commonmark.node.StrongEmphasis;
import org.commonmark.node.Text;
import org.commonmark.node.ThematicBreak;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.core.factory.BlockQuoteSpanFactory;
import io.noties.markwon.core.factory.CodeBlockSpanFactory;
import io.noties.markwon.core.factory.CodeSpanFactory;
import io.noties.markwon.core.factory.EmphasisSpanFactory;
import io.noties.markwon.core.factory.HeadingSpanFactory;
import io.noties.markwon.core.factory.LinkSpanFactory;
import io.noties.markwon.core.factory.ListItemSpanFactory;
import io.noties.markwon.core.factory.StrongEmphasisSpanFactory;
import io.noties.markwon.core.factory.ThematicBreakSpanFactory;
import io.noties.markwon.core.spans.BulletListItemSpan;
import io.noties.markwon.core.spans.CodeBlockSpan;
import com.fluid.afm.span.CodeLanguageSpan;
import com.fluid.afm.span.HeadingTopOrBottomSpacingSpan;
import com.fluid.afm.span.LinkWithIconSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
import com.fluid.afm.span.ParagraphSpacingSpan;
import io.noties.markwon.core.spans.TextViewSpan;
import io.noties.markwon.image.ImageProps;
import com.fluid.afm.utils.Utils;
/**
* @see CoreProps
* @since 3.0.0
*/
public class CorePlugin extends AbstractMarkwonPlugin {
/**
* @see #addOnTextAddedListener(OnTextAddedListener)
* @since 4.0.0
*/
public interface OnTextAddedListener {
/**
* Will be called when new text is added to resulting {@link SpannableBuilder}.
* Please note that only text represented by {@link Text} node will trigger this callback
* (text inside code and code-blocks won\'t trigger it).
*
* Please note that if you wish to add spans you must use {@code start} parameter
* in order to place spans correctly ({@code start} represents the index at which {@code text}
* was added). So, to set a span for the whole length of the text added one should use:
*
* {@code
* visitor.builder().setSpan(new MySpan(), start, start + text.length(), 0);
* }
*
* @param visitor {@link MarkwonVisitor}
* @param text literal that had been added
* @param start index in {@code visitor} as which text had been added
* @see #addOnTextAddedListener(OnTextAddedListener)
*/
void onTextAdded(@NonNull MarkwonVisitor visitor, @NonNull String text, int start);
}
@NonNull
public static CorePlugin create(Context context) {
return new CorePlugin(context);
}
/**
* @return a set with enabled by default block types
* @since 4.4.0
*/
@NonNull
public static Set> enabledBlockTypes() {
return new HashSet<>(Arrays.asList(
BlockQuote.class,
Heading.class,
FencedCodeBlock.class,
HtmlBlock.class,
ThematicBreak.class,
ListBlock.class,
IndentedCodeBlock.class
));
}
// @since 4.0.0
private final List onTextAddedListeners = new ArrayList<>(0);
// @since 4.5.0
private boolean hasExplicitMovementMethod;
private final Context context;
protected CorePlugin(Context context) {
this.context = context;
}
/**
* @since 4.5.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
public CorePlugin hasExplicitMovementMethod(boolean hasExplicitMovementMethod) {
this.hasExplicitMovementMethod = hasExplicitMovementMethod;
return this;
}
/**
* Can be useful to post-process text added. For example for auto-linking capabilities.
*
* @see OnTextAddedListener
* @since 4.0.0
*/
@SuppressWarnings("UnusedReturnValue")
@NonNull
public CorePlugin addOnTextAddedListener(@NonNull OnTextAddedListener onTextAddedListener) {
onTextAddedListeners.add(onTextAddedListener);
return this;
}
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
text(builder);
strongEmphasis(builder);
emphasis(builder);
blockQuote(builder);
code(builder);
fencedCodeBlock(builder);
indentedCodeBlock(builder);
image(builder);
bulletList(builder);
orderedList(builder);
listItem(builder);
thematicBreak(builder);
heading(builder);
softLineBreak(builder);
hardLineBreak(builder);
paragraph(context, builder);
link(builder);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
// reuse this one for both code-blocks (indent & fenced)
final CodeBlockSpanFactory codeBlockSpanFactory = new CodeBlockSpanFactory();
builder
.setFactory(StrongEmphasis.class, new StrongEmphasisSpanFactory())
.setFactory(Emphasis.class, new EmphasisSpanFactory())
.setFactory(BlockQuote.class, new BlockQuoteSpanFactory())
.setFactory(Code.class, new CodeSpanFactory())
.setFactory(FencedCodeBlock.class, codeBlockSpanFactory)
.setFactory(IndentedCodeBlock.class, codeBlockSpanFactory)
.setFactory(ListItem.class, new ListItemSpanFactory())
.setFactory(Heading.class, new HeadingSpanFactory())
.setFactory(Link.class, new LinkSpanFactory())
.setFactory(ThematicBreak.class, new ThematicBreakSpanFactory());
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
OrderedListItemSpan.measure(textView, markdown);
// @since 4.4.0
// we do not break API compatibility, instead we introduce the `instance of` check
if (markdown instanceof Spannable) {
final Spannable spannable = (Spannable) markdown;
TextViewSpan.applyTo(spannable, textView);
}
}
@Override
public void afterSetText(@NonNull TextView textView) {
// let's ensure that there is a movement method applied
// we do it `afterSetText` so any user-defined movement method won't be
// replaced (it should be done in `beforeSetText` or manually on a TextView)
// @since 4.5.0 we additionally check if we should apply _implicit_ movement method
if (!hasExplicitMovementMethod && textView.getMovementMethod() == null) {
textView.setMovementMethod(LinkMovementMethod.getInstance());
}
}
private void text(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Text.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Text text) {
final String literal = text.getLiteral();
visitor.builder().append(literal);
// @since 4.0.0
if (!onTextAddedListeners.isEmpty()) {
// calculate the start position
final int length = visitor.length() - literal.length();
for (OnTextAddedListener onTextAddedListener : onTextAddedListeners) {
onTextAddedListener.onTextAdded(visitor, literal, length);
}
}
}
});
}
private static void strongEmphasis(@NonNull MarkwonVisitor.Builder builder) {
builder.on(StrongEmphasis.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull StrongEmphasis strongEmphasis) {
final int length = visitor.length();
visitor.visitChildren(strongEmphasis);
visitor.setSpansForNodeOptional(strongEmphasis, length);
}
});
}
private static void emphasis(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Emphasis.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Emphasis emphasis) {
final int length = visitor.length();
visitor.visitChildren(emphasis);
visitor.setSpansForNodeOptional(emphasis, length);
}
});
}
private static void blockQuote(@NonNull MarkwonVisitor.Builder builder) {
builder.on(BlockQuote.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull BlockQuote blockQuote) {
visitor.blockStart(blockQuote);
final int length = visitor.length();
visitor.visitChildren(blockQuote);
visitor.setSpansForNodeOptional(blockQuote, length);
visitor.blockEnd(blockQuote);
int space = visitor.configuration().theme().blockQuoteStyle().paragraphSpacing() + visitor.configuration().theme().blockQuoteStyle().bottomMargin() - visitor.configuration().theme().getParagraphBreakHeight();
int start = visitor.length();
visitor.builder().append('\n').append('\u00a0');
if (space > 0) {
visitor.setSpans(start, ParagraphSpacingSpan.create(space));
} else {
visitor.setSpans(start, ParagraphSpacingSpan.create(Utils.dpToPx(8f)));
}
}
});
}
private static void code(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Code.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Code code) {
final int length = visitor.length();
// NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces
// unfortunately we cannot use this for multiline code as we cannot control where a new line break will be inserted
SpannableBuilder builder = visitor.builder();
builder.append(code.getLiteral());
visitor.setSpansForNodeOptional(code, length);
}
});
}
private static void fencedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
builder.on(FencedCodeBlock.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull FencedCodeBlock fencedCodeBlock) {
final int length = visitor.length();
visitCodeBlock(visitor, fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral(), fencedCodeBlock);
handleCodeBlockLeftMargin(length, visitor);
int start = visitor.length();
visitor.builder()
.append('\n')
.append('\u00a0');
visitor.setSpans(start + 1, ParagraphSpacingSpan.create(visitor.configuration().theme().getParagraphBreakHeight()));
}
});
}
private static void handleCodeBlockLeftMargin(int length, MarkwonVisitor visitor) {
if (bulletListItemSpan != null || orderedListItemSpan != null) {
List itemSpans = visitor.builder().getSpans(length + 1, length + 2);
if (itemSpans == null) {
return;
}
for (SpannableBuilder.Span itemSpan : itemSpans) {
Object object = itemSpan.what;
if (object instanceof CodeBlockSpan) {
MDLogger.d("MYA_CorePlugin", "handleCodeBlockLeftMargin bulletListItemSpan="
+ bulletListItemSpan + ", orderedListItemSpan=" + orderedListItemSpan);
((CodeBlockSpan) object).setListItemSpan(bulletListItemSpan, orderedListItemSpan);
}
}
clear();
} else {
codeBlockStartIndex = length;
}
}
private static void indentedCodeBlock(@NonNull MarkwonVisitor.Builder builder) {
builder.on(IndentedCodeBlock.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull IndentedCodeBlock indentedCodeBlock) {
visitCodeBlock(visitor, null, indentedCodeBlock.getLiteral(), indentedCodeBlock);
}
});
}
// @since 4.0.0
// his method is moved from ImagesPlugin. Alternative implementations must set SpanFactory
// for Image node in order for this visitor to function
private static void image(MarkwonVisitor.Builder builder) {
builder.on(Image.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
// if there is no image spanFactory, ignore
final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class);
if (spanFactory == null) {
visitor.visitChildren(image);
return;
}
final int length = visitor.length();
visitor.visitChildren(image);
// we must check if anything _was_ added, as we need at least one char to render
if (length == visitor.length()) {
visitor.builder().append('\uFFFC');
}
final MarkwonConfiguration configuration = visitor.configuration();
final Node parent = image.getParent();
final boolean link = parent instanceof Link;
final String destination = configuration
.imageDestinationProcessor()
.process(image.getDestination());
final RenderProps props = visitor.renderProps();
// apply image properties
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
// properties after we applied span (we could though)
ImageProps.DESTINATION.set(props, destination);
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link);
ImageProps.IMAGE_SIZE.set(props, null);
visitor.setSpans(length, spanFactory.getSpans(configuration, props));
}
});
}
private static BulletListItemSpan bulletListItemSpan;
private static OrderedListItemSpan orderedListItemSpan;
private static Integer codeBlockStartIndex = null;
private static void clear() {
bulletListItemSpan = null;
orderedListItemSpan = null;
}
@VisibleForTesting
static void visitCodeBlock(
@NonNull MarkwonVisitor visitor,
@Nullable String info,
@NonNull String code,
@NonNull Node node) {
visitor.blockStart(node);
final int length = visitor.length();
StringBuilder language = new StringBuilder();
visitor.builder()
.append('\u00a0').append('\n')
.append(visitor.configuration().syntaxHighlight().highlight(info, code, language));
visitor.ensureNewLine();
visitor.builder().append('\u00a0');
// @since 4.1.1
CoreProps.CODE_BLOCK_INFO.set(visitor.renderProps(), info);
visitor.setSpansForNodeOptional(node, length);
// store language
visitor.setSpans(length + 2, new CodeLanguageSpan(language.toString()));
visitor.blockEnd(node);
}
private static void bulletList(@NonNull MarkwonVisitor.Builder builder) {
builder.on(BulletList.class, new SimpleBlockNodeVisitor());
}
private static void orderedList(@NonNull MarkwonVisitor.Builder builder) {
builder.on(OrderedList.class, new SimpleBlockNodeVisitor());
}
private static void listItem(@NonNull MarkwonVisitor.Builder builder) {
builder.on(ListItem.class, (visitor, listItem) -> {
final int length = visitor.length();
codeBlockStartIndex = null;
// it's important to visit children before applying render props (
// we can have nested children, who are list items also, thus they will
// override out props (if we set them before visiting children)
visitor.visitChildren(listItem);
final Node parent = listItem.getParent();
if (parent instanceof OrderedList) {
final int start = ((OrderedList) parent).getStartNumber();
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
// after we have visited the children increment start number
final OrderedList orderedList = (OrderedList) parent;
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
} else {
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
}
visitor.setSpansForNodeOptional(listItem, length);
getListItemSpanAndResetCodeBlockLeftMargin(length, visitor);
if (visitor.hasNext(listItem)) {
visitor.ensureNewLine();
}
});
}
private static void getListItemSpanAndResetCodeBlockLeftMargin(int length, MarkwonVisitor visitor) {
clear();
List itemSpans = visitor.builder().getSpans(length, length + 1);
for (SpannableBuilder.Span itemSpan : itemSpans) {
Object object = itemSpan.what;
if (object instanceof BulletListItemSpan) {
bulletListItemSpan = (BulletListItemSpan) object;
} else if (object instanceof OrderedListItemSpan) {
orderedListItemSpan = (OrderedListItemSpan) object;
}
}
if (codeBlockStartIndex != null) {
handleCodeBlockLeftMargin(codeBlockStartIndex, visitor);
codeBlockStartIndex = null;
}
clear();
}
private static int listLevel(@NonNull Node node) {
int level = 0;
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof ListItem) {
level += 1;
}
parent = parent.getParent();
}
if (level < 0) {
level = 0;
}
return level;
}
private static void thematicBreak(@NonNull MarkwonVisitor.Builder builder) {
builder.on(ThematicBreak.class, (visitor, thematicBreak) -> {
visitor.blockStart(thematicBreak);
final int length = visitor.length();
// without space it won't render
visitor.builder().append('\u00a0');
visitor.setSpansForNodeOptional(thematicBreak, length);
visitor.blockEnd(thematicBreak);
});
}
private static void heading(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Heading.class, (visitor, heading) -> {
TitleStyle titleStyle = visitor.configuration().theme().getTitleStyle(heading.getLevel());
if (heading.getPrevious() != null
&& !(heading.getPrevious() instanceof Heading)
&& !(heading.getPrevious() instanceof ThematicBreak)
&& titleStyle != null && titleStyle.paragraphSpacingBefore() > 0) {
int paragraphBreakHeight = titleStyle.paragraphSpacingBefore();
final int start = visitor.length();
visitor.builder().append("\n\u00a0");
visitor.setSpans(start + 1, new HeadingTopOrBottomSpacingSpan(paragraphBreakHeight));
}
visitor.blockStart(heading);
final int length = visitor.length();
visitor.visitChildren(heading);
CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel());
visitor.setSpansForNodeOptional(heading, length);
if (titleStyle != null && titleStyle.paragraph().paragraphSpacing() > 0) {
int paragraphBreakHeight = titleStyle.paragraph().paragraphSpacing();
final int start = visitor.length();
visitor.builder().append("\n\u00a0");
visitor.setSpans(start + 1, new HeadingTopOrBottomSpacingSpan(paragraphBreakHeight));
} else if (titleStyle == null) {
final int start = visitor.length();
visitor.builder().append("\n\u00a0");
visitor.setSpans(start + 1, new HeadingTopOrBottomSpacingSpan(visitor.configuration().theme().getParagraphBreakHeight() + Utils.dpToPx(2)));
}
visitor.blockEnd(heading);
});
}
private static void softLineBreak(@NonNull MarkwonVisitor.Builder builder) {
builder.on(SoftLineBreak.class, (visitor, softLineBreak) -> visitor.builder().append(' '));
}
private static void hardLineBreak(@NonNull MarkwonVisitor.Builder builder) {
builder.on(HardLineBreak.class, (visitor, hardLineBreak) -> visitor.ensureNewLine());
}
private static void paragraph(Context context, @NonNull MarkwonVisitor.Builder builder) {
builder.on(Paragraph.class, (visitor, paragraph) -> {
if (paragraph.getParent() instanceof BlockQuote) {
visitor.ensureNewLine();
final int start = visitor.length();
visitor.builder().append('\u00a0').append('\n');
visitor.setSpans(start, ParagraphSpacingSpan.create(Utils.dpToPx(3)));
}
int paragraphStart = visitor.length();
final boolean inTightList = isInTightList(paragraph);
if (!inTightList) {
visitor.blockStart(paragraph);
}
final int length = visitor.length();
visitor.visitChildren(paragraph);
CoreProps.PARAGRAPH_IS_IN_TIGHT_LIST.set(visitor.renderProps(), inTightList);
// @since 1.1.1 apply paragraph span
visitor.setSpansForNodeOptional(paragraph, length);
int paragraphBreakHeight = ensureParagraphBreakHeight(context, visitor, paragraph);
boolean addParagraphBreak;
if (paragraphBreakHeight <= 0 || paragraph.getParent() == null) {
addParagraphBreak = false;
} else {
addParagraphBreak = paragraph.getParent().getParent() != null || paragraph.getParent().getLastChild() != paragraph;
}
if (addParagraphBreak) {
visitor.ensureNewLine();
final int start = visitor.length();
visitor.builder().append('\u00a0');
visitor.setSpans(start, ParagraphSpacingSpan.create(paragraphBreakHeight));
}
if (!inTightList) {
visitor.blockEnd(paragraph);
}
if (paragraph.getParent() instanceof BlockQuote && visitor.configuration().theme().blockQuoteStyle().fontColor() != 0) {
visitor.setSpans(paragraphStart, new ForegroundColorSpan(visitor.configuration().theme().blockQuoteStyle().fontColor()));
}
});
}
private static int ensureParagraphBreakHeight(Context context, @NonNull MarkwonVisitor visitor, @NonNull Paragraph paragraph) {
int height = visitor.configuration().theme().getParagraphBreakHeight();
try {
Node next = paragraph.getNext();
if (next != null && next.getFirstChild() instanceof HtmlInline) {
HtmlInline htmlInline = (HtmlInline) next.getFirstChild();
Pattern pattern = Pattern.compile("para-space-before\\s*:\\s*(\\d+)rpx");
Matcher matcher = pattern.matcher(htmlInline.getLiteral());
if (matcher.find()) {
String mg = matcher.group(1);
if (!TextUtils.isEmpty(mg) && TextUtils.isDigitsOnly(mg)) {
height = (int) Utils.rpxToPx(Float.parseFloat(mg), context);
}
}
}
} catch (Exception e) {
MDLogger.e("Paragraph", "Parse custom space error: " + e.getMessage());
}
return height;
}
private static boolean isInTightList(@NonNull Paragraph paragraph) {
final Node parent = paragraph.getParent();
if (parent != null) {
final Node gramps = parent.getParent();
if (gramps instanceof ListBlock) {
ListBlock list = (ListBlock) gramps;
return list.isTight();
}
}
return false;
}
private static void link(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Link.class, (visitor, link) -> {
final int length = visitor.length();
visitor.visitChildren(link);
if (Utils.isInTableNode(link)) {
return;
}
final String destination = link.getDestination();
CoreProps.LINK_DESTINATION.set(visitor.renderProps(), destination);
CoreProps.LINK_TEXT_DESCRIPTION.set(visitor.renderProps(), link.getTitle());
visitor.setSpansForNodeOptional(link, length);
int lengthAfter = visitor.length();
if (length < lengthAfter && !TextUtils.isEmpty(visitor.configuration().theme().linkStyle().icon())) {
SpannableBuilder.setSpans(visitor.builder(), new LinkWithIconSpan(visitor.configuration().theme()), lengthAfter - 1, lengthAfter);
}
});
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/CoreProps.java
================================================
package io.noties.markwon.core;
import io.noties.markwon.Prop;
/**
* @since 3.0.0
*/
public abstract class CoreProps {
public static final Prop LIST_ITEM_TYPE = Prop.of("list-item-type");
public static final Prop BULLET_LIST_ITEM_LEVEL = Prop.of("bullet-list-item-level");
public static final Prop ORDERED_LIST_ITEM_NUMBER = Prop.of("ordered-list-item-number");
public static final Prop HEADING_LEVEL = Prop.of("heading-level");
public static final Prop LINK_DESTINATION = Prop.of("link-destination");
public static final Prop PARAGRAPH_IS_IN_TIGHT_LIST = Prop.of("paragraph-is-in-tight-list");
public static final Prop LINK_TEXT_DESCRIPTION = Prop.of("link-text-description");
public static final Prop COLOR = Prop.of("style-color");
public static final Prop LINK_TEXT_DECORATION = Prop.of("link-text-decoration");
public static final Prop FONT_WEIGHT = Prop.of("font-weight");
public static final Prop SOURCE = Prop.of("click-source");
/**
* @since 4.1.1
*/
public static final Prop CODE_BLOCK_INFO = Prop.of("code-block-info");
public enum ListItemType {
BULLET,
ORDERED
}
private CoreProps() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/MarkwonTheme.java
================================================
package io.noties.markwon.core;
import android.content.Context;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.TextUtils;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.fluid.afm.styles.BlockQuoteStyle;
import com.fluid.afm.styles.BulletStyle;
import com.fluid.afm.styles.CodeStyle;
import com.fluid.afm.styles.FootnoteStyle;
import com.fluid.afm.styles.HorizonRuleStyle;
import com.fluid.afm.styles.LinkStyle;
import com.fluid.afm.styles.MarkdownStyles;
import com.fluid.afm.styles.OrderStyle;
import com.fluid.afm.styles.ParagraphStyle;
import com.fluid.afm.styles.TableStyle;
import com.fluid.afm.styles.TitleStyle;
import com.fluid.afm.styles.UnderlineStyle;
import com.fluid.afm.utils.Utils;
import java.util.List;
import java.util.Map;
import io.noties.markwon.MarkwonPlugin;
import io.noties.markwon.utils.ColorUtils;
/**
* Class to hold theming information for rending of markdown.
*
* Since version 3.0.0 this class should be considered as CoreTheme as its
* information holds data for core features only. But based on this other components can still use it
* to display markdown consistently.
*
* Since version 3.0.0 this class should not be instantiated manually. Instead a {@link MarkwonPlugin}
* should be used: {@link MarkwonPlugin#configureTheme(Builder)}
*
* Since version 3.0.0 properties related to strike-through, tables and HTML
* are moved to specific plugins in independent artifacts
*
* @see CorePlugin
* @see MarkwonPlugin#configureTheme(Builder)
*/
@SuppressWarnings("WeakerAccess")
public class MarkwonTheme {
private static final float THEMATIC_BREAK_DEF_WIDTH = 5f;
protected static final int BLOCK_QUOTE_DEF_COLOR_ALPHA = 25;
protected static final float CODE_DEF_TEXT_SIZE_RATIO = .87F;
public static final int CODE_BLOCK_HEADER_HEIGHT = Utils.dpToPx(40);
public static final int THEMATIC_BREAK_DEF_ALPHA = 25;
private ParagraphStyle paragraph;
private HorizonRuleStyle horizonRule;
private LinkStyle link;
private TableStyle table;
private TitleStyle[] titleStyles;
private Map orderStyles;
private OrderStyle baseOrder;
private Map bulletStyles;
private BulletStyle baseBullet;
private FootnoteStyle footnote;
private BlockQuoteStyle blockQuote;
private CodeStyle code;
private UnderlineStyle underline;
private final TextView textView;
/**
* Create an empty instance of {@link Builder} with no default values applied
*
* Since version 3.0.0 manual construction of {@link MarkwonTheme} is not required, instead a
* {@link MarkwonPlugin#configureTheme(Builder)} should be used in order
* to change certain theme properties
*
* @since 3.0.0
*/
@SuppressWarnings("unused")
@NonNull
public static Builder emptyBuilder() {
return new Builder();
}
/**
* Factory method to create a {@link Builder} instance and initialize it with values
* from supplied {@link MarkwonTheme}
*
* @param copyFrom {@link MarkwonTheme} to copy values from
* @return {@link Builder} instance
* @see #builderWithDefaults(Context)
* @since 1.0.0
*/
@NonNull
public static Builder builder(@NonNull MarkwonTheme copyFrom) {
return new Builder(copyFrom);
}
/**
* Factory method to obtain a {@link Builder} instance initialized with default values taken
* from current application theme.
*
* @param context Context to obtain default styling values (colors, etc)
* @return {@link Builder} instance
* @since 1.0.0
*/
@NonNull
public static Builder builderWithDefaults(@NonNull Context context) {
return new Builder();
}
protected MarkwonTheme(@NonNull Builder builder) {
this.textView = builder.textView;
updateStyles(builder.mProductStyles);
}
public void updateStyles(MarkdownStyles styles) {
this.paragraph = styles.paragraphStyle();
this.horizonRule = styles.horizonRuleStyle();
this.link = styles.linkStyle();
this.table = styles.tableStyle();
this.titleStyles = styles.titleStyles();
this.orderStyles = styles.getOrderStylesMap();
this.baseOrder = styles.getBaseOrderStyle();
this.bulletStyles = styles.getBulletStylesMap();
this.baseBullet = styles.getBaseBulletStyle();
this.footnote = styles.getFootnoteStyle();
this.blockQuote = styles.blockQuoteStyle();
this.code = styles.codeStyle();
this.underline = styles.underlineStyle();
}
public UnderlineStyle underlineStyle() {
return underline;
}
public BlockQuoteStyle blockQuoteStyle() {
return blockQuote;
}
public LinkStyle linkStyle() {
return link;
}
public FootnoteStyle footnoteStyle() {
return footnote;
}
public CodeStyle codeStyle() {
return code;
}
public TextView getTextView() {
return textView;
}
public void applyLinkStyle(@NonNull TextPaint paint, int textColor, boolean isBold, String decoration) {
paint.setUnderlineText(link.underline());
if (textColor!= 0) {
paint.setColor(textColor);
} else if (link.fontColor() != 0) {
paint.setColor(link.fontColor());
} else {
paint.setColor(paint.linkColor);
}
if (isBold || link.isBold()) {
paint.setFakeBoldText(true);
}
if (TextUtils.equals("none", decoration)) {
paint.setUnderlineText(false);
} else if (TextUtils.equals("line-through", decoration)) {
paint.setUnderlineText(false);
paint.setStrikeThruText(true);
} else if (TextUtils.equals("underline", decoration)) {
paint.setUnderlineText(true);
} else {
paint.setUnderlineText(link.underline());
}
}
/**
* @since 1.0.5
*/
public void applyLinkStyle(@NonNull TextPaint paint) {
paint.setUnderlineText(link.underline());
if (link.fontColor() != 0) {
paint.setColor(link.fontColor());
}
if (link.isBold()) {
paint.setFakeBoldText(true);
}
}
public void applyLinkStyle(@NonNull Paint paint) {
paint.setUnderlineText(link.underline());
if (link.fontColor() != 0) {
// by default we will be using text color
paint.setColor(link.fontColor());
} else {
// @since 1.0.5, if link color is specified during configuration, _try_ to use the
// default one (if provided paint is an instance of TextPaint)
if (paint instanceof TextPaint) {
paint.setColor(((TextPaint) paint).linkColor);
}
}
}
public void applyBlockQuoteStyle(@NonNull Paint paint) {
int color = blockQuote.lineColor();
if (color == 0) {
color = ColorUtils.applyAlpha(paint.getColor(), BLOCK_QUOTE_DEF_COLOR_ALPHA);
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(color);
}
public int getCodeBlockMargin() {
return code.blockLeading();
}
public int getInlineCodeTextColor() {
return code.inlineFontColor();
}
/**
* @since 3.0.0
*/
public void applyInlineCodeTextStyle(@NonNull Paint paint) {
paint.setTextSize(code.inlineFontSize());
paint.setColor(code.inlineFontColor());
paint.setTypeface(code.codeTypeface());
}
/**
* @since 3.0.0
*/
public void applyCodeBlockTextStyle(@NonNull Paint paint) {
// apply text color, first check for block specific value,
// then check for code (inline), else do nothing (keep original color of text)
int textColor;
Typeface typeface;
int textSize;
textColor = code.codeFontColor();
typeface = code.codeTypeface();
textSize = code.codeFontSize();
if (textColor != 0) {
paint.setColor(textColor);
}
if (typeface != null) {
paint.setTypeface(typeface);
// please note that we won't be calculating textSize
// (like we do when no Typeface is provided), if it's some specific typeface
// we would confuse users about textSize
if (textSize > 0) {
paint.setTextSize(textSize);
}
} else {
// by default use monospace
paint.setTypeface(Typeface.MONOSPACE);
if (textSize > 0) {
paint.setTextSize(textSize);
} else {
// calculate default value
paint.setTextSize(paint.getTextSize() * CODE_DEF_TEXT_SIZE_RATIO);
}
}
}
/**
* @since 3.0.0
*/
public void applyCodeBackgroundColor(@NonNull Paint paint) {
paint.setColor(code.codeBackgroundColor());
}
public void applyInlineCodeBackgroundColor(@NonNull Paint paint) {
paint.setColor(code.inlineBackgroundColor());
}
public int getCodeBackgroundRadius() {
return code.codeBackgroundRadius();
}
/**
* @since 3.0.0
*/
public int getCodeBlockBackgroundColor(@NonNull Paint paint) {
return code.codeBackgroundColor();
}
public void applyThematicBreakStyle(@NonNull Paint paint) {
final int color = horizonRule.getColor() == 0 ? ColorUtils.applyAlpha(paint.getColor(), THEMATIC_BREAK_DEF_ALPHA) : horizonRule.getColor();
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
if (horizonRule.getHeight() >= 0) {
paint.setStrokeWidth((float) horizonRule.getHeight());
} else {
paint.setStrokeWidth(THEMATIC_BREAK_DEF_WIDTH);
}
}
public int getParagraphBreakHeight() {
return paragraph.paragraphSpacing();
}
public TitleStyle getTitleStyle(int level) {
return titleStyles[level - 1];
}
public TitleStyle getTitleStyles(int level) {
return titleStyles[level];
}
public BulletStyle getBulletStyle(int level) {
BulletStyle bulletStyle = bulletStyles.get(level);
if (bulletStyle == null) {
if (baseBullet == null) {
baseBullet = BulletStyle.create();
}
bulletStyle = baseBullet;
}
return bulletStyle;
}
public TableStyle getTableStyle() {
return table;
}
public OrderStyle orderBean(int level) {
OrderStyle orderBean = orderStyles.get(level);
if (orderBean == null) {
if (baseOrder == null) {
baseOrder = OrderStyle.create();
}
orderBean = baseOrder;
}
return orderBean;
}
public LinkStyle getLinkStyles() {
return link;
}
public HorizonRuleStyle getHorizonRule() {
return horizonRule;
}
@SuppressWarnings("unused")
public static class Builder {
private TextView textView;
private MarkdownStyles mProductStyles;
Builder() {
}
Builder(@NonNull MarkwonTheme theme) {
this.textView = theme.getTextView();
mProductStyles = new MarkdownStyles()
.paragraphStyle(theme.paragraph)
.codeStyle(theme.code)
.linkStyle(theme.link)
.blockQuoteStyle(theme.blockQuote)
.horizonRuleStyle(theme.horizonRule)
.tableStyle(theme.table)
.setTitleStyles(theme.titleStyles)
.setOrderStyles(theme.orderStyles)
.setBaseOrderStyle(theme.baseOrder)
.setBulletStyles(theme.bulletStyles)
.baseBulletStyle(theme.baseBullet)
.footnoteStyle(theme.footnote)
.underlineStyle(theme.underline);
}
public Builder setTextView(TextView textView) {
this.textView = textView;
return this;
}
public Builder setStyles(MarkdownStyles productStyles) {
this.mProductStyles = productStyles;
return this;
}
@NonNull
public MarkwonTheme build(List plugins) {
if (plugins != null) {
for (MarkwonPlugin plugin : plugins) {
plugin.configureTheme(this);
}
}
return new MarkwonTheme(this);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/SimpleBlockNodeVisitor.java
================================================
package io.noties.markwon.core;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
import io.noties.markwon.MarkwonVisitor;
/**
* A {@link MarkwonVisitor.NodeVisitor} that ensures that a markdown
* block starts with a new line, all children are visited and if further content available
* ensures a new line after self. Does not render any spans
*
* @since 3.0.0
*/
public class SimpleBlockNodeVisitor implements MarkwonVisitor.NodeVisitor {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
visitor.blockStart(node);
// @since 3.0.1 we keep track of start in order to apply spans (optionally)
final int length = visitor.length();
visitor.visitChildren(node);
// @since 3.0.1 we apply optional spans
visitor.setSpansForNodeOptional(node, length);
visitor.blockEnd(node);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/BlockQuoteSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.BlockQuoteSpan;
public class BlockQuoteSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new BlockQuoteSpan(configuration.theme());
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/CodeBlockSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.CodeBlockSpan;
public class CodeBlockSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new CodeBlockSpan(configuration.theme());
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/CodeSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.CodeSpan;
public class CodeSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new CodeSpan(configuration.theme());
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/EmphasisSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.EmphasisSpan;
public class EmphasisSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new EmphasisSpan();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/HeadingSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.HeadingSpan;
public class HeadingSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new HeadingSpan(
configuration.theme(),
CoreProps.HEADING_LEVEL.require(props)
);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/LinkSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.LinkSpan;
public class LinkSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new LinkSpan(
configuration.theme(),
CoreProps.LINK_DESTINATION.require(props),
configuration.linkResolver()
);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/ListItemSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.BulletListItemSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
public class ListItemSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
// type of list item
// bullet : level
// ordered: number
final Object spans;
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
spans = new BulletListItemSpan(
configuration.theme(),
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
);
} else {
spans = new OrderedListItemSpan(
configuration.theme(),
CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props),
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
);
}
return spans;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/StrongEmphasisSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.StrongEmphasisSpan;
public class StrongEmphasisSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new StrongEmphasisSpan();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/factory/ThematicBreakSpanFactory.java
================================================
package io.noties.markwon.core.factory;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
import io.noties.markwon.core.spans.ThematicBreakSpan;
public class ThematicBreakSpanFactory implements SpanFactory {
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new ThematicBreakSpan(configuration.theme());
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/BlockQuoteSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import androidx.annotation.NonNull;
import com.fluid.afm.styles.BlockQuoteStyle;
import io.noties.markwon.core.MarkwonTheme;
public class BlockQuoteSpan implements LeadingMarginSpan {
private final MarkwonTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private int mLineWidth;
private int mSpace;
private int mLineCornerRadius;
private int mLineColor;
private int mBottomDistance;
public BlockQuoteSpan(@NonNull MarkwonTheme theme) {
this.theme = theme;
applyStyles();
}
private void applyStyles() {
BlockQuoteStyle blockQuoteStyle = theme.blockQuoteStyle();
mLineWidth = blockQuoteStyle.lineWidth();
mSpace = blockQuoteStyle.lineMargin() + blockQuoteStyle.lineWidth() + blockQuoteStyle.leftMargin();
mLineCornerRadius = blockQuoteStyle.lineCornerRadius();
mLineColor = blockQuoteStyle.lineColor();
mBottomDistance = blockQuoteStyle.bottomMargin();
}
@Override
public int getLeadingMargin(boolean first) {
return mSpace;
}
@Override
public void drawLeadingMargin(
Canvas c,
Paint p,
int x,
int dir,
int top,
int baseline,
int bottom,
CharSequence text,
int start,
int end,
boolean first,
Layout layout) {
final int width = mLineWidth;
paint.set(p);
if (mLineColor != 0) {
paint.setColor(mLineColor);
} else {
theme.applyBlockQuoteStyle(paint);
}
final int l = x + theme.blockQuoteStyle().leftMargin();
final int r = l + (dir * width);
final int left = Math.min(l, r);
final int right = Math.max(l, r);
if (selfEnd(end, text, this)) {
rect.set(left, top, right, Math.max(bottom - theme.getParagraphBreakHeight() + mBottomDistance, top));
} else if (bottom > top){
rect.set(left, top, right, bottom);
}
// get span start/end
final Spanned spanned = (Spanned) text;
final int spanStart = spanned.getSpanStart(this);
final int spanEnd = spanned.getSpanEnd(this);
// is first/last line
boolean isFirstLine = layout.getLineForOffset(start) == layout.getLineForOffset(spanStart);
boolean isLastLine = layout.getLineForOffset(end) == layout.getLineForOffset(spanEnd);
RectF rectF = new android.graphics.RectF(
rect.left, rect.top, rect.right, rect.bottom
);
paint.setAlpha((int) (0.6f * 255));
if (mLineCornerRadius > 0 && (isFirstLine || isLastLine)) {
int save = c.save();
if (isFirstLine) {
c.clipRect(rectF);
c.drawRoundRect(rectF.left, rectF.top, rectF.right, rectF.bottom + mLineCornerRadius, mLineCornerRadius, mLineCornerRadius, paint);
} else { // last line
if (rectF.bottom < rectF.top) {
rectF.set(rectF.left, rectF.top, rectF.right, rectF.top + mLineCornerRadius);
}
c.clipRect(rectF);
c.drawRoundRect(rectF.left, rectF.top - mLineCornerRadius, rectF.right, rectF.bottom, mLineCornerRadius, mLineCornerRadius, paint);
}
c.restoreToCount(save);
} else {
c.drawRect(rectF, paint);
}
}
private static boolean selfEnd(int end, CharSequence text, Object span) {
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanEnd == end || spanEnd == end - 1;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/BulletListItemSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import com.fluid.afm.span.BaseIconTextSpan;
import com.fluid.afm.styles.BulletStyle;
import com.fluid.afm.styles.Shape;
import com.fluid.afm.utils.Utils;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.utils.LeadingMarginUtils;
/**
* 无序列表项
*/
public class BulletListItemSpan extends BaseIconTextSpan {
private static final boolean IS_NOUGAT;
private static final int DEFAULT_STROKE_WIDTH = Utils.dpToPx(1);
private static final int DEFAULT_LEADING_MARGIN = Utils.dpToPx(24);
private Integer leadingMargin = null;
static {
final int sdk = Build.VERSION.SDK_INT;
IS_NOUGAT = Build.VERSION_CODES.N == sdk || Build.VERSION_CODES.N_MR1 == sdk;
}
private final MarkwonTheme theme;
private final Paint paint = ObjectsPool.paint();
private final RectF circle = ObjectsPool.rectF();
private final Rect rectangle = ObjectsPool.rect();
private final int level;
private BulletStyle style;
public BulletListItemSpan(
@NonNull MarkwonTheme theme,
@IntRange(from = 0) int level) {
super(theme.getTextView());
this.theme = theme;
this.level = level;
style = theme.getBulletStyle(level);
}
@Override
protected String getImageUrl() {
if (style != null && style.shape() != null) {
return style.shape().icon();
}
return "";
}
@Override
protected void applyStyle(TextPaint paint) {
applyStyleInner(paint);
}
private void applyStyleInner(Paint p) {
style = theme.getBulletStyle(level);
if (TextUtils.isEmpty(getImageUrl())) {
paint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
}
if (validShape() && style.shape().color() != 0) {
paint.setColor(style.shape().color());
} else {
paint.setColor(p.getColor());
}
}
public int getLeadingMarginIfUsed() {
return leadingMargin == null ? 0 : leadingMargin;
}
@Override
public int getLeadingMargin(boolean first) {
if (style.leadingSpacing() >= 0) {
leadingMargin = style.leadingSpacing();
} else {
leadingMargin = (int) (realTextSize + 0.5) / 2;
}
if (style.leading() > 0) {
leadingMargin += style.leading();
} else {
leadingMargin += (int) (realTextSize + 0.5) / 2;
}
if (style.shape() != null && style.shape().size() > 0) {
leadingMargin += style.shape().size();
} else {
final int textLineHeight = (int) (paint.descent() - paint.ascent() + .5F);
final int side = getSpotSize(textLineHeight);
leadingMargin += getSpotSize(side) + DEFAULT_LEADING_MARGIN;
}
return leadingMargin;
}
private boolean validShape() {
if (style.shape() == null) {
return false;
}
if (style.shape().type() == Shape.SHAPE_CIRCLE || style.shape().type() == Shape.SHAPE_RECT || style.shape().type() == Shape.SHAPE_RING || style.shape().type() == Shape.SHAPE_RECT_OUTLINE) {
return style.shape().size() > 0;
} else {
return style.shape().type() == Shape.SHAPE_ICON;
}
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
// if there was a line break, we don't need to draw anything
if (!first
|| !LeadingMarginUtils.selfStart(start, text, this)) {
return;
}
paint.set(p);
applyStyleInner(paint);
boolean validShape = validShape();
if (validShape && style.shape().type() == Shape.SHAPE_ICON) {
if (mDrawableWrapper != null && mDrawableWrapper.getDrawable() != null) {
c.save();
c.translate(x, baseline - iconOffset);
mDrawableWrapper.getDrawable().draw(c);
c.restore();
}
return;
}
final int save = c.save();
try {
final int width = leadingMargin;
// @since 1.0.6 we no longer rely on (bottom-top) calculation in order to detect line height
// it lead to bad rendering as first & last lines received different results even
// if text size is the same (first line received greater amount and bottom line -> less)
final int textLineHeight = (int) (paint.descent() - paint.ascent() + .5F);
final int side = getSpotSize(textLineHeight);
final int marginLeft = validShape ? style.leading() : (width - side) / 2;
// in order to support RTL
final int l;
final int r;
{
// @since 4.2.1 to correctly position bullet
// when nested inside other LeadingMarginSpans (sorry, Nougat)
if (IS_NOUGAT) {
// @since 2.0.2
// There is a bug in Android Nougat, when this span receives an `x` that
// doesn't correspond to what it should be (text is placed correctly though).
// Let's make this a general rule -> manually calculate difference between expected/actual
// and add this difference to resulting left/right values. If everything goes well
// we do not encounter a bug -> this `diff` value will be 0
final int diff;
if (dir < 0) {
// rtl
diff = x - (layout.getWidth() - (width * level));
} else {
diff = (width * level) - x;
}
final int left = x + (dir * marginLeft);
final int right = left + (dir * side);
l = Math.min(left, right) + (dir * diff);
r = Math.max(left, right) + (dir * diff);
} else {
if (dir > 0) {
l = x + marginLeft;
} else {
l = x - width + marginLeft;
}
r = l + side;
}
}
final int t = baseline + (int) (((paint.descent() + paint.ascent()) / 2.F + .5F) * (1 + Utils.FONT_SPACING_IN_LINE)) - (side / 2);
final int b = t + side;
boolean drawn = false;
if (validShape) {
if (style.shape().type() == Shape.SHAPE_CIRCLE || style.shape().type() == Shape.SHAPE_RING) {
circle.set(l, t, r, b);
Paint.Style paintStyle = Paint.Style.FILL;
if (style.shape().type() == Shape.SHAPE_RING || (!validShape && level == 0)) {
paintStyle = Paint.Style.STROKE;
}
paint.setStyle(paintStyle);
c.drawOval(circle, paint);
drawn = true;
} else if (style.shape().type() == Shape.SHAPE_RECT || style.shape().type() == Shape.SHAPE_RECT_OUTLINE) {
rectangle.set(l, t, r, b);
if (style.shape().type() == Shape.SHAPE_RECT) {
paint.setStyle(Paint.Style.FILL);
} else {
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(style.shape().lineWidth());
}
c.drawRect(rectangle, paint);
drawn = true;
}
}
if (!drawn) {
if (level == 0 || level == 1) {
circle.set(l, t, r, b);
final Paint.Style style = level == 0
? Paint.Style.FILL
: Paint.Style.STROKE;
paint.setStyle(style);
c.drawOval(circle, paint);
} else {
rectangle.set(l, t, r, b);
paint.setStyle(Paint.Style.FILL);
c.drawRect(rectangle, paint);
}
}
} finally {
c.restoreToCount(save);
}
}
private int getSpotSize(int textLineHeight) {
if (validShape() && style.shape().size() > 0) {
return style.shape().size();
}
return textLineHeight / 3;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/CodeBlockSpan.java
================================================
package io.noties.markwon.core.spans;
import static io.noties.markwon.core.MarkwonTheme.CODE_BLOCK_HEADER_HEIGHT;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import com.fluid.afm.utils.MDLogger;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.utils.LeadingMarginUtils;
import io.noties.markwon.utils.SpanUtils;
/**
* @since 3.0.0 split inline and block spans
*/
public class CodeBlockSpan extends MetricAffectingSpan implements LeadingMarginSpan {
private static final String TAG = "MYA_CodeBlockSpan";
private final MarkwonTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private final int radius;
private int leftOffset;
private BulletListItemSpan bulletListItemSpan;
private OrderedListItemSpan orderedListItemSpan;
public CodeBlockSpan(@NonNull MarkwonTheme theme) {
this.theme = theme;
this.radius = theme.getCodeBackgroundRadius();
this.leftOffset = 0;
}
public void setListItemSpan(BulletListItemSpan bulletListItemSpan,
OrderedListItemSpan orderedListItemSpan) {
this.bulletListItemSpan = bulletListItemSpan;
this.orderedListItemSpan = orderedListItemSpan;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint ds) {
apply(ds);
}
private void apply(TextPaint p) {
theme.applyCodeBlockTextStyle(p);
}
@Override
public int getLeadingMargin(boolean first) {
if (bulletListItemSpan != null) {
leftOffset = bulletListItemSpan.getLeadingMarginIfUsed();
} else if (orderedListItemSpan != null) {
leftOffset = orderedListItemSpan.getLeadingMarginIfUsed();
}
if (leftOffset != 0) {
MDLogger.d(TAG, "getLeadingMargin leftOffset=" + leftOffset
+ ",bulletListItemSpan=" + bulletListItemSpan
+ ",orderedListItemSpan=" + orderedListItemSpan);
}
return theme.codeStyle().blockLeading() - leftOffset;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
if (!SpanUtils.isSelf(start, end, text, this)) {
return;
}
paint.setStyle(Paint.Style.FILL);
paint.setColor(theme.getCodeBlockBackgroundColor(p));
int left;
final int right;
if (dir > 0) {
left = 0;
right = layout.getWidth();
} else {
left = x - layout.getWidth();
right = x;
}
left -= leftOffset;
left = Math.max(0, left);
final boolean lastLine = selfEnd(end, text, this);
final boolean firstLine = LeadingMarginUtils.selfStart(start, text, this);
if (lastLine) {
// draw bottom
Paint.FontMetricsInt fontMetrics = p.getFontMetricsInt();
final int fontHeight = fontMetrics.bottom - fontMetrics.top;
final int lineHeight = bottom - top;
if (lineHeight < fontHeight) {
bottom += (fontHeight - lineHeight / 2 );
MDLogger.d(TAG, "drawLeadingMargin bottom=" + bottom
+ ", fontHeight=" + fontHeight
+ ", lineHeight=" + lineHeight);
}
// reduce last line height
bottom -= lineHeight / 2;
drawRectWithBottomRound(c, paint, left, top, right, bottom, radius);
} else if (firstLine) {
// draw top
if(theme.codeStyle().isShowTitle()) {
rect.set(left, top + CODE_BLOCK_HEADER_HEIGHT, right, bottom);
c.drawRect(rect, paint);
} else {
drawRectWithTopRound(c, paint, left, top, right, bottom, radius);
}
} else {
// draw middle left and right line
rect.set(left, top, right, bottom);
c.drawRect(rect, paint);
}
if (theme.codeStyle().isDrawBorder()) {
paint.setStyle(Paint.Style.STROKE);
paint.setColor(theme.codeStyle().borderColor());
paint.setStrokeWidth(theme.codeStyle().borderWidth());
float halfStrokeWidth = paint.getStrokeWidth() / 2;
int saveCount = c.save();
if (firstLine) {
if(theme.codeStyle().isShowTitle()) {
c.clipRect(left, top + CODE_BLOCK_HEADER_HEIGHT + halfStrokeWidth, right, bottom);
} else {
c.clipRect(left, top + radius + halfStrokeWidth, right, bottom);
}
} else {
c.clipRect(left, top, right, bottom);
}
if (lastLine) {
c.drawRoundRect(left + halfStrokeWidth, top - paint.getStrokeWidth() - radius, right - halfStrokeWidth, bottom - paint.getStrokeWidth(), radius, radius, paint);
} else {
c.drawRect(left + halfStrokeWidth, top - paint.getStrokeWidth(), right - halfStrokeWidth, bottom + paint.getStrokeWidth(), paint);
}
c.restoreToCount(saveCount);
paint.setStyle(Paint.Style.FILL);
}
}
private void drawRectWithTopRound(Canvas canvas, Paint paint,
float left, float top, float right, float bottom,
float radius) {
int count = canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawRoundRect(left, top, right, bottom + radius, radius, radius, paint);
canvas.restoreToCount(count);
}
private void drawRectWithBottomRound(Canvas canvas, Paint paint,
float left, float top, float right, float bottom,
float radius) {
int count = canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawRoundRect(left, top - radius, right, bottom, radius, radius, paint);
canvas.restoreToCount(count);
}
private boolean selfEnd(int end, CharSequence text, Object span) {
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanEnd == end || spanEnd == end - 1;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/CodeSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
import io.noties.markwon.core.MarkwonTheme;
/**
* @since 3.0.0 split inline and block spans
*/
public class CodeSpan extends MetricAffectingSpan {
private final MarkwonTheme theme;
public CodeSpan(@NonNull MarkwonTheme theme) {
this.theme = theme;
}
@Override
public void updateMeasureState(TextPaint p) {
apply(p);
}
@Override
public void updateDrawState(TextPaint ds) {
apply(ds);
theme.applyCodeBackgroundColor(ds);
}
private void apply(TextPaint p) {
theme.applyInlineCodeTextStyle(p);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/CustomTypefaceSpan.java
================================================
package io.noties.markwon.core.spans;
import android.annotation.SuppressLint;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
/**
* A span implementation that allow applying custom Typeface. Although it is
* not used directly by the library, it\'s helpful for customizations.
*
* Please note that this implementation does not validate current paint state
* and won\'t be updating/modifying supplied Typeface unless {@code mergeStyles} is specified
*
* @since 3.0.0
*/
public class CustomTypefaceSpan extends MetricAffectingSpan {
@NonNull
public static CustomTypefaceSpan create(@NonNull Typeface typeface) {
return create(typeface, false);
}
/**
* NB! in order to merge typeface styles, supplied typeface must be
* able to be created via {@code Typeface.create(Typeface, int)} method. This would mean that bundled fonts
* inside {@code assets} folder would be able to display styles properly.
*
* @param mergeStyles control if typeface styles must be merged, for example, if
* this span (bold) is contained by other span (italic),
* {@code mergeStyles=true} would result in bold-italic
* @since 4.6.1
*/
@NonNull
public static CustomTypefaceSpan create(@NonNull Typeface typeface, boolean mergeStyles) {
return new CustomTypefaceSpan(typeface, mergeStyles);
}
private final Typeface typeface;
private final boolean mergeStyles;
/**
* @deprecated 4.6.1 use {{@link #create(Typeface)}}
* or {@link #create(Typeface, boolean)} factory method
*/
@Deprecated
public CustomTypefaceSpan(@NonNull Typeface typeface) {
this(typeface, false);
}
// @since 4.6.1
CustomTypefaceSpan(@NonNull Typeface typeface, boolean mergeStyles) {
this.typeface = typeface;
this.mergeStyles = mergeStyles;
}
@Override
public void updateMeasureState(@NonNull TextPaint paint) {
updatePaint(paint);
}
@Override
public void updateDrawState(@NonNull TextPaint paint) {
updatePaint(paint);
}
private void updatePaint(@NonNull TextPaint paint) {
final Typeface oldTypeface = paint.getTypeface();
if (!mergeStyles ||
oldTypeface == null ||
oldTypeface.getStyle() == Typeface.NORMAL) {
paint.setTypeface(typeface);
} else {
final int oldStyle = oldTypeface.getStyle();
@SuppressLint("WrongConstant") final int want = oldStyle | typeface.getStyle();
final Typeface styledTypeface = Typeface.create(typeface, want);
paint.setTypeface(styledTypeface);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/EmphasisSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class EmphasisSpan extends MetricAffectingSpan {
@Override
public void updateMeasureState(TextPaint p) {
p.setTextSkewX(-0.25F);
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setTextSkewX(-0.25F);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/HeadingSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.TextPaint;
import android.text.TextUtils;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import com.fluid.afm.span.BaseIconTextSpan;
import com.fluid.afm.styles.TitleStyle;
import io.noties.markwon.core.MarkwonTheme;
/**
* 标题
*/
public class HeadingSpan extends BaseIconTextSpan {
private final MarkwonTheme theme;
private final int level;
private TitleStyle style;
public HeadingSpan(@NonNull MarkwonTheme theme, @IntRange(from = 1, to = 6) int level) {
super(theme.getTextView());
this.theme = theme;
this.level = level;
style = theme.getTitleStyles(level - 1);
}
@Override
protected String getImageUrl() {
if (style != null) {
return style.icon();
}
return "";
}
@Override
protected void applyStyle(TextPaint paint) {
style = theme.getTitleStyles(level - 1);
style.apply(paint);
}
@Override
public int getLeadingMargin(boolean first) {
// no margin actually, but we need to access Canvas to draw break
if (style != null && first && !TextUtils.isEmpty(style.icon())) {
return realTextSize;
}
return 0;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
if (first && mDrawableWrapper != null && mDrawableWrapper.getDrawable() != null) {
c.save();
c.translate(x, baseline - iconOffset);
mDrawableWrapper.getDrawable().draw(c);
c.restore();
}
}
/**
* @since 4.2.0
*/
public int getLevel() {
return level;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/LastLineSpacingSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Paint;
import android.text.Spanned;
import android.text.style.LineHeightSpan;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
/**
* @since 4.0.0
*/
public class LastLineSpacingSpan implements LineHeightSpan {
@NonNull
public static LastLineSpacingSpan create(@Px int spacing) {
return new LastLineSpacingSpan(spacing);
}
private final int spacing;
public LastLineSpacingSpan(@Px int spacing) {
this.spacing = spacing;
}
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
if (selfEnd(end, text, this)) {
// let's just add what we want
fm.descent += spacing;
fm.bottom += spacing;
}
}
private static boolean selfEnd(int end, CharSequence text, Object span) {
// this is some kind of interesting magic here... only the last
// span will receive correct _end_ argument, but previous spans
// receive it tilted by one (1). Most likely it's just a new-line character... and
// if needed we could check for that
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanEnd == end || spanEnd == end - 1;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/LinkSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.TextPaint;
import android.text.style.URLSpan;
import android.view.View;
import androidx.annotation.NonNull;
import io.noties.markwon.LinkResolver;
import io.noties.markwon.core.MarkwonTheme;
public class LinkSpan extends URLSpan {
private final MarkwonTheme theme;
private final String link;
private final LinkResolver resolver;
public LinkSpan(
@NonNull MarkwonTheme theme,
@NonNull String link,
@NonNull LinkResolver resolver) {
super(link);
this.theme = theme;
this.link = link;
this.resolver = resolver;
}
@Override
public void onClick(View widget) {
resolver.resolve(widget, link);
}
@Override
public void updateDrawState(@NonNull TextPaint ds) {
theme.applyLinkStyle(ds);
}
/**
* @since 4.2.0
*/
@NonNull
public String getLink() {
return link;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/ObjectsPool.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
abstract class ObjectsPool {
// maybe it's premature optimization, but as all the drawing is done in one thread
// and we apply needed values before actual drawing it's (I assume) safe to reuse some frequently used objects
// if one of the spans need some really specific handling for Paint object (like colorFilters, masks, etc)
// it should instantiate own instance of it
private static final Rect RECT = new Rect();
private static final RectF RECT_F = new RectF();
private static final Paint PAINT = new Paint(Paint.ANTI_ALIAS_FLAG);
static Rect rect() {
return RECT;
}
static RectF rectF() {
return RECT_F;
}
static Paint paint() {
return PAINT;
}
private ObjectsPool() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/OrderedListItemSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.LeadingMarginSpan;
import android.widget.TextView;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import com.fluid.afm.span.BaseIconTextSpan;
import com.fluid.afm.styles.OrderStyle;
import com.fluid.afm.styles.Shape;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.utils.LeadingMarginUtils;
import com.fluid.afm.utils.Utils;
public class OrderedListItemSpan extends BaseIconTextSpan implements LeadingMarginSpan {
private static final int DEFAULT_STROKE_WIDTH = Utils.dpToPx(1);
private static final int ICON_SPACING = Utils.dpToPx(5);
private static final int DEFAULT_LEADING_MARGIN = Utils.dpToPx(24);
/**
* Process supplied `text` argument and supply TextView paint to all OrderedListItemSpans
* in order for them to measure number.
*
* NB, this method must be called before setting text to a TextView (`TextView#setText`
* internally can trigger new Layout creation which will ask for leading margins right away)
*
* @param textView to which markdown will be applied
* @param text parsed markdown to process
* @since 2.0.1
*/
private static final int BASE_CHAR_WIDTH = Utils.dpToPx(9f);
private static final int SPACE = Utils.dpToPx(5f);
public static void measure(@NonNull TextView textView, @NonNull CharSequence text) {
if (!(text instanceof Spanned)) {
// nothing to do here
return;
}
final OrderedListItemSpan[] spans = ((Spanned) text).getSpans(
0,
text.length(),
OrderedListItemSpan.class);
if (spans != null) {
final TextPaint paint = textView.getPaint();
for (OrderedListItemSpan span : spans) {
span.margin = (int) (paint.measureText(span.number) + .5F);
}
}
}
private final MarkwonTheme theme;
private final String number;
private final Paint paint = ObjectsPool.paint();
private Integer leadingMargin = null;
// we will use this variable to check if our order number text exceeds block margin,
// so we will use it instead of block margin
// @since 1.0.3
private float margin;
private final int level;
private int numberLength;
private OrderStyle style;
public OrderedListItemSpan(
@NonNull MarkwonTheme theme,
@IntRange(from = 0) int number,
@IntRange(from = 0) int level
) {
super(theme.getTextView());
style = theme.orderBean(level);
this.theme = theme;
this.level = level;
if (style != null && style.shape() != null) {
this.number = String.valueOf(number);
} else {
this.number = number + "." + '\u00a0';
}
numberLength = String.valueOf(number).length();
}
public int getLeadingMarginIfUsed() {
return getProductLeading();
}
public int getProductLeading() {
if (style != null) {
if (style.shape() != null) {
if (style.shape().type() == Shape.SHAPE_ICON) {
return getIconSize() + style.leading() + style.leadingSpacing();
} else if (style.shape().type() == Shape.SHAPE_RECT) {
return style.shape().size() + style.leading() + style.leadingSpacing();
}
} else {
if (style.orderFontSize() > 0) {
paint.setTextSize(style.orderFontSize());
}
float baseW = paint.measureText("4"); // 4 is the widest number
return (int) (style.leading() + numberLength * baseW + style.leadingSpacing());
}
}
return (int) margin + DEFAULT_LEADING_MARGIN;
}
@Override
public int getLeadingMargin(boolean first) {
leadingMargin = getProductLeading();
return leadingMargin;
}
private void applyListItemStyle(@NonNull Paint paint) {
paint.setStrokeWidth(DEFAULT_STROKE_WIDTH);
paint.setLetterSpacing(0);
if (style.orderFontSize() > 0) {
paint.setTextSize(style.orderFontSize());
}
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
// if there was a line break, we don't need to draw anything
if (!first
|| !LeadingMarginUtils.selfStart(start, text, this)) {
return;
}
paint.set(p);
boolean drawIcon = mDrawableWrapper != null && mDrawableWrapper.getDrawable() != null;
float oldSize = paint.getTextSize();
float baselineOffset = 0;
boolean isBold = paint.isFakeBoldText();
if (drawIcon || (style != null && style.shape() != null)) {
float textsize;
if (style != null && style.orderFontSize() > 0) {
textsize = style.orderFontSize();
} else {
float numberRatio = 0.62f;
textsize = oldSize * numberRatio;
}
paint.setTextSize(textsize);
if (style != null && style.isBold()) {
paint.setFakeBoldText(true);
}
baselineOffset = (oldSize - textsize) / 2f * Utils.FONT_HEIGHT_IN_LINE;
}
applyListItemStyle(paint);
// if we could force usage of #measure method then we might want skip this measuring here
// but this won't hold against new values that a TextView can receive (new text size for
// example...)
String drawText = number;
float numberWidth = paint.measureText(number);
// @since 1.0.3
float width = leadingMargin == null ? 0 : leadingMargin;
if (style != null && style.shape() != null) {
width = style.shape().size();
}
boolean isNumber = true;
if (numberWidth > width) {
// let's keep this logic here in case a user decided not to call #measure and is fine
// with current implementation
if (style == null || style.shape() == null) {
width = numberWidth;
margin = numberWidth;
} else {
isNumber = false;
drawText = "...";
numberWidth = (int) (paint.measureText(drawText) + .5F);
}
} else {
margin = 0;
}
float left;
if (style != null) {
left = x;
} else if (dir > 0) {
left = x + (width * dir);
} else {
left = x + (width * dir) + width;
}
if (drawIcon) {
left -= getIconSize() + ICON_SPACING;
c.save();
c.translate(left, baseline - iconOffset);
mDrawableWrapper.getDrawable().draw(c);
c.restore();
float numberOffset = (getIconSize() - numberWidth) / 2f;
left += numberOffset;
} else if (style != null && style.shape() != null && style.shape().type() == Shape.SHAPE_RECT) {
float fontTop = baseline - iconOffset;
float rectTop = fontTop + (getIconSize() - style.shape().size()) / 2f;
int oldColor = paint.getColor();
Paint.Style oldStyle = paint.getStyle();
paint.setStyle(Paint.Style.FILL);
paint.setColor(style.shape().color());
left += style.leading();
c.drawRoundRect(left, rectTop, left + style.shape().size(), rectTop + style.shape().size(), style.shape().radius(), style.shape().radius(), paint);
paint.setStyle(oldStyle);
paint.setColor(oldColor);
float numberOffset = (style.shape().size() - numberWidth) / 2f;
if (isNumber) {
numberOffset -= 1;
}
left += numberOffset;
} else if (style == null) {
left -= numberWidth;
} else if (style.isBold()) {
left += style.leading();
paint.setFakeBoldText(true);
}
int oldColor = paint.getColor();
if (style != null && style.orderFontColor() != 0) {
paint.setColor(style.orderFontColor());
}
// @since 1.1.1 we are using `baseline` argument to position text
c.drawText(drawText, left, baseline - baselineOffset, paint);
paint.setColor(oldColor);
paint.setTextSize(oldSize);
paint.setFakeBoldText(isBold);
}
@Override
protected String getImageUrl() {
if (style != null && style.shape() != null && style.shape().type() == Shape.SHAPE_ICON) {
return style.shape().icon();
}
return "";
}
@Override
protected void applyStyle(TextPaint paint) {
if (style == null || style.orderFontSize() == 0) {
this.paint.setTextSize(paint.getTextSize());
} else {
this.paint.setTextSize(style.orderFontSize());
}
if (style == null || style.orderFontColor() == 0) {
this.paint.setColor(paint.getColor());
} else {
this.paint.setColor(style.orderFontColor());
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/StrongEmphasisSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class StrongEmphasisSpan extends MetricAffectingSpan {
@Override
public void updateMeasureState(TextPaint p) {
p.setFakeBoldText(true);
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setFakeBoldText(true);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/TextLayoutSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
/**
* @since 4.4.0
*/
public class TextLayoutSpan {
/**
* @see #applyTo(Spannable, Layout)
*/
@Nullable
public static Layout layoutOf(@NonNull CharSequence cs) {
if (cs instanceof Spanned) {
return layoutOf((Spanned) cs);
}
return null;
}
@Nullable
public static Layout layoutOf(@NonNull Spanned spanned) {
final TextLayoutSpan[] spans = spanned.getSpans(
0,
spanned.length(),
TextLayoutSpan.class
);
return spans != null && spans.length > 0
? spans[0].layout()
: null;
}
public static void applyTo(@NonNull Spannable spannable, @NonNull Layout layout) {
// remove all current ones (only one should be present)
final TextLayoutSpan[] spans = spannable.getSpans(0, spannable.length(), TextLayoutSpan.class);
if (spans != null) {
for (TextLayoutSpan span : spans) {
spannable.removeSpan(span);
}
}
final TextLayoutSpan span = new TextLayoutSpan(layout);
spannable.setSpan(
span,
0,
spannable.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE
);
}
private final WeakReference reference;
@SuppressWarnings("WeakerAccess")
TextLayoutSpan(@NonNull Layout layout) {
this.reference = new WeakReference<>(layout);
}
@Nullable
public Layout layout() {
return reference.get();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/TextViewSpan.java
================================================
package io.noties.markwon.core.spans;
import android.text.Spannable;
import android.text.Spanned;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
/**
* A special span that allows to obtain {@code TextView} in which spans are displayed
*
* @since 4.4.0
*/
public class TextViewSpan {
@Nullable
public static TextView textViewOf(@NonNull CharSequence cs) {
if (cs instanceof Spanned) {
return textViewOf((Spanned) cs);
}
return null;
}
@Nullable
public static TextView textViewOf(@NonNull Spanned spanned) {
final TextViewSpan[] spans = spanned.getSpans(0, spanned.length(), TextViewSpan.class);
return spans != null && spans.length > 0
? spans[0].textView()
: null;
}
public static void applyTo(@NonNull Spannable spannable, @NonNull TextView textView) {
final TextViewSpan[] spans = spannable.getSpans(0, spannable.length(), TextViewSpan.class);
if (spans != null) {
for (TextViewSpan span : spans) {
spannable.removeSpan(span);
}
}
final TextViewSpan span = new TextViewSpan(textView);
// `SPAN_INCLUSIVE_INCLUSIVE` to persist in case of possible text change (deletion, etc)
spannable.setSpan(
span,
0,
spannable.length(),
Spanned.SPAN_INCLUSIVE_INCLUSIVE
);
}
private final WeakReference reference;
public TextViewSpan(@NonNull TextView textView) {
this.reference = new WeakReference<>(textView);
}
@Nullable
public TextView textView() {
return reference.get();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/core/spans/ThematicBreakSpan.java
================================================
package io.noties.markwon.core.spans;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineHeightSpan;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.fluid.afm.styles.HorizonRuleStyle;
import io.noties.markwon.core.MarkwonTheme;
public class ThematicBreakSpan implements LeadingMarginSpan, LineHeightSpan {
private final MarkwonTheme theme;
private final Rect rect = ObjectsPool.rect();
private final Paint paint = ObjectsPool.paint();
private HorizonRuleStyle mStyle;
private float textViewMultiplier = 1.0f;
public ThematicBreakSpan(@NonNull MarkwonTheme theme) {
this.theme = theme;
mStyle = theme.getHorizonRule();
}
@Override
public int getLeadingMargin(boolean first) {
return 0;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
int lineTop;
if (mStyle.paragraph().paragraphSpacing() <= 0 && mStyle.paragraphSpacingBefore() <= 0) {
lineTop = top + ((bottom - top) / 2) - mStyle.getHeight() / 2;
} else if (mStyle.paragraphSpacingBefore() > 0) {
lineTop = (int) (top + mStyle.paragraphSpacingBefore() / textViewMultiplier);
} else {
lineTop = bottom - mStyle.paragraph().paragraphSpacing();
}
paint.set(p);
if (mStyle.getColor() == 0 || mStyle.getHeight() < 0) {
theme.applyThematicBreakStyle(paint);
} else {
mStyle.apply(paint);
}
final int height = (int) (paint.getStrokeWidth() + .5F);
final int left;
final int right;
if (dir > 0) {
left = x;
right = c.getWidth();
} else {
left = x - c.getWidth();
right = x;
}
rect.set(left, lineTop, right, lineTop + height);
c.drawRect(rect, paint);
}
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lh, Paint.FontMetricsInt fm) {
if (mStyle != null && (mStyle.paragraph().paragraphSpacing() > 0 || mStyle.paragraphSpacingBefore() > 0 || mStyle.getHeight() > 3) && selfStart(start, text, this)) {
int lineHeight = (fm.descent - fm.ascent);
int targetHeight = mStyle.paragraph().paragraphSpacing() + mStyle.paragraphSpacingBefore() + mStyle.getHeight();
if (text instanceof Spanned) {
final Spanned spanned = (Spanned) text;
final TextView textView = TextViewSpan.textViewOf(spanned);
if (textView != null) {
float multiplier = textView.getLineSpacingMultiplier();
if (multiplier > 0) {
targetHeight = (int) (targetHeight / multiplier);
textViewMultiplier = multiplier;
}
}
}
final float ratio = targetHeight * 1.0f / lineHeight;
fm.descent = Math.round(fm.descent * ratio);
fm.ascent = fm.descent - targetHeight;
}
}
private static boolean selfStart(int start, CharSequence text, Object span) {
final int spanStart = ((Spanned) text).getSpanStart(span);
return spanStart == start;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawable.java
================================================
package io.noties.markwon.image;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class AsyncDrawable extends Drawable {
public static final String TAG = "MD_AsyncDrawable";
private final String destination;
private final AsyncDrawableLoader loader;
private final ImageSize imageSize;
private final ImageSizeResolver imageSizeResolver;
// @since 4.5.0
private final Drawable placeholder;
private Drawable result;
private Callback callback;
public static int lastCanvasWidth;
private int canvasWidth;
private float textSize;
// @since 2.0.1 for use-cases when image is loaded faster than span is drawn and knows canvas width
private boolean waitingForDimensions;
// @since 4.5.0 in case if result is Animatable and this drawable was detached, we
// keep the state to resume when we are going to be attached again (when used in RecyclerView)
private boolean wasPlayingBefore = false;
/**
* @since 1.0.1
*/
public AsyncDrawable(
@NonNull String destination,
@NonNull AsyncDrawableLoader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize
) {
this.destination = destination;
this.loader = loader;
this.imageSizeResolver = imageSizeResolver;
this.imageSize = imageSize;
Log.d(TAG,"AsyncDrawable destination = " + destination);
final Drawable placeholder = this.placeholder = loader.placeholder(this);
if (placeholder != null) {
Log.d(TAG,"setPlaceholderResult");
setPlaceholderResult(placeholder);
}
}
@NonNull
public String getDestination() {
return destination;
}
public Drawable getPlaceholder(){
return placeholder;
}
/**
* @since 4.0.0
*/
@SuppressWarnings("WeakerAccess")
@Nullable
public ImageSize getImageSize() {
return imageSize;
}
/**
* @since 4.0.0
*/
@SuppressWarnings("unused")
@NonNull
public ImageSizeResolver getImageSizeResolver() {
return imageSizeResolver;
}
/**
* @since 4.2.1
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public boolean hasKnownDimensions() {
return canvasWidth > 0;
}
/**
* @see #hasKnownDimensions()
* @since 4.0.0
*/
public int getLastKnownCanvasWidth() {
return canvasWidth;
}
/**
* @see #hasKnownDimensions()
* @since 4.0.0
*/
@SuppressWarnings("WeakerAccess")
public float getLastKnowTextSize() {
return textSize;
}
public Drawable getResult() {
return result;
}
public boolean hasResult() {
return result != null;
}
public boolean isAttached() {
return getCallback() != null;
}
public void setCallback2(@Nullable Callback cb) {
Log.d(TAG,"setCallback2 cb = " + cb);
if(cb == null){
return;
}
// @since 4.2.1
// wrap callback so invalidation happens to this AsyncDrawable instance
// and not for wrapped result/placeholder
this.callback = cb == null
? null
: new WrappedCallback(cb);
super.setCallback(cb);
// if not null -> means we are attached
if (callback != null) {
// as we have a placeholder now, it's important to check it our placeholder
// has a proper callback at this point. This is not required in most cases,
// as placeholder should be static, but if it's not -> it can operate as usual
if (result != null
&& result.getCallback() == null) {
result.setCallback(callback);
}
// @since 4.5.0 we trigger loading only if we have no result (and result is not placeholder)
final boolean shouldLoad = result == null || result == placeholder || TextUtils.isEmpty(destination);
Log.d(TAG,"shouldLoad = " + shouldLoad);
if (result != null) {
result.setCallback(callback);
// @since 4.5.0
if (result instanceof Animatable && wasPlayingBefore) {
((Animatable) result).start();
}
}
if (shouldLoad) {
Log.d(TAG,"load here");
loader.load(this);
}else {
Log.d(TAG,"has loaded");
}
} else {
Log.d(TAG,"loader.cancel(this);");
return;
// if (result != null) {
//
// result.setCallback(null);
//
// // let's additionally stop if it Animatable
// if (result instanceof Animatable) {
// final Animatable animatable = (Animatable) result;
// final boolean isPlaying = wasPlayingBefore = animatable.isRunning();
// if (isPlaying) {
// animatable.stop();
// }
// }
// }
// loader.cancel(this);
}
}
/**
* @since 3.0.1
*/
@SuppressWarnings("WeakerAccess")
protected void setPlaceholderResult(@NonNull Drawable placeholder) {
Log.d(TAG,"setPlaceholderResult waitingForDimensions = " + waitingForDimensions);
// okay, if placeholder has bounds -> use it, otherwise use original imageSize
// it's important to NOT pass to imageSizeResolver when placeholder has bounds
// this is done, so actual result and placeholder can have _different_
// bounds. Assume image is loaded with HTML and has ImageSize width=100%,
// so, even if placeholder has exact bounds, it will still be scaled up.
// this condition should not be true for placeholder (at least for now)
// (right now this method is always called from constructor)
if (result != null) {
// but it is, unregister current result
result.setCallback(null);
}
final Rect rect = placeholder.getBounds();
if (rect.isEmpty()) {
// check for intrinsic bounds
final Rect intrinsic = DrawableUtils.intrinsicBounds(placeholder);
if (intrinsic.isEmpty()) {
// @since 4.2.2
// if intrinsic bounds are empty, use _any_ non-empty bounds,
// they must be non-empty so when result is obtained - proper invalidation will occur
// (0, 0, 1, 0) is still considered empty
placeholder.setBounds(0, 0, 1, 1);
} else {
// use them
placeholder.setBounds(intrinsic);
}
// it is very important (if we have a placeholder) to set own bounds to it (and they must not be empty
// otherwise result won't be rendered)
// @since 4.2.2
setBounds(placeholder.getBounds());
setResult(placeholder);
} else {
Log.d(TAG,"set result as placeholder");
// this method is not the same as above, as we do not want to trigger image-size-resolver
// in case when placeholder has exact bounds
// placeholder has bounds specified -> use them until we have real result
this.result = placeholder;
this.result.setCallback(callback);
// use bounds directly
setBounds(rect);
// just in case -> so we do not update placeholder when we have canvas dimensions
waitingForDimensions = false;
}
}
public void setResult(@NonNull Drawable result) {
Log.d(TAG,"set setResult");
// @since 4.5.0 revert this flag when we have new source
wasPlayingBefore = false;
// if we have previous one, detach it
if (this.result != null) {
this.result.setCallback(null);
}
this.result = result;
// this.result.setCallback(callback);
initBounds();
}
/**
* Remove result from this drawable (for example, in case of cancellation)
*
* @since 3.0.1
*/
public void clearResult() {
final Drawable result = this.result;
if (result != null) {
result.setCallback(null);
this.result = null;
// clear bounds
setBounds(0, 0, 0, 0);
}
}
private void initBounds() {
Log.d(TAG,"initBounds canvasWidth = " + canvasWidth);
if (canvasWidth == 0 && lastCanvasWidth == 0) {
Log.d(TAG,"initBounds canvasWidth 0");
// we still have no bounds - wait for them
waitingForDimensions = true;
// we cannot have empty bounds - otherwise in case if text contains
// a single AsyncDrawableSpan, it won't be displayed
setBounds(noDimensionsBounds(result));
return;
}
if(canvasWidth == 0 && lastCanvasWidth != 0){
canvasWidth = lastCanvasWidth;
}
waitingForDimensions = false;
final Rect bounds = resolveBounds();
Log.d(TAG,"initBounds bounds = " + bounds);
result.setBounds(bounds);
// @since 4.2.1, we set callback after bounds are resolved
// to reduce number of invalidations
result.setCallback(callback);
// so, this method will check if there is previous bounds and call invalidate _BEFORE_
// applying new bounds. This is why it is important to have initial bounds empty.
setBounds(bounds);
invalidateSelf();
lastCanvasWidth = canvasWidth;
}
/**
* @since 4.3.0
*/
@NonNull
private static Rect noDimensionsBounds(@Nullable Drawable result) {
Log.d(TAG,"noDimensionsBounds");
if (result != null) {
Log.d(TAG,"result exist");
final Rect bounds = result.getBounds();
if (!bounds.isEmpty()) {
Log.d(TAG,"bounds exist ");
return bounds;
}
final Rect intrinsicBounds = DrawableUtils.intrinsicBounds(result);
if (!intrinsicBounds.isEmpty()) {
Log.d(TAG,"intrinsicBounds exist");
return intrinsicBounds;
}
}
Log.d(TAG,"return 0 0 1 1");
return new Rect(0, 0, 1, 1);
}
/**
* @since 1.0.1
*/
@SuppressWarnings("WeakerAccess")
public void initWithKnownDimensions(int width, float textSize) {
Log.d(TAG,"initWithKnownDimensions");
this.canvasWidth = width;
this.textSize = textSize;
if (waitingForDimensions) {
initBounds();
}
}
@Override
public void draw(@NonNull Canvas canvas) {
if (hasResult()) {
result.draw(canvas);
}
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
final int opacity;
if (hasResult()) {
opacity = result.getOpacity();
} else {
opacity = PixelFormat.TRANSPARENT;
}
return opacity;
}
@Override
public int getIntrinsicWidth() {
final int out;
if (hasResult()) {
out = result.getIntrinsicWidth();
} else {
// @since 4.0.0, must not be zero in order to receive canvas dimensions
out = 1;
}
return out;
}
@Override
public int getIntrinsicHeight() {
final int out;
if (hasResult()) {
out = result.getIntrinsicHeight();
} else {
// @since 4.0.0, must not be zero in order to receive canvas dimensions
out = 1;
}
return out;
}
/**
* @since 1.0.1
*/
@NonNull
private Rect resolveBounds() {
Log.d(TAG,"resolveBounds ");
// @since 2.0.0 previously we were checking if image is greater than canvas width here
// but as imageSizeResolver won't be null anymore, we should transfer this logic
// there
return imageSizeResolver.resolveImageSize(this);
}
@NonNull
@Override
public String toString() {
return "AsyncDrawable{" +
"destination='" + destination + '\'' +
", imageSize=" + imageSize +
", result=" + result +
", canvasWidth=" + canvasWidth +
", textSize=" + textSize +
", waitingForDimensions=" + waitingForDimensions +
'}';
}
// @since 4.2.1
// Wrapped callback to trigger invalidation for this AsyncDrawable instance (and not result/placeholder)
private class WrappedCallback implements Callback {
private final Callback callback;
WrappedCallback(@NonNull Callback callback) {
this.callback = callback;
}
@Override
public void invalidateDrawable(@NonNull Drawable who) {
callback.invalidateDrawable(AsyncDrawable.this);
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
callback.scheduleDrawable(AsyncDrawable.this, what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
callback.unscheduleDrawable(AsyncDrawable.this, what);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableLoader.java
================================================
package io.noties.markwon.image;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public abstract class AsyncDrawableLoader {
/**
* @since 3.0.0
*/
@NonNull
public static AsyncDrawableLoader noOp() {
return new AsyncDrawableLoaderNoOp();
}
/**
* @since 4.0.0
*/
public abstract void load(@NonNull AsyncDrawable drawable);
/**
* @since 4.0.0
*/
public abstract void cancel(@NonNull AsyncDrawable drawable);
@Nullable
public abstract Drawable placeholder(@NonNull AsyncDrawable drawable);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableLoaderNoOp.java
================================================
package io.noties.markwon.image;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
class AsyncDrawableLoaderNoOp extends AsyncDrawableLoader {
@Override
public void load(@NonNull AsyncDrawable drawable) {
}
@Override
public void cancel(@NonNull AsyncDrawable drawable) {
}
@Nullable
@Override
public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return null;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java
================================================
package io.noties.markwon.image;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Looper;
import android.os.SystemClock;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.R;
public abstract class AsyncDrawableScheduler {
public static void schedule(@NonNull final TextView textView) {
// we need a simple check if current text has already scheduled drawables
// we need this in order to allow multiple calls to schedule (different plugins
// might use AsyncDrawable), but we do not want to repeat the task
//
// hm... we need the same thing for unschedule then... we can check if last hash is !null,
// if it's not -> unschedule, else ignore
// @since 4.0.0
final Integer lastTextHashCode =
(Integer) textView.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode);
final int textHashCode = textView.getText().hashCode();
if (lastTextHashCode != null
&& lastTextHashCode == textHashCode) {
return;
}
textView.setTag(R.id.markwon_drawables_scheduler_last_text_hashcode, textHashCode);
final AsyncDrawableSpan[] spans = extractSpans(textView);
if (spans != null
&& spans.length > 0) {
if (textView.getTag(R.id.markwon_drawables_scheduler) == null) {
final View.OnAttachStateChangeListener listener = new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
}
@Override
public void onViewDetachedFromWindow(View v) {
unschedule(textView);
v.removeOnAttachStateChangeListener(this);
v.setTag(R.id.markwon_drawables_scheduler, null);
}
};
textView.addOnAttachStateChangeListener(listener);
textView.setTag(R.id.markwon_drawables_scheduler, listener);
}
// @since 4.1.0
final DrawableCallbackImpl.Invalidator invalidator = new TextViewInvalidator(textView);
AsyncDrawable drawable;
for (AsyncDrawableSpan span : spans) {
drawable = span.getDrawable();
drawable.setCallback2(new DrawableCallbackImpl(textView, invalidator, drawable.getBounds()));
}
}
}
// must be called when text manually changed in TextView
public static void unschedule(@NonNull TextView view) {
// @since 4.0.0
if (view.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode) == null) {
return;
}
view.setTag(R.id.markwon_drawables_scheduler_last_text_hashcode, null);
final AsyncDrawableSpan[] spans = extractSpans(view);
if (spans != null
&& spans.length > 0) {
for (AsyncDrawableSpan span : spans) {
span.getDrawable().setCallback2(null);
}
}
}
@Nullable
private static AsyncDrawableSpan[] extractSpans(@NonNull TextView textView) {
final CharSequence cs = textView.getText();
final int length = cs != null
? cs.length()
: 0;
if (length == 0
|| !(cs instanceof Spanned)) {
return null;
}
// we also could've tried the `nextSpanTransition`, but strangely it leads to worse performance
// than direct getSpans
return ((Spanned) cs).getSpans(0, length, AsyncDrawableSpan.class);
}
private AsyncDrawableScheduler() {
}
private static class DrawableCallbackImpl implements Drawable.Callback {
// @since 4.1.0
// interface to be used when bounds change and view must be invalidated
interface Invalidator {
void invalidate();
}
private final TextView view;
private final Invalidator invalidator; // @since 4.1.0
private Rect previousBounds;
DrawableCallbackImpl(
@NonNull TextView view,
@NonNull Invalidator invalidator,
Rect initialBounds) {
this.view = view;
this.invalidator = invalidator;
this.previousBounds = new Rect(initialBounds);
}
@Override
public void invalidateDrawable(@NonNull final Drawable who) {
if (Looper.myLooper() != Looper.getMainLooper()) {
view.post(new Runnable() {
@Override
public void run() {
invalidateDrawable(who);
}
});
return;
}
final Rect rect = who.getBounds();
// okay... the thing is IF we do not change bounds size, normal invalidate would do
// but if the size has changed, then we need to update the whole layout...
if (!previousBounds.equals(rect)) {
// @since 4.1.0
// invalidation moved to upper level (so invalidation can be deferred,
// and multiple calls combined)
invalidator.invalidate();
previousBounds = new Rect(rect);
} else {
view.postInvalidate();
}
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
final long delay = when - SystemClock.uptimeMillis();
view.postDelayed(what, delay);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
view.removeCallbacks(what);
}
}
private static class TextViewInvalidator implements DrawableCallbackImpl.Invalidator, Runnable {
private final TextView textView;
TextViewInvalidator(@NonNull TextView textView) {
this.textView = textView;
}
@Override
public void invalidate() {
textView.removeCallbacks(this);
textView.post(this);
}
@Override
public void run() {
textView.setText(textView.getText());
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableSpan.java
================================================
package io.noties.markwon.image;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.text.Spanned;
import android.text.style.LineHeightSpan;
import android.text.style.ReplacementSpan;
import android.util.Log;
import android.widget.TextView;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import io.noties.markwon.core.MarkwonTheme;
import com.fluid.afm.func.IImageClickCallback;
import com.fluid.afm.span.IClickableSpan;
import io.noties.markwon.core.spans.TextViewSpan;
import io.noties.markwon.utils.SpanUtils;
@SuppressWarnings("WeakerAccess")
public class AsyncDrawableSpan extends ReplacementSpan implements LineHeightSpan, IClickableSpan {
@Override
public String getLiteral() {
return IClickableSpan.super.getLiteral();
}
@IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TEXT_BOTTOM})
@Retention(RetentionPolicy.SOURCE)
@interface Alignment {
}
public static final String TAG = "AsyncDrawableSpan";
public static final int ALIGN_BOTTOM = 0;
public static final int ALIGN_BASELINE = 1;
public static final int ALIGN_CENTER = 2; // will only center if drawable height is less than text line height
public static final int ALIGN_TEXT_BOTTOM = 3;
private final MarkwonTheme theme;
private final AsyncDrawable drawable;
private final int alignment;
private final boolean replacementTextIsLink;
private int mTop;
private int mBottom;
private boolean clickable = true;
private IImageClickCallback mClickCallback;
private String imageDescription;
public AsyncDrawableSpan(
@NonNull MarkwonTheme theme,
@NonNull AsyncDrawable drawable,
@Alignment int alignment,
boolean replacementTextIsLink,
IImageClickCallback clickCallback) {
this.theme = theme;
this.drawable = drawable;
this.alignment = alignment;
this.replacementTextIsLink = replacementTextIsLink;
mClickCallback = clickCallback;
Log.d(TAG, "this.replacementTextIsLink = " + this.replacementTextIsLink);
// @since 4.2.1 we do not set intrinsic bounds
// at this point they will always be 0,0-1,1, but this
// will trigger another invalidation when we will have bounds
}
public AsyncDrawableSpan(
@NonNull MarkwonTheme theme,
@NonNull AsyncDrawable drawable,
@Alignment int alignment,
boolean replacementTextIsLink, boolean clickable) {
this.theme = theme;
this.drawable = drawable;
this.alignment = alignment;
this.replacementTextIsLink = replacementTextIsLink;
this.clickable = clickable;
Log.d(TAG, "this.replacementTextIsLink = " + this.replacementTextIsLink);
// @since 4.2.1 we do not set intrinsic bounds
// at this point they will always be 0,0-1,1, but this
// will trigger another invalidation when we will have bounds
}
@Override
public String getUrl() {
return clickable && drawable != null ? drawable.getDestination() : "";
}
@Override
public String getType() {
return clickable ? "image" : "";
}
@Override
public int getTop() {
return mTop;
}
@Override
public int getBottom() {
return mBottom;
}
public void click() {
if (clickable && mClickCallback != null) {
mClickCallback.imageClicked(drawable.getDestination(), imageDescription);
}
}
public boolean isClickable() {
return clickable;
}
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
// if we have no async drawable result - we will just render text
final int size;
if (drawable.hasResult()) {
final Rect rect = drawable.getBounds();
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
size = rect.right;
} else {
// we will apply style here in case if theme modifies textSize or style (affects metrics)
if (replacementTextIsLink) {
theme.applyLinkStyle(paint);
}
// NB, no specific text handling (no new lines, etc)
size = (int) (paint.measureText(text, start, end) + .5F);
}
return size;
}
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int lineHeight, Paint.FontMetricsInt fm) {
if (fm == null) {
return;
}
if (!drawable.hasResult()) {
return;
}
if (!SpanUtils.isSelf(start, end, text, this)) {
return;
}
int drawableLineHeight = drawable.getBounds().height() + theme.getParagraphBreakHeight();
if (text instanceof Spanned) {
final Spanned spanned = (Spanned) text;
final TextView textView = TextViewSpan.textViewOf(spanned);
if (textView != null) {
float multiplier = textView.getLineSpacingMultiplier();
if (multiplier > 0) {
drawableLineHeight = (int) (drawableLineHeight / multiplier);
}
}
}
final int originHeight = fm.descent - fm.ascent;
if (originHeight <= 0) {
return;
}
final float ratio = drawableLineHeight * 1.0f / originHeight;
fm.descent = Math.round(fm.descent * ratio);
fm.ascent = fm.descent - drawableLineHeight;
}
@Override
public void draw(
@NonNull Canvas canvas,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
float x,
int top,
int y,
int bottom,
@NonNull Paint paint) {
// @since 4.4.0 use SpanUtils instead of `canvas.getWidth`
drawable.initWithKnownDimensions(
SpanUtils.width(canvas, text),
paint.getTextSize()
);
final AsyncDrawable drawable = this.drawable;
if (drawable.hasResult()) {
final int b = bottom - drawable.getBounds().bottom;
final int save = canvas.save();
try {
int translationY;
if (ALIGN_CENTER == alignment) {
translationY = (top - paint.getFontMetricsInt().bottom * 2) + ((paint.getFontMetricsInt().bottom * 4 + bottom - top - drawable.getBounds().height()) / 2);
} else if (ALIGN_BASELINE == alignment) {
translationY = b - paint.getFontMetricsInt().bottom;
} else if (ALIGN_TEXT_BOTTOM == alignment) {
final int textHeight = paint.getFontMetricsInt().bottom - paint.getFontMetricsInt().top;
final int lineHeight = bottom - top;
final int paddingBottom = lineHeight - textHeight;
final int calibrate = 5;
translationY = b - paddingBottom + calibrate;
} else {
translationY = b;
}
canvas.translate(x, translationY);
drawable.draw(canvas);
mTop = translationY;
mBottom = translationY + drawable.getBounds().height();
} finally {
canvas.restoreToCount(save);
}
} else {
// will it make sense to have additional background/borders for an image replacement?
// let's focus on main functionality and then think of it
final float textY = textCenterY(top, bottom, paint);
if (replacementTextIsLink) {
theme.applyLinkStyle(paint);
}
// NB, no specific text handling (no new lines, etc)
canvas.drawText(text, start, end, x, textY, paint);
}
imageDescription = text.subSequence(start, end).toString();
}
@NonNull
public AsyncDrawable getDrawable() {
return drawable;
}
private static float textCenterY(int top, int bottom, @NonNull Paint paint) {
// @since 1.1.1 it's `top +` and not `bottom -`
return (int) (top + ((bottom - top) / 2) - ((paint.descent() + paint.ascent()) / 2.F + .5F));
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/DrawableUtils.java
================================================
package io.noties.markwon.image;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
/**
* @since 3.0.1
*/
public abstract class DrawableUtils {
@NonNull
@CheckResult
public static Rect intrinsicBounds(@NonNull Drawable drawable) {
return new Rect(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
public static void applyIntrinsicBounds(@NonNull Drawable drawable) {
drawable.setBounds(intrinsicBounds(drawable));
}
public static void applyIntrinsicBoundsIfEmpty(@NonNull Drawable drawable) {
if (drawable.getBounds().isEmpty()) {
drawable.setBounds(intrinsicBounds(drawable));
}
}
private DrawableUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/ImageProps.java
================================================
package io.noties.markwon.image;
import io.noties.markwon.Prop;
/**
* @since 3.0.0
*/
public abstract class ImageProps {
public static final Prop DESTINATION = Prop.of("image-destination");
public static final Prop REPLACEMENT_TEXT_IS_LINK =
Prop.of("image-replacement-text-is-link");
public static final Prop IMAGE_SIZE = Prop.of("image-size");
private ImageProps() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/ImageSize.java
================================================
package io.noties.markwon.image;
import androidx.annotation.Nullable;
/**
* @since 1.0.1
*/
@SuppressWarnings("WeakerAccess")
public class ImageSize {
public static class Dimension {
public final float value;
public final String unit;
public Dimension(float value, @Nullable String unit) {
this.value = value;
this.unit = unit;
}
@Override
public String toString() {
return "Dimension{" +
"value=" + value +
", unit='" + unit + '\'' +
'}';
}
}
public final Dimension width;
public final Dimension height;
public ImageSize(@Nullable Dimension width, @Nullable Dimension height) {
this.width = width;
this.height = height;
}
@Override
public String toString() {
return "ImageSize{" +
"width=" + width +
", height=" + height +
'}';
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/ImageSizeResolver.java
================================================
package io.noties.markwon.image;
import android.graphics.Rect;
import androidx.annotation.NonNull;
/**
* @see ImageSizeResolverDef
* @see io.noties.markwon.MarkwonConfiguration.Builder#imageSizeResolver(ImageSizeResolver)
* @since 1.0.1
*/
public abstract class ImageSizeResolver {
/**
* @since 4.0.0
*/
@NonNull
public abstract Rect resolveImageSize(@NonNull AsyncDrawable drawable);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/ImageSizeResolverDef.java
================================================
package io.noties.markwon.image;
import android.content.Context;
import android.graphics.Rect;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* @since 1.0.1
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class ImageSizeResolverDef extends ImageSizeResolver {
public static final String TAG = "MD_ImageSizeResolverDef";
// we track these two, others are considered to be pixels
protected static final String UNIT_PERCENT = "%";
protected static final String UNIT_EM = "em";
public static final String UNIT_RPX = "rpx";
public static final String UNIT_PX = "px";
@NonNull
@Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
Log.d(TAG,"resolveImageSize ");
return resolveImageSize(drawable,
drawable.getImageSize(),
drawable.getResult().getBounds(),
drawable.getLastKnownCanvasWidth(),
drawable.getLastKnowTextSize());
}
@NonNull
protected Rect resolveImageSize(
@NonNull AsyncDrawable drawable,
@Nullable ImageSize imageSize,
@NonNull Rect imageBounds,
int canvasWidth,
float textSize
) {
if (imageSize == null) {
// @since 2.0.0 post process bounds to fit canvasWidth (previously was inside AsyncDrawable)
// must be applied only if imageSize is null
final Rect rect;
int w = imageBounds.width();
int h = imageBounds.height();
int designedWidth = 638;
int designedHeight = 360;
float dpRatio = ((float)canvasWidth * 2)/designedWidth;
if((h < w) && (canvasWidth > 0)) {
float widthRatio = ((float) canvasWidth)/w;
w = canvasWidth;
h = (int)(widthRatio*h);
int standardHeight = (int) ((designedHeight/2)*dpRatio);
if(h>standardHeight){
h = standardHeight;
}
}else if((w <= h) && (canvasWidth > 0)){
h = (int) ((designedHeight / 2 ) * dpRatio);
w = (int) ((((float)h) /(imageBounds.height())) * w);
}
imageBounds = new Rect(0,0,w,h);
if (w > canvasWidth) {
final float reduceRatio = (float) w / canvasWidth;
rect = new Rect(
0,
0,
canvasWidth,
(int) (imageBounds.height() / reduceRatio + .5F)
);
} else {
rect = imageBounds;
}
return rect;
}
final Rect rect;
final ImageSize.Dimension width = imageSize.width;
final ImageSize.Dimension height = imageSize.height;
final int imageWidth = imageBounds.width();
final int imageHeight = imageBounds.height();
final float ratio = (float) imageWidth / imageHeight;
if (width != null) {
final int w;
final int h;
if (UNIT_PERCENT.equals(width.unit)) {
w = (int) (canvasWidth * (width.value / 100.F) + .5F);
} else {
w = resolveAbsolute(width, imageWidth, textSize);
}
if (height == null
|| UNIT_PERCENT.equals(height.unit)) {
h = (int) (w / ratio + .5F);
} else {
h = resolveAbsolute(height, imageHeight, textSize);
}
rect = new Rect(0, 0, w, h);
} else if (height != null) {
if (!UNIT_PERCENT.equals(height.unit)) {
final int h = resolveAbsolute(height, imageHeight, textSize);
final int w = (int) (h * ratio + .5F);
rect = new Rect(0, 0, w, h);
} else {
rect = imageBounds;
}
} else {
rect = imageBounds;
}
return rect;
}
private int dpToPx(Context context, float dp){
if (context != null) {
float density = context.getResources().getDisplayMetrics().density;
return Math.round(dp * density);
}
return Math.round(dp * 2);
}
protected int resolveAbsolute(@NonNull ImageSize.Dimension dimension, int original, float textSize) {
final int out;
if (UNIT_EM.equals(dimension.unit)) {
out = (int) (dimension.value * textSize + .5F);
} else {
out = (int) (dimension.value + .5F);
}
return out;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/ImageSpanFactory.java
================================================
package io.noties.markwon.image;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.fluid.afm.func.IImageClickCallback;
import java.util.concurrent.ConcurrentHashMap;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
public class ImageSpanFactory implements SpanFactory {
public static final String TAG = "MD_ImageSpanFactory";
public ConcurrentHashMap cacheAsyncDrawableSpan = new ConcurrentHashMap<>();
private boolean mIsStreamOutput;
private IImageClickCallback mClickCallback ;
public ImageSpanFactory(){
}
@Nullable
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
String destination = ImageProps.DESTINATION.require(props);
ConcurrentHashMap cache = cacheAsyncDrawableSpan;
AsyncDrawableSpan cacheSpan = cache.get(destination);
Log.d(TAG," destination = " + destination + " cacheSpan = " + cacheSpan + "isStreamOutput = " + mIsStreamOutput);
if(isIsStreamOutput() && cacheSpan != null){
Log.d(TAG,"ready to set cacheSpan");
return cacheSpan;
}else {
Log.d(TAG,"set new AsyncDrawableSpan");
AsyncDrawableSpan asyncDrawableSpan = new AsyncDrawableSpan(
configuration.theme(),
new AsyncDrawable(
ImageProps.DESTINATION.require(props),
configuration.asyncDrawableLoader(),
configuration.imageSizeResolver(),
ImageProps.IMAGE_SIZE.get(props)
),
AsyncDrawableSpan.ALIGN_CENTER,
ImageProps.REPLACEMENT_TEXT_IS_LINK.get(props, false),
mClickCallback
);
cache.put(destination,asyncDrawableSpan);
return asyncDrawableSpan;
}
}
public void setImageCallback(IImageClickCallback clickCallback) {
mClickCallback = clickCallback;
}
private boolean isIsStreamOutput() {
return mIsStreamOutput;
}
public void onStreamOutStateChanged(boolean isStreamingOutput) {
mIsStreamOutput = isStreamingOutput;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessor.java
================================================
package io.noties.markwon.image.destination;
import androidx.annotation.NonNull;
/**
* Process destination of image nodes
*
* @since 4.4.0
*/
public abstract class ImageDestinationProcessor {
@NonNull
public abstract String process(@NonNull String destination);
@NonNull
public static ImageDestinationProcessor noOp() {
return new NoOp();
}
private static class NoOp extends ImageDestinationProcessor {
@NonNull
@Override
public String process(@NonNull String destination) {
return destination;
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorAssets.java
================================================
package io.noties.markwon.image.destination;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* {@link ImageDestinationProcessor} that treats all destinations without scheme
* information as pointing to the {@code assets} folder of an application. Please note that this
* processor only adds required {@code file:///android_asset/} prefix to destinations and
* actual image loading must take that into account (implement this functionality).
*
* {@code FileSchemeHandler} from the {@code image} module supports asset images when created with
* {@code createWithAssets} factory method
*
* @since 4.4.0
*/
public class ImageDestinationProcessorAssets extends ImageDestinationProcessor {
@NonNull
public static ImageDestinationProcessorAssets create(@Nullable ImageDestinationProcessor parent) {
return new ImageDestinationProcessorAssets(parent);
}
static final String MOCK = "https://android.asset/";
static final String BASE = "file:///android_asset/";
private final ImageDestinationProcessorRelativeToAbsolute assetsProcessor
= new ImageDestinationProcessorRelativeToAbsolute(MOCK);
private final ImageDestinationProcessor processor;
public ImageDestinationProcessorAssets() {
this(null);
}
public ImageDestinationProcessorAssets(@Nullable ImageDestinationProcessor parent) {
this.processor = parent;
}
@NonNull
@Override
public String process(@NonNull String destination) {
final String out;
final Uri uri = Uri.parse(destination);
if (TextUtils.isEmpty(uri.getScheme())) {
out = assetsProcessor.process(destination).replace(MOCK, BASE);
} else {
if (processor != null) {
out = processor.process(destination);
} else {
out = destination;
}
}
return out;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/image/destination/ImageDestinationProcessorRelativeToAbsolute.java
================================================
package io.noties.markwon.image.destination;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.net.MalformedURLException;
import java.net.URL;
/**
* @since 4.4.0
*/
public class ImageDestinationProcessorRelativeToAbsolute extends ImageDestinationProcessor {
@NonNull
public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull String base) {
return new ImageDestinationProcessorRelativeToAbsolute(base);
}
public static ImageDestinationProcessorRelativeToAbsolute create(@NonNull URL base) {
return new ImageDestinationProcessorRelativeToAbsolute(base);
}
private final URL base;
public ImageDestinationProcessorRelativeToAbsolute(@NonNull String base) {
this.base = obtain(base);
}
public ImageDestinationProcessorRelativeToAbsolute(@NonNull URL base) {
this.base = base;
}
@NonNull
@Override
public String process(@NonNull String destination) {
String out = destination;
if (base != null) {
try {
final URL u = new URL(base, destination);
out = u.toString();
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
return out;
}
@Nullable
private static URL obtain(String base) {
try {
return new URL(base);
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/movement/MovementMethodPlugin.java
================================================
package io.noties.markwon.movement;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.core.CorePlugin;
/**
* @since 3.0.0
*/
public class MovementMethodPlugin extends AbstractMarkwonPlugin {
/**
* Creates plugin that will ensure that there is movement method registered on a TextView.
* Uses Android system LinkMovementMethod as default
*
* @see #create(MovementMethod)
* @see #link()
* @deprecated 4.5.0 use {@link #link()}
*/
@NonNull
@Deprecated
public static MovementMethodPlugin create() {
return create(LinkMovementMethod.getInstance());
}
/**
* @since 4.5.0
*/
@NonNull
public static MovementMethodPlugin link() {
return create(LinkMovementMethod.getInstance());
}
/**
* Special {@link MovementMethodPlugin} that is not applying a MovementMethod on a TextView
* implicitly
*
* @since 4.5.0
*/
@NonNull
public static MovementMethodPlugin none() {
return new MovementMethodPlugin(null);
}
@NonNull
public static MovementMethodPlugin create(@NonNull MovementMethod movementMethod) {
return new MovementMethodPlugin(movementMethod);
}
@Nullable
private final MovementMethod movementMethod;
/**
* Since 4.5.0 change to be nullable
*/
@SuppressWarnings("WeakerAccess")
MovementMethodPlugin(@Nullable MovementMethod movementMethod) {
this.movementMethod = movementMethod;
}
@Override
public void configure(@NonNull Registry registry) {
registry.require(CorePlugin.class)
.hasExplicitMovementMethod(true);
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
// @since 4.5.0 check for equality
final MovementMethod current = textView.getMovementMethod();
if (current != movementMethod) {
textView.setMovementMethod(movementMethod);
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/node/OiintNode.java
================================================
package io.noties.markwon.node;
import org.commonmark.node.CustomNode;
public class OiintNode extends CustomNode {
private String integrationLimit;
public void setIntegrationLimit(String limit) {
this.integrationLimit = limit;
}
public String getIntegrationLimit() {
return integrationLimit;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/syntax/SyntaxHighlight.java
================================================
package io.noties.markwon.syntax;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@SuppressWarnings("WeakerAccess")
public interface SyntaxHighlight {
@NonNull
CharSequence highlight(@Nullable String info, @NonNull String code, StringBuilder language);
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/syntax/SyntaxHighlightNoOp.java
================================================
package io.noties.markwon.syntax;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class SyntaxHighlightNoOp implements SyntaxHighlight {
@NonNull
@Override
public CharSequence highlight(@Nullable String info, @NonNull String code, StringBuilder language) {
return code;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/ColorUtils.java
================================================
package io.noties.markwon.utils;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
public abstract class ColorUtils {
@ColorInt
public static int applyAlpha(
@ColorInt int color,
@IntRange(from = 0, to = 255) int alpha) {
return (color & 0x00FFFFFF) | (alpha << 24);
}
// blend two colors w/ specified ratio, resulting color won't have alpha channel
@ColorInt
public static int blend(
@ColorInt int foreground,
@ColorInt int background,
@FloatRange(from = 0.0F, to = 1.0F) float ratio) {
return Color.rgb(
(int) (((1F - ratio) * Color.red(foreground)) + (ratio * Color.red(background))),
(int) (((1F - ratio) * Color.green(foreground)) + (ratio * Color.green(background))),
(int) (((1F - ratio) * Color.blue(foreground)) + (ratio * Color.blue(background)))
);
}
private ColorUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/Dip.java
================================================
package io.noties.markwon.utils;
import android.content.Context;
import androidx.annotation.NonNull;
public class Dip {
@NonNull
public static Dip create(@NonNull Context context) {
return new Dip(context.getResources().getDisplayMetrics().density);
}
@NonNull
public static Dip create(float density) {
return new Dip(density);
}
private final float density;
@SuppressWarnings("WeakerAccess")
public Dip(float density) {
this.density = density;
}
public int toPx(int dp) {
return (int) (dp * density + .5F);
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/DrawableUtils.java
================================================
package io.noties.markwon.utils;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
/**
* @deprecated Please use {@link io.noties.markwon.image.DrawableUtils}
*/
@Deprecated
public abstract class DrawableUtils {
public static void intrinsicBounds(@NonNull Drawable drawable) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
private DrawableUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/DumpNodes.java
================================================
package io.noties.markwon.utils;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import org.commonmark.node.Visitor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// utility class to print parsed Nodes hierarchy
@SuppressWarnings({"unused", "WeakerAccess"})
public abstract class DumpNodes {
/**
* Creates String representation of a node which will be used in output
*/
public interface NodeProcessor {
@NonNull
String process(@NonNull Node node);
}
@NonNull
@CheckResult
public static String dump(@NonNull Node node) {
return dump(node, null);
}
@NonNull
@CheckResult
public static String dump(@NonNull Node node, @Nullable NodeProcessor nodeProcessor) {
final NodeProcessor processor = nodeProcessor != null
? nodeProcessor
: new NodeProcessorToString();
final Indent indent = new Indent();
final StringBuilder builder = new StringBuilder();
final Visitor visitor = (Visitor) Proxy.newProxyInstance(
Visitor.class.getClassLoader(),
new Class[]{Visitor.class},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
final Node argument = (Node) args[0];
// initial indent
indent.appendTo(builder);
// node info
builder.append(processor.process(argument));
// @since 4.6.0 check for first child instead of casting to Block
// (regular nodes can contain other nodes, for example Text)
if (argument.getFirstChild() != null) {
builder.append(" [\n");
indent.increment();
visitChildren((Visitor) proxy, argument);
indent.decrement();
indent.appendTo(builder);
builder.append("]\n");
} else {
builder.append("\n");
}
return null;
}
});
node.accept(visitor);
return builder.toString();
}
private DumpNodes() {
}
private static class Indent {
private int count;
void increment() {
count += 1;
}
void decrement() {
count -= 1;
}
void appendTo(@NonNull StringBuilder builder) {
for (int i = 0; i < count; i++) {
builder
.append(' ')
.append(' ');
}
}
}
private static void visitChildren(@NonNull Visitor visitor, @NonNull Node parent) {
Node node = parent.getFirstChild();
while (node != null) {
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
// node after visiting it. So get the next node before visiting.
Node next = node.getNext();
node.accept(visitor);
node = next;
}
}
private static class NodeProcessorToString implements NodeProcessor {
@NonNull
@Override
public String process(@NonNull Node node) {
return node.toString();
}
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/LayoutUtils.java
================================================
package io.noties.markwon.utils;
import android.os.Build;
import android.text.Layout;
import androidx.annotation.NonNull;
/**
* @since 4.4.0
*/
public abstract class LayoutUtils {
private static final float DEFAULT_EXTRA = 0F;
private static final float DEFAULT_MULTIPLIER = 1F;
public static int getLineBottomWithoutPaddingAndSpacing(
@NonNull Layout layout,
int line
) {
final int bottom = layout.getLineBottom(line);
final boolean lastLineSpacingNotAdded = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
final boolean isSpanLastLine = line == (layout.getLineCount() - 1);
final int lineBottom;
final float lineSpacingExtra = layout.getSpacingAdd();
final float lineSpacingMultiplier = layout.getSpacingMultiplier();
// simplified check
final boolean hasLineSpacing = lineSpacingExtra != DEFAULT_EXTRA
|| lineSpacingMultiplier != DEFAULT_MULTIPLIER;
if (!hasLineSpacing
|| (isSpanLastLine && lastLineSpacingNotAdded)) {
lineBottom = bottom;
} else {
final float extra;
if (Float.compare(DEFAULT_MULTIPLIER, lineSpacingMultiplier) != 0) {
final int lineHeight = getLineHeight(layout, line);
extra = lineHeight -
((lineHeight - lineSpacingExtra) / lineSpacingMultiplier);
} else {
extra = lineSpacingExtra;
}
lineBottom = (int) (bottom - extra + .5F);
}
// check if it is the last line that span is occupying **and** that this line is the last
// one in TextView
if (isSpanLastLine
&& (line == layout.getLineCount() - 1)) {
return lineBottom - layout.getBottomPadding();
}
return lineBottom;
}
public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) {
final int top = layout.getLineTop(line);
if (line == 0) {
return top - layout.getTopPadding();
}
return top;
}
public static int getLineHeight(@NonNull Layout layout, int line) {
return layout.getLineTop(line + 1) - layout.getLineTop(line);
}
private LayoutUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/LeadingMarginUtils.java
================================================
package io.noties.markwon.utils;
import android.text.Spanned;
public abstract class LeadingMarginUtils {
public static boolean selfStart(int start, CharSequence text, Object span) {
return text instanceof Spanned && ((Spanned) text).getSpanStart(span) == start;
}
public static boolean selfEnd(int end, CharSequence text, Object span) {
return text instanceof Spanned && ((Spanned) text).getSpanEnd(span) == end;
}
private LeadingMarginUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/NoCopySpannableFactory.java
================================================
package io.noties.markwon.utils;
import android.text.Spannable;
import android.text.SpannableString;
import androidx.annotation.NonNull;
/**
* Utility SpannableFactory that re-uses Spannable instance between multiple
* `TextView#setText` calls.
*
* @since 3.0.0
*/
public class NoCopySpannableFactory extends Spannable.Factory {
@NonNull
public static NoCopySpannableFactory getInstance() {
return Holder.INSTANCE;
}
@Override
public Spannable newSpannable(CharSequence source) {
return source instanceof Spannable
? (Spannable) source
: new SpannableString(source);
}
static class Holder {
private static final NoCopySpannableFactory INSTANCE = new NoCopySpannableFactory();
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/ParserUtils.java
================================================
package io.noties.markwon.utils;
import androidx.annotation.NonNull;
import org.commonmark.node.Node;
/**
* @since 4.6.0
*/
public abstract class ParserUtils {
public static void moveChildren(@NonNull Node to, @NonNull Node from) {
Node next = from.getNext();
Node temp;
while (next != null) {
// appendChild would unlink passed node (thus making next info un-available)
temp = next.getNext();
to.appendChild(next);
next = temp;
}
}
private ParserUtils() {
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/java/io/noties/markwon/utils/SpanUtils.java
================================================
package io.noties.markwon.utils;
import android.graphics.Canvas;
import android.text.Layout;
import android.text.Spanned;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.noties.markwon.core.spans.TextLayoutSpan;
import io.noties.markwon.core.spans.TextViewSpan;
/**
* @since 4.4.0
*/
public abstract class SpanUtils {
public static int width(Canvas canvas, @NonNull CharSequence cs) {
// Layout
// TextView
// canvas
if (cs instanceof Spanned) {
final Spanned spanned = (Spanned) cs;
// if we are displayed with layout information -> use it
final Layout layout = TextLayoutSpan.layoutOf(spanned);
if (layout != null) {
return layout.getWidth();
}
// if we have TextView -> obtain width from it (exclude padding)
final TextView textView = TextViewSpan.textViewOf(spanned);
if (textView != null) {
return textView.getWidth() - textView.getPaddingLeft() - textView.getPaddingRight();
}
}
// else just use canvas width
if (canvas != null) {
return canvas.getWidth();
}
return 0;
}
public static boolean isSelfEnd(int end, CharSequence text, Object span) {
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanEnd == end || spanEnd == end - 1;
}
public static boolean isSelfStart(int start, CharSequence text, Object span) {
final int spanStart = ((Spanned) text).getSpanStart(span);
return spanStart == start;
}
public static boolean isSelf(int start, int end, CharSequence text, Object span) {
final int spanStart = ((Spanned) text).getSpanStart(span);
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanStart <= start && spanEnd >= end - 1;
}
}
================================================
FILE: Android/AntFluid/markwon-core/src/main/res/values/ids.xml
================================================
================================================
FILE: Android/AntFluid/markwon-core/src/test/java/io/noties/markwon/ExampleUnitTest.kt
================================================
package io.noties.markwon
import org.junit.Assert.*
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/.gitignore
================================================
/build
================================================
FILE: Android/AntFluid/markwon-ext-latex/build.gradle
================================================
plugins {
alias(libs.plugins.android.library)
}
android {
namespace 'io.noties.markwon.ext.latex'
compileSdk 35
defaultConfig {
minSdk 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.androidx.appcompat
compileOnly project(':markwon-core')
compileOnly project(':markwon-inline-parser')
implementation 'ru.noties:jlatexmath-android:0.2.0'
implementation 'ru.noties:jlatexmath-android-font-cyrillic:0.2.0'
implementation 'ru.noties:jlatexmath-android-font-greek:0.2.0'
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/consumer-rules.pro
================================================
================================================
FILE: Android/AntFluid/markwon-ext-latex/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/androidTest/java/io/noties/markwon/ext/latex/ExampleInstrumentedTest.java
================================================
package io.noties.markwon.ext.latex;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.noties.markwon.ext.latex.test", appContext.getPackageName());
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexAsyncDrawableSpan.java
================================================
package io.noties.markwon.ext.latex;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import org.scilab.forge.jlatexmath.TeXIcon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawableSpan;
import ru.noties.jlatexmath.JLatexMathDrawable;
import ru.noties.jlatexmath.awt.Color;
/**
* @since 4.3.0
*/
public class JLatexAsyncDrawableSpan extends AsyncDrawableSpan {
private final JLatextAsyncDrawable drawable;
private final int color;
private boolean appliedTextColor;
public JLatexAsyncDrawableSpan(
@NonNull MarkwonTheme theme,
@NonNull JLatextAsyncDrawable drawable,
@ColorInt int color) {
super(theme, drawable, ALIGN_CENTER, false, false);
this.drawable = drawable;
this.color = color;
// if color is not 0 -> then no need to apply text color
this.appliedTextColor = color != 0;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
if (!appliedTextColor && drawable.hasResult()) {
// it is important to check for type (in case of an error, or custom placeholder or whatever
// this result can be of other type)
final Drawable drawableResult = drawable.getResult();
if (drawableResult instanceof JLatexMathDrawable) {
final JLatexMathDrawable result = (JLatexMathDrawable) drawableResult;
final TeXIcon icon = result.icon();
icon.setForeground(new Color(paint.getColor()));
appliedTextColor = true;
}
}
super.draw(canvas, text, start, end, x, top, y, bottom, paint);
}
@NonNull
public JLatextAsyncDrawable drawable() {
return drawable;
}
@ColorInt
public int color() {
return color;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexBlockImageSizeResolver.java
================================================
package io.noties.markwon.ext.latex;
import android.graphics.Rect;
import androidx.annotation.NonNull;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.ImageSizeResolver;
// we must make drawable fit canvas (if specified), but do not keep the ratio whilst scaling up
// @since 4.0.0
class JLatexBlockImageSizeResolver extends ImageSizeResolver {
private final boolean fitCanvas;
JLatexBlockImageSizeResolver(boolean fitCanvas) {
this.fitCanvas = fitCanvas;
}
@NonNull
@Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
final Rect imageBounds = drawable.getResult().getBounds();
final int canvasWidth = drawable.getLastKnownCanvasWidth();
if (fitCanvas) {
// we modify bounds only if `fitCanvas` is true
final int w = imageBounds.width();
if (w < canvasWidth) {
// increase width and center formula (keep height as-is)
return new Rect(0, 0, canvasWidth, imageBounds.height());
}
// @since 4.0.2 we additionally scale down the resulting formula (keeping the ratio)
// the thing is - JLatexMathDrawable will do it anyway, but it will modify its own
// bounds (which AsyncDrawable won't catch), thus leading to an empty space after the formula
if (w > canvasWidth) {
// here we must scale it down (keeping the ratio)
final float ratio = (float) w / imageBounds.height();
final int h = (int) (canvasWidth / ratio + .5F);
return new Rect(0, 0, canvasWidth, h);
}
}
return imageBounds;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexInlineAsyncDrawableSpan.java
================================================
package io.noties.markwon.ext.latex;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawable;
/**
* @since 4.3.0
*/
class JLatexInlineAsyncDrawableSpan extends JLatexAsyncDrawableSpan {
private final AsyncDrawable drawable;
JLatexInlineAsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull JLatextAsyncDrawable drawable, @ColorInt int color) {
super(theme, drawable, color);
this.drawable = drawable;
}
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
// if we have no async drawable result - we will just render text
final int size;
if (drawable.hasResult()) {
final Rect rect = drawable.getBounds();
if (fm != null) {
final int half = rect.bottom / 2;
fm.ascent = -half;
fm.descent = half;
fm.top = fm.ascent;
fm.bottom = 0;
}
size = rect.right;
} else {
// NB, no specific text handling (no new lines, etc)
size = (int) (paint.measureText(text, start, end) + .5F);
}
return size;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlock.java
================================================
package io.noties.markwon.ext.latex;
import org.commonmark.node.CustomBlock;
public class JLatexMathBlock extends CustomBlock {
private String latex;
public String latex() {
return latex;
}
public void latex(String latex) {
this.latex = latex;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParser.java
================================================
package io.noties.markwon.ext.latex;
import androidx.annotation.NonNull;
import org.commonmark.internal.util.Parsing;
import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
/**
* @since 4.3.0 (although there was a class with the same name,
* which is renamed now to {@link JLatexMathBlockParserLegacy})
*/
class JLatexMathBlockParser extends AbstractBlockParser {
private static final char DOLLAR = '$';
private static final char SPACE = ' ';
private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder();
private final int signs;
JLatexMathBlockParser(int signs) {
this.signs = signs;
}
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState parserState) {
final int nextNonSpaceIndex = parserState.getNextNonSpaceIndex();
final CharSequence line = parserState.getLine();
final int length = line.length();
// check for closing
if (parserState.getIndent() < Parsing.CODE_BLOCK_INDENT) {
if (consume(DOLLAR, line, nextNonSpaceIndex, length) == signs) {
// okay, we have our number of signs
// let's consume spaces until the end
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) == length) {
return BlockContinue.finished();
}
}
}
return BlockContinue.atIndex(parserState.getIndex());
}
@Override
public void addLine(CharSequence line) {
builder.append(line);
builder.append('\n');
}
@Override
public void closeBlock() {
block.latex(builder.toString());
}
public static class Factory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
// let's define the spec:
// * 0-3 spaces before are allowed (Parsing.CODE_BLOCK_INDENT = 4)
// * 2+ subsequent `$` signs
// * any optional amount of spaces
// * new line
// * block is closed when the same amount of opening signs is met
final int indent = state.getIndent();
// check if it's an indented code block
if (indent >= Parsing.CODE_BLOCK_INDENT) {
return BlockStart.none();
}
final int nextNonSpaceIndex = state.getNextNonSpaceIndex();
final CharSequence line = state.getLine();
final int length = line.length();
final int signs = consume(DOLLAR, line, nextNonSpaceIndex, length);
// 2 is minimum
if (signs < 2) {
return BlockStart.none();
}
// consume spaces until the end of the line, if any other content is found -> NONE
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) != length) {
return BlockStart.none();
}
return BlockStart.of(new JLatexMathBlockParser(signs))
.atIndex(length + 1);
}
}
@SuppressWarnings("SameParameterValue")
private static int consume(char c, @NonNull CharSequence line, int start, int end) {
for (int i = start; i < end; i++) {
if (c != line.charAt(i)) {
return i - start;
}
}
// all consumed
return end - start;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParserLegacy.java
================================================
package io.noties.markwon.ext.latex;
import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
/**
* @since 4.3.0 (although it is just renamed parser from previous versions)
*/
class JLatexMathBlockParserLegacy extends AbstractBlockParser {
private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder();
private boolean isClosed;
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState parserState) {
if (isClosed) {
return BlockContinue.finished();
}
return BlockContinue.atIndex(parserState.getIndex());
}
@Override
public void addLine(CharSequence line) {
if (builder.length() > 0) {
builder.append('\n');
}
builder.append(line);
final int length = builder.length();
if (length > 1) {
isClosed = '$' == builder.charAt(length - 1)
&& '$' == builder.charAt(length - 2);
if (isClosed) {
builder.replace(length - 2, length, "");
}
}
}
@Override
public void closeBlock() {
if(isClosed){
block.latex(builder.toString());
}else {
block.latex("");
}
}
public static class Factory extends AbstractBlockParserFactory {
public static CharSequence lastLine = "";
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
final CharSequence line = state.getLine();
final int length = line != null
? line.length()
: 0;
if (length > 1) {
if ('$' == line.charAt(0)
&& '$' == line.charAt(1)) {
if(line.equals(lastLine)){
return BlockStart.none();
}
lastLine = line;
return BlockStart.of(new JLatexMathBlockParserLegacy())
.atIndex(state.getIndex() + 2);
}
}
return BlockStart.none();
}
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathInlineProcessor.java
================================================
package io.noties.markwon.ext.latex;
import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import java.util.regex.Pattern;
import io.noties.markwon.inlineparser.InlineProcessor;
/**
* @since 4.3.0
*/
class JLatexMathInlineProcessor extends InlineProcessor {
// support single $ character
private static final Pattern RE = Pattern.compile("(\\${2}|\\${1})([\\s\\S]+?)\\1");
@Override
public char specialCharacter() {
return '$';
}
@Nullable
@Override
protected Node parse() {
final String latex = match(RE);
if (latex == null) {
return null;
}
// support single $ character
int dollarCount = latex.startsWith("$$") ? 2 : 1;
final JLatexMathNode node = new JLatexMathNode();
node.latex(latex.substring(dollarCount, latex.length() - dollarCount));
return node;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathNode.java
================================================
package io.noties.markwon.ext.latex;
import org.commonmark.node.CustomNode;
/**
* @since 4.3.0
*/
public class JLatexMathNode extends CustomNode {
private String latex;
public String latex() {
return latex;
}
public void latex(String latex) {
this.latex = latex;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java
================================================
package io.noties.markwon.ext.latex;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.annotation.VisibleForTesting;
import com.fluid.afm.StreamOutStateObserver;
import com.fluid.afm.utils.MDLogger;
import org.commonmark.parser.Parser;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.AsyncDrawableScheduler;
import io.noties.markwon.image.AsyncDrawableSpan;
import io.noties.markwon.image.DrawableUtils;
import io.noties.markwon.image.ImageSizeResolver;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import ru.noties.jlatexmath.JLatexMathAndroid;
import ru.noties.jlatexmath.JLatexMathDrawable;
/**
* @since 3.0.0
*/
public class JLatexMathPlugin extends AbstractMarkwonPlugin implements StreamOutStateObserver {
private boolean mIsStreamingOutput = false;
public ConcurrentHashMap mLatexDesCache = new ConcurrentHashMap<>();
public static void init(Context context) {
JLatexMathAndroid.init(context);
}
@Override
public void onStreamOutStateChanged(boolean isStreamingOutput) {
mIsStreamingOutput = isStreamingOutput;
if (!isStreamingOutput) {
mLatexDesCache.clear();
}
}
/**
* @since 4.3.0
*/
public interface ErrorHandler {
/**
* @param latex that caused the error
* @param error occurred
* @return (optional) error drawable that will be used instead (if drawable will have bounds
* it will be used, if not intrinsic bounds will be set)
*/
@Nullable
Drawable handleError(@NonNull String latex, @NonNull Throwable error);
}
public interface BuilderConfigure {
void configureBuilder(@NonNull Builder builder);
}
@NonNull
public static JLatexMathPlugin create(float textSize) {
return new JLatexMathPlugin(builder(textSize).build());
}
/**
* @since 4.3.0
*/
@NonNull
public static JLatexMathPlugin create(@Px float inlineTextSize, @Px float blockTextSize) {
return new JLatexMathPlugin(builder(inlineTextSize, blockTextSize).build());
}
@NonNull
public static JLatexMathPlugin create(@NonNull Config config) {
return new JLatexMathPlugin(config);
}
@NonNull
public static JLatexMathPlugin create(@Px float textSize, @NonNull BuilderConfigure builderConfigure) {
final Builder builder = builder(textSize);
builderConfigure.configureBuilder(builder);
return new JLatexMathPlugin(builder.build());
}
/**
* @since 4.3.0
*/
@NonNull
public static JLatexMathPlugin create(
@Px float inlineTextSize,
@Px float blockTextSize,
@NonNull BuilderConfigure builderConfigure) {
final Builder builder = builder(inlineTextSize, blockTextSize);
builderConfigure.configureBuilder(builder);
return new JLatexMathPlugin(builder.build());
}
@NonNull
public static Builder builder(@Px float textSize) {
return new Builder(JLatexMathTheme.builder(textSize));
}
/**
* @since 4.3.0
*/
@NonNull
public static Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
return new Builder(JLatexMathTheme.builder(inlineTextSize, blockTextSize));
}
@VisibleForTesting
static class Config {
// @since 4.3.0
final JLatexMathTheme theme;
// @since 4.3.0
final boolean blocksEnabled;
final boolean blocksLegacy;
final boolean inlinesEnabled;
// @since 4.3.0
final ErrorHandler errorHandler;
final ExecutorService executorService;
Config(@NonNull Builder builder) {
this.theme = builder.theme.build();
this.blocksEnabled = builder.blocksEnabled;
this.blocksLegacy = builder.blocksLegacy;
this.inlinesEnabled = builder.inlinesEnabled;
this.errorHandler = builder.errorHandler;
// @since 4.0.0
ExecutorService executorService = builder.executorService;
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}
this.executorService = executorService;
}
}
@VisibleForTesting
final Config config;
private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader;
private final JLatexBlockImageSizeResolver jLatexBlockImageSizeResolver;
private final ImageSizeResolver inlineImageSizeResolver;
@SuppressWarnings("WeakerAccess")
JLatexMathPlugin(@NonNull Config config) {
this.config = config;
this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config);
this.jLatexBlockImageSizeResolver = new JLatexBlockImageSizeResolver(false);
this.inlineImageSizeResolver = new InlineImageSizeResolver();
}
@Override
public void configure(@NonNull Registry registry) {
if (config.inlinesEnabled) {
registry.require(MarkwonInlineParserPlugin.class)
.factoryBuilder()
.addInlineProcessor(new JLatexMathInlineProcessor());
}
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
// @since 4.3.0
if (config.blocksEnabled) {
if (config.blocksLegacy) {
builder.customBlockParserFactory(new JLatexMathBlockParserLegacy.Factory());
} else {
builder.customBlockParserFactory(new JLatexMathBlockParser.Factory());
}
}
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
addBlockVisitor(builder);
addInlineVisitor(builder);
}
private void addBlockVisitor(@NonNull MarkwonVisitor.Builder builder) {
if (!config.blocksEnabled) {
return;
}
builder.on(JLatexMathBlock.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) {
visitor.blockStart(jLatexMathBlock);
final String latex = jLatexMathBlock.latex();
final int length = visitor.length();
// @since 4.0.2 we cannot append _raw_ latex as a placeholder-text,
// because Android will draw formula for each line of text, thus
// leading to formula duplicated (drawn on each line of text)
visitor.builder().append(prepareLatexTextPlaceholder(latex));
handleVisitorAndJLatex(visitor,latex,length,"BlockVisitor");
visitor.blockEnd(jLatexMathBlock);
}
});
}
private void handleVisitorAndJLatex(MarkwonVisitor visitor,String latex,int length,String tag){
if(TextUtils.isEmpty(latex)){
return;
}
AsyncDrawableSpan span = mLatexDesCache.get(latex);
if (isStreamingOutput() && span != null) {
// if (span.getDrawable().hasResult()) {
// Drawable drawable = span.getDrawable().getResult();
// ImageSpan imageSpan = new ImageSpan(drawable);
// visitor.setSpans(length, imageSpan);
// } else {
visitor.setSpans(length, span);
// }
} else {
final MarkwonConfiguration configuration = visitor.configuration();
span = new JLatexAsyncDrawableSpan(
configuration.theme(),
new JLatextAsyncDrawable(
latex,
jLatextAsyncDrawableLoader,
"BlockVisitor".equals(tag)?jLatexBlockImageSizeResolver:inlineImageSizeResolver,
null,
true),
config.theme.blockTextColor()
);
visitor.setSpans(length, span);
mLatexDesCache.put(latex,span);
}
}
private boolean isStreamingOutput() {
return mIsStreamingOutput;
}
private void addInlineVisitor(@NonNull MarkwonVisitor.Builder builder) {
if (!config.inlinesEnabled) {
return;
}
builder.on(JLatexMathNode.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathNode jLatexMathNode) {
final String latex = jLatexMathNode.latex();
final int length = visitor.length();
// @since 4.0.2 we cannot append _raw_ latex as a placeholder-text,
// because Android will draw formula for each line of text, thus
// leading to formula duplicated (drawn on each line of text)
visitor.builder().append(prepareLatexTextPlaceholder(latex));
handleVisitorAndJLatex(visitor,latex,length,"InlineVisitor");
}
});
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
AsyncDrawableScheduler.unschedule(textView);
}
@Override
public void afterSetText(@NonNull TextView textView) {
AsyncDrawableScheduler.schedule(textView);
}
// @since 4.0.2
@VisibleForTesting
@NonNull
static String prepareLatexTextPlaceholder(@NonNull String latex) {
return latex.replace('\n', ' ').trim();
}
@SuppressWarnings({"unused", "UnusedReturnValue"})
public static class Builder {
// @since 4.3.0
private final JLatexMathTheme.Builder theme;
// @since 4.3.0
private boolean blocksEnabled = true;
private boolean blocksLegacy;
private boolean inlinesEnabled;
// @since 4.3.0
private ErrorHandler errorHandler;
// @since 4.0.0
private ExecutorService executorService;
Builder(@NonNull JLatexMathTheme.Builder builder) {
this.theme = builder;
}
@NonNull
public JLatexMathTheme.Builder theme() {
return theme;
}
/**
* @since 4.3.0
*/
@NonNull
public Builder blocksEnabled(boolean blocksEnabled) {
this.blocksEnabled = blocksEnabled;
return this;
}
/**
* @param blocksLegacy indicates if blocks should be handled in legacy mode ({@code pre 4.3.0})
* @since 4.3.0
*/
@NonNull
public Builder blocksLegacy(boolean blocksLegacy) {
this.blocksLegacy = blocksLegacy;
return this;
}
/**
* @param inlinesEnabled indicates if inline parsing should be enabled.
* NB, this requires `MarkwonInlineParserPlugin` to be used when creating `MarkwonInstance`
* @since 4.3.0
*/
@NonNull
public Builder inlinesEnabled(boolean inlinesEnabled) {
this.inlinesEnabled = inlinesEnabled;
return this;
}
@NonNull
public Builder errorHandler(@Nullable ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
/**
* @since 4.0.0
*/
@SuppressWarnings("WeakerAccess")
@NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService;
return this;
}
@NonNull
public Config build() {
return new Config(this);
}
}
// @since 4.0.0
static class JLatextAsyncDrawableLoader extends AsyncDrawableLoader {
private final Config config;
private final Handler handler = new Handler(Looper.getMainLooper());
private final Map> cache = new HashMap<>(3);
JLatextAsyncDrawableLoader(@NonNull Config config) {
this.config = config;
}
@Override
public void load(@NonNull final AsyncDrawable drawable) {
// this method must be called from main-thread only (thus synchronization can be skipped)
// check for currently running tasks associated with provided drawable
final Future> future = cache.get(drawable);
// if it's present -> proceed with new execution
// as asyncDrawable is immutable, it won't have destination changed (so there is no need
// to cancel any started tasks)
if (future == null) {
cache.put(drawable, config.executorService.submit(new Runnable() {
@Override
public void run() {
// @since 4.0.1 wrap in try-catch block and add error logging
try {
execute();
} catch (Throwable t) {
// @since 4.3.0 add error handling
final ErrorHandler errorHandler = config.errorHandler;
if (errorHandler == null) {
// as before
MDLogger.e(
"JLatexMathPlugin",
"Error displaying latex: `" + drawable.getDestination() + "`",
t);
} else {
// just call `getDestination` without casts and checks
final Drawable errorDrawable = errorHandler.handleError(
drawable.getDestination(),
t
);
if (errorDrawable != null) {
DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable);
setResult(drawable, errorDrawable);
}
}
}
}
private void execute() {
final JLatexMathDrawable jLatexMathDrawable;
final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable;
if (jLatextAsyncDrawable.isBlock()) {
jLatexMathDrawable = createBlockDrawable(jLatextAsyncDrawable);
} else {
jLatexMathDrawable = createInlineDrawable(jLatextAsyncDrawable);
}
setResult(drawable, jLatexMathDrawable);
}
}));
}
}
@Override
public void cancel(@NonNull AsyncDrawable drawable) {
// this method also must be called from main thread only
final Future> future = cache.remove(drawable);
if (future != null) {
future.cancel(true);
}
// remove all callbacks (via runnable) and messages posted for this drawable
handler.removeCallbacksAndMessages(drawable);
}
@Nullable
@Override
public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return null;
}
// @since 4.3.0
@NonNull
private JLatexMathDrawable createBlockDrawable(@NonNull JLatextAsyncDrawable drawable) {
final String latex = drawable.getDestination();
final JLatexMathTheme theme = config.theme;
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.blockBackgroundProvider();
final JLatexMathTheme.Padding padding = theme.blockPadding();
final int color = theme.blockTextColor();
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
.textSize(theme.blockTextSize())
.align(theme.blockHorizontalAlignment());
if (backgroundProvider != null) {
builder.background(backgroundProvider.provide());
}
if (padding != null) {
builder.padding(padding.left, padding.top, padding.right, padding.bottom);
}
if (color != 0) {
builder.color(color);
}
return builder.build();
}
// @since 4.3.0
@NonNull
private JLatexMathDrawable createInlineDrawable(@NonNull JLatextAsyncDrawable drawable) {
final String latex = drawable.getDestination();
final JLatexMathTheme theme = config.theme;
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.inlineBackgroundProvider();
final JLatexMathTheme.Padding padding = theme.inlinePadding();
final int color = theme.inlineTextColor();
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
.textSize(theme.inlineTextSize());
if (backgroundProvider != null) {
builder.background(backgroundProvider.provide());
}
if (padding != null) {
builder.padding(padding.left, padding.top, padding.right, padding.bottom);
}
if (color != 0) {
builder.color(color);
}
return builder.build();
}
// @since 4.3.0
private void setResult(@NonNull final AsyncDrawable drawable, @NonNull final Drawable result) {
// we must post to handler, but also have a way to identify the drawable
// for which we are posting (in case of cancellation)
handler.postAtTime(new Runnable() {
@Override
public void run() {
// remove entry from cache (it will be present if task is not cancelled)
if (cache.remove(drawable) != null
&& drawable.isAttached()) {
drawable.setResult(result);
}
}
}, drawable, SystemClock.uptimeMillis());
}
}
private static class InlineImageSizeResolver extends ImageSizeResolver {
@NonNull
@Override
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
// @since 4.4.0 resolve inline size (scale down if exceed available width)
final Rect imageBounds = drawable.getResult().getBounds();
final int canvasWidth = drawable.getLastKnownCanvasWidth();
final int w = imageBounds.width();
if (w > canvasWidth) {
// here we must scale it down (keeping the ratio)
final float ratio = (float) w / imageBounds.height();
final int h = (int) (canvasWidth / ratio + .5F);
return new Rect(0, 0, canvasWidth, h);
}
return imageBounds;
}
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathTheme.java
================================================
package io.noties.markwon.ext.latex;
import android.graphics.drawable.Drawable;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import ru.noties.jlatexmath.JLatexMathDrawable;
/**
* @since 4.3.0
*/
public abstract class JLatexMathTheme {
@NonNull
public static JLatexMathTheme create(@Px float textSize) {
return builder(textSize).build();
}
@NonNull
public static JLatexMathTheme create(@Px float inlineTextSize, @Px float blockTextSize) {
return builder(inlineTextSize, blockTextSize).build();
}
@NonNull
public static Builder builder(@Px float textSize) {
return new Builder(textSize, 0F, 0F);
}
@NonNull
public static Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
return new Builder(0F, inlineTextSize, blockTextSize);
}
/**
* Moved from {@link JLatexMathPlugin} in {@code 4.3.0} version
*
* @since 4.0.0
*/
public interface BackgroundProvider {
@NonNull
Drawable provide();
}
/**
* Special immutable class to hold padding information
*/
@SuppressWarnings("WeakerAccess")
public static class Padding {
public final int left;
public final int top;
public final int right;
public final int bottom;
public Padding(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
@NonNull
@Override
public String toString() {
return "Padding{" +
"left=" + left +
", top=" + top +
", right=" + right +
", bottom=" + bottom +
'}';
}
@NonNull
public static Padding all(int value) {
return new Padding(value, value, value, value);
}
@NonNull
public static Padding symmetric(int vertical, int horizontal) {
return new Padding(horizontal, vertical, horizontal, vertical);
}
/**
* @since 4.5.0
*/
@NonNull
public static Padding of(int left, int top, int right, int bottom) {
return new Padding(left, top, right, bottom);
}
}
/**
* @return text size in pixels for inline LaTeX
* @see #blockTextSize()
*/
@Px
public abstract float inlineTextSize();
/**
* @return text size in pixels for block LaTeX
* @see #inlineTextSize()
*/
@Px
public abstract float blockTextSize();
@Nullable
public abstract BackgroundProvider inlineBackgroundProvider();
@Nullable
public abstract BackgroundProvider blockBackgroundProvider();
/**
* @return boolean if block LaTeX must fit the width of canvas
*/
public abstract boolean blockFitCanvas();
/**
* @return horizontal alignment of block LaTeX if {@link #blockFitCanvas()}
* is enabled (thus space for alignment is available)
*/
@JLatexMathDrawable.Align
public abstract int blockHorizontalAlignment();
@Nullable
public abstract Padding inlinePadding();
@Nullable
public abstract Padding blockPadding();
@ColorInt
public abstract int inlineTextColor();
@ColorInt
public abstract int blockTextColor();
@SuppressWarnings({"unused", "UnusedReturnValue"})
public static class Builder {
private final float textSize;
private final float inlineTextSize;
private final float blockTextSize;
private BackgroundProvider backgroundProvider;
private BackgroundProvider inlineBackgroundProvider;
private BackgroundProvider blockBackgroundProvider;
private boolean blockFitCanvas = true;
// horizontal alignment (when there is additional horizontal space)
private int blockHorizontalAlignment = JLatexMathDrawable.ALIGN_CENTER;
private Padding padding;
private Padding inlinePadding;
private Padding blockPadding;
private int textColor;
private int inlineTextColor;
private int blockTextColor;
Builder(float textSize, float inlineTextSize, float blockTextSize) {
this.textSize = textSize;
this.inlineTextSize = inlineTextSize;
this.blockTextSize = blockTextSize;
}
@NonNull
public Builder backgroundProvider(@Nullable BackgroundProvider backgroundProvider) {
this.backgroundProvider = backgroundProvider;
this.inlineBackgroundProvider = backgroundProvider;
this.blockBackgroundProvider = backgroundProvider;
return this;
}
@NonNull
public Builder inlineBackgroundProvider(@Nullable BackgroundProvider inlineBackgroundProvider) {
this.inlineBackgroundProvider = inlineBackgroundProvider;
return this;
}
@NonNull
public Builder blockBackgroundProvider(@Nullable BackgroundProvider blockBackgroundProvider) {
this.blockBackgroundProvider = blockBackgroundProvider;
return this;
}
/**
* Configure if `LaTeX` formula should take all available widget width.
* By default - `true`
*/
@NonNull
public Builder blockFitCanvas(boolean blockFitCanvas) {
this.blockFitCanvas = blockFitCanvas;
return this;
}
@NonNull
public Builder blockHorizontalAlignment(@JLatexMathDrawable.Align int blockHorizontalAlignment) {
this.blockHorizontalAlignment = blockHorizontalAlignment;
return this;
}
@NonNull
public Builder padding(@Nullable Padding padding) {
this.padding = padding;
this.inlinePadding = padding;
this.blockPadding = padding;
return this;
}
@NonNull
public Builder inlinePadding(@Nullable Padding inlinePadding) {
this.inlinePadding = inlinePadding;
return this;
}
@NonNull
public Builder blockPadding(@Nullable Padding blockPadding) {
this.blockPadding = blockPadding;
return this;
}
@NonNull
public Builder textColor(@ColorInt int textColor) {
this.textColor = textColor;
return this;
}
@NonNull
public Builder inlineTextColor(@ColorInt int inlineTextColor) {
this.inlineTextColor = inlineTextColor;
return this;
}
@NonNull
public Builder blockTextColor(@ColorInt int blockTextColor) {
this.blockTextColor = blockTextColor;
return this;
}
@NonNull
public JLatexMathTheme build() {
return new Impl(this);
}
}
static class Impl extends JLatexMathTheme {
private final float textSize;
private final float inlineTextSize;
private final float blockTextSize;
private final BackgroundProvider backgroundProvider;
private final BackgroundProvider inlineBackgroundProvider;
private final BackgroundProvider blockBackgroundProvider;
private final boolean blockFitCanvas;
// horizontal alignment (when there is additional horizontal space)
private int blockHorizontalAlignment;
private final Padding padding;
private final Padding inlinePadding;
private final Padding blockPadding;
private final int textColor;
private final int inlineTextColor;
private final int blockTextColor;
Impl(@NonNull Builder builder) {
this.textSize = builder.textSize;
this.inlineTextSize = builder.inlineTextSize;
this.blockTextSize = builder.blockTextSize;
this.backgroundProvider = builder.backgroundProvider;
this.inlineBackgroundProvider = builder.inlineBackgroundProvider;
this.blockBackgroundProvider = builder.blockBackgroundProvider;
this.blockFitCanvas = builder.blockFitCanvas;
this.blockHorizontalAlignment = builder.blockHorizontalAlignment;
this.padding = builder.padding;
this.inlinePadding = builder.inlinePadding;
this.blockPadding = builder.blockPadding;
this.textColor = builder.textColor;
this.inlineTextColor = builder.inlineTextColor;
this.blockTextColor = builder.blockTextColor;
}
@Override
public float inlineTextSize() {
if (inlineTextSize > 0F) {
return inlineTextSize;
}
return textSize;
}
@Override
public float blockTextSize() {
if (blockTextSize > 0F) {
return blockTextSize;
}
return textSize;
}
@Nullable
@Override
public BackgroundProvider inlineBackgroundProvider() {
if (inlineBackgroundProvider != null) {
return inlineBackgroundProvider;
}
return backgroundProvider;
}
@Nullable
@Override
public BackgroundProvider blockBackgroundProvider() {
if (blockBackgroundProvider != null) {
return blockBackgroundProvider;
}
return backgroundProvider;
}
@Override
public boolean blockFitCanvas() {
return blockFitCanvas;
}
@Override
public int blockHorizontalAlignment() {
return blockHorizontalAlignment;
}
@Nullable
@Override
public Padding inlinePadding() {
if (inlinePadding != null) {
return inlinePadding;
}
return padding;
}
@Nullable
@Override
public Padding blockPadding() {
if (blockPadding != null) {
return blockPadding;
}
return padding;
}
@Override
public int inlineTextColor() {
if (inlineTextColor != 0) {
return inlineTextColor;
}
return textColor;
}
@Override
public int blockTextColor() {
if (blockTextColor != 0) {
return blockTextColor;
}
return textColor;
}
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatextAsyncDrawable.java
================================================
package io.noties.markwon.ext.latex;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.ImageSize;
import io.noties.markwon.image.ImageSizeResolver;
/**
* @since 4.3.0
*/
class JLatextAsyncDrawable extends AsyncDrawable {
private final boolean isBlock;
JLatextAsyncDrawable(
@NonNull String destination,
@NonNull AsyncDrawableLoader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize,
boolean isBlock
) {
super(destination, loader, imageSizeResolver, imageSize);
this.isBlock = isBlock;
}
public boolean isBlock() {
return isBlock;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/ExampleUnitTest.java
================================================
package io.noties.markwon.ext.latex;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see Testing documentation
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/.gitignore
================================================
/build
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/build.gradle
================================================
plugins {
alias(libs.plugins.android.library)
}
android {
namespace 'io.noties.markwon.ext.strikethrough'
compileSdk 35
defaultConfig {
minSdk 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.androidx.appcompat
api fileTree(dir: 'libs', include: ['*.jar'])
compileOnly project(':markwon-core')
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/consumer-rules.pro
================================================
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/src/androidTest/java/io/noties/markwon/ext/strikethrough/ExampleInstrumentedTest.java
================================================
package io.noties.markwon.ext.strikethrough;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.noties.markwon.ext.strikethrough.test", appContext.getPackageName());
}
}
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/src/main/java/io/noties/markwon/ext/strikethrough/StrikethroughPlugin.java
================================================
package io.noties.markwon.ext.strikethrough;
import android.text.style.StrikethroughSpan;
import androidx.annotation.NonNull;
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
import org.commonmark.parser.Parser;
import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.RenderProps;
import io.noties.markwon.SpanFactory;
/**
* Plugin to add strikethrough markdown feature. This plugin will extend commonmark-java.Parser
* with strikethrough extension, add SpanFactory and register commonmark-java.Strikethrough node
* visitor
*
* @see #create()
* @since 3.0.0
*/
public class StrikethroughPlugin extends AbstractMarkwonPlugin {
@NonNull
public static StrikethroughPlugin create() {
return new StrikethroughPlugin();
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.extensions(Collections.singleton(StrikethroughExtension.create()));
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Strikethrough.class, new SpanFactory() {
@Override
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
return new StrikethroughSpan();
}
});
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Strikethrough.class, new MarkwonVisitor.NodeVisitor() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Strikethrough strikethrough) {
final int length = visitor.length();
visitor.visitChildren(strikethrough);
visitor.setSpansForNodeOptional(strikethrough, length);
}
});
}
}
================================================
FILE: Android/AntFluid/markwon-ext-strikethrough/src/test/java/io/noties/markwon/ext/strikethrough/ExampleUnitTest.java
================================================
package io.noties.markwon.ext.strikethrough;
import static org.junit.Assert.*;
import org.junit.Test;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see Testing documentation
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/.gitignore
================================================
/build
================================================
FILE: Android/AntFluid/markwon-ext-tables/build.gradle
================================================
plugins {
alias(libs.plugins.android.library)
}
android {
namespace 'io.noties.markwon.ext.tables'
compileSdk 35
defaultConfig {
minSdk 21
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.androidx.appcompat
api 'com.atlassian.commonmark:commonmark-ext-gfm-tables:0.13.0'
compileOnly project(':markwon-core')
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/consumer-rules.pro
================================================
================================================
FILE: Android/AntFluid/markwon-ext-tables/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/androidTest/java/io/noties/markwon/ext/tables/ExampleInstrumentedTest.java
================================================
package io.noties.markwon.ext.tables;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Instrumented test, which will execute on an Android device.
*
* @see Testing documentation
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("io.noties.markwon.ext.tables.test", appContext.getPackageName());
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/AndroidManifest.xml
================================================
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/BaseBlockTitleSpan.java
================================================
package com.fluid.afm;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.LeadingMarginSpan;
import android.view.View;
import android.widget.Toast;
import com.fluid.afm.utils.MDLogger;
import java.util.HashMap;
import java.util.Map;
import com.fluid.afm.span.CodeLanguageSpan;
import io.noties.markwon.ext.tables.R;
import io.noties.markwon.utils.SpanUtils;
public class BaseBlockTitleSpan implements LeadingMarginSpan {
protected static final String TAG = BaseBlockTitleSpan.class.getSimpleName();
protected final Rect rect = new Rect();
protected Drawable copyIcon;
protected Drawable magnifyIcon;
protected final HashMap copyRectMap;
protected final int parentHeight;
protected final int textSize;
protected int tableIndex;
protected String blockTitle = "";
public BaseBlockTitleSpan(Context context, int height, int textSize) {
this.copyRectMap = new HashMap<>();
this.parentHeight = height;
this.textSize = textSize;
}
public BaseBlockTitleSpan(Context context, int height, int textSize, int tableIndex) {
this.copyRectMap = new HashMap<>();
this.parentHeight = height;
this.textSize = textSize;
this.tableIndex = tableIndex;
}
@Override
public int getLeadingMargin(boolean b) {
return 0;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
if (!SpanUtils.isSelfStart(start, text, this)) return;
int save = c.save();
try {
final float oldStrokeWidth = p.getStrokeWidth();
final int oldColor = p.getColor();
final Paint.Style oldStyle = p.getStyle();
final float oldTextSize = p.getTextSize();
drawBackground(c, p, top, layout.getWidth());
drawBorder(c, p, top, layout.getWidth());
drawHeaderText(c, p, text, start, top, x);
drawCopyIcon(c, p, start, end, top, layout.getWidth(), magnifyIcon != null);
drawMagnifyIcon(c, p, start, end, top, layout.getWidth());
p.setStrokeWidth(oldStrokeWidth);
p.setColor(oldColor);
p.setStyle(oldStyle);
p.setTextSize(oldTextSize);
} finally {
c.restoreToCount(save);
}
}
private void drawBorder(Canvas c, Paint p, int top, int layoutWidth) {
if (!drawBorder()) {
return;
}
applyBorderStyle(p);
int saveCount = c.save();
float radius = getBackgroundRadius();
c.clipRect(0, 0, layoutWidth, top + rect.height());
if (p.getStrokeWidth() > 0 && p.getStrokeWidth() <= 1) {
p.setStrokeWidth(2);
}
float halfWidth = p.getStrokeWidth() / 2;
c.drawRoundRect(rect.left + halfWidth , rect.top + halfWidth, rect.right - halfWidth, rect.bottom + radius + p.getStrokeWidth(), radius, radius, p);
c.restoreToCount(saveCount);
p.setStyle(Paint.Style.FILL);
p.setStrokeWidth(1);
}
protected boolean drawBorder() {
return false;
}
protected void applyBorderStyle(Paint paint) {
}
protected int getBackgroundColor() {
return Color.parseColor("#E7E7EC");
}
/**
* draw background and border
*
* @param c
* @param p
* @param top
* @param layoutWidth
*/
protected void drawBackground(Canvas c, Paint p, int top, int layoutWidth) {
float startX = 0;
float startY = top + parentHeight;
float stopX = startX + layoutWidth;
// background
p.setAlpha(1);
p.setStyle(Paint.Style.FILL);
rect.set(0, top, (int) stopX, (int) startY);
p.setColor(getBackgroundColor());
drawRectWithTopRound(c, p, rect.left, rect.top, rect.right, rect.bottom, getBackgroundRadius());
// 画分割线
if(drawLine()) {
p.setStrokeWidth(getBorderWidth());
p.setColor(getBorderColor());
c.drawLine(startX, startY, stopX, startY, p);
}
}
protected int getBorderColor() {
return 0xFFD3D8E1;
}
protected int getBorderWidth() {
return 3;
}
protected boolean drawLine() {
return true;
}
protected int getBackgroundRadius() {
return 24;
}
/**
* draw text (Code title)
*
* @param c
* @param p
* @param text
* @param start
* @param top
* @param x
*/
private void drawHeaderText(Canvas c, Paint p, CharSequence text, int start, int top, int x) {
blockTitle = getBlockTitle(text, start);
Paint.FontMetricsInt fm = p.getFontMetricsInt();
int textHeight = fm.bottom - fm.top;
float centerY = (parentHeight - textHeight) / 2f - fm.top;
p.setColor(getTextColor());
boolean isbold = p.isFakeBoldText();
p.setFakeBoldText(true);
p.setTextSize(getTextSize());
c.drawText(blockTitle, x + getHeaderTextLeftPadding(), top + centerY, p);
p.setFakeBoldText(isbold);
}
protected float getTextSize() {
return textSize;
}
protected int getTextColor() {
return 0xFF13113E;
}
protected int getHeaderTextLeftPadding() {
return 0;
}
private void drawCopyIcon(Canvas c, Paint p, int start, int end, int top, int layoutWidth, boolean isDrawMagnifyIcon) {
if (copyIcon == null) return;
float w = (float) copyIcon.getBounds().width();
float left = isDrawMagnifyIcon ? layoutWidth - (w + (w / 4F)) * 2 : layoutWidth - (w + (w / 4F));
CodeBlockTitleSpan.CodeSpanModel copyModel = new CodeBlockTitleSpan.CodeSpanModel();
copyModel.spanStart = start;
copyModel.spanEnd = end;
copyModel.clickRectType = CodeBlockTitleSpan.ClickRectType.TYPE_COPY;
Rect copyRect = new Rect((int) left, top, (int) (left + w), top + parentHeight);
copyRectMap.put(copyRect, copyModel);
final int iconTopPadding = (parentHeight - copyIcon.getBounds().height()) / 2;
c.save();
c.translate(left, top + iconTopPadding);
copyIcon.draw(c);
c.restore();
}
private void drawMagnifyIcon(Canvas c, Paint p, int start, int end, int top, int layoutWidth) {
if (magnifyIcon == null) return;
int rightPadding = getIconRightMargin();
float w = (float) magnifyIcon.getBounds().width();
float left = layoutWidth - w - (w / 4F) - rightPadding;
// 保存copy icon的坐标
CodeBlockTitleSpan.CodeSpanModel magnifyModel = new CodeBlockTitleSpan.CodeSpanModel();
magnifyModel.spanStart = start;
magnifyModel.spanEnd = end;
magnifyModel.clickRectType = CodeBlockTitleSpan.ClickRectType.TYPE_MAGNIFY;
Rect magnifyRect = new Rect((int) left, top, (int) (left + w), top + parentHeight);
copyRectMap.put(magnifyRect, magnifyModel);
// 画magnify icon
final int iconTopPadding = (parentHeight - magnifyIcon.getBounds().height()) / 2;
c.save();
c.translate(left, top + iconTopPadding);
magnifyIcon.draw(c);
c.restore();
}
protected int getIconRightMargin() {
return 0;
}
private void drawRectWithTopRound(Canvas canvas, Paint paint,
float left, float top, float right, float bottom,
float radius) {
Path path = new Path();
path.moveTo(left, top + radius);
path.arcTo(new RectF(left, top, left + 2 * radius, top + 2 * radius), 180, 90);
path.lineTo(right - radius, top);
path.arcTo(new RectF(right - 2 * radius, top, right, top + 2 * radius), 270, 90);
path.lineTo(right, bottom);
path.lineTo(left, bottom);
path.lineTo(left, top + radius);
path.close();
canvas.drawPath(path, paint);
}
protected String getBlockTitle(CharSequence text, int start) {
if (text instanceof Spanned) {
CodeLanguageSpan[] codeLanguageSpans = ((Spanned) text).getSpans(start, text.length(), CodeLanguageSpan.class);
if (codeLanguageSpans == null || codeLanguageSpans.length == 0) {
return "Code";
}
return codeLanguageSpans[codeLanguageSpans.length - 1].getLanguage();
}
return "Code";
}
protected CodeBlockTitleSpan.CodeSpanModel whoClicked(int x, int y) {
for (Map.Entry entry : copyRectMap.entrySet()) {
Rect key = entry.getKey();
CodeBlockTitleSpan.CodeSpanModel value = entry.getValue();
if (x >= key.left && x <= key.right && y >= key.top && y <= key.bottom) {
return value;
}
}
return null;
}
public boolean handleClickEvent(View widget, Spanned spanned, int x, int y) {
return false;
}
/**
* copy
*
* @param widget
* @param codeContent
*/
public void onCopyClicked(final View widget, final String codeContent) {
if (TextUtils.isEmpty(codeContent)) {
MDLogger.e(TAG, "onCopyClicked...code is null");
Toast.makeText(widget.getContext(), R.string.copy_failed, Toast.LENGTH_SHORT).show();
return;
}
MDLogger.d(TAG, "onCopyClicked...");
ClipboardManager clipboard = (ClipboardManager)
widget.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("", codeContent);
clipboard.setPrimaryClip(clip);
Toast.makeText(widget.getContext(), R.string.copied, Toast.LENGTH_SHORT).show();
}
public void onMagnifyClicked(View widget, String content) {
try {
Intent intent = new Intent(widget.getContext(), Class.forName("com.fluid.afm.ui.MarkDownPreviewActivity"));
intent.putExtras(getPreviewBundle(content));
widget.getContext().startActivity(intent);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
protected Bundle getPreviewBundle( String content) {
Bundle bundle = new Bundle();
bundle.putString("content", content);
return bundle;
}
public static class CodeSpanModel {
public int spanStart;
public int spanEnd;
public ClickRectType clickRectType;
@Override
public String toString() {
return spanStart + "," + spanEnd + ",clickRectType:" + clickRectType;
}
}
public enum ClickRectType {
/**
* copy
*/
TYPE_COPY,
/**
* magnify
*/
TYPE_MAGNIFY
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/CodeBlockLineSpacingSpan.java
================================================
package com.fluid.afm;
import android.graphics.Paint;
import android.text.Spanned;
public class CodeBlockLineSpacingSpan extends TopSpacingSpan {
private final int reducedLineHeight;
private final int firstLastLineReducedHeight;
public CodeBlockLineSpacingSpan(int topLineSpacingHeight, int reducedLineHeight) {
super(topLineSpacingHeight);
this.reducedLineHeight = reducedLineHeight;
this.firstLastLineReducedHeight = (int) ((reducedLineHeight - 0.5f) * 6);
}
@Override
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv,
int v, Paint.FontMetricsInt fm) {
super.chooseHeight(text, start, end, spanstartv, v, fm);
fm.ascent += (reducedLineHeight / 2);
fm.descent -= (reducedLineHeight / 2);
if (selfStart(start, text, this)) {
fm.top += firstLastLineReducedHeight;
fm.ascent += firstLastLineReducedHeight;
} else if (selfEnd(start, text, this)) {
fm.bottom -= firstLastLineReducedHeight;
fm.descent -= firstLastLineReducedHeight;
}
}
private boolean selfEnd(int end, CharSequence text, Object span) {
final int spanEnd = ((Spanned) text).getSpanEnd(span);
return spanEnd == end || spanEnd == end - 1;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/CodeBlockTitleSpan.java
================================================
package com.fluid.afm;
import android.content.Context;
import android.graphics.Paint;
import android.os.Bundle;
import android.text.Spanned;
import android.view.View;
import androidx.annotation.Keep;
import androidx.appcompat.content.res.AppCompatResources;
import com.fluid.afm.utils.MDLogger;
import io.noties.markwon.core.MarkwonTheme;
import com.fluid.afm.span.CodeLanguageSpan;
import io.noties.markwon.ext.tables.R;
import com.fluid.afm.utils.Utils;
@Keep
public class CodeBlockTitleSpan extends BaseBlockTitleSpan {
protected static final String TAG = CodeBlockTitleSpan.class.getSimpleName();
private MarkwonTheme mMarkwonTheme;
public CodeBlockTitleSpan(Context context, MarkwonTheme theme, int height, int textSize) {
super(context, height, textSize);
try {
this.mMarkwonTheme = theme;
magnifyIcon = AppCompatResources.getDrawable(context, R.drawable.icon_table_magnify_light);
copyIcon = AppCompatResources.getDrawable(context, R.drawable.icon_table_copy_light);
final int size = Utils.dpToPx(context, 22);
copyIcon.setBounds(0, 0, size, size);
magnifyIcon.setBounds(0, 0, size, size);
} catch (Throwable e) {
MDLogger.d(TAG, "CodeBlockHeaderSpan e:" + e);
}
}
protected int getBackgroundRadius() {
if (mMarkwonTheme != null && mMarkwonTheme.getCodeBackgroundRadius() >= 0) {
return mMarkwonTheme.getCodeBackgroundRadius();
}
return 24;
}
public CodeBlockTitleSpan(Context context, int height, int textSize, int tableIndex) {
super(context, height, textSize, tableIndex);
try {
copyIcon = AppCompatResources.getDrawable(context, R.drawable.icon_table_copy_light);
if (copyIcon != null) {
final int width = Utils.dpToPx(context, 22);
copyIcon.setBounds(0, 0, width, width);
}
} catch (Throwable e) {
MDLogger.e(TAG, e);
}
}
@Override
protected boolean drawBorder() {
return mMarkwonTheme.codeStyle().isDrawBorder();
}
@Override
protected void applyBorderStyle(Paint paint) {
paint.setStyle(Paint.Style.STROKE);
paint.setColor(mMarkwonTheme.codeStyle().borderColor());
paint.setStrokeWidth(mMarkwonTheme.codeStyle().borderWidth());
}
protected String getBlockContent(ClickRectType clickRectType, Spanned spanned, int start) {
StringBuilder stringBuilder = new StringBuilder();
try {
String code = spanned.subSequence(start, spanned.getSpanEnd(this)).toString().trim();
if (clickRectType == ClickRectType.TYPE_MAGNIFY) {
stringBuilder.append("```").append(blockTitle).append(code.replace("\u00A0", "")).append("```");
} else if (clickRectType == ClickRectType.TYPE_COPY) {
stringBuilder.append(code.replace("\u00A0", ""));
}
} catch (Exception e) {
MDLogger.e(TAG, "getBlockContent", e);
}
return stringBuilder.toString();
}
@Override
public boolean handleClickEvent(View widget, Spanned spanned, int x, int y) {
CodeSpanModel spanModel = whoClicked(x, y);
if (spanModel == null) {
MDLogger.d(TAG, "handleClickEvent nobody clicked");
return false;
}
MDLogger.d(TAG, "handleClickEvent:" + spanModel);
String code = getBlockContent(spanModel.clickRectType, spanned, spanModel.spanStart);
if (spanModel.clickRectType == ClickRectType.TYPE_COPY) {
// copy clicked
onCopyClicked(widget, code);
} else if (spanModel.clickRectType == ClickRectType.TYPE_MAGNIFY) {
// magnify clicked
onMagnifyClicked(widget, code);
}
return true;
}
protected String getBlockTitle(CharSequence text, int start) {
if (text instanceof Spanned) {
CodeLanguageSpan[] codeLanguageSpans = ((Spanned) text).getSpans(start, text.length(), CodeLanguageSpan.class);
if (codeLanguageSpans == null || codeLanguageSpans.length == 0) {
return "";
}
return codeLanguageSpans[codeLanguageSpans.length - 1].getLanguage();
}
return "";
}
@Override
protected int getTextColor() {
if (mMarkwonTheme.codeStyle().titleFontColor() != 0) {
return mMarkwonTheme.codeStyle().titleFontColor();
}
return super.getTextColor();
}
@Override
protected float getTextSize() {
if (mMarkwonTheme.codeStyle().titleFontSize() > 0) {
return mMarkwonTheme.codeStyle().titleFontSize();
}
return super.getTextSize();
}
@Override
protected int getBackgroundColor() {
if (mMarkwonTheme.codeStyle().titleBackgroundColor() != 0) {
return mMarkwonTheme.codeStyle().titleBackgroundColor();
}
return super.getBackgroundColor();
}
@Override
protected int getBorderColor() {
if (mMarkwonTheme.codeStyle().borderColor() != 0) {
return mMarkwonTheme.codeStyle().borderColor();
}
return super.getBorderColor();
}
@Override
protected int getBorderWidth() {
return mMarkwonTheme.codeStyle().borderWidth();
}
protected Bundle getPreviewBundle(String content) {
Bundle bundle = super.getPreviewBundle(content);
bundle.putParcelable("codeStyle", mMarkwonTheme.codeStyle());
bundle.putBoolean("isCode", true);
return bundle;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/IMarkdownLayer.java
================================================
package com.fluid.afm;
public interface IMarkdownLayer {
int getViewMaxWidth();
String getOriginText();
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/MarkdownAwareMovementMethod.java
================================================
package com.fluid.afm;
import android.text.Layout;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.fluid.afm.utils.MDLogger;
import io.noties.markwon.image.AsyncDrawableSpan;
/**
* @since 4.6.0
*/
public class MarkdownAwareMovementMethod implements MovementMethod {
private static final String TAG = MarkdownAwareMovementMethod.class.getSimpleName();
@NonNull
public static MarkdownAwareMovementMethod wrap(@NonNull MovementMethod movementMethod) {
return new MarkdownAwareMovementMethod(movementMethod);
}
/**
* Wraps LinkMovementMethod
*/
@NonNull
public static MarkdownAwareMovementMethod create() {
return new MarkdownAwareMovementMethod(LinkMovementMethod.getInstance());
}
public static boolean handleCodeBlockTouchEvent(
@NonNull TextView widget,
@NonNull Spannable buffer,
@NonNull MotionEvent event) {
// handle only action up (originally action down is used in order to handle selection,
// which tables do no have)
if (event.getAction() != MotionEvent.ACTION_UP) {
return false;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
final Layout layout = widget.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);
final CodeBlockTitleSpan[] spans = buffer.getSpans(off, off, CodeBlockTitleSpan.class);
if (spans.length == 0) {
return false;
}
return spans[0].handleClickEvent(widget, buffer, x, y);
}
public static boolean handleTableRowTouchEvent(
@NonNull TextView widget,
@NonNull Spannable buffer,
@NonNull MotionEvent event) {
// handle only action up (originally action down is used in order to handle selection,
// which tables do no have)
if (event.getAction() != MotionEvent.ACTION_UP) {
return false;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
final Layout layout = widget.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);
final TableBlockTitleBlockSpan[] spans = buffer.getSpans(off, off, TableBlockTitleBlockSpan.class);
if (spans.length == 0) {
return false;
}
MDLogger.d(TAG, "handleTableRowTouchEvent length:" + spans.length
+ ",line:" + line
+ ",off:" + off
+ ",x:" + x + ",y:" + y);
return spans[0].handleClickEvent(widget, buffer, x, y);
}
private final MovementMethod wrapped;
public MarkdownAwareMovementMethod(@NonNull MovementMethod wrapped) {
this.wrapped = wrapped;
}
@Override
public void initialize(TextView widget, Spannable text) {
wrapped.initialize(widget, text);
}
@Override
public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
return wrapped.onKeyDown(widget, text, keyCode, event);
}
@Override
public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
return wrapped.onKeyUp(widget, text, keyCode, event);
}
@Override
public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
return wrapped.onKeyOther(view, text, event);
}
@Override
public void onTakeFocus(TextView widget, Spannable text, int direction) {
wrapped.onTakeFocus(widget, text, direction);
}
@Override
public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
return wrapped.onTrackballEvent(widget, text, event);
}
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
// let wrapped handle first, then if super handles nothing, search for table row spans
return wrapped.onTouchEvent(widget, buffer, event)
|| handleTableRowTouchEvent(widget, buffer, event)
|| handleCodeBlockTouchEvent(widget, buffer, event)
|| handleImageTouchEvent(widget, buffer, event);
}
private boolean handleImageTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_UP) {
return false;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
final Layout layout = widget.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);
final AsyncDrawableSpan[] spans = buffer.getSpans(off, off, AsyncDrawableSpan.class);
if (spans.length == 0) {
return false;
}
for (AsyncDrawableSpan span : spans) {
if (span.getTop() <= y && span.getBottom() >= y) {
span.click();
}
}
return false;
}
@Override
public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
return wrapped.onGenericMotionEvent(widget, text, event);
}
@Override
public boolean canSelectArbitrarily() {
return wrapped.canSelectArbitrarily();
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/TableBlockTitleBlockSpan.java
================================================
package com.fluid.afm;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Bundle;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import androidx.appcompat.content.res.AppCompatResources;
import com.fluid.afm.styles.TableStyle;
import com.fluid.afm.utils.MDLogger;
import com.fluid.afm.utils.Utils;
import org.commonmark.ext.gfm.tables.TableBlock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.ext.tables.R;
public class TableBlockTitleBlockSpan extends BaseBlockTitleSpan {
private static final String TAG = "TableBlockTitleBlockSpan";
private final int textLeftPadding;
private final TableStyle mStyle;
private int columnCount;
private boolean countIndex = false;
private TableBlock mTableBlock;
private static TableBlock sCurrentTableBlock;
public static void setCurrentTableBlock(TableBlock currentTableBlock) {
sCurrentTableBlock = currentTableBlock;
}
public static TableBlock getCurrentTableBlock() {
return sCurrentTableBlock;
}
public TableBlockTitleBlockSpan(Context context, int height, TableStyle style, int tableIndex, int columnCount, boolean countIndex, TableBlock tableBlock) {
super(context, height, (int) style.title().fontSize(), tableIndex);
this.countIndex = countIndex;
try {
magnifyIcon = AppCompatResources.getDrawable(context, R.drawable.icon_table_magnify_light);
final int width = Utils.dpToPx(context, 20);
magnifyIcon.setBounds(0, 0, width, width);
this.columnCount = columnCount;
} catch (Throwable e) {
MDLogger.e(TAG, e);
}
mStyle = style;
textLeftPadding = mStyle.cellLeftRightPadding();
mTableBlock = tableBlock;
}
public void setTableBlock(TableBlock tableBlock) {
mTableBlock = tableBlock;
}
@Override
protected int getBackgroundColor() {
return mStyle.title().backgroundColor();
}
protected boolean drawLine() {
return false;
}
@Override
protected float getTextSize() {
return mStyle.title().fontSize();
}
@Override
protected void drawBackground(Canvas c, Paint p, int top, int layoutWidth) {
super.drawBackground(c, p, top, layoutWidth);
if (!mStyle.drawBorder()) {
return;
}
mStyle.applyTableBorderStyle(p);
// 绘制圆角矩形的描边
int saveCount = c.save();
float radius = getBackgroundRadius(); // 圆角半径,应与 drawRectWithTopRound 中使用的值一致
c.clipRect(0, 0, layoutWidth, top + rect.height());
if (p.getStrokeWidth() > 0 && p.getStrokeWidth() <= 1) {
p.setStrokeWidth(2);
}
float halfWidth = p.getStrokeWidth() / 2;
c.drawRoundRect(rect.left + halfWidth, rect.top + halfWidth, rect.right - halfWidth, rect.bottom + radius + p.getStrokeWidth(), radius, radius, p);
c.restoreToCount(saveCount);
// 恢复Paint状态
p.setStyle(Paint.Style.FILL); // 恢复为填充模式
p.setStrokeWidth(1); // 恢复默认线宽
}
@Override
protected boolean drawBorder() {
return mStyle.drawBorder();
}
@Override
protected void applyBorderStyle(Paint paint) {
mStyle.applyTableBorderStyle(paint);
}
@Override
protected int getBorderColor() {
return mStyle.borderColor();
}
protected int getBackgroundRadius() {
return mStyle.titleBackgroundRadius();
}
protected int getIconRightMargin() {
return 16;
}
protected int getHeaderTextLeftPadding() {
return textLeftPadding;
}
/**
* 获取当前table block的语音类型
*
* @param text
* @param start
* @return
*/
protected String getBlockTitle(CharSequence text, int start) {
if (ContextHolder.getContext() != null) {
return ContextHolder.getContext().getResources().getString(R.string.table);
}
return "Table";
}
@Override
protected int getTextColor() {
return mStyle.title().fontColor();
}
protected String getBlockContent(View view) {
return getTableMarkdown(view);
}
@Override
public boolean handleClickEvent(View widget, Spanned spanned, int x, int y) {
CodeSpanModel spanModel = whoClicked(x, y);
if (spanModel == null) {
MDLogger.d(TAG, "handleClickEvent nobody clicked");
return false;
} else {
MDLogger.d(TAG, "handleClickEvent:" + spanModel);
String code = getBlockContent(widget);
if (spanModel.clickRectType == CodeBlockTitleSpan.ClickRectType.TYPE_COPY) {
onCopyClicked(widget, code);
} else if (spanModel.clickRectType == CodeBlockTitleSpan.ClickRectType.TYPE_MAGNIFY) {
onMagnifyClicked(widget, "");
}
return true;
}
}
@Override
public void onMagnifyClicked(View widget, String content) {
try {
Intent intent = new Intent(widget.getContext(), Class.forName("com.fluid.afm.ui.MarkDownPreviewActivity"));
Bundle bundle = new Bundle();
if (mTableBlock != null) {
sCurrentTableBlock = mTableBlock;
}
if (mStyle != null) {
bundle.putBoolean("hasTableStyle",true);
bundle.putParcelable("tableStyle", mStyle);
} else {
bundle.putBoolean("hasTableStyle", false);
}
bundle.putInt("columnCount", columnCount);
bundle.putBoolean("isTable", true);
intent.putExtras(bundle);
widget.getContext().startActivity(intent);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
protected Bundle getPreviewBundle(String content) {
Bundle bundle = super.getPreviewBundle(content);
bundle.putParcelable("tableStyle", mStyle);
bundle.putInt("columnCount", columnCount);
bundle.putBoolean("isTable", true);
return bundle;
}
protected String getTableMarkdown(View view) {
if (!(view instanceof TextView)) {
return null;
} else {
String markdown = null;
if (view instanceof IMarkdownLayer) {
markdown = ((IMarkdownLayer) view).getOriginText();
}
if (TextUtils.isEmpty(markdown)) {
return "";
} else {
String regex = "\\|[^\\n]*\\|(\\n\\|[:\\s\\-\\|]*\\|)+\\n(\\|[^\\n]*\\|\\n?)*";
Pattern pattern = Pattern.compile(regex, Pattern.DOTALL);
Matcher matcher = pattern.matcher(markdown);
int index = tableIndex;
String table = null;
if (countIndex) {
while (index >= 0 && matcher.find()) {
table = matcher.group();
index--;
}
} else if (matcher.find()) {
table = matcher.group();
}
MDLogger.d(TAG, "getTableMarkdown get table markdown: " + table);
return table;
}
}
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/TableLineSpacingSpan.java
================================================
package com.fluid.afm;
import android.graphics.Paint;
import android.text.style.LineHeightSpan;
public class TableLineSpacingSpan implements LineHeightSpan {
private final int reducedLineHeight;
private final boolean header;
public TableLineSpacingSpan(boolean header, int reducedLineHeight) {
this.header = header;
this.reducedLineHeight = reducedLineHeight;
}
@Override
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv,
int v, Paint.FontMetricsInt fm) {
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/com/fluid/afm/TopSpacingSpan.java
================================================
package com.fluid.afm;
import android.graphics.Paint;
import android.text.Spanned;
import android.text.style.LineHeightSpan;
public class TopSpacingSpan implements LineHeightSpan {
private final int height;
public TopSpacingSpan(int height) {
this.height = height;
}
@Override
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv,
int v, Paint.FontMetricsInt fm) {
if (selfStart(start, text, this)) {
fm.top -= height;
fm.ascent -= height;
}
}
protected boolean selfStart(int start, CharSequence text, Object span) {
// this is some kind of interesting magic here... only the last
// span will receive correct _end_ argument, but previous spans
// receive it tilted by one (1). Most likely it's just a new-line character... and
// if needed we could check for that
final int spanStart = ((Spanned) text).getSpanStart(span);
return spanStart == start;
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/Table.java
================================================
package io.noties.markwon.ext.tables;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.CustomNode;
import java.util.ArrayList;
import java.util.List;
import io.noties.markwon.Markwon;
/**
* A class to parse TableBlock and return a data-structure that is not dependent
* on commonmark-java table extension. Can be useful when rendering tables require special
* handling (multiple views, specific table view) for example when used with `markwon-recycler` artifact
*
* @see #parse(Markwon, TableBlock)
* @since 3.0.0
*/
public class Table {
/**
* Factory method to obtain an instance of {@link Table}
*
* @param markwon Markwon
* @param tableBlock TableBlock to parse
* @return parsed {@link Table} or null
*/
@Nullable
public static Table parse(@NonNull Markwon markwon, @NonNull TableBlock tableBlock) {
final Table table;
final ParseVisitor visitor = new ParseVisitor(markwon);
tableBlock.accept(visitor);
final List rows = visitor.rows();
if (rows == null) {
table = null;
} else {
table = new Table(rows);
}
return table;
}
public static class Row {
private final boolean isHeader;
private final List columns;
public Row(
boolean isHeader,
@NonNull List columns) {
this.isHeader = isHeader;
this.columns = columns;
}
public boolean header() {
return isHeader;
}
@NonNull
public List columns() {
return columns;
}
@Override
public String toString() {
return "Row{" +
"isHeader=" + isHeader +
", columns=" + columns +
'}';
}
}
public static class Column {
private final Alignment alignment;
private final Spanned content;
public Column(@NonNull Alignment alignment, @NonNull Spanned content) {
this.alignment = alignment;
this.content = content;
}
@NonNull
public Alignment alignment() {
return alignment;
}
@NonNull
public Spanned content() {
return content;
}
@Override
public String toString() {
return "Column{" +
"alignment=" + alignment +
", content=" + content +
'}';
}
}
public enum Alignment {
LEFT,
CENTER,
RIGHT
}
private final List rows;
public Table(@NonNull List rows) {
this.rows = rows;
}
@NonNull
public List rows() {
return rows;
}
@Override
public String toString() {
return "Table{" +
"rows=" + rows +
'}';
}
static class ParseVisitor extends AbstractVisitor {
private final Markwon markwon;
private List rows;
private List pendingRow;
private boolean pendingRowIsHeader;
ParseVisitor(@NonNull Markwon markwon) {
this.markwon = markwon;
}
@Nullable
public List rows() {
return rows;
}
@Override
public void visit(CustomNode customNode) {
if (customNode instanceof TableCell) {
final TableCell cell = (TableCell) customNode;
if (pendingRow == null) {
pendingRow = new ArrayList<>(2);
}
pendingRow.add(new Table.Column(alignment(cell.getAlignment()), markwon.render(cell)));
pendingRowIsHeader = cell.isHeader();
return;
}
if (customNode instanceof TableHead
|| customNode instanceof TableRow) {
visitChildren(customNode);
// this can happen, ignore such row
if (pendingRow != null && pendingRow.size() > 0) {
if (rows == null) {
rows = new ArrayList<>(2);
}
rows.add(new Table.Row(pendingRowIsHeader, pendingRow));
}
pendingRow = null;
pendingRowIsHeader = false;
return;
}
visitChildren(customNode);
}
@NonNull
private static Table.Alignment alignment(@NonNull TableCell.Alignment alignment) {
final Table.Alignment out;
if (TableCell.Alignment.RIGHT == alignment) {
out = Table.Alignment.RIGHT;
} else if (TableCell.Alignment.CENTER == alignment) {
out = Table.Alignment.CENTER;
} else {
out = Table.Alignment.LEFT;
}
return out;
}
}
}
================================================
FILE: Android/AntFluid/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TablePlugin.java
================================================
package io.noties.markwon.ext.tables;
import android.content.Context;
import android.text.Spanned;
import android.text.TextUtils;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.fluid.afm.StreamOutStateObserver;
import com.fluid.afm.TableBlockTitleBlockSpan;
import com.fluid.afm.TableLineSpacingSpan;
import com.fluid.afm.TopSpacingSpan;
import com.fluid.afm.span.ParagraphSpacingSpan;
import com.fluid.afm.styles.TableStyle;
import com.fluid.afm.utils.MDLogger;
import com.fluid.afm.utils.Utils;
import org.commonmark.Extension;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.ext.gfm.tables.TableBody;
import org.commonmark.ext.gfm.tables.TableCell;
import org.commonmark.ext.gfm.tables.TableHead;
import org.commonmark.ext.gfm.tables.TableRow;
import org.commonmark.ext.gfm.tables.internal.TableHtmlNodeRenderer;
import org.commonmark.ext.gfm.tables.internal.TableTextContentNodeRenderer;
import org.commonmark.node.Block;
import org.commonmark.node.Document;
import org.commonmark.node.Node;
import org.commonmark.parser.InlineParser;
import org.commonmark.parser.Parser;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
import org.commonmark.renderer.html.HtmlRenderer;
import org.commonmark.renderer.text.TextContentRenderer;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SpannableBuilder;
import io.noties.markwon.core.MarkwonTheme;
/**
* @since 3.0.0
*/
public class TablePlugin extends AbstractMarkwonPlugin implements StreamOutStateObserver {
private static final String TAG = "MD_TablePlugin";
private final TableVisitor visitor;
@NonNull
public static TablePlugin create(@NonNull Context context, boolean isHideHeader) {
return new TablePlugin(context, isHideHeader);
}
@NonNull
public static TablePlugin create(Context context) {
return new TablePlugin(context);
}
@SuppressWarnings("WeakerAccess")
TablePlugin( Context context) {
this.visitor = new TableVisitor(context);
}
TablePlugin(Context context, boolean isHideHeader) {
this.visitor = new TableVisitor(context, isHideHeader);
}
@Override
public void onStreamOutStateChanged(boolean isStreamingOutput) {
visitor.setStreamingOutput(isStreamingOutput);
if (!isStreamingOutput) {
visitor.mTableSpanCache.clear();
}
}
@Override
public void configure(@NonNull Registry registry) {
super.configure(registry);
}
@Override
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
super.configureTheme(builder);
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
super.configureConfiguration(builder);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
super.configureSpansFactory(builder);
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
visitor.originMarkdown = markdown;
return super.processMarkdown(markdown);
}
@Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
super.afterRender(node, visitor);
MDLogger.d(TAG, "afterRender node.getNext = " + node.getNext());
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.extensions(Collections.singleton(MyTablesExtension.create()));
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
visitor.configure(builder);
}
@Override
public void beforeRender(@NonNull Node node) {
// clear before rendering (as visitor has some internal mutable state)
visitor.clear();
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
TableRowsScheduler.unschedule(textView);
}
@Override
public void afterSetText(@NonNull TextView textView) {
TableRowsScheduler.schedule(textView);
}
private static class TableVisitor {
public ConcurrentHashMap mTableIndexMap = new ConcurrentHashMap<>(); // key: all content String index, value: table index
public ConcurrentHashMap> mPendingTableRow = new ConcurrentHashMap<>(); // key: table index, value: blockLength
public ConcurrentHashMap mTableblockLength = new ConcurrentHashMap<>(); // key: table index, value: blockLength
public ConcurrentHashMap mTableIsHeader = new ConcurrentHashMap<>(); // key: table index
public ConcurrentHashMap mTableRows = new ConcurrentHashMap<>(); // key: table index
public ConcurrentHashMap> mPendingTableRowSpanList = new ConcurrentHashMap<>(); // key: table index
private final int tableReducedLineHeight = Utils.dpToPx(2);
private final WeakReference