1 //===-- HTMLLogger.js -----------------------------------------------------===//
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 //===----------------------------------------------------------------------===//
9 // Based on selected objects, hide/show sections & populate data from templates.
11 // For example, if the selection is {bb="BB4", elt="BB4.6" iter="BB4:2"}:
12 // - show the "block" and "element" sections
13 // - re-render templates within these sections (if selection changed)
14 // - apply "bb-select" to items with class class "BB4", etc
16 function updateSelection(changes
, data
) {
17 Object
.assign(selection
, changes
);
19 data
= Object
.create(data
);
20 data
.selection
= selection
;
21 for (root
of document
.querySelectorAll('[data-selection]'))
22 updateSection(root
, data
);
24 for (var k
in changes
)
25 applyClassIf(k
+ '-select', classSelector(changes
[k
]));
28 // Given <section data-selection="x,y">:
29 // - hide section if selections x or y are null
30 // - re-render templates if x or y have changed
31 function updateSection(root
, data
) {
32 let changed
= root
.selection
== null;
33 root
.selection
||= {};
34 for (key
of root
.dataset
.selection
.split(',')) {
36 if (data
.selection
[key
] != root
.selection
[key
]) {
37 root
.selection
[key
] = data
.selection
[key
];
40 if (data
.selection
[key
] == null) {
47 for (tmpl
of root
.getElementsByTagName('template'))
48 reinflate(tmpl
, data
);
52 // Expands template `tmpl` based on input `data`:
53 // - interpolates {{expressions}} in text and attributes
54 // - <template> tags can modify expansion: if, for etc
55 // Outputs to `parent` element, inserting before `next`.
56 function inflate(tmpl
, data
, parent
, next
) {
57 // We use eval() as our expression language in templates!
58 // The templates are static and trusted.
59 let evalExpr
= (expr
, data
) => eval('with (data) { ' + expr
+ ' }');
60 let interpolate
= (str
, data
) =>
61 str
.replace(/\{\{(.*?)\}\}/g, (_
, expr
) => evalExpr(expr
, data
))
62 // Anything other than <template> tag: copy, interpolate, recursively inflate.
63 if (tmpl
.nodeName
!= 'TEMPLATE') {
64 let clone
= tmpl
.cloneNode();
65 clone
.inflated
= true;
66 if (clone
instanceof Text
)
67 clone
.textContent
= interpolate(clone
.textContent
, data
);
68 if (clone
instanceof Element
) {
69 for (attr
of clone
.attributes
)
70 attr
.value
= interpolate(attr
.value
, data
);
71 for (c
of tmpl
.childNodes
)
72 inflate(c
, data
, clone
, /*next=*/null);
74 return parent
.insertBefore(clone
, next
);
76 // data-use="xyz": use <template id="xyz"> instead. (Allows recursion.)
77 if ('use' in tmpl
.dataset
)
78 return inflate(document
.getElementById(tmpl
.dataset
.use), data
, parent
, next
);
79 // <template> tag handling. Base case: recursively inflate.
80 function handle(data
) {
81 for (c
of tmpl
.content
.childNodes
)
82 inflate(c
, data
, parent
, next
);
84 // Directives on <template> tags modify behavior.
86 // data-for="x in expr": expr is enumerable, bind x to each in turn
87 'for': (nameInExpr
, data
, proceed
) => {
88 let [name
, expr
] = nameInExpr
.split(' in ');
89 let newData
= Object
.create(data
);
91 for (val
of evalExpr(expr
, data
) || []) {
93 newData
[name
+ '_index'] = index
++;
97 // data-if="expr": only include contents if expression is truthy
98 'if': (expr
, data
, proceed
) => { if (evalExpr(expr
, data
)) proceed(data
); },
99 // data-let="x = expr": bind x to value of expr
100 'let': (nameEqExpr
, data
, proceed
) => {
101 let [name
, expr
] = nameEqExpr
.split(' = ');
102 let newData
= Object
.create(data
);
103 newData
[name
] = evalExpr(expr
, data
);
107 // Compose directive handlers on top of the base handler.
108 for (let [dir
, value
] of Object
.entries(tmpl
.dataset
).reverse()) {
109 if (dir
in directives
) {
110 let proceed
= handle
;
111 handle
= (data
) => directives
[dir
](value
, data
, proceed
);
116 // Expand a template, after first removing any prior expansion of it.
117 function reinflate(tmpl
, data
) {
118 // Clear previously rendered template contents.
119 while (tmpl
.nextSibling
&& tmpl
.nextSibling
.inflated
)
120 tmpl
.parentNode
.removeChild(tmpl
.nextSibling
);
121 inflate(tmpl
, data
, tmpl
.parentNode
, tmpl
.nextSibling
);
124 // Handle a mouse event on a region containing selectable items.
125 // This might end up changing the hover state or the selection state.
127 // targetSelector describes what target HTML element is selectable.
128 // targetToID specifies how to determine the selection from it:
129 // hover: a function from target to the class name to highlight
130 // bb: a function from target to the basic-block name to select (BB4)
131 // elt: a function from target to the CFG element name to select (BB4.5)
132 // iter: a function from target to the BB iteration to select (BB4:2)
133 // If an entry is missing, the selection is unmodified.
134 // If an entry is null, the selection is always cleared.
135 function mouseEventHandler(event
, targetSelector
, targetToID
, data
) {
136 var target
= event
.type
== "mouseout" ? null : event
.target
.closest(targetSelector
);
137 let selTarget
= k
=> (target
&& targetToID
[k
]) ? targetToID
[k
](target
) : null;
138 if (event
.type
== "click") {
140 for (var k
in targetToID
) {
141 if (k
== 'hover') continue;
142 let t
= selTarget(k
);
145 updateSelection(newSel
, data
);
146 } else if ("hover" in targetToID
) {
147 applyClassIf("hover", classSelector(selTarget("hover")));
150 function watch(rootSelector
, targetSelector
, targetToID
, data
) {
151 var root
= document
.querySelector(rootSelector
);
152 for (event
of ['mouseout', 'mousemove', 'click'])
153 root
.addEventListener(event
, e
=> mouseEventHandler(e
, targetSelector
, targetToID
, data
));
155 function watchSelection(data
) {
156 let lastIter
= (bb
) => `${bb}:${data.cfg[bb].iters}`;
157 watch('#code', '.c', {
158 hover
: e
=> e
.dataset
.elt
,
159 bb
: e
=> e
.dataset
.bb
,
160 elt
: e
=> e
.dataset
.elt
,
161 // If we're already viewing an iteration of this BB, stick with the same.
162 iter
: e
=> (selection
.iter
&& selection
.bb
== e
.dataset
.bb
) ? selection
.iter
: lastIter(e
.dataset
.bb
),
164 watch('#cfg', '.bb', {
167 elt
: e
=> e
.id
+ ".0",
168 iter
: e
=> lastIter(e
.id
),
170 watch('#timeline', '.entry', {
171 hover
: e
=> [e
.id
, e
.dataset
.bb
],
172 bb
: e
=> e
.dataset
.bb
,
173 elt
: e
=> e
.dataset
.bb
+ ".0",
176 watch('#bb-elements', 'tr', {
180 watch('#iterations', '.chooser', {
181 hover
: e
=> e
.dataset
.iter
,
182 iter
: e
=> e
.dataset
.iter
,
184 updateSelection({}, data
);
186 function applyClassIf(cls
, query
) {
187 document
.querySelectorAll('.' + cls
).forEach(elt
=> elt
.classList
.remove(cls
));
188 document
.querySelectorAll(query
).forEach(elt
=> elt
.classList
.add(cls
));
190 // Turns a class name into a CSS selector matching it, with some wrinkles:
191 // - we treat id="foo" just like class="foo" to avoid repetition in the HTML
192 // - cls can be an array of strings, we match them all
193 function classSelector(cls
) {
194 if (cls
== null) return null;
195 if (Array
.isArray(cls
)) return cls
.map(classSelector
).join(', ');
196 var escaped
= cls
.replace('.', '\\.').replace(':', '\\:');
197 // don't require id="foo" class="foo"
198 return '.' + escaped
+ ", #" + escaped
;
201 // Add a stylesheet defining colors for n basic blocks.
202 function addBBColors(n
) {
203 let sheet
= new CSSStyleSheet();
204 // hex values to subtract from fff to get a base color
205 options
= [0x001, 0x010, 0x011, 0x100, 0x101, 0x110, 0x111];
206 function color(hex
) {
207 return "#" + hex
.toString(16).padStart(3, "0");
209 function add(selector
, property
, hex
) {
210 sheet
.insertRule(`${selector} { ${property}: ${color(hex)}; }`)
212 for (var i
= 0; i
< n
; ++i
) {
213 let opt
= options
[i
%options
.length
];
214 add(`.B${i}`, 'background-color', 0xfff - 2*opt
);
215 add(`#B${i} polygon`, 'fill', 0xfff - 2*opt
);
216 add(`#B${i} polygon`, 'stroke', 0x888 - 4*opt
);
218 document
.adoptedStyleSheets
.push(sheet
);