Hi Jillani,
I have used what you have recommended
replica.description = workItem.“Microsoft.VSTS.TCM.ReproSteps”
Thanks
Vimalraj
Hi Jillani,
I have used what you have recommended
replica.description = workItem.“Microsoft.VSTS.TCM.ReproSteps”
Thanks
Vimalraj
Hi Jillani,
Below is the finding,
ADO text field with single line feeds/carriage returns do not convert correctly from ADO ->Jira


On ADO side, if I look at the code using inspector, I see this:
If I add the returns on Jira side and then sync back to ADO side, I see this:
When I add more text on ADO side:
The code looks like this:
…and after the sync, on the Jira side I see this:
Hi @Vimalraj
Thank you for the update and let me have this checked, will get back to you with an update.
Hi @Vimalraj
Can you please try this snippet in the Jira Incoming:
package com.exalate.transform
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.TextNode
import org.jsoup.select.Elements
import org.jsoup.safety.Whitelist
import org.jsoup.nodes.Node
class HtmlToWiki {
/**
* This map contains the translation between an html tag and a wiki tag.
* Note that some tags require a start and stop (like in the case of bold tags)
*/
static Map<String, List<String>> tagMap = [
// html tag : [ <starttag>, <stoptag> ]
"h1" : ["h1. ", "\n"],
"h2" : ["h2. ", "\n"],
"h3" : ["h3. ", "\n"],
"h4" : ["h4. ", "\n"],
"h5" : ["h5. ", "\n"],
"h6" : ["h6. ", "\n"],
"i" : ["_", "_"],
"u" : ["", ""],
"strike": ["", ""],
"em" : ["_", "_"],
"b" : ["*", "*"],
"strong": ["*", "*"],
"p" : ["", "\n"], // Ensuring p tags are converted to line breaks
"br" : ["", "\n"], // Ensuring br tags are converted to line breaks
"hr" : ["", "-----"],
"table" : ["", ""],
"tbody" : ["", ""],
"div" : ["", "\n"],
"tr" : ["", ""],
"td" : ["", ""],
"th" : ["", ""],
"pre" : ["{noformat}","{noformat}"],
"code" : ["{code}","{code}"]
]
// The dummy is to meet the jsoup requirement that relative URL's can only be resolved if a basic URL is provided
static private String DUMMY = "http://dummy/"
private int listLevel = 0 // Keep track of indentations
private boolean ignoreCodeTag = false // Code tags in preformatted clause must be ignored
private Map<String, String> imageNames = [:]
private Whitelist safeList
private Integer imgWidth = 0
// Constructor is empty
HtmlToWiki() {
makeSafeList()
}
// Constructor for image attachments
HtmlToWiki(List imageAttachments) {
makeSafeList()
imageAttachments.each {
imageNames.put(it.remoteIdStr as String, it.filename as String)
}
}
// Set the image width if needed
void setImgWidth(Integer imageWidth) {
this.imgWidth = imageWidth
}
String transform(String htmlText) {
if (!htmlText || htmlText == "")
return ""
Document doc = Jsoup.parse(cleanup(htmlText))
if (doc == null) {
throw new Exception("Euh - I really can't parse ${htmlText} the parser returns a null")
}
Elements bodyChildren = doc.body().childNodes()
return process(bodyChildren)
}
// Ensure only allowable tags are processed, and avoid any type of xss
private makeSafeList() {
safeList = Whitelist.basicWithImages()
String[] fullTagList = tagMap.keySet().toArray(new String[tagMap.size()])
safeList
.addTags(fullTagList)
.preserveRelativeLinks(true)
.addAttributes("span", "style")
}
// Clean out the text from unsafe html. Only allow the Safelist
private String cleanup(String htmlText) {
Document.OutputSettings outputSettings = new Document.OutputSettings()
outputSettings.prettyPrint(false)
htmlText = Jsoup.clean(htmlText, DUMMY, safeList, outputSettings)
return htmlText
}
// Process the elements from the parsed HTML content
private String process(Elements clauses) {
String result = ""
clauses.each {
clause -> result += process(clause)
}
return result
}
// Processing a single element (like img, a, ol, ul, etc.)
private String process(Element clause) {
String tagName = clause.tagName()
switch (tagName) {
case "img":
return processImage(clause)
case "a":
return processHref(clause)
case "ol":
return processList(clause, "#")
case "ul":
return processList(clause, "*")
case "p":
// Explicitly wrap <p> tags with line breaks
return "\n" + processChilds(clause) + "\n" // Change made here
case "br":
// Explicitly handle <br> tags to create newlines
return "\n" // Change made here
case "span":
return processSpan(clause)
case "pre":
return processPre(clause)
case "table":
return processTable(clause)
case "code":
if (ignoreCodeTag) {
return processChilds(clause)
}
}
return startTag(tagName) + processChilds(clause) + stopTag(tagName)
}
// Handle list items and ensure proper line breaks
private String processList(Element clause, String listItemMarkUp) {
listLevel++
String result = clause
.childNodes()
.inject("") { r, node ->
if (node instanceof Element && node.tagName() == "li") {
def lineBreakOrNothingIfLast = node.nextSibling() == null ? "" : "\n"
r += (listItemMarkUp * listLevel) + " " + processChilds((Element) node) + lineBreakOrNothingIfLast
} else if (node instanceof TextNode) {
r += node.wholeText
.replaceAll("\n", "")
.replaceAll("\r", "")
}
r
}
listLevel--
return result
}
// A single line item can contain clauses or sublists and so on
private String processListItem(Element clause, String listItemMarkUp) {
if (clause.tagName() == "li")
return (listItemMarkUp * listLevel) + " " + processChilds(clause) + "\n"
else return ""
}
private String processChilds(Element clause) {
String result = clause
.childNodes()
.inject("") { r, node ->
r += process(node)
r
}
return result
}
private String processListItem(TextNode textNode, String ignore) {
return textNode.getWholeText()
}
private String processHref(Element clause) {
String href = clause.attr("href")
String result = ""
clause.childNodes().each {
child -> result += process(child)
}
return result > "" ? "[${result}|${href}]" : "[${href}]"
}
private String processImage(Element clause) {
if (clause.attr("title") == "database image")
return ""
String sourceName = clause.attr("src")
if (!sourceName) return ""
// Handle image filename transformation for service now and Azure DevOps
if (sourceName.contains("sys_attachment.do") && !imageNames.isEmpty()) {
def matcher = sourceName =~ /sys_attachment.do\?sys_id=(\S+)/
if (matcher.hasGroup()) {
String sys_id = matcher[0][1] as String
sourceName = imageNames[sys_id]
}
}
if (sourceName.contains("/_apis/wit/attachments/")) {
def matcher = sourceName =~ /fileName=(\S+)/
if (matcher.hasGroup()) {
sourceName = matcher[0][1] as String
}
}
return imgWidth == 0 ? "\n!${sourceName}!" : "\n!${sourceName}|width=${imgWidth}!"
}
private String processSpan(Element clause) {
String styleAttribute = clause.attr("style")
String result = ""
clause.childNodes().each {
child -> result += process(child)
}
// Only call processColorHash if the styleAttribute contains color properties
if (styleAttribute && (styleAttribute.contains("color:") || styleAttribute.contains("background-color:"))) {
result = processColorHash(styleAttribute, result)
}
// Only call processColorRGB if the styleAttribute contains color properties
if (styleAttribute && (styleAttribute.contains("color:") || styleAttribute.contains("background-color:"))) {
result = processColorRGB(styleAttribute, result) // Change made here
}
return result
}
private String processPre(Element clause) {
ignoreCodeTag = true
String result = startTag("pre") + processChilds(clause) + stopTag("pre")
ignoreCodeTag = false
return result
}
private String processTable(Element clause) {
def result
List<Element> trs = clause
.childNodes()
.inject([] as List<Element>, collectTrs)
result = trs
.collect { Element tr ->
def thAndTds = tr
.childNodes()
.inject([] as List<Element>, collectThsAndTds)
def trStr = thAndTds
.inject("") { str, thOrTd ->
def prefix = thOrTd.tagName() == "th" ? "||" : "|"
def thOrTdBody = process(thOrTd)
def wrapped = (thOrTdBody.contains("\n")) ? "{panel}" + thOrTdBody + "{panel}" : thOrTdBody
return str + prefix + wrapped
}
return trStr + "|"
}
.join("\n")
return result
}
private Closure<List<Element>> collectTrs;
{
collectTrs = { List<Element> trs, Node e ->
if (e instanceof Element && ((Element)e).tagName() == "tr") {
trs.add((Element)e)
return trs
} else {
return e
.childNodes()
.inject(trs, collectTrs)
}
}
}
private Closure<List<Element>> collectThsAndTds
{
collectThsAndTds = { List<Element> thAndTds, Node e ->
if (e instanceof Element && (((Element)e).tagName() == "th" || ((Element)e).tagName() == "td")) {
thAndTds.add((Element)e)
return thAndTds
} else {
return e
.childNodes()
.inject(thAndTds, collectThsAndTds)
}
}
}
private String startTag(String tagName) {
return tagMap[tagName] ? tagMap[tagName].get(0) : "?${tagName}?"
}
private String stopTag(String tagName) {
return tagMap[tagName] ? tagMap[tagName].get(1) : "?${tagName}?"
}
private String process(TextNode text) {
return text.getWholeText()
}
}
Hi Jillani,
Thanks for your input.
I managed to fix the problem yesterdat by changing the “div”. Please refer to the code snippet.
static Map<String, List> tagMap = [
// html tag : [ <starttag>, <stoptag> ]
"h1" : ["h1. ", "\n"],
"h2" : ["h2. ", "\n"],
"h3" : ["h3. ", "\n"],
"h4" : ["h4. ", "\n"],
"h5" : ["h5. ", "\n"],
"h6" : ["h6. ", "\n"],
"i" : ["_", "_"],
"u" : ["", ""],
"strike": ["", ""],
"em" : ["_", "_"],
"b" : ["*", "*"],
"strong": ["*", "*"],
"p" : ["", ""],
"br" : ["", ""],
"hr" : ["", "-----"],
"table" : ["", ""],
"tbody" : ["", ""],
"div" : ["", "\n"],
"tr" : ["", ""],
"td" : ["", ""],
"th" : ["", ""],
"pre" : ["{noformat}","{noformat}"],
"code" : ["{code}","{code}"]
]
Hi @Vimalraj
These are the same changes, which I shared with you yesterday and glad that you already worked on it.![]()
Thank you for your cooperation as we both worked together towards a solution. Much appreciated.
Marking your last comment as a solution and sharing here again the updated snippet for the other users.
Incoming Jira.txt (15.5 KB)
This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.