com.vladsch.flexmark.formatter.internal.TranslationHandlerImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of flexmark Show documentation
Show all versions of flexmark Show documentation
Core of flexmark-java (implementation of CommonMark for parsing markdown and rendering to HTML)
package com.vladsch.flexmark.formatter.internal;
import com.vladsch.flexmark.ast.AnchorRefTarget;
import com.vladsch.flexmark.formatter.*;
import com.vladsch.flexmark.html.renderer.HtmlIdGenerator;
import com.vladsch.flexmark.html.renderer.HtmlIdGeneratorFactory;
import com.vladsch.flexmark.util.ast.Document;
import com.vladsch.flexmark.util.ast.Node;
import com.vladsch.flexmark.util.data.DataHolder;
import com.vladsch.flexmark.util.data.MutableDataSet;
import com.vladsch.flexmark.util.sequence.BasedSequence;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import static com.vladsch.flexmark.formatter.RenderPurpose.TRANSLATED_SPANS;
import static java.lang.Character.isWhitespace;
public class TranslationHandlerImpl implements TranslationHandler {
final FormatterOptions myFormatterOptions;
final HashMap myNonTranslatingTexts; // map placeholder to non-translating text replaced before translation so it can be replaced after translation
final HashMap myAnchorTexts; // map anchor id to non-translating text replaced before translation so it can be replaced after translation
final HashMap myTranslatingTexts; // map placeholder to translating original text which is to be translated separately from its context and is replaced with placeholder for main context translation
final HashMap myTranslatedTexts; // map placeholder to translated text which is to be translated separately from its context and was replaced with placeholder for main context translation
final ArrayList myTranslatingPlaceholders; // list of placeholders to index in translating and translated texts
final ArrayList myTranslatingSpans;
final ArrayList myNonTranslatingSpans;
final ArrayList myTranslatedSpans;
final HtmlIdGeneratorFactory myIdGeneratorFactory;
final Pattern myPlaceHolderMarkerPattern;
final MutableDataSet myTranslationStore;
final HashMap myOriginalRefTargets; // map ref target id to translation index
final HashMap myTranslatedRefTargets; // map translation index to translated ref target id
final HashMap myOriginalAnchors; // map placeholder id to original ref id
final HashMap myTranslatedAnchors; // map placeholder id to translated ref target id
private int myPlaceholderId = 0;
private int myAnchorId = 0;
private int myTranslatingSpanId = 0;
private int myNonTranslatingSpanId = 0;
private RenderPurpose myRenderPurpose;
private MarkdownWriter myWriter;
private HtmlIdGenerator myIdGenerator;
private TranslationPlaceholderGenerator myPlaceholderGenerator;
private Function myNonTranslatingPostProcessor = null;
private MergeContext myMergeContext = null;
public TranslationHandlerImpl(DataHolder options, HtmlIdGeneratorFactory idGeneratorFactory) {
myFormatterOptions = new FormatterOptions(options);
myIdGeneratorFactory = idGeneratorFactory;
myNonTranslatingTexts = new HashMap<>();
myAnchorTexts = new HashMap<>();
myTranslatingTexts = new HashMap<>();
myTranslatedTexts = new HashMap<>();
myOriginalAnchors = new HashMap<>();
myTranslatedAnchors = new HashMap<>();
myTranslatedRefTargets = new HashMap<>();
myOriginalRefTargets = new HashMap<>();
myTranslatingSpans = new ArrayList<>();
myTranslatedSpans = new ArrayList<>();
myTranslatingPlaceholders = new ArrayList<>();
myNonTranslatingSpans = new ArrayList<>();
myPlaceHolderMarkerPattern = Pattern.compile(myFormatterOptions.translationExcludePattern); //Pattern.compile("^[\\[\\](){}<>]*_{1,2}\\d+_[\\[\\](){}<>]*$");
myTranslationStore = new MutableDataSet();
}
@Override
public MergeContext getMergeContext() {
return myMergeContext;
}
@Override
public void setMergeContext(@NotNull MergeContext context) {
myMergeContext = context;
}
@NotNull
@Override
public MutableDataSet getTranslationStore() {
return myTranslationStore;
}
@Override
public HtmlIdGenerator getIdGenerator() {
return myIdGenerator;
}
@Override
public void beginRendering(@NotNull Document node, @NotNull NodeFormatterContext context, @NotNull MarkdownWriter appendable) {
// collect anchor ref ids
myWriter = appendable;
myIdGenerator = myIdGeneratorFactory.create();
myIdGenerator.generateIds(node);
}
static boolean isNotBlank(CharSequence csq) {
int iMax = csq.length();
for (int i = 0; i < iMax; i++) {
if (!isWhitespace(csq.charAt(i))) return true;
}
return false;
}
@NotNull
@Override
public List getTranslatingTexts() {
myTranslatingPlaceholders.clear();
myTranslatingPlaceholders.ensureCapacity(myTranslatedSpans.size() + myTranslatedTexts.size());
ArrayList translatingSnippets = new ArrayList<>(myTranslatedSpans.size() + myTranslatedTexts.size());
HashMap repeatedTranslatingIndices = new HashMap<>();
// collect all the translating snippets first
for (Map.Entry entry : myTranslatingTexts.entrySet()) {
if (isNotBlank(entry.getValue()) && !myPlaceHolderMarkerPattern.matcher(entry.getValue()).matches()) {
// see if it is repeating
if (!repeatedTranslatingIndices.containsKey(entry.getValue())) {
// new, index
repeatedTranslatingIndices.put(entry.getValue(), translatingSnippets.size());
translatingSnippets.add(entry.getValue());
myTranslatingPlaceholders.add(entry.getKey());
}
}
}
for (CharSequence text : myTranslatingSpans) {
if (isNotBlank(text) && !myPlaceHolderMarkerPattern.matcher(text).matches()) {
translatingSnippets.add(text.toString());
}
}
return translatingSnippets;
}
@Override
public void setTranslatedTexts(@NotNull List extends CharSequence> translatedTexts) {
myTranslatedTexts.clear();
myTranslatedTexts.putAll(myTranslatingTexts);
myTranslatedSpans.clear();
myTranslatedSpans.ensureCapacity(myTranslatingSpans.size());
// collect all the translating snippets first
int i = 0;
int iMax = translatedTexts.size();
int placeholderSize = myTranslatingPlaceholders.size();
HashMap repeatedTranslatingIndices = new HashMap<>();
for (Map.Entry entry : myTranslatingTexts.entrySet()) {
if (isNotBlank(entry.getValue()) && !myPlaceHolderMarkerPattern.matcher(entry.getValue()).matches()) {
Integer index = repeatedTranslatingIndices.get(entry.getValue());
if (index == null) {
if (i >= placeholderSize) break;
// new, index
repeatedTranslatingIndices.put(entry.getValue(), i);
myTranslatedTexts.put(entry.getKey(), translatedTexts.get(i).toString());
i++;
} else {
myTranslatedTexts.put(entry.getKey(), translatedTexts.get(index).toString());
}
// } else {
// // already has the same value
}
}
for (CharSequence text : myTranslatingSpans) {
if (isNotBlank(text) && !myPlaceHolderMarkerPattern.matcher(text).matches()) {
myTranslatedSpans.add(translatedTexts.get(i).toString());
i++;
} else {
// add original blank sequence
myTranslatedSpans.add(text.toString());
}
}
}
@Override
public void setRenderPurpose(@NotNull RenderPurpose renderPurpose) {
myAnchorId = 0;
myTranslatingSpanId = 0;
myPlaceholderId = 0;
myRenderPurpose = renderPurpose;
myNonTranslatingSpanId = 0;
}
@NotNull
@Override
public RenderPurpose getRenderPurpose() {
return myRenderPurpose;
}
@Override
public boolean isTransformingText() {
return myRenderPurpose != RenderPurpose.FORMAT;
}
@NotNull
@Override
public CharSequence transformAnchorRef(@NotNull CharSequence pageRef, @NotNull CharSequence anchorRef) {
switch (myRenderPurpose) {
case TRANSLATION_SPANS:
String replacedTextId = String.format(myFormatterOptions.translationIdFormat, ++myAnchorId);
myAnchorTexts.put(replacedTextId, anchorRef.toString());
return replacedTextId;
case TRANSLATED_SPANS:
return String.format(myFormatterOptions.translationIdFormat, ++myAnchorId);
case TRANSLATED:
String anchorIdText = String.format(myFormatterOptions.translationIdFormat, ++myAnchorId);
String resolvedPageRef = myNonTranslatingTexts.get(pageRef.toString());
if (resolvedPageRef != null && resolvedPageRef.length() == 0) {
// self reference, add it to the list
String refId = myAnchorTexts.get(anchorIdText);
if (refId != null) {
// original ref id for the heading we should have them all
Integer spanIndex = myOriginalRefTargets.get(refId);
if (spanIndex != null) {
// have the index to translatingSpans
String translatedRefId = myTranslatedRefTargets.get(spanIndex);
if (translatedRefId != null) {
return translatedRefId;
}
}
return refId;
}
} else {
String resolvedAnchorRef = myAnchorTexts.get(anchorIdText);
if (resolvedAnchorRef != null) {
return resolvedAnchorRef;
}
}
case FORMAT:
default:
return anchorRef;
}
}
@Override
public void customPlaceholderFormat(@NotNull TranslationPlaceholderGenerator generator, @NotNull TranslatingSpanRender render) {
if (myRenderPurpose != TRANSLATED_SPANS) {
TranslationPlaceholderGenerator savedGenerator = myPlaceholderGenerator;
myPlaceholderGenerator = generator;
render.render(myWriter.getContext(), myWriter);
myPlaceholderGenerator = savedGenerator;
}
}
@Override
public void translatingSpan(@NotNull TranslatingSpanRender render) {
translatingRefTargetSpan(null, render);
}
private String renderInSubContext(TranslatingSpanRender render, boolean copyToMain) {
StringBuilder span = new StringBuilder();
MarkdownWriter savedMarkdown = myWriter;
NodeFormatterContext subContext = myWriter.getContext().getSubContext();
MarkdownWriter writer = subContext.getMarkdown();
myWriter = writer;
render.render(subContext, writer);
// trim off eol added by toString(0)
String spanText = writer.toString(2, -1);
myWriter = savedMarkdown;
if (copyToMain) {
myWriter.append(spanText);
}
return spanText;
}
@Override
public void translatingRefTargetSpan(@Nullable Node target, @NotNull TranslatingSpanRender render) {
switch (myRenderPurpose) {
case TRANSLATION_SPANS: {
String spanText = renderInSubContext(render, true);
if (target != null) {
if (!(target instanceof AnchorRefTarget) || !((AnchorRefTarget) target).isExplicitAnchorRefId()) {
String id = myIdGenerator.getId(target);
myOriginalRefTargets.put(id, myTranslatingSpans.size());
}
}
myTranslatingSpans.add(spanText);
return;
}
case TRANSLATED_SPANS: {
// we output translated text instead of render
renderInSubContext(render, false);
String translated = myTranslatedSpans.get(myTranslatingSpanId);
if (target != null) {
if (!(target instanceof AnchorRefTarget) || !((AnchorRefTarget) target).isExplicitAnchorRefId()) {
// only if does not have an explicit id then map to translated text id
String id = myIdGenerator.getId(translated);
myTranslatedRefTargets.put(myTranslatingSpanId, id);
//} else {
// myTranslatedRefTargets.remove(myTranslatingSpanId);
}
}
myTranslatingSpanId++;
myWriter.append(translated);
return;
}
case TRANSLATED:
if (target != null) {
if (!(target instanceof AnchorRefTarget) || !((AnchorRefTarget) target).isExplicitAnchorRefId()) {
// only if does not have an explicit id then map to translated text id
String id = myIdGenerator.getId(target);
myTranslatedRefTargets.put(myTranslatingSpanId, id);
//} else {
// myTranslatedRefTargets.remove(myTranslatingSpanId);
}
}
myTranslatingSpanId++;
renderInSubContext(render, true);
return;
case FORMAT:
default:
render.render(myWriter.getContext(), myWriter);
}
}
public void nonTranslatingSpan(@NotNull TranslatingSpanRender render) {
switch (myRenderPurpose) {
case TRANSLATION_SPANS: {
String spanText = renderInSubContext(render, false);
myNonTranslatingSpans.add(spanText);
myNonTranslatingSpanId++;
String replacedTextId = getPlaceholderId(myFormatterOptions.translationIdFormat, myNonTranslatingSpanId, null, null, null);
myWriter.append(replacedTextId);
return;
}
case TRANSLATED_SPANS: {
// we output translated text instead of render
renderInSubContext(render, false);
String translated = myNonTranslatingSpans.get(myNonTranslatingSpanId);
myNonTranslatingSpanId++;
myWriter.append(translated);
return;
}
case TRANSLATED: {
// we output translated text instead of render
renderInSubContext(render, true);
myNonTranslatingSpanId++;
return;
}
case FORMAT:
default:
render.render(myWriter.getContext(), myWriter);
}
}
public String getPlaceholderId(String format, int placeholderId, CharSequence prefix, CharSequence suffix, CharSequence suffix2) {
String replacedTextId = myPlaceholderGenerator != null ? myPlaceholderGenerator.getPlaceholder(placeholderId) : String.format(format, placeholderId);
if (prefix == null && suffix == null && suffix2 == null) return replacedTextId;
return addPrefixSuffix(replacedTextId, prefix, suffix, suffix2);
}
public static String addPrefixSuffix(CharSequence placeholderId, CharSequence prefix, CharSequence suffix, CharSequence suffix2) {
if (prefix == null && suffix == null && suffix2 == null) return placeholderId.toString();
StringBuilder sb = new StringBuilder();
if (prefix != null) sb.append(prefix);
sb.append(placeholderId);
if (suffix != null) sb.append(suffix);
if (suffix2 != null) sb.append(suffix2);
return sb.toString();
}
@Override
public void postProcessNonTranslating(@NotNull Function postProcessor, @NotNull Runnable scope) {
Function savedValue = myNonTranslatingPostProcessor;
try {
myNonTranslatingPostProcessor = postProcessor;
scope.run();
} finally {
myNonTranslatingPostProcessor = savedValue;
}
}
@NotNull
@Override
public T postProcessNonTranslating(@NotNull Function postProcessor, @NotNull Supplier scope) {
Function savedValue = myNonTranslatingPostProcessor;
try {
myNonTranslatingPostProcessor = postProcessor;
return scope.get();
} finally {
myNonTranslatingPostProcessor = savedValue;
}
}
@Override
public boolean isPostProcessingNonTranslating() {
return myNonTranslatingPostProcessor != null;
}
@NotNull
@Override
public CharSequence transformNonTranslating(CharSequence prefix, @NotNull CharSequence nonTranslatingText, CharSequence suffix, CharSequence suffix2) {
// need to transfer trailing EOLs to id
CharSequence trimmedEOL;
if (suffix2 != null) {
trimmedEOL = suffix2;
} else {
BasedSequence basedSequence = BasedSequence.of(nonTranslatingText);
trimmedEOL = basedSequence.trimmedEOL();
}
switch (myRenderPurpose) {
case TRANSLATION_SPANS:
String replacedTextId = getPlaceholderId(myFormatterOptions.translationIdFormat, ++myPlaceholderId, prefix, suffix, trimmedEOL);
String useReplacedTextId = replacedTextId;
if (myNonTranslatingPostProcessor != null) {
useReplacedTextId = myNonTranslatingPostProcessor.apply(replacedTextId).toString();
}
myNonTranslatingTexts.put(useReplacedTextId, nonTranslatingText.toString());
return useReplacedTextId;
case TRANSLATED_SPANS:
String placeholderId = getPlaceholderId(myFormatterOptions.translationIdFormat, ++myPlaceholderId, prefix, suffix, trimmedEOL);
if (myNonTranslatingPostProcessor != null) {
return myNonTranslatingPostProcessor.apply(placeholderId);
} else {
return placeholderId;
}
case TRANSLATED:
if (nonTranslatingText.length() > 0) {
String text = myNonTranslatingTexts.get(nonTranslatingText.toString());
if (text == null) {
text = "";
}
if (myNonTranslatingPostProcessor != null) {
return myNonTranslatingPostProcessor.apply(text);
}
return text;
}
return nonTranslatingText;
case FORMAT:
default:
return nonTranslatingText;
}
}
@NotNull
@Override
public CharSequence transformTranslating(CharSequence prefix, @NotNull CharSequence translatingText, CharSequence suffix, CharSequence suffix2) {
switch (myRenderPurpose) {
case TRANSLATION_SPANS:
String replacedTextId = getPlaceholderId(myFormatterOptions.translationIdFormat, ++myPlaceholderId, prefix, suffix, suffix2);
myTranslatingTexts.put(replacedTextId, translatingText.toString());
return replacedTextId;
case TRANSLATED_SPANS:
return getPlaceholderId(myFormatterOptions.translationIdFormat, ++myPlaceholderId, prefix, suffix, suffix2);
case TRANSLATED:
CharSequence replacedText = prefix == null && suffix == null && suffix2 == null ? translatingText : addPrefixSuffix(translatingText, prefix, suffix, suffix2);
CharSequence text = myTranslatedTexts.get(replacedText.toString());
if (text != null && !(prefix == null && suffix == null && suffix2 == null)) {
return addPrefixSuffix(text, prefix, suffix, suffix2);
}
if (text != null) {
return text;
}
case FORMAT:
default:
return translatingText;
}
}
}