// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chrome://resources/js/assert.js';
import { CustomElement } from 'chrome://resources/js/custom_element.js';
import { getTemplate } from './info_view.html.js';
// 
import { VulkanInfo } from './vulkan_info.js';
// 
/**
 * Given a blob and a filename, prompts user to
 * save as a file.
 */
const saveData = (function () {
    const a = document.createElement('a');
    a.style.display = 'none';
    document.body.appendChild(a);
    return function saveData(blob, fileName) {
        const url = window.URL.createObjectURL(blob);
        a.href = url;
        a.download = fileName;
        a.click();
    };
}());
function getProblemTextAndUrl(problem) {
    let text = problem.description;
    let url = '';
    const pattern = ' Please update your graphics driver via this link: ';
    const pos = text.search(pattern);
    if (pos > 0) {
        url = text.substring(pos + pattern.length);
        text = text.substring(0, pos);
    }
    return { text, url };
}
/**
 * Calls a function to insert an element between every element
 * of an existing array
 */
function insertBetweenElements(array, fn) {
    const newArray = array.slice(0, 1);
    for (let i = 1; i < array.length; ++i) {
        newArray.push(fn(i), array[i]);
    }
    return newArray;
}
/** Inserts <span>, </span> between every element in array */
function separateByCommas(array, comma = ', ') {
    return insertBetweenElements(array, () => createElem('span', comma));
}
/**
 * Conditionally add elements to an array
 *
 * ```js
 * const array = [
 *   "carrots",
 *   "potatoes",
 *   ...addIf(haveFruit, () => ["apple", "cherry"]),
 * ]
 * ```
 *
 * The function is not called if `cond` is false.
 */
function addIf(cond, fn) {
    return cond ? fn() : [];
}
/**
 * Word wraps a string, prefixing each line.
 */
function wordWrap(s, prefix = '    ', maxLength) {
    maxLength = maxLength || (80 - prefix.length);
    const lines = [];
    const words = s.split(' ');
    const line = [];
    let length = 0;
    for (const word of words) {
        if (length + word.length + 1 >= maxLength) {
            lines.push(line.join(' '));
            line.length = 0;
            length = 0;
        }
        line.push(word);
        length += word.length + 1;
    }
    if (line.length) {
        lines.push(line.join(' '));
    }
    return lines.map(s => `${prefix}${s}`).join('\n');
}
/**
 * Creates an HTMLElement with optional attributes and children
 *
 * Examples:
 *
 * ```js
 *   br = createElem('br');
 *   p = createElem('p', 'hello world');
 *   a = createElem('a', {href: 'https://google.com', textContent: 'Google'});
 *   ul = createElement('ul', {}, [
 *     createElem('li', 'apple'),
 *     createElem('li', 'banana'),
 *   ]);
 *   h1 = createElem('h1', { style: { color: 'red' }, textContent: 'Title'})
 * ```
 */
function createElem(tag, attrs = {}, children = []) {
    const elem = document.createElement(tag);
    if (typeof attrs === 'string') {
        elem.textContent = attrs;
    }
    else {
        const elemAsAttribs = elem;
        for (const [key, value] of Object.entries(attrs)) {
            if (typeof value === 'function' && key.startsWith('on')) {
                const eventName = key.substring(2).toLowerCase();
                elem.addEventListener(eventName, value, { passive: false });
            }
            else if (typeof value === 'object') {
                for (const [k, v] of Object.entries(value)) {
                    elemAsAttribs[key][k] = v;
                }
            }
            else if (elemAsAttribs[key] === undefined) {
                elem.setAttribute(key, value);
            }
            else {
                elemAsAttribs[key] = value;
            }
        }
    }
    for (const child of children) {
        elem.appendChild(child);
    }
    return elem;
}
/** Creates the td elements for a table row */
function createInfoElements(data, padSize) {
    const desc = createElem('td', {}, [
        createElem('span', data.description.padEnd(padSize)),
        createHidden(':'),
    ]);
    if (Array.isArray(data.value)) {
        return [
            desc,
            createElem('td', {}, [createInfoTable(data.value)]),
        ];
    }
    else {
        return [
            desc,
            createElem('td', {
                textContent: data.value.toString().trim(),
                id: data.id,
            }),
        ];
    }
}
/** Creates a table from the given data */
function createInfoTable(data) {
    const longestDesc = Math.min(32, data
        .reduce((longest, { description }) => Math.max(longest, description.length), 0));
    return createElem('table', { className: 'info-table' }, [
        createElem('tbody', {}, data.map(data => createElem('tr', {}, createInfoElements(data, longestDesc)))),
    ]);
}
/**
 * Creates a hidden span that will only be used when the when
 * the user copies or downloads text.
 */
