github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/path.js (about) 1 /** 2 * Copyright 2018 The WPT Dashboard Project. All rights reserved. 3 * Use of this source code is governed by a BSD-style license that can be 4 * found in the LICENSE file. 5 */ 6 7 /* 8 `<path-part>` is a stateless component for displaying part of a test path. 9 */ 10 import '../node_modules/@polymer/paper-styles/color.js'; 11 import { html, PolymerElement } from '../node_modules/@polymer/polymer/polymer-element.js'; 12 13 const PathInfo = (superClass) => class extends superClass { 14 static get properties() { 15 return { 16 path: { 17 type: String, 18 }, 19 encodedPath: { 20 type: String, 21 computed: 'encodeTestPath(path)' 22 }, 23 scheme: { 24 type: String, 25 computed: 'computeTestScheme(path)' 26 }, 27 pathIsATestFile: { 28 type: Boolean, 29 computed: 'computePathIsATestFile(path)' 30 }, 31 pathIsASubfolder: { 32 type: Boolean, 33 computed: 'computePathIsASubfolder(path)' 34 }, 35 pathIsRootDir: { 36 type: Boolean, 37 computed: 'computePathIsRootDir(path)' 38 } 39 }; 40 } 41 42 encodeTestPath(path) { 43 path = path || '/'; 44 console.assert(path.startsWith('/')); 45 const url = new URL(path || '/', window.location); 46 let parts = url.pathname.split('/'); 47 parts.pop(); 48 let lastPart = path.substr(parts.join('/').length + 1); 49 parts.push(encodeURIComponent(lastPart)); 50 return parts.join('/'); 51 } 52 53 computeTestScheme(path) { 54 // This should (close enough) match up with the logic in: 55 // https://github.com/web-platform-tests/wpt/blob/master/tools/manifest/item.py 56 // https://github.com/web-platform-tests/wpt/blob/master/tools/wptrunner/wptrunner/wpttest.py 57 path = path || ''; 58 return ['.https.', '.serviceworker.'].some(x => path.includes(x)) ? 'https' : 'http'; 59 } 60 61 computePathIsASubfolder(path) { 62 if (!path || this.computePathIsATestFile(path)) { 63 return false; 64 } 65 // Strip out query params/anchors. 66 path = new URL(path, window.location).pathname; 67 return path.split('/').filter(p => p).length > 0; 68 } 69 70 computePathIsATestFile(path) { 71 // Strip out query params/anchors. 72 path = new URL(path || '', window.location).pathname; 73 return /(\.(html|htm|py|svg|xhtml|xht|xml)(\?.*)?$)/.test(path); 74 } 75 76 computePathIsRootDir(path) { 77 return path && path === '/'; 78 } 79 80 isParentDir(path, dir) { 81 if (dir.startsWith(path)) { 82 const relativePath = dir.substring(path.length); 83 return relativePath.split('/').filter(p => p).length === 1; 84 } 85 return false; 86 } 87 88 getDirname(path) { 89 path = path || '/'; 90 console.assert(path.startsWith('/')); 91 return path.substring(0, path.lastIndexOf('/')); 92 } 93 94 splitPathIntoLinkedParts(inputPath) { 95 // Remove the leading slash. 96 const encoded = this.encodeTestPath(inputPath).slice(1); 97 if (encoded === '') { 98 // Return an empty array instead of an array with an empty string. 99 return []; 100 } 101 const parts = encoded.split('/'); 102 let path = ''; 103 const linkedParts = parts.map(part => { 104 path += `/${part}`; 105 return { 106 name: part, 107 path: path, 108 }; 109 }); 110 // Decode the last part's name (in case it was escaped). 111 let last = linkedParts.pop(); 112 last.name = decodeURIComponent(last.name); 113 linkedParts.push(last); 114 return linkedParts; 115 } 116 }; 117 118 class PathPart extends PathInfo(PolymerElement) { 119 static get template() { 120 return html` 121 <style> 122 a { 123 text-decoration: none; 124 color: var(--paper-blue-700); 125 font-family: monospace; 126 } 127 a:hover { 128 cursor: pointer; 129 color: var(--paper-blue-900); 130 } 131 .dir-path { 132 font-weight: bold; 133 } 134 </style> 135 136 <a class\$="{{ styleClass }}" href="{{ href }}" onclick="{{ navigate }}"> 137 {{ relativePath }} 138 </a> 139 `; 140 } 141 142 static get is() { 143 return 'path-part'; 144 } 145 146 static get properties() { 147 return { 148 query: { 149 type: String 150 }, 151 // Domain path-prefix, e.g. '/result/' 152 prefix: { 153 type: String, 154 default: '/' 155 }, 156 isDir: { 157 type: Boolean 158 }, 159 navigate: { 160 type: Function 161 }, 162 relativePath: { 163 type: String, 164 computed: 'computeDisplayableRelativePath(path)' 165 }, 166 href: { 167 type: Location, 168 computed: 'computeHref(prefix, path, query, isTriageMode)' 169 }, 170 isTriageMode: { 171 type: Boolean, 172 value: false 173 }, 174 styleClass: { 175 type: String, 176 computed: 'computePathClass(isDir)' 177 } 178 }; 179 } 180 181 computeHref(prefix, path, query, isTriageMode) { 182 // Disable the link if triage mode is enabled 183 // and this cell is viable for triage (a test file). 184 if (isTriageMode && this.pathIsATestFile) { 185 return 'javascript:void(0)'; 186 } 187 const encodedPath = this.encodeTestPath(path); 188 const href = new URL(window.location); 189 href.pathname = `${prefix || ''}${encodedPath}`; 190 if (query) { 191 href.search = query; 192 } 193 return href; 194 } 195 196 computeDisplayableRelativePath(path) { 197 if (!this.isDir) { 198 path = this.encodeTestPath(path || ''); 199 return decodeURIComponent(path.substr(path.lastIndexOf('/') + 1)); 200 } 201 const windowPath = window.location.pathname.replace(`${this.prefix || ''}`, ''); 202 const pathPrefix = new RegExp(`^${windowPath}${windowPath.endsWith('/') ? '' : '/'}`); 203 return `${path.replace(pathPrefix, '')}${this.isDir ? '/' : ''}`; 204 } 205 206 computePathClass(isDir) { 207 return isDir ? 'dir-path' : 'file-path'; 208 } 209 } 210 211 window.customElements.define(PathPart.is, PathPart); 212 213 export { PathPart, PathInfo };