(function() {
/** Text transformation/format handling for UR harmonization */
Help4.engine.ur.Transformation = class {
// demo content:
// '<HTML> <HEAD> <META http-equiv="Content-Type" content="text/html; charset=utf-16le"> <TITLE>CDUSERNAME</TITLE> </HEAD> <BODY> <H3>Definition</H3> <P>USER name for which change documents should be displayed.</P> </BODY> </HTML>'
static format(html) {
// ATTENTION: if "Help4.removeXSSCode" is ever changed - also review and adopt here!
// it was not used because it does not remove script content
const docuLinks = [];
if (!html) return {content: '', docuLinks}; // don't add invalid HTML error comment in this case
// sanity check
if (html.match(/<head>/gi)?.length !== 1 ||
html.match(/<\/head>/gi)?.length !== 1 ||
html.match(/<body>/gi)?.length !== 1 ||
html.match(/<\/body>/gi)?.length !== 1)
{
return {content: '<!-- Error: Received invalid HTML - Wrong number of head or body elements -->'};
}
// separate head and body, so head content (e.g. div) doesn't end up in body when parsing
const m = html.match(/<head>(?<head>.*)<\/head>.*?<body>(?<body>.*?)<\/body>/is);
const d = document.implementation.createHTMLDocument();
d.head.innerHTML = m?.groups?.head || '';
d.body.innerHTML = m?.groups?.body || '';
// link, meta, and title will be removed at the end by only returning body
// removing all icons (svg) until we can render them properly
d.querySelectorAll('script, iframe, embed')
.forEach(e => e.remove());
// remove inline event handlers
d.querySelectorAll('*')
.forEach(e => {
for (const {name} of e.attributes) {
if (name.startsWith('on')) e.setAttribute(name, '');
}
});
d.querySelectorAll('style')
.forEach(s => {
// convert to inline
// allowing list-style-type (e.g. none) because of an OL with inconsistent numbering
// the numbering was done in text, not from the LI entries
// extend here if other styles are allowed
for (const {selectorText, style} of s?.sheet?.cssRules) {
const t = style['list-style-type'];
if (!t) continue;
d.body.querySelectorAll(selectorText)
.forEach(e => {
Help4.extendObject(e.style, {'list-style-type': t});
});
}
// keeping this here for future reference:
// if there is no access to style.sheet while detached from document, we need to examine textContent
// potentially multiple matches with capture groups per match
// const all = style.textContent?.matchAll(/([^\{\}]+?)\s?{(?:[^;]+;)?\s?(list-style-type.*?;)[^\}]?}/g) || [];
// for (const [value, selector] of all) {
// style.parentElement.querySelectorAll(selector)
// .forEach(e => {
// const existing = e.getAttribute('style') || ''; // keep existing inline styles
// e.setAttribute('style', value + existing);
// });
// }
s.remove();
});
//svg
// WebGUI example:
// <svg data-sap-ls-svg-inline="true"
// data-sap-ls-svg-inlinehtmlexchange="true"
// id="M0:46:::19:2-icon"
// focusable="false"
// aria-hidden="true"
// preserveAspectRatio="none"
// viewBox="0 0 100 100"
// className="lsSvgAppIcon urSvgAppIconVAlign urSvgAppIconColorBase urSvgAppIconMetric lsAbapList--image">
// <use xlink:href="#_SAPGUI-icons_1_s_b_nocr"></use>
// </svg>
// original horizon theme fill - #1d2d3e !important
const {
engine: {ur: {UrHarmonizationEngine: {ICON_LIB_ID}}},
Element
} = Help4;
const _toLibIconId = id => `${ICON_LIB_ID}-${id}`;
d.querySelectorAll('svg')
.forEach((svg) => {
// Help4.Element.setAttribute doesn't support focusable, etc.
svg.setAttribute('focusable', 'false');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('viewBox', "0 0 100 100");
Element.addClass(svg, 'sap-ui-icons');
const use = document.createElement('use');
use.setAttribute('href','#' + _toLibIconId(svg.id));
svg.removeAttribute('id'); // HTML element ids should be unique
svg.append(use);
});
// links
// - remove internal anchor links #, href doesn't start with # when accessed from the element
// - a.href is uri# not just # somehow
d.querySelectorAll('a[href*="#"]')
.forEach(a => a.replaceWith(...a.childNodes));
// - handle DOCU_LINK, remove other links
d.querySelectorAll('a')
.forEach(a => {
// XRAY-5003 remove links
// remove script, potential XSS
// XRAY-4837 special handling for DOCU_LINK
const key = a.href.match(/^sapevent:docu_link\(([\w.]+)\)$/i)?.[1];
if (key) {
/** convert to A[data-docu-link] to be handled by _handleLinkClick in {@link Help4.control2.bubble.content.Html.onEvent}*/
docuLinks.push(key);
a.dataset.docuLink = key;
a.href = `javascript:ctx.cfg_show('#',false)`; // href is required for link underline
} else if (/^(?:sapevent|javascript):/i.test(a.href)) {
a.replaceWith(...a.childNodes);
} else {
a.href = `javascript:ctx.cfg_show('${a.href}',true,null)`;
}
});
// XRAY-4855: adopt formats
// - deprecated: TT, CENTER
// - italic
d.querySelectorAll('i, em, samp')
.forEach(el => {
const span = document.createElement('span');
span.className = 'span_italic';
span.innerText = el.innerText;
el.replaceWith(span);
});
// - monospace
d.querySelectorAll('code, tt')
.forEach(el => {
const span = document.createElement('span');
span.className = 'codeph';
span.innerText = el.innerText;
el.replaceWith(span);
});
// - table
d.querySelectorAll('table')
.forEach(el => el.classList.add('BorderTable'));
// suggest bubble size based on paragraph length
let size = 's';
if (!d.querySelector('[data-bubble-size]')) {
let maxLength = 0;
for (const p of d.querySelectorAll('p')) {
if (p.innerText.length > maxLength) maxLength = p.innerText.length;
}
if (maxLength > 150) size = 'm';
if (maxLength > 200) size = 'l';
}
return {content: d.body?.innerHTML || '', docuLinks, size};
}
/**
* Reduce HTML heading elements to h(maximum level) and below; smaller numbers mean bigger headings;
* h2 > h3 > h4 > h5 > h6;
* this is a second level processing step, assuming input html is valid;
* maxLevel is the largest allowed heading level in the output html
* @param {string} html
* @param {number} maxLevel
* @return {string}
*/
static reduceHeadingLevels(html, maxLevel) {
if (!html) return html;
const d = document.implementation.createHTMLDocument();
d.body.innerHTML = html;
// find first heading element
let firstHeading;
for (let k = 1; k < 7; k++) {
firstHeading = d.querySelector('h' + k);
if (firstHeading) break;
}
if (!firstHeading) return html;
// determine how many levels to reduce if at all; do not increase levels
const firstLevel = Number(firstHeading.nodeName.at(-1));
const step = maxLevel - firstLevel;
if (step <= 0) return html;
// apply change for each heading
for (let i = 6, level; i >= firstLevel; i--) {
level = Math.min(i + step, 6);
d.querySelectorAll(`h${i}`)
.forEach(h => {
h.outerHTML = `<h${level}>${h.innerHTML}</h${level}>`;
});
}
return d.body.innerHTML;
}
}
})();