Merge pull request #4268 from Valentine14th/bug/4023-image-rendering

fix: image rendering in nodes
This commit is contained in:
Justin Greywolf 2023-04-10 10:09:40 -07:00 committed by GitHub
commit 727bf30824
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 289 deletions

View File

@ -685,6 +685,16 @@ A ~~~ B
{ titleTopMargin: 0 } { titleTopMargin: 0 }
); );
}); });
it('4023: Should render html labels with images and-or text correctly', () => {
imgSnapshotTest(
`flowchart TD
B[<img src='https://mermaid.js.org/mermaid-logo.svg'>]
B-->C[<img src="https://mermaid.js.org/mermaid-logo.svg"> more text <img src='https://mermaid.js.org/mermaid-logo.svg'>]
B-->D(<img src='https://mermaid.js.org/mermaid-logo.svg'> some text)
B-->E(plain)`,
{}
);
});
describe('Markdown strings flowchart (#4220)', () => { describe('Markdown strings flowchart (#4220)', () => {
describe('html labels', () => { describe('html labels', () => {
it('With styling and classes', () => { it('With styling and classes', () => {

View File

@ -96,7 +96,7 @@ mermaid.initialize(config);
#### Defined in #### Defined in
[mermaidAPI.ts:667](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L667) [mermaidAPI.ts:673](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/mermaidAPI.ts#L673)
## Functions ## Functions

View File

@ -14,7 +14,7 @@ import { insertCluster, clear as clearClusters } from './clusters';
import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges'; import { insertEdgeLabel, positionEdgeLabel, insertEdge, clear as clearEdges } from './edges';
import { log } from '../logger'; import { log } from '../logger';
const recursiveRender = (_elem, graph, diagramtype, parentCluster) => { const recursiveRender = async (_elem, graph, diagramtype, parentCluster) => {
log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster); log.info('Graph in recursive render: XXX', graphlibJson.write(graph), parentCluster);
const dir = graph.graph().rankdir; const dir = graph.graph().rankdir;
log.trace('Dir in recursive render - dir:', dir); log.trace('Dir in recursive render - dir:', dir);
@ -35,7 +35,8 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
// Insert nodes, this will insert them into the dom and each node will get a size. The size is updated // Insert nodes, this will insert them into the dom and each node will get a size. The size is updated
// to the abstract node and is later used by dagre for the layout // to the abstract node and is later used by dagre for the layout
graph.nodes().forEach(function (v) { await Promise.all(
graph.nodes().map(async function (v) {
const node = graph.node(v); const node = graph.node(v);
if (parentCluster !== undefined) { if (parentCluster !== undefined) {
const data = JSON.parse(JSON.stringify(parentCluster.clusterData)); const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
@ -51,7 +52,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
if (node && node.clusterNode) { if (node && node.clusterNode) {
// const children = graph.children(v); // const children = graph.children(v);
log.info('Cluster identified', v, node.width, graph.node(v)); log.info('Cluster identified', v, node.width, graph.node(v));
const o = recursiveRender(nodes, node.graph, diagramtype, graph.node(v)); const o = await recursiveRender(nodes, node.graph, diagramtype, graph.node(v));
const newEl = o.elem; const newEl = o.elem;
updateNodeBounds(node, newEl); updateNodeBounds(node, newEl);
node.diff = o.diff || 0; node.diff = o.diff || 0;
@ -69,10 +70,11 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
// insertCluster(clusters, graph.node(v)); // insertCluster(clusters, graph.node(v));
} else { } else {
log.info('Node - the non recursive path', v, node.id, node); log.info('Node - the non recursive path', v, node.id, node);
insertNode(nodes, graph.node(v), dir); await insertNode(nodes, graph.node(v), dir);
} }
} }
}); })
);
// Insert labels, this will insert them into the dom so that the width can be calculated // Insert labels, this will insert them into the dom so that the width can be calculated
// Also figure out which edges point to/from clusters and adjust them accordingly // Also figure out which edges point to/from clusters and adjust them accordingly
@ -146,7 +148,7 @@ const recursiveRender = (_elem, graph, diagramtype, parentCluster) => {
return { elem, diff }; return { elem, diff };
}; };
export const render = (elem, graph, markers, diagramtype, id) => { export const render = async (elem, graph, markers, diagramtype, id) => {
insertMarkers(elem, markers, diagramtype, id); insertMarkers(elem, markers, diagramtype, id);
clearNodes(); clearNodes();
clearEdges(); clearEdges();
@ -157,7 +159,7 @@ export const render = (elem, graph, markers, diagramtype, id) => {
adjustClustersAndEdges(graph); adjustClustersAndEdges(graph);
log.warn('Graph after:', graphlibJson.write(graph)); log.warn('Graph after:', graphlibJson.write(graph));
// log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph)); // log.warn('Graph ever after:', graphlibJson.write(graph.node('A').graph));
recursiveRender(elem, graph, diagramtype); await recursiveRender(elem, graph, diagramtype);
}; };
// const shapeDefinitions = {}; // const shapeDefinitions = {};

View File

@ -8,8 +8,8 @@ import note from './shapes/note';
import { parseMember } from '../diagrams/class/svgDraw'; import { parseMember } from '../diagrams/class/svgDraw';
import { evaluate } from '../diagrams/common/common'; import { evaluate } from '../diagrams/common/common';
const question = (parent, node) => { const question = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -69,8 +69,8 @@ const choice = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const hexagon = (parent, node) => { const hexagon = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const f = 4; const f = 4;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -96,8 +96,8 @@ const hexagon = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const rect_left_inv_arrow = (parent, node) => { const rect_left_inv_arrow = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -122,8 +122,8 @@ const rect_left_inv_arrow = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const lean_right = (parent, node) => { const lean_right = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -145,8 +145,8 @@ const lean_right = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const lean_left = (parent, node) => { const lean_left = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -168,8 +168,8 @@ const lean_left = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const trapezoid = (parent, node) => { const trapezoid = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -191,8 +191,8 @@ const trapezoid = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const inv_trapezoid = (parent, node) => { const inv_trapezoid = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -214,8 +214,8 @@ const inv_trapezoid = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const rect_right_inv_arrow = (parent, node) => { const rect_right_inv_arrow = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -238,8 +238,8 @@ const rect_right_inv_arrow = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const cylinder = (parent, node) => { const cylinder = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const rx = w / 2; const rx = w / 2;
@ -310,8 +310,13 @@ const cylinder = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const rect = (parent, node) => { const rect = async (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true); const { shapeSvg, bbox, halfPadding } = await labelHelper(
parent,
node,
'node ' + node.classes,
true
);
// add the rect // add the rect
const rect = shapeSvg.insert('rect', ':first-child'); const rect = shapeSvg.insert('rect', ':first-child');
@ -352,8 +357,8 @@ const rect = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const labelRect = (parent, node) => { const labelRect = async (parent, node) => {
const { shapeSvg } = labelHelper(parent, node, 'label', true); const { shapeSvg } = await labelHelper(parent, node, 'label', true);
log.trace('Classes = ', node.classes); log.trace('Classes = ', node.classes);
// add the rect // add the rect
@ -539,8 +544,8 @@ const rectWithTitle = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const stadium = (parent, node) => { const stadium = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
const w = bbox.width + h / 4 + node.padding; const w = bbox.width + h / 4 + node.padding;
@ -565,8 +570,8 @@ const stadium = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const circle = (parent, node) => { const circle = async (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
const circle = shapeSvg.insert('circle', ':first-child'); const circle = shapeSvg.insert('circle', ':first-child');
// center the circle around its coordinate // center the circle around its coordinate
@ -590,8 +595,8 @@ const circle = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const doublecircle = (parent, node) => { const doublecircle = async (parent, node) => {
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox, halfPadding } = await labelHelper(parent, node, undefined, true);
const gap = 5; const gap = 5;
const circleGroup = shapeSvg.insert('g', ':first-child'); const circleGroup = shapeSvg.insert('g', ':first-child');
const outerCircle = circleGroup.insert('circle'); const outerCircle = circleGroup.insert('circle');
@ -626,8 +631,8 @@ const doublecircle = (parent, node) => {
return shapeSvg; return shapeSvg;
}; };
const subroutine = (parent, node) => { const subroutine = async (parent, node) => {
const { shapeSvg, bbox } = labelHelper(parent, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(parent, node, undefined, true);
const w = bbox.width + node.padding; const w = bbox.width + node.padding;
const h = bbox.height + node.padding; const h = bbox.height + node.padding;
@ -976,7 +981,7 @@ const shapes = {
let nodeElems = {}; let nodeElems = {};
export const insertNode = (elem, node, dir) => { export const insertNode = async (elem, node, dir) => {
let newEl; let newEl;
let el; let el;
@ -989,9 +994,9 @@ export const insertNode = (elem, node, dir) => {
target = node.linkTarget || '_blank'; target = node.linkTarget || '_blank';
} }
newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target); newEl = elem.insert('svg:a').attr('xlink:href', node.link).attr('target', target);
el = shapes[node.shape](newEl, node, dir); el = await shapes[node.shape](newEl, node, dir);
} else { } else {
el = shapes[node.shape](elem, node, dir); el = await shapes[node.shape](elem, node, dir);
newEl = el; newEl = el;
} }
if (node.tooltip) { if (node.tooltip) {
@ -1017,6 +1022,7 @@ export const clear = () => {
export const positionNode = (node) => { export const positionNode = (node) => {
const el = nodeElems[node.id]; const el = nodeElems[node.id];
log.trace( log.trace(
'Transforming node', 'Transforming node',
node.diff, node.diff,

View File

@ -3,12 +3,17 @@ import { log } from '../../logger';
import { getConfig } from '../../config'; import { getConfig } from '../../config';
import intersect from '../intersect/index.js'; import intersect from '../intersect/index.js';
const note = (parent, node) => { const note = async (parent, node) => {
const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart.htmlLabels; const useHtmlLabels = node.useHtmlLabels || getConfig().flowchart.htmlLabels;
if (!useHtmlLabels) { if (!useHtmlLabels) {
node.centerLabel = true; node.centerLabel = true;
} }
const { shapeSvg, bbox, halfPadding } = labelHelper(parent, node, 'node ' + node.classes, true); const { shapeSvg, bbox, halfPadding } = await labelHelper(
parent,
node,
'node ' + node.classes,
true
);
log.info('Classes = ', node.classes); log.info('Classes = ', node.classes);
// add the rect // add the rect

View File

@ -4,7 +4,8 @@ import { getConfig } from '../../config';
import { decodeEntities } from '../../mermaidAPI'; import { decodeEntities } from '../../mermaidAPI';
import { select } from 'd3'; import { select } from 'd3';
import { evaluate, sanitizeText } from '../../diagrams/common/common'; import { evaluate, sanitizeText } from '../../diagrams/common/common';
export const labelHelper = (parent, node, _classes, isNode) => {
export const labelHelper = async (parent, node, _classes, isNode) => {
let classes; let classes;
const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels); const useHtmlLabels = node.useHtmlLabels || evaluate(getConfig().flowchart.htmlLabels);
if (!_classes) { if (!_classes) {
@ -51,17 +52,47 @@ export const labelHelper = (parent, node, _classes, isNode) => {
// Get the size of the label // Get the size of the label
let bbox = text.getBBox(); let bbox = text.getBBox();
const halfPadding = node.padding / 2;
if (evaluate(getConfig().flowchart.htmlLabels)) { if (evaluate(getConfig().flowchart.htmlLabels)) {
const div = text.children[0]; const div = text.children[0];
const dv = select(text); const dv = select(text);
// if there are images, need to wait for them to load before getting the bounding box
const images = div.getElementsByTagName('img');
if (images) {
const noImgText = labelText.replace(/<img[^>]*>/g, '').trim() === '';
await Promise.all(
[...images].map(
(img) =>
new Promise((res) =>
img.addEventListener('load', function () {
img.style.display = 'flex';
img.style.flexDirection = 'column';
if (noImgText) {
// default size if no text
const bodyFontSize = getConfig().fontSize
? getConfig().fontSize
: window.getComputedStyle(document.body).fontSize;
const enlargingFactor = 5;
img.style.width = parseInt(bodyFontSize, 10) * enlargingFactor + 'px';
} else {
img.style.width = '100%';
}
res(img);
})
)
)
);
}
bbox = div.getBoundingClientRect(); bbox = div.getBoundingClientRect();
dv.attr('width', bbox.width); dv.attr('width', bbox.width);
dv.attr('height', bbox.height); dv.attr('height', bbox.height);
} }
const halfPadding = node.padding / 2;
// Center the label // Center the label
if (useHtmlLabels) { if (useHtmlLabels) {
label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')'); label.attr('transform', 'translate(' + -bbox.width / 2 + ', ' + -bbox.height / 2 + ')');

View File

@ -248,7 +248,7 @@ export const setConf = function (cnf: any) {
* @param _version - * @param _version -
* @param diagObj - * @param diagObj -
*/ */
export const draw = function (text: string, id: string, _version: string, diagObj: any) { export const draw = async function (text: string, id: string, _version: string, diagObj: any) {
log.info('Drawing class - ', id); log.info('Drawing class - ', id);
// TODO V10: Why flowchart? Might be a mistake when copying. // TODO V10: Why flowchart? Might be a mistake when copying.
@ -300,7 +300,7 @@ export const draw = function (text: string, id: string, _version: string, diagOb
// Run the renderer. This is what draws the final graph. // Run the renderer. This is what draws the final graph.
// @ts-ignore Ignore type error for now // @ts-ignore Ignore type error for now
const element = root.select('#' + id + ' g'); const element = root.select('#' + id + ' g');
render( await render(
element, element,
g, g,
['aggregation', 'extension', 'composition', 'dependency', 'lollipop'], ['aggregation', 'extension', 'composition', 'dependency', 'lollipop'],

View File

@ -35,13 +35,14 @@ let nodeDb = {};
// * @param doc // * @param doc
// * @param diagObj // * @param diagObj
// */ // */
export const addVertices = function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) { export const addVertices = async function (vert, svgId, root, doc, diagObj, parentLookupDb, graph) {
const svg = root.select(`[id="${svgId}"]`); const svg = root.select(`[id="${svgId}"]`);
const nodes = svg.insert('g').attr('class', 'nodes'); const nodes = svg.insert('g').attr('class', 'nodes');
const keys = Object.keys(vert); const keys = Object.keys(vert);
// Iterate through each item in the vertex object (containing all the vertices found) in the graph definition // Iterate through each item in the vertex object (containing all the vertices found) in the graph definition
keys.forEach(function (id) { await Promise.all(
keys.map(async function (id) {
const vertex = vert[id]; const vertex = vert[id];
/** /**
@ -197,14 +198,14 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
// } // }
// vertexNode = svgLabel; // vertexNode = svgLabel;
// const bbox = vertexNode.getBBox(); // const bbox = vertexNode.getBBox();
const { shapeSvg, bbox } = labelHelper(nodes, node, undefined, true); const { shapeSvg, bbox } = await labelHelper(nodes, node, undefined, true);
labelData.width = bbox.width; labelData.width = bbox.width;
labelData.wrappingWidth = getConfig().flowchart.wrappingWidth; labelData.wrappingWidth = getConfig().flowchart.wrappingWidth;
labelData.height = bbox.height; labelData.height = bbox.height;
labelData.labelNode = shapeSvg.node(); labelData.labelNode = shapeSvg.node();
node.labelData = labelData; node.labelData = labelData;
} }
// const { shapeSvg, bbox } = labelHelper(svg, node, undefined, true); // const { shapeSvg, bbox } = await labelHelper(svg, node, undefined, true);
const data = { const data = {
id: vertex.id, id: vertex.id,
@ -257,7 +258,8 @@ export const addVertices = function (vert, svgId, root, doc, diagObj, parentLook
// padding: getConfig().flowchart.padding, // padding: getConfig().flowchart.padding,
// parent: parentLookupDb.parentById[vertex.id], // parent: parentLookupDb.parentById[vertex.id],
// }); // });
}); })
);
return graph; return graph;
}; };
@ -861,7 +863,7 @@ export const draw = async function (text, id, _version, diagObj) {
// in order to get the size of the node. You can't get the size of a node // in order to get the size of the node. You can't get the size of a node
// that is not in the dom so we need to add it to the dom, get the size // that is not in the dom so we need to add it to the dom, get the size
// we will position the nodes when we get the layout from elkjs // we will position the nodes when we get the layout from elkjs
graph = addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph, svg); graph = await addVertices(vert, id, root, doc, diagObj, parentLookupDb, graph);
// Time for the edges, we start with adding an element in the node to hold the edges // Time for the edges, we start with adding an element in the node to hold the edges
const edgesEl = svg.insert('g').attr('class', 'edges edgePath'); const edgesEl = svg.insert('g').attr('class', 'edges edgePath');

View File

@ -362,7 +362,7 @@ export const getClasses = function (text, diagObj) {
* @param id * @param id
*/ */
export const draw = function (text, id, _version, diagObj) { export const draw = async function (text, id, _version, diagObj) {
log.info('Drawing flowchart'); log.info('Drawing flowchart');
diagObj.db.clear(); diagObj.db.clear();
flowDb.setGen('gen-2'); flowDb.setGen('gen-2');
@ -451,7 +451,7 @@ export const draw = function (text, id, _version, diagObj) {
// Run the renderer. This is what draws the final graph. // Run the renderer. This is what draws the final graph.
const element = root.select('#' + id + ' g'); const element = root.select('#' + id + ' g');
render(element, g, ['point', 'circle', 'cross'], 'flowchart', id); await render(element, g, ['point', 'circle', 'cross'], 'flowchart', id);
utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle()); utils.insertTitle(svg, 'flowchartTitleText', conf.titleTopMargin, diagObj.db.getDiagramTitle());

View File

@ -382,7 +382,7 @@ const getDir = (parsedItem, defaultDir = DEFAULT_NESTED_DOC_DIR) => {
* @param _version * @param _version
* @param diag * @param diag
*/ */
export const draw = function (text, id, _version, diag) { export const draw = async function (text, id, _version, diag) {
log.info('Drawing state diagram (v2)', id); log.info('Drawing state diagram (v2)', id);
// diag.sb.clear(); // diag.sb.clear();
nodeDb = {}; nodeDb = {};
@ -436,7 +436,7 @@ export const draw = function (text, id, _version, diag) {
// Run the renderer. This is what draws the final graph. // Run the renderer. This is what draws the final graph.
const element = root.select('#' + id + ' g'); const element = root.select('#' + id + ' g');
render(element, g, ['barb'], CSS_DIAGRAM, id); await render(element, g, ['barb'], CSS_DIAGRAM, id);
const padding = 8; const padding = 8;

View File

@ -406,6 +406,12 @@ const render = async function (
// clean up text CRLFs // clean up text CRLFs
text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;; text = text.replace(/\r\n?/g, '\n'); // parser problems on CRLF ignore all CR and leave LF;;
// clean up html tags so that all attributes use single quotes, parser throws error on double quotes
text = text.replace(
/<(\w+)([^>]*)>/g,
(match, tag, attributes) => '<' + tag + attributes.replace(/="([^"]*)"/g, "='$1'") + '>'
);
const idSelector = '#' + id; const idSelector = '#' + id;
const iFrameID = 'i' + id; const iFrameID = 'i' + id;
const iFrameID_selector = '#' + iFrameID; const iFrameID_selector = '#' + iFrameID;