| <!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> |