JS plugin to automatically rewrite XML (was: Conref URI parsing issue)

Post here questions and problems related to editing and publishing DITA content.
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

JS plugin to automatically rewrite XML (was: Conref URI parsing issue)

Post by craigcharlie »

Hi,
We recently moved from XMetaL to Oxygen Author 23.1. When we insert conrefs to our Tridion CMS topics, the links look like this:

Code: Select all

<row conref="GUID-FA47B387-E74E-4284-99EB-6C1831AECB5D#GUID-FA47B387-E74E-4284-99EB-6C1831AECB5D/OUTPUT_FORMAT" id="GUID-DB9E8AE7-42E7-4171-A620-659AE785C088">
However, XMetaL used a conref format like this:

Code: Select all

<row conref="GUID-FA47B387-E74E-4284-99EB-6C1831AECB5D#OUTPUT_FORMAT" id="GUID-DB9E8AE7-42E7-4171-A620-659AE785C086">
Both conref URI formats successfully display the content in our CMS client tools, and both are successfully rendered in our outputs.

However, when we open files created by XMetaL, Oxygen reports invalid conrefs and doesn't display the conref'ed content, which makes the files impossible to work with. Can I change our Oxygen configuration so that it:
a) successfully displays the conref'ed content in our legacy format, and
b) if possible, continue to add URIs in the legacy format?

thanks,
Charlie
Last edited by craigcharlie on Wed Sep 25, 2024 9:52 pm, edited 2 times in total.
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: Conref URI parsing issue

Post by Radu »

Hi Charlie,

The general syntax for a DITA conref is something like this:
https://www.oxygenxml.com/dita/1.3/spec ... ibute.html
path/to/topic.dita#topicId/elementId
From your details it seems that XMetal did not add the topicId/ part in the conref path reference. If this is how it behaves, this is incorrect according to the DITA 1.3 specification.
Oxygen does not have settings to support a syntax which is not correct according to the specification.

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

Thanks for the quick reply Radu!

I've created a custom Author action that uses jsoperation to fix affected XML files, and exposed it as a menu action (code included at the end of this post).
However, I'd prefer to have this fix run automatically every time a topic is opened, at least for a few months until the bulk of files with this problem are fixed.

Is there a way to automatically run an action or XSLT transformation when an editable topic is opened? I could also handle this through XSLT if necessary.

Fyi, I tried leveraging Tridion Docs WriteXML plugin, which the connector runs when docs are opened. Turns out it's not a good candidate for this, there's not enough control/granularity over changes.

**************

Code: Select all

function doOperation() {
var results = authorAccess.getDocumentController().findNodesByXPath("//*[contains(@conref, '#') and not(contains(@conref, '/'))]", true, true, true);
    for (var i = 0; i < results.length; i++) {
        var node = results[i];
        var conrefAttr = node.getAttribute("conref");
        var origText = conrefAttr.getValue();
        var correctPattern = /^([A-Z0-9\-]+)\#\1\/.*/;
        if (!correctPattern.test(origText)) {
            var regex = /^([^#]*)#/;
            var replacedText = String.prototype.replace.call(origText, regex, function(match, p1) {
                return p1 + '#' + p1 + '/'; 
            });
            if (replacedText !== origText) {
	            authorAccess.getDocumentController().setAttribute("conref", new Packages.ro.sync.ecss.extensions.api.node.AttrValue(replacedText), results[i]);
            } 
        }
    }
}
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: Conref URI parsing issue

Post by Radu »

Hi,

There is this Javascript-based Oxygen plugin which applies an XSLT when the document is opened and when it is saved:
https://github.com/oxygenxml/wsaccess-j ... XSLTFilter
Maybe you can use it as an example. You can also modify the plugin to handle things from the Javascript code without using XSLT.

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

That's perfect, ty Radu!
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

Hi Radu,
I've created my own native js plugin, and it's working mostly fine. However I'm having a couple of issues.
Firstly, I discovered that I need to use editorSelected in the editorOpenListener, because that's the only state that appears to catch the Tridion checkout operation - editorOpened is not fired when checkout happens.
This works well in one sense, because the contents of the rewritten conrefs show up correctly in opened read-only documents. However, it also sets the modified asterisk on the editor tab (*). I'm able to remove this with editorAccess.setModified(false). However, this isn't ideal for checked out documents, because if someone reloads without saving, then the changes go away. I tried to use isEditable() on both the page and editor to detect if the doc is read-only at open time, neither seem to return false for read-only docs. Any ideas on how to successfully detect whether a doc was originally read-only?

Finally, even though my operations correct the XML, the validation that happens when the editor is first opened still display in the author tab, see attached image.
I'm trying to use the following code to clear the tab's validation results, but I don't know how to identify the correct tabKey value for getAllResults - represented as question marks in the code below. I've Googled to try to find any implementation of getAllResults, but nothing is coming up.

Code: Select all

var PluginWorkspaceProvider = Packages.ro.sync.exml.workspace.api.PluginWorkspaceProvider;
var workspace = PluginWorkspaceProvider.getPluginWorkspace();
if (workspace !== null) {
    var resultsManager = workspace.getResultsManager(); 
    var allResults = resultsManager.getAllResults('?????');
    if (allResults != null) {
        for (var i = allResults.size() - 1; i >= 0; i--) {
            var result = allResults.get(i);
            resultsManager.removeResult(result);
        }
    }
}
Attachments
image.png
image.png (19.79 KiB) Viewed 137 times
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: Conref URI parsing issue

Post by Radu »

Hi,
Firstly, I discovered that I need to use editorSelected in the editorOpenListener, because that's the only state that appears to catch the Tridion checkout operation - editorOpened is not fired when checkout happens.
What does the SDL "checkout" operation do?
1) Open the file and check it out.
or
2) Checkout an already opened file, a file which has already opened in the editor.