function createHidden(textContent) {
    return createElem('span', { className: 'copy', textContent });
}
/**
 * Given a string or Attributes returns the `textContent`
 * and the attributes with `textContent` removed
 */
function separateTextContentFromAttributes(attrs = {}) {
    return typeof attrs === 'string' ? { textContent: attrs, attribs: {} } : {
        textContent: attrs['textContent'] || '',
        attribs: { ...attrs, textContent: '' },
    };
}
/**
 * Creates a list item with a hidden `*   ` span prepended for copy
 */
function createLi(attrs = {}, children = []) {
    const { textContent, attribs } = separateTextContentFromAttributes(attrs);
    return createElem('li', attribs, [
        createHidden('*   '),
        createElem('span', textContent),
        ...children,
    ]);
}
/**
 * Creates a heading tag with hidden text for copying
 * so the copy will be like markdown.
 */
function createHeading(tag, padChar, attrs = {}, children = []) {
    const { textContent, attribs } = separateTextContentFromAttributes(attrs);
    return createElem(tag, attribs, [
        createHidden('\n\n'),
        createElem('span', textContent),
        createHidden(`\n${''.padEnd(textContent.length, padChar)}`),
        ...children,
    ]);
}
/**
 * Creates a link pair with an anchor tag that is visible
 * in the page and hidden text for copying so the copy
 * will appear as (href)
 */
function createLinkPair(textContent, href) {
    return [
        createElem('a', {
            className: 'hide-on-copy',
            textContent,
            href,
        }),
        createHidden(`(${href})`),
    ];
}
/**
 * Get a string data value
 */
function getDataValue(data) {
    return Array.isArray(data.value) ?
        data.value.map(data => getDataValue(data)).join(',') :
        data.value.toString();
}
/**
 * Go through Datas and find ones that start with 'GPUx'
 * return the first with who's value ends itn '*ACTIVE*'
 * or else the first one.
 * @param data
 * @returns
 */
function getActiveGPU(data) {
    // get list of GPUs
    const gpus = [...data].filter(({ description }) => /^GPU\d+$/.test(description));
    // get list of active GPUs
    const active = gpus.filter(data => getDataValue(data).endsWith('*ACTIVE*'));
    const all = [...active, ...gpus];
    // get the first one
    if (all.length > 0) {
        const gpu = getDataValue(all[0]);
        const parts = gpu.split(', ')[0].split('=');
        return parts.length === 2 && parts[0] === 'VENDOR' ? parseInt(parts[1]) :
            0;
    }
    return 0;
}
/** convert a value to a string or empty string if null or undefined */
function safeString(value) {
    return typeof value === 'undefined' || value === null ? '' : String(value);
}
const kSections = {
    featureStatus: ['Graphics Feature Status', 'ul'],
    clientInfo: ['Version Information', 'div'],
    basicInfo: ['Driver Information', 'div'],
    workarounds: ['Driver Bug Workarounds', 'ul'],
    problems: ['Problems Detected', 'ul'],
    angleFeatures: ['ANGLE Features', 'ul'],
    dawnInfo: ['Dawn Info', 'ul'],
    compositorInfo: ['Compositor Information', 'div'],
    displayInfo: ['Display(s) Information', 'div'],
    videoAccelerationInfo: ['Video Acceleration Information', 'div'],
    vulkanInfo: ['Vulkan Information', 'div'],
    devicePerfInfo: ['Device Performance Information', 'div'],
    diagnostics: ['Diagnostics', 'div'],
    basicInfoForHardwareGpu: ['Driver Information for Hardware GPU', 'div'],
    featureStatusForHardwareGpu: ['Graphics Feature Status for Hardware GPU', 'ul'],
    workaroundsForHardwareGpu: ['Driver Bug Workarounds for Hardware GPU', 'ul'],
    problemsForHardwareGpu: ['Problems Detected for Hardware GPU', 'ul'],
    logMessages: ['Log Messages', 'ul'],
};
/**
 * @fileoverview This view displays information on the current GPU
 * hardware.  Its primary usefulness is to allow users to copy-paste
 * their data in an easy to read format for bug reports.
 */
