Skip to content

Instantly share code, notes, and snippets.

@yu-tang
Last active July 10, 2017 20:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yu-tang/30cf1e94772ddcabaccf to your computer and use it in GitHub Desktop.
Save yu-tang/30cf1e94772ddcabaccf to your computer and use it in GitHub Desktop.
adapt_tags_to_match_target.groovy
/* :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."
}
@kosivantsov
Copy link

Doesn't seem to work anymore, there's a problem with import org.omegat.util.StaticUtils.TagType. Tag handling in OmegaT has been changed recently, making this script unusable.

Kos

@yu-tang
Copy link
Author

yu-tang commented Aug 18, 2015

Thank you for the notification.
Please try to use updated version 0.2.

@kosivantsov
Copy link

An error occured
javax.script.ScriptException: groovy.lang.MissingPropertyException: No such property: menuItem for class: org.omegat.gui.main.MainWindowMenu

@yu-tang
Copy link
Author

yu-tang commented Aug 19, 2015

Oops.
Now fixed in 0.2.1.

@klyok
Copy link

klyok commented Jul 10, 2017

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