If it does (1), the "editorOpened" should be triggered, I do not see how SDL could open a new file in Oxygen without this "editorOpened" callback being triggered.
If it does (2), where is the checkout action mounted in Oxygen? Is it in the main menu?
I tried to use isEditable() on both the page and editor to detect if the doc is read-only at open time, neither seem to return false for read-only docs.
In general if the opened document does not allow editing inside it, the API "ro.sync.exml.workspace.api.editor.page.WSEditorPage.isEditable()" should return "false". Is the document not editable in this case also in the Text editing mode?
I'm trying to use the following code to clear the tab's validation results, but I don't know how to identify the correct tabKey value for getAllResults - represented as question marks in the code below.
The "tabKey" parameter should be exactly the name of the result tab, in this case "Author".
How about if you call:

Code: Select all

resultsManager.setResults("Author", new ArrayList(), ResultType.GENERIC)
?

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

[Removed]

Post by craigcharlie »

[Removed]
Last edited by craigcharlie on Wed Sep 25, 2024 9:54 pm, edited 1 time in total.
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

Hi Radu, thanks for the quick reply.
"Check out" is an option on the Tridion Docs menu. They don't expose their API, so unfortunately I can't hook into it directly. What I'm trying to achieve is to detect any open or checkout operation, and then perform xml updates on the backend. Regarding the editor modified state, I'm testing for this at the top of my listener:

Code: Select all

var isEditable = authorAccess.isEditable() && editorAccess.isEditable();
This should be false if either returns false. Then after I perform the xml replacement, I try this:

Code: Select all

if(!isEditable){editorAccess.setModified(false);}
However, isEditable is never false. If I take out the if statement and just call editorAccess.setModified(false), it does work. This is an issue for docs that are editable though, as a reload from the disk wipes the changes. I also can't save the editor without knowing if the doc was originally modifiable, as this causes issues with the repo paradigm - non-checked-out docs that become modifiable are then assumed to be checked out.

You asked if the editor was in text mode, it's not, it's in author mode. Here's the full code I'm using to call isEditable, in case you might see some incorrect structure:

Code: Select all

