Skip to content

Instantly share code, notes, and snippets.

@yu-tang
Last active July 10, 2017 20:57
Show Gist options
  • Select an option

  • Save yu-tang/30cf1e94772ddcabaccf to your computer and use it in GitHub Desktop.

Select an option

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
Copy Markdown

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

yu-tang commented Aug 18, 2015

Copy link
Copy Markdown
Author

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

@kosivantsov

Copy link
Copy Markdown

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

@yu-tang

yu-tang commented Aug 19, 2015

Copy link
Copy Markdown
Author

Oops.
Now fixed in 0.2.1.

@klyok

klyok commented Jul 10, 2017

Copy link
Copy Markdown

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