Instantly share code, notes, and snippets.
Last active
July 10, 2017 20:57
-
Save yu-tang/30cf1e94772ddcabaccf to your computer and use it in GitHub Desktop.
adapt_tags_to_match_target.groovy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* :name=Adapt standard tags :description=Adapt standard tags when Replace with Match command invoked | |
* | |
* The workaround by script for RFE #841: | |
* Adapt tags to match target | |
* http://sourceforge.net/p/omegat/feature-requests/841/ | |
* | |
* | Editor | Match | |
* -------+--------------+-------------- | |
* Source | <a1>foo</a1> | <a9>foo</a9> | |
* -------+--------------+-------------- | |
* Target | <a9>bar</a9> | <a9>bar</a9> | |
* | <--adapt | |
* <a1>bar</a1> | |
* | |
* Note: | |
* This script does NOT cover user defined custom tags. Only take OmegaT standard tags. | |
* | |
* @author Yu Tang | |
* @date 2017-03-11 | |
* @version 0.3.11 | |
*/ | |
package me.goat.groovy.scripting | |
import groovy.text.Template | |
import org.codehaus.groovy.runtime.GStringImpl | |
import org.omegat.core.Core | |
import org.omegat.core.data.ExternalTMX | |
import org.omegat.core.matching.NearString | |
import org.omegat.util.TagUtil.TagType | |
import javax.swing.SwingUtilities | |
import java.awt.event.ActionEvent | |
import java.awt.event.ActionListener | |
import java.util.regex.Matcher | |
import static org.omegat.util.PatternConsts.OMEGAT_TAG | |
import static org.omegat.util.PatternConsts.OMEGAT_TAG_DECOMPILE | |
def menuItem = mainWindow.mainMenu.editOverwriteTranslationMenuItem | |
RemoveOldTagAdapters(menuItem) // just in case | |
TagAdapter adapter = new TagAdapter() | |
//test(adapter) | |
menuItem.addActionListener adapter | |
"adapt_tags_to_match_target script is available in current session." | |
class TagAdapter implements ActionListener { | |
static private List<Closure> adapters = [this.&adaptCurrentSourceHasNoTags, | |
this.&adaptEachTags1to1, | |
this.&adaptTagBlocks, | |
this.&removeNonExistentTags] | |
@Override | |
void actionPerformed(ActionEvent actionEvent) { | |
SwingUtilities.invokeLater({ | |
NearString near = Core.matcher.activeMatch | |
if (near != null) { | |
def currentTranslation = Core.editor.currentTranslation | |
def currentSource = Core.editor.currentEntry.srcText | |
//def matchTranslation = near.translation | |
def matchSource = near.source | |
String adapted = adapt(currentSource, matchSource, currentTranslation) | |
// replace an edit text with a tag adapted text | |
if (currentTranslation != adapted) { | |
replaceEditText(adapted, near) | |
} | |
} | |
} as Runnable) | |
} | |
static void replaceEditText(String adapted, NearString near) { | |
if (near.comesFrom == NearString.MATCH_SOURCE.TM | |
&& ExternalTMX.isInPath(new File(Core.project.projectProperties.TMRoot, 'mt'), | |
new File(near.projs[0]))) { | |
Core.editor.replaceEditTextAndMark adapted | |
} else { | |
Core.editor.replaceEditText adapted | |
} | |
Core.editor.requestFocus() | |
} | |
static String adapt(String currentSource, String matchSource, String currentTranslation) { | |
String result = null | |
use(TagStringCategory) { | |
if (currentTranslation.hasStandardTags()) { | |
result = adapters.findResult { Closure adapter -> | |
adapter(currentSource,matchSource, currentTranslation) | |
} | |
} | |
} | |
result ?: currentTranslation | |
} | |
static private Map<String, String> toMap(List<String> keys, List<String> values) { | |
[keys, values].transpose().flatten().toSpreadMap() | |
} | |
/** | |
* Adapter for the case of the current source has no tags. | |
* | |
* | Editor | Match | |
* -------+--------------+-------------- | |
* Source | foo | <a9>foo</a9> | |
* -------+--------------+-------------- | |
* Target | <a9>bar</a9> | <a9>bar</a9> | |
* | <--adapt | |
* bar | |
* | |
* @return an adjusted translation or null | |
*/ | |
static private String adaptCurrentSourceHasNoTags(String currentSource, String matchSource, String currentTranslation) { | |
if (! currentSource.hasStandardTags()) { | |
currentTranslation.replaceAll(OMEGAT_TAG, "") | |
} | |
} | |
/** | |
* Adapter for the case of 1-to-1 strictly tag matching. | |
* | |
* | Editor | Match | |
* -------+----------------+-------------- | |
* Source | <a1>foo</a1> | <a9>a foo</a9> | |
* -------+----------------+-------------- | |
* Target | <a9>a bar</a9> | <a9>a bar</a9> | |
* | <--adapt | |
* <a1>a bar</a1> | |
* | |
* @return an adjusted translation or null | |
*/ | |
static private String adaptEachTags1to1(String currentSource, String matchSource, String currentTranslation) { | |
List<String> keys = matchSource.tags | |
List<String> values = currentSource.tags | |
if (! correspondExactly(keys, values)) { | |
return null | |
} | |
Map<String, String> map = toMap(keys, values) | |
def taggedString = new TagTemplate(currentTranslation) | |
taggedString.make(map) | |
} | |
/** | |
* Check if tags1 list corresponds exactly to tags2 list. | |
* They must be; | |
* 1. not empty | |
* 2. same size | |
* 3. same tag type order | |
* 4. same tag name order | |
* | |
* Examples) | |
* correspondExactly([<a1>, <s2>, </s2>, </a1>], | |
* [<a6>, <s7>, </s7>, </a6>]) // true | |
* correspondExactly([], []) // false (empty list) | |
* correspondExactly([<a1>, <s2>, </s2>, </a1>], | |
* [<a1>, <s2>, </s2>]) // false (size un-match) | |
* correspondExactly([<a1>, <a2>, </a2>, </a1>], | |
* [<a1>, <a2/>, <a2>, </a1>]) // false (type order un-match) | |
* correspondExactly([<a1>, <s2>, </s2>, </a1>], | |
* [<a1>, <s2>, </a1>, </s2>]) // false (name order un-match) | |
* @param tags1 | |
* @param tags2 | |
* @return | |
*/ | |
static private boolean correspondExactly(List<String> tags1, List<String> tags2) { | |
// Empty or different sizes? | |
if (tags1.isEmpty() || tags2.isEmpty() || tags1.size() != tags2.size()) { | |
return false | |
} | |
// same order for tag types and tag names? | |
Map t1IndexOf, t2IndexOf | |
t1IndexOf = [:].withDefault { t1IndexOf.size() } | |
t2IndexOf = [:].withDefault { t2IndexOf.size() } | |
[tags1, tags2].transpose().every { String tag1, String tag2 -> | |
(tag1.type == tag2.type) && (t1IndexOf[tag1.name] == t2IndexOf[tag2.name]) | |
} | |
} | |
/** | |
* Adapter for the case of the tag block matching between current source and match source. | |
* | |
* | Editor | Match | |
* -------+--------------------+------------------- | |
* Source | <a1>foo</a1> | <a9><f10/>foo</a9> | |
* -------+--------------------+------------------- | |
* Target | <a9><f10/>bar</a9> | <a9><f10/>bar</a9> | |
* | <--adapt | |
* <a1>bar</a1> | |
* | |
* @return an adjusted translation or null | |
*/ | |
static private String adaptTagBlocks(String currentSource, String matchSource, String currentTranslation) { | |
if (currentSource.withoutTags != matchSource.withoutTags) { | |
return null | |
} | |
// Check if currentSource tag blocks corresponds exactly to matchSource tag blocks. | |
Map<Integer, String> currentSourceTagBlockInfos = currentSource.tagBlockInfos | |
Map<Integer, String> matchSourceTagBlockInfos = matchSource.tagBlockInfos | |
if (! matchSourceTagBlockInfos.keySet().containsAll(currentSourceTagBlockInfos.keySet())) { | |
return null | |
} | |
// create a recursive Map from matchSource to currentSource | |
Map<String, String> map = toFlexMap(matchSourceTagBlockInfos, currentSourceTagBlockInfos) | |
// create a template and apply the Map | |
def taggedString = new TagTemplate(currentTranslation, { Matcher tag, String beforeTag, int prevTagEnd -> | |
if (!delegate._binding.isEmpty() && tag.start() == prevTagEnd) { | |
// a continuous tag in a block | |
delegate._binding << (delegate._binding.pop() + tag.group()) | |
} else { | |
// begin new tag block | |
delegate._strings << beforeTag | |
delegate._binding << tag.group() | |
} | |
}) | |
taggedString.make(map) | |
} | |
/** | |
* Transform KeyMap and ValueMap into a FlexMap. | |
* | |
* ex. map == [a:x, b:y, c:z] | |
* map[a] //-> x | |
* map[ab] //-> xy | |
* map[cba] //-> zyx | |
* | |
* @param keyBlockInfos | |
* @param valueBlockInfos | |
* @return | |
*/ | |
static private Map<String, String> toFlexMap(Map<Integer, String> keyBlockInfos, Map<Integer, String> valueBlockInfos) { | |
Map<String, String> map = new FlexMap() | |
keyBlockInfos.each {Integer k, String v -> | |
map[v] = valueBlockInfos[k] // the value can be empty string if it's not found in current source. | |
} | |
map | |
} | |
/** | |
* Adapter for removing non-existent tags from the current translation against current source. | |
* | |
* | Editor | Match | |
* -------+--------------+-------------- | |
* Source | foo | <a1>foo</a1> | |
* -------+--------------+-------------- | |
* Target | <a1>bar</a1> | <a1>bar</a1> | |
* | <--adapt | |
* bar | |
* | |
* @return an adjusted translation or null | |
*/ | |
static private String removeNonExistentTags(String currentSource, String matchSource, String currentTranslation) { | |
if (! currentTranslation.hasStandardTags()) { | |
return null | |
} | |
// A Map: key and value pair has the same tag string | |
Map<String, String> map = currentSource.tags.collectEntries([:].withDefault {""}) { | |
[(it):it] | |
} | |
// create a template and apply the Map | |
def taggedString = new TagTemplate(currentTranslation) | |
taggedString.make(map) | |
} | |
} | |
@Category(String) | |
class TagStringCategory { | |
boolean hasStandardTags() { | |
OMEGAT_TAG.matcher(this).find() | |
} | |
List<String> getTags() { | |
this.findAll(OMEGAT_TAG) | |
} | |
String getWithoutTags() { | |
this.replaceAll(OMEGAT_TAG, "") | |
} | |
TagType getType() { | |
this[-2] == "/" ? TagType.SINGLE : | |
this[1] == "/" ? TagType.END : | |
TagType.START | |
} | |
String getName() { | |
def m = OMEGAT_TAG_DECOMPILE.matcher(this) | |
m.matches() ? m.group(2) + m.group(3) : this | |
} | |
/** | |
* Extract tag blocks and their relative positions for the display text. | |
* @return A map constructed by block positions as keys and tag block text as values | |
*/ | |
Map<Integer, String> getTagBlockInfos() { | |
Map<Integer, String> result = [:].withDefault {""} | |
def (int position, int prevTagEnd) = [0, 0] | |
Matcher tag = OMEGAT_TAG.matcher(this) | |
while (tag.find()) { | |
if (!result.isEmpty() && tag.start() == prevTagEnd) { | |
// a continuous tag in a block | |
result[position] += tag.group() | |
} else { | |
// begin new tag block | |
position += tag.start() - prevTagEnd // update current position | |
result[position] = tag.group() | |
} | |
prevTagEnd = tag.end() | |
} | |
result | |
} | |
/** Output a string to the console in Scripting window for debug. */ | |
void print() { | |
org.omegat.gui.scripting.ScriptingWindow.window.logResult(this) | |
} | |
} | |
/** | |
* Represents a String which contains OmegaT standard tags as placeholders. | |
*/ | |
class TagTemplate implements Template { | |
final List<String> _strings = [] | |
final List<String> _binding = [] | |
TagTemplate(String text) { | |
this(text, { Matcher tag, String beforeTag -> | |
delegate._strings << beforeTag | |
delegate._binding << tag.group() | |
}) | |
} | |
TagTemplate(String text, Closure tagProcessor) { | |
tagProcessor.delegate = this | |
tagProcessor.resolveStrategy = Closure.DELEGATE_ONLY | |
Matcher tag = OMEGAT_TAG.matcher(text) | |
int prevTagEnd = 0 | |
while (tag.find()) { | |
String beforeTag = text[prevTagEnd..<tag.start()] | |
switch (tagProcessor.maximumNumberOfParameters) { | |
case 2: tagProcessor(tag, beforeTag); break | |
case 3: tagProcessor(tag, beforeTag, prevTagEnd); break | |
default: throw new IllegalArgumentException("Class TagTemplate constructor is applicable for 2 or 3 arguments Closure.") | |
} | |
prevTagEnd = tag.end() | |
} | |
if (prevTagEnd < text.size()) { | |
_strings << text[prevTagEnd..<text.size()] | |
} | |
} | |
@Override | |
public String toString() { | |
getGString() | |
} | |
@Override | |
public Writable make() { | |
getGString() | |
} | |
@Override | |
public Writable make(Map binding) { | |
new GStringImpl(_binding.collect{ binding[it] } as Object[], _strings as String[]) | |
} | |
private GString getGString() { | |
new GStringImpl(_binding as Object[], _strings as String[]) | |
} | |
List<String> getValues() { | |
_binding.clone() as List<String> | |
} | |
} | |
class FlexMap extends HashMap<String, String> { | |
@Override | |
String put(String s, String s2) { | |
String result = super.put(s, s2) | |
// split tags and put them | |
List<String> keyTags = s.tags | |
if (keyTags.size() > 1) { | |
List<String> valueTags = s2.tags | |
if (keyTags.size() == valueTags.size()) { | |
[keyTags, valueTags].transpose().each {k, v -> super.put(k, v)} | |
} | |
} | |
return result | |
} | |
@Override | |
String get(Object o) { | |
List<String> nextSearchTags = o.toString().tags | |
List<String> result = [] | |
List<String> searchTags = nextSearchTags | |
while(! searchTags.isEmpty()) { | |
String value = super.get(searchTags.join("")) | |
switch(true) { | |
// found | |
case value != null: | |
result << value | |
// not found: give it up | |
case searchTags.size() == 1: | |
nextSearchTags = nextSearchTags.drop(searchTags.size()) | |
searchTags = nextSearchTags | |
break | |
// not found: reduce searchTags and continue | |
default: | |
searchTags = searchTags.dropRight(1) | |
} | |
} | |
return result ? result.join("") : "" | |
} | |
} | |
void RemoveOldTagAdapters(menuItem) { | |
menuItem.actionListeners.findAll { | |
// it instanceof TagAdapter // each time script executions generates different classes even if they share the same name | |
it.class.name == TagAdapter.name | |
}.each { | |
menuItem.removeActionListener it | |
} | |
} | |
void test(TagAdapter adapter) { | |
console.println ">> Run all test cases." | |
// OK: 1-to-1 tag adapting | |
assert "<s1><a2>Lorem</a2> ipsum!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", // Current Source | |
"<s4><a5>Hello</s4> World!</a5>", // Match Translation | |
"<s4><a5>Lorem</a5> ipsum!</s4>") // Current Translation | |
// OK: source has no tags, all tags are removed from translation | |
assert "Lorem ipsum!" == adapter.adapt( | |
"Hello World!", | |
"<s4></a4>Hello<s5> World!</a5>", | |
"<s4><a5>Lorem</a5> ipsum!</s4>") | |
// NG: different text, tag type un-match, all tags which do not exist in source are removed from translation | |
assert "Lorem sit ipsum!" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", | |
"<s4></s4>Hello<a5> My World!</a5>", | |
"<s4><a5>Lorem</a5> sit ipsum!</s4>") | |
// NG: different text, tag name un-match, all tags which do not exist in source are removed from translation | |
assert "Lorem sit ipsum!" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", | |
"<s4><a5>Hello</s5> My World!</a4>", | |
"<s4><a5>Lorem</a5> sit ipsum!</s4>") | |
// OK: same text, tag block replacing (1) | |
assert "<s1><a2>Lorem</a2> ipsum!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", | |
"<s4><a5>Hello</s4> World!</a5>", | |
"<s4><a5>Lorem</a5> ipsum!</s4>") | |
// OK: same text, tag block replacing (2) | |
assert "<s1><a2></a2>Lorem ipsum!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", | |
"<s4><a5><i6/>Hello</s4> World!</a5>", | |
"<s4><a5><i6/></a5>Lorem ipsum!</s4>") | |
// OK: same text, tag block replace but isolated tags | |
// (i.e. they exists only in match source) will be removed | |
assert "<s1><a2></a2>Lorem ipsum!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</s1> World!</a2>", | |
"<s4><a5><i6/>Hello</s4> <i7/>World!</a5>", | |
"<s4><a5><i6/></a5>Lorem <i7/>ipsum!</s4>") | |
// Partially adaptation: same text, same numbers of tag block. | |
// Some tag block is divided in current translation and | |
// we can NOT trace their 1-to-1 paths. | |
// Maybe you need to insert missing tags manually. | |
assert "Lorem ipsum</a2>!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</a2> World!</s1>", | |
"<s5><i6/><a7>Hello</a7> World!</s5>", | |
"<s5><i6/>Lorem <a7>ipsum</a7>!</s5>") | |
// OK: same text, same numbers of tag block. | |
// Some tag block is divided in current translation and | |
// we can trace their 1-to-1 paths. | |
assert "<s1>Lorem <a2>ipsum</a2>!</s1>" == adapter.adapt( | |
"<s1><a2>Hello</a2> World!</s1>", | |
"<s5><a6>Hello</a6><i7/> World!</s5>", | |
"<s5>Lorem <a6>ipsum</a6><i7/>!</s5>") | |
console.println ">> Done. All green." | |
} |
It looks like the script doesn't work with recent version of OmegaT (4.1.2). It starts well, but after trying to "replace with a fuzzy match" no tags are replacing as intended. Here is OmegaT log:
13097: Error: Uncatched exception in thread [AWT-EventQueue-0]
13097: Error: groovy.lang.MissingMethodException: No signature of method: static org.omegat.core.data.ExternalTMX.isInPath() is applicable for argument types: (java.io.File, java.io.File) values: [/home/klyok/Dropbox/Дакумэнты/Падпрацоўка/Lingva/Праекты/MS/tm/mt, ...]
13097: Error: at groovy.lang.MetaClassImpl.invokeStaticMissingMethod(MetaClassImpl.java:1506)
13097: Error: at groovy.lang.MetaClassImpl.invokeStaticMethod(MetaClassImpl.java:1492)
13097: Error: at org.codehaus.groovy.runtime.callsite.StaticMetaClassSite.call(StaticMetaClassSite.java:53)
13097: Error: at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:133)
13097: Error: at me.goat.groovy.scripting.TagAdapter.replaceEditText(Script2.groovy:79)
13097: Error: at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
13097: Error: at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
13097: Error: at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
13097: Error: at java.lang.reflect.Method.invoke(Method.java:498)
13097: Error: at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
13097: Error: at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
13097: Error: at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:384)
13097: Error: at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1027)
13097: Error: at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.callCurrent(PogoMetaClassSite.java:69)
13097: Error: at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callCurrent(AbstractCallSite.java:174)
13097: Error: at me.goat.groovy.scripting.TagAdapter$_actionPerformed_closure1.doCall(Script2.groovy:70)
13097: Error: at me.goat.groovy.scripting.TagAdapter$_actionPerformed_closure1.doCall(Script2.groovy)
13097: Error: at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
13097: Error: at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
13097: Error: at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
13097: Error: at java.lang.reflect.Method.invoke(Method.java:498)
13097: Error: at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
13097: Error: at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
13097: Error: at org.codehaus.groovy.runtime.metaclass.ClosureMetaClass.invokeMethod(ClosureMetaClass.java:294)
13097: Error: at groovy.lang.MetaClassImpl.invokeMethod(MetaClassImpl.java:1027)
13097: Error: at groovy.lang.Closure.call(Closure.java:414)
13097: Error: at groovy.lang.Closure.call(Closure.java:408)
13097: Error: at groovy.lang.Closure.run(Closure.java:495)
13097: Error: at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
13097: Error: at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
13097: Error: at java.awt.EventQueue.access$500(EventQueue.java:97)
13097: Error: at java.awt.EventQueue$3.run(EventQueue.java:709)
13097: Error: at java.awt.EventQueue$3.run(EventQueue.java:703)
13097: Error: at java.security.AccessController.doPrivileged(Native Method)
13097: Error: at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:76)
13097: Error: at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
13097: Error: at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
13097: Error: at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
13097: Error: at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
13097: Error: at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
13097: Error: at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
13097: Error: at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oops.
Now fixed in 0.2.1.