export class InfoViewElement extends CustomElement {
    browserBridge;
    sections;
    static get template() {
        return getTemplate();
    }
    addBrowserBridgeListeners(browserBridge) {
        browserBridge.addEventListener('gpuInfoUpdate', this.refresh.bind(this, browserBridge));
        browserBridge.addEventListener('logMessagesChange', this.refresh.bind(this, browserBridge));
        browserBridge.addEventListener('clientInfoChange', this.refresh.bind(this, browserBridge));
        this.refresh(browserBridge);
    }
    /**
     * public interface for testing
     */
    getInfo(category, feature = '') {
        const gpuInfo = this.browserBridge?.gpuInfo;
        if (!gpuInfo) {
            throw new Error('no gpuInfo');
        }
        switch (category) {
            case 'feature-status-for-hardware-gpu-list':
                return safeString(gpuInfo.featureStatusForHardwareGpu?.featureStatus[feature]);
            case 'feature-status-list':
                return safeString(gpuInfo.featureStatus?.featureStatus[feature]);
            case 'active-gpu-for-hardware':
                return safeString(getActiveGPU(gpuInfo.basicInfoForHardwareGpu));
            case 'active-gpu':
                return safeString(getActiveGPU(gpuInfo.basicInfo));
            case 'workarounds':
                return (gpuInfo.featureStatus || gpuInfo.featureStatusForHardwareGpu)
                    ?.workarounds ||
                    [];
            default:
                throw new Error(`unknown category: ${category}`);
        }
    }
    getSelectionText(all) {
        const dynamicStyle = this.getRequiredElement('#dynamic-style');
        dynamicStyle.textContent = `
      #content { white-space: pre !important; }
      .copy { display: initial; }
      .hide-on-copy { display: none; }
    `;
        const contentDiv = this.getRequiredElement('#content');
        // document.getSelection doesn't work through shadowDom
        // and shadowRoot getSelection is non-standard chromium
        const shadowDoc = this.shadowRoot;
        const selection = shadowDoc.getSelection();
        if (all) {
            selection.removeAllRanges();
            selection.selectAllChildren(contentDiv);
        }
        else {
            const position = selection.anchorNode?.compareDocumentPosition(selection.focusNode);
            const [startNode, startOffset, endNode, endOffset] = ((position || 0) & Node.DOCUMENT_POSITION_FOLLOWING) ?
                [
                    selection.anchorNode,
                    selection.anchorOffset,
                    selection.focusNode,
                    selection.focusOffset,
                ] :
                [
                    selection.focusNode,
                    selection.focusOffset,
                    selection.anchorNode,
                    selection.anchorOffset,
                ];
            if (startOffset === 0) {
                // Given the selection between > and <
                //
                //   * >abc
                //   * def<
                //
                // We need to move the start of the selection back to the parent
                // otherwise the selection above will copied as
                //
                //   abc
                //   * def
                //
                // since the * (the list item's bullet) is not selectable directly.
                const li = startNode.parentElement?.closest('li');
                selection.setBaseAndExtent(li || startNode.parentNode, 0, endNode, endOffset);
            }
        }
        // Get text and remove superfluous lines and whitespace.
        const text = selection.toString()
            .replace(/\s*\n\s*\n\s*\n+/g, '\n\n')
            .replace(/\t/g, ' ')
            .trim();
        if (all) {
            shadowDoc.getSelection()?.removeAllRanges();
        }
        dynamicStyle.textContent = '';
        return text;
    }
    connectedCallback() {
        // Add handler to 'download report to clipboard' button
        const downloadButton = this.getRequiredElement('#download-to-file');
        assert(downloadButton);
        downloadButton.onclick = (() => {
            const text = this.getSelectionText(true);
            const blob = new Blob([text], { type: 'text/text' });
            const filename = `about-gpu-${new Date().toISOString().replace(/[^a-z0-9-]/ig, '-')}.txt`;
            saveData(blob, filename);
        });
        // Add a copy handler to massage the text for plain text.
        document.addEventListener('copy', (event) => {
            const text = this.getSelectionText(false);
            event.clipboardData.setData('text/plain', text);
            event.preventDefault();
        });
        const contentDiv = this.getRequiredElement('#content');
        this.sections = Object.fromEntries(Object.entries(kSections).map(([propName, [title, tag]]) => {
            const div = createHeading('h3', '=', title);
            const list = createElem(tag);
            const wrap = createElem('div', {}, [
                div,
                list,
            ]);
            contentDiv.appendChild(wrap);
            return [propName, { div, list, wrap }];
        }));
    }
    /**
     * Updates the view based on its currently known data
     */
    refresh(browserBridge) {
        this.browserBridge = browserBridge;
        let clientInfo;
        function createSourcePermalink(revisionIdentifier, filepath) {
            if (revisionIdentifier.length !== 40) {
                // If the revision id isn't a hash, just use the 0.0.0.0 version
                // from the Chrome version string "Chrome/0.0.0.0".
                revisionIdentifier = clientInfo.version.split('/')[1];
            }
            return `https://chromium.googlesource.com/chromium/src/+/${revisionIdentifier}/${filepath}`;
        }
        const sections = this.sections;
        // Client info
        if (browserBridge.clientInfo) {
            clientInfo = browserBridge.clientInfo;
            this.setTable_(sections.clientInfo, [
                { description: 'Data exported', value: (new Date()).toISOString() },
                { description: 'Chrome version', value: clientInfo.version },
                { description: 'Operating system', value: clientInfo.operating_system },
                {
                    description: 'Software rendering list URL',
                    value: createSourcePermalink(clientInfo.revision_identifier, 'gpu/config/software_rendering_list.json'),
                },
                {
                    description: 'Driver bug list URL',
                    value: createSourcePermalink(clientInfo.revision_identifier, 'gpu/config/gpu_driver_bug_list.json'),
                },
                { description: 'ANGLE commit id', value: clientInfo.angle_commit_id },
                {
                    description: '2D graphics backend',
                    value: clientInfo.graphics_backend,
                },
                { description: 'Command Line', value: clientInfo.command_line },
            ]);
        }
        else {
            sections.clientInfo.list.textContent = '... loading ...';
        }
        const gpuInfo = browserBridge.gpuInfo;
        if (gpuInfo) {
            // Not using jstemplate here for blocklist status because we construct
            // href from data, which jstemplate can't seem to do.
            if (gpuInfo.featureStatus) {
                this.appendFeatureInfo_(gpuInfo.featureStatus, sections.featureStatus.list, sections.problems, sections.workarounds);
            }
            else {
                sections.featureStatus.list.textContent = '';
                sections.problems.list.hidden = true;
                sections.workarounds.list.hidden = true;
            }
            const hideHardware = !gpuInfo.featureStatusForHardwareGpu;
            sections.basicInfoForHardwareGpu.div.hidden = hideHardware;
            sections.featureStatusForHardwareGpu.div.hidden = hideHardware;
            sections.problemsForHardwareGpu.div.hidden = hideHardware;
            sections.workaroundsForHardwareGpu.div.hidden = hideHardware;
            if (!hideHardware) {
                this.appendFeatureInfo_(gpuInfo.featureStatusForHardwareGpu, sections.featureStatusForHardwareGpu.list, sections.problemsForHardwareGpu, sections.workaroundsForHardwareGpu);
                this.setTable_(sections.basicInfoForHardwareGpu, gpuInfo.basicInfoForHardwareGpu);
            }
            this.setTable_(sections.basicInfo, gpuInfo.basicInfo);
            this.setTable_(sections.compositorInfo, gpuInfo.compositorInfo);
            this.setTable_(sections.displayInfo, gpuInfo.displayInfo);
            this.setTable_(sections.videoAccelerationInfo, gpuInfo.videoAcceleratorsInfo);
            this.updateSectionList_(sections.angleFeatures, gpuInfo.ANGLEFeatures, angleFeature => this.createAngleFeatureEl_(angleFeature));
            this.updateSection_(sections.dawnInfo, () => {
                const show = !!gpuInfo.dawnInfo && gpuInfo.dawnInfo.length > 0;
                if (show) {
                    this.createDawnInfoEl_(sections.dawnInfo.list, gpuInfo.dawnInfo);
                }
                return show;
            });
            this.updateSectionTable_(sections.diagnostics, gpuInfo.diagnostics);
            // 
            this.setTable_(sections.vulkanInfo, gpuInfo.vulkanInfo ? [{
                    'description': 'info',
                    'value': new VulkanInfo(gpuInfo.vulkanInfo).toString(),
                    'id': 'vulkan-info-value',
                }] :
                []);
            // 
            this.setTable_(sections.devicePerfInfo, gpuInfo.devicePerfInfo);
        }
        else {
            sections.basicInfo.list.textContent = '... loading ...';
            sections.diagnostics.div.hidden = true;
            sections.featureStatus.list.textContent = '';
            sections.problems.div.hidden = true;
            sections.dawnInfo.div.hidden = true;
        }
        // Log messages
        sections.logMessages.list.textContent = '';
        browserBridge.logMessages.forEach(messageObj => {
            sections.logMessages.list.appendChild(createElem('li', `${messageObj.header}: ${messageObj.message}`));
        });
    }
    /**
     * Clears a section and then updates it by calling fn. If fn returns false
     * it hides the section.
     */
    updateSection_(section, fn) {
        section.list.textContent = '';
        const show = fn();
        section.div.hidden = !show;
    }
    /**
     * Clears and and updates a section from a list. If the list is empty it
     * hides the section
     */
    updateSectionList_(section, list, fn) {
        this.updateSection_(section, () => {
            if (list) {
                for (const item of list) {
                    section.list.appendChild(fn(item));
                }
            }
            return !!list && list.length > 0;
        });
    }
    /** Update a table, hiding it of the table has no elements */
    updateSectionTable_(section, inputData) {
        this.updateSection_(section, () => {
            this.setTable_(section, inputData);
            return !!inputData && inputData.length > 0;
        });
    }
    appendFeatureInfo_(featureInfo, featureStatusList, problems, workarounds) {
        // Feature map
        const featureLabelMap = {
            '2d_canvas': 'Canvas',
            'gpu_compositing': 'Compositing',
            'webgl': 'WebGL',
            'multisampling': 'WebGL multisampling',
            'texture_sharing': 'Texture Sharing',
            'video_decode': 'Video Decode',
            'rasterization': 'Rasterization',
            'opengl': 'OpenGL',
            'metal': 'Metal',
            'vulkan': 'Vulkan',
            'multiple_raster_threads': 'Multiple Raster Threads',
            'native_gpu_memory_buffers': 'Native GpuMemoryBuffers',
            'protected_video_decode': 'Hardware Protected Video Decode',
            'surface_control': 'Surface Control',
            'vpx_decode': 'VPx Video Decode',
            'canvas_oop_rasterization': 'Canvas out-of-process rasterization',
            'raw_draw': 'Raw Draw',
            'video_encode': 'Video Encode',
            'direct_rendering_display_compositor': 'Direct Rendering Display Compositor',
            'webgpu': 'WebGPU',
            'skia_graphite': 'Skia Graphite',
            'webnn': 'WebNN',
            'trees_in_viz': 'TreesInViz',
            'webgpu_on_vk_via_gl_interop': 'WebGPU interop',
        };
        const statusMap = {
            'disabled_software': {
                'label': 'Software only. Hardware acceleration disabled',
                'class': 'feature-yellow',
            },
            'disabled_off': { 'label': 'Disabled', 'class': 'feature-red' },
            'disabled_off_ok': { 'label': 'Disabled', 'class': 'feature-yellow' },
            'unavailable_software': {
                'label': 'Software only, hardware acceleration unavailable',
                'class': 'feature-yellow',
            },
            'unavailable_off': { 'label': 'Unavailable', 'class': 'feature-red' },
            'unavailable_off_ok': {
                'label': 'Unavailable',
                'class': 'feature-yellow',
            },
            'enabled_readback': {
                'label': 'Hardware accelerated but at reduced performance',
                'class': 'feature-yellow',
            },
            'enabled_force': {
                'label': 'Hardware accelerated on all pages',
                'class': 'feature-green',
            },
            'enabled': { 'label': 'Hardware accelerated', 'class': 'feature-green' },
            'enabled_on': { 'label': 'Enabled', 'class': 'feature-green' },
            'enabled_force_on': { 'label': 'Force enabled', 'class': 'feature-green' },
        };
        // feature status list
        featureStatusList.textContent = '';
        for (const featureName in featureInfo.featureStatus) {
            const featureStatus = featureInfo.featureStatus[featureName];
            const label = featureLabelMap[featureName];
            if (!label) {
                console.info('Missing featureLabel for', featureName);
            }
            const statusInfo = statusMap[featureStatus];
            if (!statusInfo) {
                console.info('Missing status for ', featureStatus);
            }
            featureStatusList.appendChild(createLi({}, [
                createElem('span', `${label}: `),
                createElem('span', statusInfo ? {
                    textContent: statusInfo['label'],
                    className: statusInfo['class'],
                } :
                    {
                        textContent: 'Unknown',
                        className: 'feature-red',
                    }),
            ]));
        }
        // problems list
        this.updateSectionList_(problems, featureInfo.problems, problem => this.createProblemEl_(problem));
        // driver bug workarounds list
        this.updateSectionList_(workarounds, featureInfo.workarounds, workaround => createLi(workaround));
    }
    createProblemEl_(problem) {
        const { text, url } = getProblemTextAndUrl(problem);
        return createLi({}, [
            createElem('span', text),
            // add bug separator
            ...addIf(problem.crBugs.length > 0, () => [createElem('span', ':\n    ')]),
            // add bugs
            ...separateByCommas(problem.crBugs.map((id) => {
                const bugId = parseInt(id);
                const href = `http://crbug.com/${bugId}`;
                return createElem('span', {}, createLinkPair(bugId.toString(), href));
            })),
            // add affectedGpuSettings
            ...addIf(problem.affectedGpuSettings.length > 0, () => [createElem('br'),
                createHidden('    '),
                createElem('i', {}, [
                    createElem('span', problem.tag === 'disabledFeatures' ?
                        'Disabled Features: ' :
                        'Applied Workarounds: '),
                    ...separateByCommas(problem.affectedGpuSettings.map((textContent) => createElem('span', {
                        textContent,
                        className: problem.tag === 'disabledFeatures' ?
                            'feature-red' :
                            'feature-yellow',
                    })), ',\n        '),
                ]),
            ]),
            // add driver update link
            ...addIf(!!url, () => [createElem('br'),
                createHidden('    '),
                createElem('b', { className: 'bg-yellow' }, [
                    createElem('span', 'Please update your graphics drive via '),
                    createElem('a', { textContent: 'this link', href: url }),
                ]),
            ]),
            // for copy spacing
            createElem('span', '\n\n'),
        ]);
    }
    createAngleFeatureEl_(angleFeature) {
        return createLi({}, [
            // Name comes first, bolded
            createElem('b', angleFeature.name),
            // If there's a category, it follows the name in parentheses
            ...addIf(!!angleFeature.category, () => [createElem('span', ` (${angleFeature.category})`),
            ]),
            // Follow with a colon, and the status (colored)
            createElem('span', ': '),
            createElem('span', angleFeature.status === 'enabled' ?
                { className: 'feature-green', textContent: 'Enabled' } :
                { className: 'feature-red', textContent: 'Disabled' }),
            // for copy spacing
            createElem('span', '\n\n'),
        ]);
    }
    setTable_(section, inputData) {
        section.list.textContent = '';
        section.list.appendChild(createInfoTable(inputData || []));
    }
    createDawnInfoEl_(dawnInfoList, gpuDawnInfo) {
        dawnInfoList.textContent = '';
        let inProcessingToggles = false;
        for (let i = 0; i < gpuDawnInfo.length; ++i) {
            const infoString = gpuDawnInfo[i];
            let infoEl;
            if (infoString.startsWith('<')) {
                // GPU type and backend type.
                // Add an empty line for the next adaptor.
                dawnInfoList.appendChild(createElem('br'));
                // e.g. <Discrete GPU> D3D12 backend
                infoEl = createHeading('h3', '-', infoString);
                inProcessingToggles = false;
            }
            else if (infoString.startsWith('[')) {
                // e.g. [Enabled Toggle Names]
                infoEl = createHeading('h4', '-', {
                    className: 'dawn-info-header',
                    textContent: infoString,
                });
                if (infoString === '[WebGPU Status]' ||
                    infoString === '[Adapter Supported Features]') {
                    inProcessingToggles = false;
                }
                else {
                    inProcessingToggles = true;
                }
            }
            else if (inProcessingToggles) {
                // Each toggle takes 3 strings
                infoEl = createLi({}, [
                    // The toggle name comes first, bolded.
                    createElem('b', `${infoString}: \n    `),
                    // URL
                    ...createLinkPair(gpuDawnInfo[++i], gpuDawnInfo[i]),
                    // Description, italicized
                    createElem('i', `:\n${wordWrap(gpuDawnInfo[++i])}`),
                    // for copy spacing
                    createElem('span', '\n\n'),
                ]);
            }
            else {
                // Display supported extensions
                infoEl = createLi(infoString);
            }
            dawnInfoList.appendChild(infoEl);
        }
    }
}
customElements.define('info-view', InfoViewElement);
