Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(206)

Side by Side Diff: docs/webui_explainer.md

Issue 2944693002: Add explainer document of some of the technical details of WebUI (Closed)
Patch Set: Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 <style>
2 .note::before {
3 content: 'Note: ';
4 font-variant: small-caps;
5 font-style: italic;
6 }
7
8 .doc h1 {
9 margin: 0;
10 }
11 </style>
12
13 # WebUI Explainer
14
15 [TOC]
16
17 <a name="What_is_webui"></a>
18 ## What is "WebUI"?
19
20 "WebUI" is a term used to loosely describe **parts of Chrome's UI
21 implemented with web technologies** (i.e. HTML, CSS, JavaScript).
22
23 Examples of WebUI in Chromium:
24
25 * Settings (chrome://settings)
26 * History (chrome://history)
27 * Downloads (chrome://downloads)
28
29 <div class="note">
30 Not all web-based UIs in Chrome have chrome:// URLs.
31 </div>
32
33 This document explains how WebUI works.
34
35 <a name="bindings"></a>
36 ## What's different from a web page?
37
38 WebUIs are granted super powers so that they can manage Chrome itself. For
39 example, it'd be very hard to implement the Settings UI without access to many
40 different privacy and security sensitive services. Access to these services are
41 not granted by default.
42
43 Only special URLs are granted WebUI "bindings" via the child security process.
44
45 Specifically, these bindings:
46
47 * give a renderer access to load [`chrome:`](#chrome_urls) URLS
48 * this is helpful for shared libraries, i.e. `chrome://resources/`
49 * allow the browser to execute arbitrary JavaScript in that renderer via
50 [`CallJavascriptFunction()`](#CallJavascriptFunction)
51 * allow communicating from the renderer to the browser with
52 [`chrome.send()`](#chrome_send) and friends
53 * ignore content settings regarding showing images or executing JavaScript
54
55 <a name="chrome_urls"></a>
56 ## How `chrome:` URLs work
57
58 <div class="note">
59 A URL is of the format &lt;protocol&gt;://&lt;host&gt;/&lt;path&gt;.
60 </div>
61
62 A `chrome:` URL loads a file from disk, memory, or can respond dynamically.
63
64 Because Chrome UIs generally need access to the browser (not just the current
65 tab), much of the C++ that handles requests or takes actions lives in the
66 browser process. The browser has many more privileges than a renderer (which is
67 sandboxed and doesn't have file access), so access is only granted for certain
68 URLs.
69
70 ### `chrome:` protocol
71
72 Chrome recognizes a list of special protocols, which it registers while starting
73 up.
74
75 Examples:
76
77 * chrome-devtools:
78 * chrome-extensions:
79 * chrome:
80 * file:
81 * view-source:
82
83 This document mainly cares about the **chrome:** protocol, but others can also
84 be granted [WebUI bindings](#bindings) or have special
85 properties.
86
87 ### `chrome:` hosts
88
89 After registering the `chrome:` protocol, a set of factories are created. These
90 factories contain a list of valid host names. A valid hostname generates a
91 controller.
92
93 In the case of `chrome:` URLs, these factories are registered early in the
94 browser process lifecycle.
95
96 ```c++
97 // ChromeBrowserMainParts::PreMainMessageLoopRunImpl():
98 content::WebUIControllerFactory::RegisterFactory(
99 ChromeWebUIControllerFactory::GetInstance());
100 ```
101
102 When a URL is requested, a new renderer is created to load the URL, and a
103 corresponding class in the browser is set up to handle messages from the
104 renderer to the browser (a `RenderFrameHost`).
105
106 The URL of the request is inspected:
107
108 ```c++
109 if (url.SchemeIs("chrome") && url.host_piece() == "donuts") // chrome://donuts
110 return &NewWebUI<DonutsUI>;
111 return nullptr; // Not a known host; no special access.
112 ```
113
114 and if a factory knows how to handle a host (returns a `WebUIFactoryFunction`),
115 the navigation machinery [grants the renderer process WebUI
116 bindings](#bindings) via the child security policy.
117
118 ```c++
119 // RenderFrameHostImpl::AllowBindings():
120 if (bindings_flags & BINDINGS_POLICY_WEB_UI) {
121 ChildProcessSecurityPolicyImpl::GetInstance()->GrantWebUIBindings(
122 GetProcess()->GetID());
123 }
124 ```
125
126 The factory creates a [`WebUIController`](#WebUIController) for a tab.
127 Here's an example:
128
129 ```c++
130 // Controller for chrome://donuts.
131 class DonutsUI : public content::WebUIController {
132 public:
133 DonutsUI(content::WebUI* web_ui) : content::WebUIController(web_ui) {
134 content::WebUIDataSource* source =
135 content::WebUIDataSource::Create("donuts"); // "donuts" == hostname
136 source->AddString("mmmDonuts", "Mmm, donuts!"); // Translations.
137 source->SetDefaultResource(IDR_DONUTS_HTML); // Home page.
138 content::WebUIDataSource::Add(source);
139
140 // Handles messages from JavaScript to C++ via chrome.send().
141 web_ui->AddMessageHandler(base::MakeUnique<OvenHandler>());
142 }
143 };
144 ```
145
146 If we assume the contents of `IDR_DONUTS_HTML` yields:
147
148 ```html
149 <h1>$i18n{mmmDonuts}</h1>
150 ```
151
152 Visiting `chrome://donuts` should show in something like:
153
154 <div style="border: 1px solid black; padding: 10px;">
155 <h1>Mmmm, donuts!</h1>
156 </div>
157
158 Delicious success.
159
160 ## C++ classes
161
162 ### WebUI
163
164 `WebUI` is a high-level class and pretty much all HTML-based Chrome UIs have
165 one. `WebUI` lives in the browser process, and is owned by a `RenderFrameHost`.
166 `WebUI`s have a concrete implementation (`WebUIImpl`) in `content/` and are
167 created in response to navigation events.
168
169 A `WebUI` knows very little about the page it's showing, and it owns a
170 [`WebUIController`](#WebUIController) that is set after creation based on the
171 hostname of a requested URL.
172
173 A `WebUI` *can* handle messages itself, but often defers these duties to
174 separate [`WebUIMessageHandler`](#WebUIMessageHandler)s, which are generally
175 designed for handling messages on certain topics.
176
177 A `WebUI` can be created speculatively, and are generally fairly lightweight.
178 Heavier duty stuff like hard initialization logic or accessing services that may
179 have side effects are more commonly done in a
180 [`WebUIController`](#WebUIController) or
181 [`WebUIMessageHandler`s](#WebUIMessageHandler).
182
183 `WebUI` are created synchronously on the UI thread in response to a URL request,
184 and are re-used where possible between navigations (i.e. refreshing a page).
185 Because they run in a separate process and can exist before a corresponding
186 renderer process has been created, special care is required to communicate with
187 the renderer if reliable message passing is required.
188
189 <a name="WebUIController"></a>
190 ### WebUIController
191
192 A `WebUIController` is the brains of the operation, and is responsible for
193 application-specific logic, setting up translations and resources, creating
194 message handlers, and potentially responding to requests dynamically. In complex
195 pages, logic is often split across multiple
196 [`WebUIMessageHandler`s](#WebUIMessageHandler) instead of solely in the
197 controller for organizational benefits.
198
199 A `WebUIController` is owned by a [`WebUI`](#WebUI), and is created and set on
200 an existing [`WebUI`](#WebUI) when the correct one is determined via URL
201 inspection (i.e. chrome://settings creates a generic [`WebUI`](#WebUI) with a
202 settings-specific `WebUIController`).
203
204 ### WebUIDataSource
205
206 <a name="WebUIMessageHandler"></a>
207 ### WebUIMessageHandler
208
209 Because some pages have many messages or share code that sends messages, message
210 handling is often split into discrete classes called `WebUIMessageHandler`s.
211 These handlers respond to specific invocations from JavaScript.
212
213 So, the given C++ code:
214
215 ```c++
216 void OvenHandler::RegisterMessages() {
217 web_ui()->RegisterMessageHandler("bakeDonuts",
218 base::Bind(&OvenHandler::HandleBakeDonuts, base::Unretained(this)));
219 }
220
221 void OverHandler::HandleBakeDonuts(const base::ListValue* args) {
222 double num_donuts;
223 CHECK(args->GetDouble(0, &num_donuts)); // JavaScript numbers are doubles.
224 GetOven()->BakeDonuts(static_cast<int>(num_donuts));
225 }
226 ```
227
228 Can be triggered in JavaScript with this example code:
229
230 ```js
231 $('bakeDonutsButton').onclick = function() {
232 chrome.send('bakeDonuts', [5]); // bake 5 donuts!
233 };
234 ```
235
236 ## Browser (C++) &rarr; Renderer (JS)
237
238 <a name="AllowJavascript"></a>
239 ### WebUIMessageHandler::AllowJavascript()
240
241 This method determines whether browser &rarr; renderer communication is allowed.
242 It is called in response to a signal from JavaScript that the page is ready to
243 communicate.
244
245 In the JS:
246
247 ```js
248 window.onload = function() {
249 app.initialize();
250 chrome.send('startPilotLight');
251 };
252 ```
253
254 In the C++:
255
256 ```c++
257 void OvenHandler::HandleStartPilotLight(cont base::ListValue* /*args*/) {
258 AllowJavascript();
259 // CallJavascriptFunction() and FireWebUIListener() are now safe to do.
260 GetOven()->StartPilotLight();
261 }
262 ```
263
264 <div class="note">
265 Relying on the <code>'load'</code> event or browser-side navigation callbacks to
266 detect page readiness omits <i>application-specific</i> initialization, and a
267 custom <code>'initialized'</code> message is often necessary.
268 </div>
269
270 <a name="CallJavascriptFunction"></a>
271 ### WebUIMessageHandler::CallJavascriptFunction()
272
273 When the browser process needs to tell the renderer/JS of an event or otherwise
274 execute code, it can use `CallJavascriptFunction()`.
275
276 <div class="note">
277 Javascript must be <a href="#AllowJavascript">allowed</a> to use
278 <code>CallJavscriptFunction()</code>.
279 </div>
280
281 ```c++
282 void OvenHandler::OnPilotLightExtinguished() {
283 CallJavascriptFunction("app.pilotLightExtinguished");
284 }
285 ```
286
287 This works by crafting a string to be evaluated in the renderer. Any arguments
288 to the call are serialized to JSON and the parameter list is wrapped with
289
290 ```
291 // See WebUI::GetJavascriptCall() for specifics:
292 "functionCallName(" + argumentsAsJson + ")"
293 ```
294
295 and sent to the renderer via a `FrameMsg_JavaScriptExecuteRequest` IPC message.
296
297 While this works, it implies that:
298
299 * a global method must exist to successfully run the Javascript request
300 * any method can be called with any parameter (far more access than required in
301 practice)
302
303 ^ These factors have resulted in less use of `CallJavascriptFunction()` in the
304 webui codebase. This functionality can easily be accomplished with the following
305 alternatives:
306
307 * [`FireWebUIListener()`](#FireWebUIListener) allows easily notifying the page
308 when an event occurs in C++ and is more loosely coupled (nothing blows up if
309 the event dispatch is ignored). JS subscribes to notifications via
310 [`cr.addWebUIListener`](#cr_addWebUIListener).
311 * [`ResolveJavascriptCallback`](#ResolveJavascriptCallback) and
312 [`RejectJavascriptCallback`](#RejectJavascriptCallback) are useful
313 when Javascript requires a response to an inquiry about C++-canonical state
314 (i.e. "Is Autofill enabled?", "Is the user incognito?")
315
316 <a name="FireWebUIListener"></a>
317 ### WebUIMessageHandler::FireWebUIListener()
318
319 `FireWebUIListener()` is used to notify a registered set of listeners that an
320 event has occurred. This is generally used for events that are not guaranteed to
321 happen in timely manner, or may be caused to happen by unpredictable events
322 (i.e. user actions).
323
324 Here's some example to detect a change to Chrome's theme:
325
326 ```js
327 cr.addWebUIListener("theme-changed", refreshThemeStyles);
328 ```
329
330 This Javascript event listener can be triggered in C++ via:
331
332 ```c++
333 void MyHandler::OnThemeChanged() {
334 FireWebUIListener("theme-changed");
335 }
336 ```
337
338 Because it's not clear when a user might want to change their theme nor what
339 theme they'll choose, this is a good candidate for an event listener.
340
341 If you simply need to get a response in Javascript from C++, consider using
342 [`cr.sendWithPromise()`](#cr_sendWithPromise) and
343 [`ResolveJavascriptCallback`](#ResolveJavascriptCallback).
344
345 <a name="OnJavascriptAllowed"></a>
346 ### WebUIMessageHandler::OnJavascriptAllowed()
347
348 `OnJavascriptDisallowed()` is a lifecycle method called in response to
349 [`AllowJavascript()`](#AllowJavascript). It is a good place to register
350 observers of global services or other callbacks that might call at unpredictable
351 times.
352
353 For example:
354
355 ```c++
356 class MyHandler : public content::WebUIMessageHandler {
357 MyHandler() {
358 GetGlobalService()->AddObserver(this); // <-- DON'T DO THIS.
359 }
360 void OnGlobalServiceEvent() {
361 FireWebUIListener("global-thing-happened");
362 }
363 };
364 ```
365
366 Because browser-side C++ handlers are created before a renderer is ready, the
367 above code may result in calling [`FireWebUIListener`](#FireWebUIListener)
368 before the renderer is ready, which may result in dropped updates or
369 accidentally running Javascript in a renderer that has navigated to a new URL.
370
371 A safer way to set up communication is:
372
373 ```c++
374 class MyHandler : public content::WebUIMessageHandler {
375 public:
376 MyHandler() : observer_(this) {}
377 void OnJavascriptAllowed() override {
378 observer_.Add(GetGlobalService()); // <-- DO THIS.
379 }
380 void OnJavascriptDisallowed() override {
381 observer_.RemoveAll(); // <-- AND THIS.
382 }
383 ScopedObserver<MyHandler, GlobalService> observer_; // <-- ALSO HANDY.
384 ```
385 when a renderer has been created and the
386 document has loaded enough to signal to the C++ that it's ready to respond to
387 messages.
388
389 <a name="OnJavascriptDisallowed"></a>
390 ### WebUIMessageHandler::OnJavascriptDisallowed()
391
392 `OnJavascriptDisallowed` is a lifecycle method called when it's unclear whether
393 it's safe to send JavaScript messsages to the renderer.
394
395 There's a number of situations that result in this method being called:
396
397 * renderer doesn't exist yet
398 * renderer exists but isn't ready
399 * renderer is ready but application-specifici JS isn't ready yet
400 * tab refresh
401 * renderer crash
402
403 Though it's possible to programmatically disable Javascript, it's uncommon to
404 need to do so.
405
406 Because there's no single strategy that works for all cases of a renderer's
407 state (i.e. queueing vs dropping messages), these lifecycle methods were
408 introduced so a WebUI application can implement these decisions itself.
409
410 Often, it makes sense to disconnect from observers in
411 `OnJavascriptDisallowed()`:
412
413 ```c++
414 void OvenHandler::OnJavascriptDisallowed() {
415 scoped_oven_observer_.RemoveAll()
416 }
417 ```
418
419 Because `OnJavascriptDisallowed()` is not guaranteed to be called before a
420 `WebUIMessageHandler`'s destructor, it is often advisable to use some form of
421 scoped observer that automatically unsubscribes on destruction but can also
422 imperatively unsubscribe in `OnJavascriptDisallowed()`.
423
424 <a name="RejectJavascriptCallback"></a>
425 ### WebUIMessageHandler::RejectJavascriptCallback()
426
427 This method is called in response to
428 [`cr.sendWithPromise()`](#cr_sendWithPromise) to reject the issued Promise. This
429 runs the rejection (second) callback in the [Promise's
430 executor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Glob al_Objects/Promise)
431 and any
432 [`catch()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Gl obal_Objects/Promise/catch)
433 callbacks in the chain.
434
435 ```c++
436 void OvenHandler::HandleBakeDonuts(const base::ListValue* args) {
437 base::Value* callback_id;
438 args->Get(0, &callback_id);
439 if (!GetOven()->HasGas()) {
440 RejectJavascriptCallback(callback_id,
441 base::StringValue("need gas to cook the donuts!"));
442 }
443 ```
444
445 This method is basically just a
446 [`CallJavascriptFunction()`](#CallJavascriptFunction) wrapper that calls a
447 global "cr.webUIResponse" method with a success value of false.
448
449 ```c++
450 // WebUIMessageHandler::RejectJavascriptCallback():
451 CallJavascriptFunction("cr.webUIResponse", callback_id, base::Value(false),
452 response);
453 ```
454
455 See also: [`ResolveJavascriptCallback`](#ResolveJavascriptCallback)
456
457 <a name="ResolveJavascriptCallback"></a>
458 ### WebUIMessageHandler::ResolveJavascriptCallback()
459
460 This method is called in response to
461 [`cr.sendWithPromise()`](#cr_sendWithPromise) to fulfill an issued Promise,
462 often with a value. This results in runnings any fulfillment (first) callbacks
463 in the associate Promise executor and any registered
464 [`then()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Glo bal_Objects/Promise/then)
465 callbacks.
466
467 So, given this JS code:
468
469 ```js
470 cr.sendWithPromise('bakeDonuts').then(function(numDonutsBaked) {
471 shop.donuts += numDonutsBaked;
472 });
473 ```
474
475 Some handling C++ might do this:
476
477 ```c++
478 void OvenHandler::HandleBakeDonuts(const base::ListValue* args) {
479 base::Value* callback_id;
480 args->Get(0, &callback_id);
481 double num_donuts_baked = GetOven()->BakeDonuts();
482 ResolveJavascriptCallback(*callback_id, num_donuts_baked);
483 }
484 ```
485
486 ## Renderer (JS) &rarr; Browser (C++)
487
488 <a name="chrome_send"></a>
489 ### chrome.send()
490
491 When the JavaScript `window` object is created, a renderer is checked for [WebUI
492 bindings](#bindings).
493
494 ```c++
495 // RenderFrameImpl::DidClearWindowObject():
496 if (enabled_bindings_ & BINDINGS_POLICY_WEB_UI)
497 WebUIExtension::Install(frame_);
498 ```
499
500 If the bindings exist, a global `chrome.send()` function is exposed to the
501 renderer:
502
503 ```c++
504 // WebUIExtension::Install():
505 v8::Local<v8::Object> chrome =
506 GetOrCreateChromeObject(isolate, context->Global());
507 chrome->Set(gin::StringToSymbol(isolate, "send"),
508 gin::CreateFunctionTemplate(
509 isolate, base::B ind(&WebUIExtension::Send))->GetFunction());
510 ```
511
512 The `chrome.send()` method takes a message name and argument list.
513
514 ```js
515 chrome.send('messageName', [arg1, arg2, ...]);
516 ```
517
518 The message name and argument list are serialized to JSON and sent via the
519 `ViewHostMsg_WebUISend` IPC message from the renderer to the browser.
520
521 ```c++
522 // In the renderer (WebUIExtension::Send()):
523 render_view->Send(new ViewHostMsg_WebUISend(render_view->GetRoutingID(),
524 frame->GetDocument().Url(),
525 message, *content));
526 ```
527 ```c++
528 // In the browser (WebUIImpl::OnMessageReceived()):
529 IPC_MESSAGE_HANDLER(ViewHostMsg_WebUISend, OnWebUISend)
530 ```
531
532 The browser-side code does a map lookup for the message name and calls the found
533 callback with the deserialized arguments:
534
535 ```c++
536 // WebUIImpl::ProcessWebUIMessage():
537 message_callbacks_.find(message)->second.Run(&args);
538 ```
539
540 <a name="cr_addWebUIListener">
541 ### cr.addWebUIListener()
542
543 WebUI listeners are a convenient way for C++ to inform JavaScript of events.
544
545 Older WebUI code exposed public methods for event notification, similar to how
546 responses to [chrome.send()](#chrome_send) used to work. They both
547 resulted in global namespace polution, but it was additionally hard to stop
548 listening for events in some cases. **cr.addWebUIListener** is preferred in new
549 code.
550
551 Adding WebUI listeners creates and inserts a unique ID into a map in JavaScript,
552 just like [cr.sendWithPromise()](#cr_sendWithPromise).
553
554 ```js
555 // addWebUIListener():
556 webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
557 webUIListenerMap[eventName][createUid()] = callback;
558 ```
559
560 The C++ responds to a globally exposed function (`cr.webUIListenerCallback`)
561 with an event name and a variable number of arguments.
562
563 ```c++
564 // WebUIMessageHandler:
565 template <typename... Values>
566 void FireWebUIListener(const std::string& event_name, const Values&... values) {
567 CallJavascriptFunction("cr.webUIListenerCallback", base::Value(event_name),
568 values...);
569 }
570 ```
571
572 C++ handlers call this `FireWebUIListener` method when an event occurs that
573 should be communicated to the JavaScript running in a tab.
574
575 ```c++
576 void OvenHandler::OnBakingDonutsFinished(size_t num_donuts) {
577 FireWebUIListener("donuts-baked", base::FundamentalValue(num_donuts));
578 }
579 ```
580
581 JavaScript can listen for WebUI events via:
582
583 ```js
584 var donutsReady = 0;
585 cr.addWebUIListener('donuts-baked', function(numFreshlyBakedDonuts) {
586 donutsReady += numFreshlyBakedDonuts;
587 });
588 ```
589
590 <a name="cr_sendWithPromise"></a>
591 ### cr.sendWithPromise()
592
593 `cr.sendWithPromise()` is a wrapper around `chrome.send()`. It's used when
594 triggering a message requires a response:
595
596 ```js
597 chrome.send('getNumberOfDonuts'); // No easy way to get response!
598 ```
599
600 In older WebUI pages, global methods were exposed simply so responses could be
601 sent. **This is discouraged** as it pollutes the global namespace and is harder
602 to make request specific or do from deeply nested code.
603
604 In newer WebUI pages, you see code like this:
605
606 ```js
607 cr.sendWithPromise('getNumberOfDonuts').then(function(numDonuts) {
608 alert('Yay, there are ' + numDonuts + ' delicious donuts left!');
609 });
610 ```
611
612 On the C++ side, the message registration is similar to
613 [`chrome.send()`](#chrome_send) except that the first argument in the
614 message handler's list is a callback ID. That ID is passed to
615 `ResolveJavascriptCallback()`, which ends up resolving the `Promise` in
616 JavaScript and calling the `then()` function.
617
618 ```c++
619 void DonutHandler::HandleGetNumberOfDonuts(const base::ListValue* args) {
620 base::Value* callback_id;
621 args->Get(0, &callback_id);
622 size_t num_donuts = GetOven()->GetNumberOfDonuts();
623 ResolveJavascriptCallback(*callback_id, base::FundamentalValue(num_donuts));
624 }
625 ```
626
627 Under the covers, a map of `Promise`s are kept in JavaScript.
628
629 The callback ID is just a namespaced, ever-increasing number. It's used to
630 insert a `Promise` into the JS-side map when created.
631
632 ```js
633 // cr.sendWithPromise():
634 var id = methodName + '_' + uidCounter++;
635 chromeSendResolverMap[id] = new PromiseResolver;
636 chrome.send(methodName, [id].concat(args));
637 ```
638
639 The corresponding number is used to look up a `Promise` and reject or resolve it
640 when the outcome is known.
641
642 ```js
643 // cr.webUIResponse():
644 var resolver = chromeSendResolverMap[id];
645 if (success)
646 resolver.resolve(response);
647 else
648 resolver.reject(response);
649 ```
650
651 This approach still relies on the C++ calling a globally exposed method, but
652 reduces the surface to only a single global (`cr.webUIResponse`) instead of
653 many. It also makes per-request responses easier, which is helpful when multiple
654 are in flight.
655
656 ## See also
657
658 * WebUI's C++ code follows the [Chromium C++ styleguide](../c++/c++.md).
659 * WebUI's HTML/CSS/JS code follows the [Chromium Web
660 Development Style Guide](../styleguide/web/web.md)
661
662
663 <script>
664 let nameEls = Array.from(document.querySelectorAll('[id], a[name]'));
665 let names = nameEls.map(nameEl => nameEl.name || nameEl.id);
666
667 let localLinks = Array.from(document.querySelectorAll('a[href^="#"]'));
668 let hrefs = localLinks.map(a => a.href.split('#')[1]);
669
670 hrefs.forEach(href => {
671 if (names.includes(href))
672 console.info('found: ' + href);
673 else
674 console.error('broken href: ' + href);
675 })
676 </script>
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698