function applicationStarted(pluginWorkspaceAccess) {
    Packages.java.lang.System.err.println("Plugin has been initialized and running.");
    var editorOpenListener = {
        editorSelected: function(editorLocation) {
			var editorAccess = pluginWorkspaceAccess.getEditorAccess(editorLocation, Packages.ro.sync.exml.workspace.api.standalone.StandalonePluginWorkspace.MAIN_EDITING_AREA);
			if (editorAccess == null) {
				return;
			}
			var authorAccess = editorAccess.getCurrentPage();
			var isEditable = authorAccess.isEditable() && editorAccess.isEditable();
Finally, using setResults throws an error "org.mozilla.javascript.EvaluatorException: Can't find method ro.sync.exml.workspace.b.g.b.setResults(string,java.util.ArrayList,object). " Using resultsManager.getAllResults("Author") doesn't throw any error but also doesn't return any results.

Thanks,
Charlie
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: Conref URI parsing issue

Post by Radu »

Hi Charlie,
Finally, using setResults throws an error "org.mozilla.javascript.EvaluatorException: Can't find method ro.sync.exml.workspace.b.g.b.setResults(string,java.util.ArrayList,object). "
Probably that third argument is somehow not properly passed. I think the javascript code should look like this:

Code: Select all

resultsManager.setResults("Author", new Packages.java.util.ArrayList(), Packages.ro.sync.exml.workspace.api.results.ResultsManager.ResultType.GENERIC)
In general the Javscript code may get quite hard to write and maintain as its complexity grows. If so maybe you can try switching to a Java-based plugin instead:
https://github.com/oxygenxml/sample-plu ... ace-access
Using resultsManager.getAllResults("Author") doesn't throw any error but also doesn't return any results.
Not sure why this does not work, maybe possibly because the "Author" tab with errors appears a bit after your code is executed?
Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

Not sure why this does not work, maybe possibly because the "Author" tab with errors appears a bit after your code is executed
Good point. Might need to set up a listener for results tab named Author, and then only run the logic if the earlier code was triggered.
Any comment on the isEditable part of my post? This is the main problem I need to solve...
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: Conref URI parsing issue

Post by Radu »

Hi,
About the "isEditable" bit, maybe the SDL code (and you could also ask on their side) sets a non-editable style on the root element.
So this API "ro.sync.exml.workspace.api.editor.page.author.WSAuthorEditorPageBase.getStyles(AuthorNode)"
https://archives.oxygenxml.com/Oxygen/E ... uthorNode-
could be called on the root element "authorEditorPage.getDocumentController().getAuthorDocumentNode().getRootElement()".

And then the obtained Styles object has an isEditable method:
https://archives.oxygenxml.com/Oxygen/E ... Editable--
and possibly this method returns "false" but I'm not sure.

Maybe we are digging too much into this, maybe we could have another approach.
Maybe just add an extra button on the toolbar or in the main menu and instruct people that when such errors occur they should click it...

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
craigcharlie
Posts: 12
Joined: Thu Oct 27, 2022 6:24 pm

Re: Conref URI parsing issue

Post by craigcharlie »

Hi Radu,
Thanks so much for all the help! It turns out that only the editorActivated state was a reliable source for isEdited, I guess the other states triggered before the doc was loaded.
Everything seems to be working perfectly now, including clearing the relevant results from the Author error result pane. Here's the full code of the plugin in case it's any use to anyone:

Code: Select all

function applicationStarted(pluginWorkspaceAccess) {
    Packages.java.lang.System.err.println("Plugin has been initialized and running.");
	//For troubleshooting
	var Dialog = Packages.ro.sync.exml.workspace.api.standalone.ui.OKCancelDialog;
	var dialog = new Dialog(
		pluginWorkspaceAccess.getParentFrame(), 
		"Troubleshooting Information", 
		true 
	);
	var JTextArea = Packages.javax.swing.JTextArea;
	var JScrollPane = Packages.javax.swing.JScrollPane;
	var textArea = new JTextArea();
	textArea.setEditable(false);
	var scrollPane = new JScrollPane(textArea);
	scrollPane.setPreferredSize(new Packages.java.awt.Dimension(300, 200));
	//Uncomment for troubleshooting
	//textArea.append(isEditable + "\n");  
	//dialog.getContentPane().add(scrollPane);
	//dialog.pack();
	//dialog.setVisible(true);

    var editorOpenListener = {
        editorActivated: function(editorLocation) {
			var editorAccess = pluginWorkspaceAccess.getEditorAccess(editorLocation, Packages.ro.sync.exml.workspace.api.standalone.StandalonePluginWorkspace.MAIN_EDITING_AREA);
			if (editorAccess == null) {
				return;
			}
			var authorAccess = editorAccess.getCurrentPage();
			if (!(authorAccess instanceof Packages.ro.sync.exml.workspace.api.editor.page.author.WSAuthorEditorPage)) {
				return;
			}
			var isEditable = false;
			isEditable = authorAccess.isEditable();
			authorAccess.setEditable(true);
			var documentEdited = false;
			var PluginWorkspaceProvider = Packages.ro.sync.exml.workspace.api.PluginWorkspaceProvider;
			var workspace = PluginWorkspaceProvider.getPluginWorkspace();
			var targetStrings = [];
			
			// Fix XMetaL conref format
			var results = authorAccess.getDocumentController().findNodesByXPath("//*[contains(@conref, '#') and not(contains(@conref, '/'))]", true, true, true);
			for (var i = 0; i < results.length; i++) {
				var node = results[i];
				var conrefAttr = node.getAttribute("conref");
				var origText = conrefAttr.getValue();
				var correctPattern = /^([A-Z0-9\-]+)\#\1\/.*/;
				var extractedText = conrefAttr.getValue().substring(conrefAttr.getValue().indexOf('#') + 1);
				if (!correctPattern.test(origText)) {
					var regex = /^([^#]*)#/;
					var replacedText = String.prototype.replace.call(origText, regex, function(match, p1) {
						return p1 + '#' + p1 + '/'; 
					});
					if (replacedText !== origText) {
						targetStrings.push(extractedText);
						authorAccess.getDocumentController().setAttribute("conref", new Packages.ro.sync.ecss.extensions.api.node.AttrValue(replacedText), results[i]);
						documentEdited = true; 
					} 
				}
			}	
			
			// Fix Duplicate IDs
			var nodesWithId = authorAccess.getDocumentController().findNodesByXPath("//*[starts-with(@id, 'GUID-')]", true, true, true);
			var ids = {};
			var uuid = Packages.java.util.UUID;
			for (var i = 0; i < nodesWithId.length; i++) {
				var idAttr = nodesWithId[i].getAttribute("id");
				if (idAttr) {
					var idVal = idAttr.getValue();
					if (ids[idVal]) {
						ids[idVal].push(nodesWithId[i]);
					} else {
						ids[idVal] = [nodesWithId[i]];
					}
				}
			}
			for (var id in ids) {
				if (ids[id].length > 1) { // More than one occurrence means duplicates exist
					// We skip the first element because it's not a duplicate
					targetStrings.push(ids[id][0].getAttribute("id").getValue());
					for (var j = 1; j < ids[id].length; j++) {
						var newGUID = "GUID-" + uuid.randomUUID().toString().toUpperCase();
						authorAccess.getDocumentController().setAttribute("id", new Packages.ro.sync.ecss.extensions.api.node.AttrValue(newGUID), ids[id][j]);
						documentEdited = true;
					}
				}
			}
			if(!isEditable){
				editorAccess.setModified(false);
				authorAccess.setEditable(false);
			} else if (documentEdited) {
				editorAccess.save();
			}
			
			// Clear results manager
			if (workspace !== null) {
				var resultsManager = workspace.getResultsManager(); 
				var allResults = resultsManager.getAllResults("Author");
				if (allResults != null) {
					for (var i = allResults.size() - 1; i >= 0; i--) {	
						var result = allResults.get(i);
						var resultText = result.toString(); 
						if (containsAnyTarget(resultText, targetStrings)) {
							resultsManager.removeResult("Author", result);
						}
					}
				}
			}
        }
    };
    editorOpenListener = new JavaAdapter(Packages.ro.sync.exml.workspace.api.listeners.WSEditorChangeListener, editorOpenListener);
    pluginWorkspaceAccess.addEditorChangeListener(editorOpenListener, Packages.ro.sync.exml.workspace.api.PluginWorkspace.MAIN_EDITING_AREA);
}

function applicationClosing(pluginWorkspaceAccess) {
}

function containsAnyTarget(resultText, targets) {
    return targets.some(function(target) {
        return resultText.includes(target);
    });
}
Radu
Posts: 9205
Joined: Fri Jul 09, 2004 5:18 pm

Re: JS plugin to automatically rewrite XML (was: Conref URI parsing issue)

Post by Radu »

Hi,

If a document is already opened and selected and someone calls "Check out" from the main menu you will not receive an "editorSelected" event because the file is already opened and selected, but indeed you receive "editorActivated" events more often, whenever focus is given to the current selected document.
When a document tab gets selected you should receive both "editorSelected" and "editorActivated" events in this order.
The SDL integration is done with the same APIs as yours so sometimes it's also possible that maybe on "editorSelected" both your code and theirs attempts to do something and it depends on which listener gets called first.

Regards,
Radu
Radu Coravu
<oXygen/> XML Editor
http://www.oxygenxml.com
Post Reply