blob: 5a6cb0f24349b6598f18ce4d693e6e6de5216842 [file] [log] [blame] [edit]
<!DOCTYPE html>
<html>
<head>
<style>
#dropzone {
border-radius: 1rem;
padding: 2rem;
margin: 2rem;
background: lightgreen;
}
.disclosure {
color: inherit;
text-decoration: underline;
font-weight: normal;
}
</style>
</head>
<body>
<div id="dropzone">Drag & drop the summary JSON here</div>
<script>
class Project {
#name;
#sources;
constructor(name)
{
this.#name = name;
this.#sources = [];
}
name() { return this.#name; }
headerCount()
{
let headerCount = 0;
for (const source of this.#sources)
headerCount += source.isHeader();
return headerCount;
}
addSource(source)
{
console.assert(source instanceof Source);
this.#sources.push(source);
}
sources() { return this.#sources.slice(); }
}
class Source {
#path;
#name;
#relativePath;
#project;
#includePaths;
#includes;
#origins;
#includeCount;
#includeSet;
#translationUnitCount;
#notFound;
constructor(path, name, relativePath, project, includes, notFound) {
console.assert(project instanceof Project);
this.#path = path;
this.#name = name;
this.#relativePath = relativePath;
this.#project = project;
this.#includePaths = includes;
this.#includes = null;
this.#origins = [];
this.#includeCount = null;
this.#includeSet = null;
this.#translationUnitCount = 0;
this.#notFound = notFound;
project.addSource(this);
}
resolveIncludes(sourceMap) {
this.#includes = [];
for (const path of this.#includePaths) {
const target = sourceMap[path];
this.#includes.push(target);
target.#origins.push(this);
}
}
name() { return this.#name; }
path() { return this.#path; }
relativePath() { return this.#relativePath; }
project() { return this.#project; }
includes() { return this.#includes.slice(); }
origins() { return this.#origins.slice(); }
translationUnitCount() { return this.#translationUnitCount; }
id() { return this.#relativePath.replace(/\//g, '-'); }
isHeader() { return this.path().endsWith('.h'); }
includeCount(workingSet=new Set)
{
return this.includeSet().size;
if (!this.#includeSet) {
this.#includeSet = new Set;
for (let source of this.#includes) {
this.#includeSet.add(source);
}
}
if (workingSet.has(this))
return 0; // Detected a cycle;
let count = 0;
const newSet = new Set(workingSet);
newSet.add(this);
for (const includedSource of this.#includes)
count += 1 + includedSource.includeCount(newSet);
return this.#includeCount;
}
includeSet(workingSet = new Set)
{
if (workingSet.has(this))
return workingSet;
if (!this.#includeSet) {
let workingSet = new Set(this.#includes);
for (let source of this.#includes)
workingSet = workingSet.union(source.includeSet(workingSet));
this.#includeSet = workingSet;
}
return this.#includeSet;
}
recursivelyIncrementTranslationUnitCount(includeSet, workingSet=new Set)
{
if (includeSet.has(this))
return;
includeSet.add(this);
if (workingSet.has(this))
return; // Detected a cycle.
this.#translationUnitCount++;
const newSet = new Set(workingSet);
newSet.add(this);
for (const source of this.#includes)
source.recursivelyIncrementTranslationUnitCount(includeSet, newSet);
}
}
function create(elementName) {
const element = document.createElement(elementName);
let attributes = {};
let children = [];
if (arguments.length >= 3) {
attributes = arguments[1]
children = arguments[2];
} else if (arguments.length) {
if (Array.isArray(arguments[1]))
children = arguments[1];
else
attributes = arguments[1];
}
for (const attribute in attributes) {
if (attribute.startsWith('on'))
element.addEventListener(attribute.substring(2), attributes[attribute]);
else
element.setAttribute(attribute, attributes[attribute]);
}
if (!Array.isArray(children))
children = [children];
element.append(...children.flat(256));
return element;
}
function compare(a, b) {
if (a < b)
return -1;
if (a > b)
return 1;
return 0;
}
async function load(summary) {
dropzone.style.display = 'none';
let projects = [];
let projectMap = {};
let sourceMap = {};
for (projectEntry of summary.projects) {
const project = new Project(projectEntry.name);
projects.push(project);
projectMap[projectEntry.name] = project;
}
for (const source of summary.sources)
sourceMap[source.path] = new Source(source.path, source.name, source.relative, projectMap[source.project], source.includes, source.notFound);
for (const path in sourceMap)
sourceMap[path].resolveIncludes(sourceMap);
for (const path in sourceMap) {
if (sourceMap[path].isHeader())
continue;
const set = new Set;
sourceMap[path].recursivelyIncrementTranslationUnitCount(set);
}
const expandable = new Map;
function expandableListItem(itemLabel, details, attributes, container, expand) {
return {
content: create('li', attributes, [
create('a',
{
class: 'disclosure',
href: '#',
onclick: function (event) {
event.preventDefault();
if (container.childNodes.length) {
container.style.display = container.style.display == 'none' ? null : 'none';
return;
}
container.append(...expand());
}
},
itemLabel),
' ',
details,
container,
]),
expand: () => {
if (container.childNodes.length) {
container.style.display = null;
return;
}
container.append(...expand());
}
};
}
function createViewLink(source) {
if (!source.relativePath().startsWith('Source/'))
return [];
return [' (', create('a', {target: '_blank', href: 'https://github.com/WebKit/WebKit/tree/main/' + source.relativePath()}, ['view']), ') '];
}
function createJumpLink(source) {
if (!source.isHeader())
return [];
return [' (', create('a', {
class: 'jump',
href: '#' + source.id(),
onclick: (event) => {
let expand = expandable.get(source.project());
if (expand)
expand();
}
}, 'jump'), ') '];
}
function createIncludeCount(source) {
if (!source.includeCount())
return [];
return [`includes ${source.includeCount()} files, `];
}
function createIncludedByCount(source) {
return ` included by ${source.translationUnitCount()} files`;
}
document.body.append(create('ul', projects.sort((a, b) => compare(a.name(), b.name())).map((project) => {
const result = expandableListItem(project.name(), `(${project.headerCount()} files)`, { }, create('ol'), (element) => {
return project.sources().sort((a, b) => b.translationUnitCount() - a.translationUnitCount()).map((source) => {
if (!source.isHeader())
return [];
let includeCount = [];
const result = expandableListItem(
source.name(),
['-', createViewLink(source), createIncludeCount(source), `included by ${source.translationUnitCount()} files`],
{id: source.id()},
create('div'),
() => {
return [
'Includes:',
create('ol', source.includes().sort((a, b) => compare(a.path(), b.path())).map((includedSource) => {
return create('li', { }, [
includedSource.relativePath(),
' - ',
createJumpLink(includedSource),
createViewLink(includedSource),
createIncludeCount(includedSource),
createIncludedByCount(includedSource),
]);
})),
'Included by:',
create('ol', source.origins().sort((a, b) => b.translationUnitCount() - a.translationUnitCount()).map((originSource) => {
return create('li', { }, [
originSource.relativePath(),
' - ',
createJumpLink(originSource),
createViewLink(originSource),
createIncludeCount(originSource),
createIncludedByCount(originSource),
]);
})),
];
});
return result.content;
});
});
expandable.set(project, result.expand);
return result.content;
})));
}
dropzone.addEventListener('dragover', (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
});
dropzone.addEventListener('drop', (event) => {
event.preventDefault();
const file = event.dataTransfer.items[0].getAsFile();
const reader = new FileReader();
reader.onload = () => {
load(JSON.parse(reader.result));
}
reader.readAsText(file);
});
</script>
</body>
</html>