1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/firebug", 6 "firebug/lib/domplate", 7 "firebug/lib/locale", 8 "firebug/lib/xpcom", 9 "firebug/lib/events", 10 "firebug/chrome/window", 11 "firebug/lib/css", 12 "firebug/lib/dom", 13 "firebug/lib/string", 14 "firebug/lib/fonts", 15 "firebug/lib/url", 16 "firebug/lib/http", 17 "firebug/net/netUtils", 18 "firebug/lib/options" 19 ], 20 function(Obj, Firebug, Domplate, Locale, Xpcom, Events, Win, Css, Dom, Str, Fonts, Url, Http, 21 NetUtils, Options) { 22 23 // ********************************************************************************************* // 24 25 // List of font content types 26 var contentTypes = 27 [ 28 "application/x-woff", 29 "application/x-font-woff", 30 "application/x-ttf", 31 "application/x-font-ttf", 32 "font/ttf", 33 "application/x-otf", 34 "application/x-font-otf" 35 ]; 36 37 // ********************************************************************************************* // 38 // Model implementation 39 40 Firebug.FontViewerModel = Obj.extend(Firebug.Module, 41 { 42 dispatchName: "fontViewer", 43 contentTypes: contentTypes, 44 45 initialize: function() 46 { 47 Firebug.TabCacheModel.addListener(this); 48 Firebug.NetMonitor.NetInfoBody.addListener(this); 49 Firebug.registerUIListener(this); 50 }, 51 52 shutdown: function() 53 { 54 Firebug.TabCacheModel.removeListener(this); 55 Firebug.NetMonitor.NetInfoBody.removeListener(this); 56 Firebug.unregisterUIListener(this); 57 }, 58 59 /** 60 * Checks whether the given file name and content are a valid font file 61 * 62 * @param contentType: MIME type of the file 63 * @param url: URL of the file 64 * @param data: File contents 65 * @return True, if the given data outlines a font file, otherwise false 66 */ 67 isFont: function(contentType, url, data) 68 { 69 if (!contentType) 70 return false; 71 72 if (NetUtils.matchesContentType(contentType, contentTypes)) 73 { 74 if (FBTrace.DBG_FONTS) 75 { 76 FBTrace.sysout("fontviewer.isFont; content type: "+contentType, 77 {url: url, data: data}); 78 } 79 80 return true; 81 } 82 83 // Workaround for font responses without proper content type 84 // Let's consider all responses starting with "wOFF" as font. In the worst 85 // case there will be an exception when parsing. This means that no-font 86 // responses (and post data) (with "wOFF") can be parsed unnecessarily, 87 // which represents a little overhead, but this happens only if the request 88 // is actually expanded by the user in the UI (Net & Console panel). 89 var extension = Url.getFileExtension(url); 90 var validExtension = /woff|otf|ttf/.exec(extension); 91 if (validExtension && (!data || Str.hasPrefix(data, "wOFF") || Str.hasPrefix(data, "OTTO"))) 92 { 93 if (FBTrace.DBG_FONTS) 94 { 95 FBTrace.sysout("fontviewer.isFont; Font without proper content type", 96 {url: url, data: data}); 97 } 98 99 return true; 100 } 101 102 contentType = contentType.split(";")[0]; 103 contentType = Str.trim(contentType); 104 return contentTypes[contentType]; 105 }, 106 107 /** 108 * Parses the file and returns information about the font 109 * 110 * @param file: File to parse 111 * @return Font related information 112 */ 113 parseFont: function(file) 114 { 115 return Fonts.getFontInfo(Firebug.currentContext, null, file.href); 116 }, 117 118 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 119 // TabCacheModel listener 120 121 shouldCacheRequest: function(request) 122 { 123 if (this.isFont(request.contentType, request.name)) 124 return true; 125 126 return false; 127 }, 128 129 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 130 // NetInfoBody listener 131 132 updateResponse: function(netInfoBox, file, context) 133 { 134 // Let listeners parse the font 135 Events.dispatch(this.fbListeners, "onParseFont", [file]); 136 137 // The font is still not there, try to parse most common cases 138 if (!file.fontObject) 139 { 140 if (this.isFont(Http.safeGetContentType(file.request), file.href, file.responseText)) 141 file.fontObject = this.parseFont(file); 142 } 143 144 if (file.fontObject) 145 { 146 var responseTextBox = netInfoBox.getElementsByClassName("netInfoResponseText").item(0); 147 this.Preview.render(responseTextBox, file, context); 148 netInfoBox.responsePresented = true; 149 150 if (FBTrace.DBG_FONTS) 151 FBTrace.sysout("fontviewer.updateResponse", file); 152 } 153 } 154 }); 155 156 // ********************************************************************************************* // 157 158 with (Domplate) { 159 Firebug.FontViewerModel.Preview = domplate( 160 { 161 bodyTag: 162 DIV({"class": "fontInfo", _repObject: "$fontObject"}, 163 DIV({"class": "fontInfoGroup fontInfoGeneralInfoTitle"}, 164 SPAN(Locale.$STR("fontviewer.General Info")) 165 ), 166 TABLE({cellpadding: 0, cellspacing: 0}, 167 TBODY({"class": "fontInfoGeneralInfoBody", "role": "list", 168 "aria-label": Locale.$STR("fontviewer.General Info")}) 169 ), 170 DIV({"class": "fontInfoGroup fontInfoMetaDataTitle", 171 $collapsed: "$fontObject|noMetaData"}, 172 SPAN(Locale.$STR("fontviewer.Meta Data")), 173 SPAN({"class": "fontInfoToggleView", onclick: "$onToggleView", 174 _sourceDisplayed: false, _rowName: "MetaData"}, 175 Locale.$STR("fontviewer.view source") 176 ) 177 ), 178 TABLE({cellpadding: 0, cellspacing: 0}, 179 TBODY({"class": "fontInfoMetaDataBody", "role": "list", 180 "aria-label": Locale.$STR("fontviewer.Meta Data")}) 181 ), 182 DIV({"class": "fontInfoGroup fontInfoPreviewTitle"}, 183 SPAN(Locale.$STR("fontviewer.Preview")), 184 SPAN({"class": "fontInfoToggleView", onclick: "$onToggleView", 185 _lettersDisplayed: false, _rowName: "Preview"}, 186 Locale.$STR("fontviewer.view characters") 187 ) 188 ), 189 DIV({"class": "fontInfoPreview"}, 190 STYLE({"class": "fontInfoPreviewStyle"}), 191 DIV({"class": "fontInfoPreviewSample"}, 192 FOR("style", "$styles", 193 P({"class": "$fontObject.CSSFamilyName|getFontFaceClass", 194 "style": "font-size: $style|getFontSize"}, 195 SPAN({"class": "fontViewerFontSize"}, "$style"), 196 SPAN(Locale.$STR("fontviewer.pangram")) 197 ) 198 ) 199 ), 200 DIV({"class": "fontInfoPreviewCharacters"}, 201 FOR("charType", "$charTypes", 202 P({"class": "$fontObject.CSSFamilyName|getFontFaceClass"}, 203 "$charType|getCharacters" 204 ) 205 ) 206 ) 207 ) 208 ), 209 210 propDataTag: 211 FOR("prop", "$props", 212 TR({"role": "listitem", _repObject: "$prop.node"}, 213 TD({"class": "fontInfoPropName", "role": "presentation"}, 214 SPAN("$prop|getPropName") 215 ), 216 TD({"class": "fontInfoPropValue", "role": "list", "aria-label": "$prop.name"}, 217 TAG("$prop|getTag", {node: "$prop.node"}) 218 ) 219 ) 220 ), 221 222 sourceTag: 223 TR({"role": "presentation"}, 224 TD({colspan: 2, "role": "presentation"}, 225 PRE({"class": "source"}) 226 ) 227 ), 228 229 translatedInfoTag: 230 DIV({"class": "fontInfoTranslatedInfo"}, 231 DIV({"class": "fontInfoLangInfo"}, 232 FOR("lang", "$node|getLanguages", 233 A({"class": "fontInfoLangTab", $selected: "$lang.selected", role: "tab", 234 onclick: "$onTranslatedLangChange"}, "$lang.name") 235 ) 236 ), 237 DIV({"class": "fontInfoTranslatedContent"}, 238 "$node|getTranslatedText" 239 ) 240 ), 241 242 vendorTag: 243 TAG("$node|getLinkedTextTag", {node: "$node"}), 244 245 licenseTag: 246 DIV({"class": "fontInfoLicense"}, 247 TAG("$node|getLinkedTextTag", {node: "$node"}), 248 TAG("$translatedInfoTag", {node: "$node"}) 249 ), 250 251 creditsTag: 252 UL({"class": "fontInfoCredits"}, 253 FOR("credit", "$node|getCredits", 254 LI( 255 TAG("$credit|getLinkedTextTag", {node: "$credit"}), 256 " ", 257 SPAN({"class": "fontInfoCreditsRole"}, "$credit|getRole") 258 ) 259 ) 260 ), 261 262 linkTag: 263 A({"class": "fontInfoLink", href: "$node|getUrl", onclick: "$onOpenUrl"}, 264 "$node|getLinkName"), 265 266 textTag: 267 SPAN("$node|getText"), 268 269 /** 270 * Handles toggling of the font information display 271 * 272 * @param event: Click event 273 */ 274 onToggleView: function(event) 275 { 276 var target = event.target; 277 var fontInfo = Dom.getAncestorByClass(target, "fontInfo"); 278 var fontObject = fontInfo.repObject; 279 280 switch (target.rowName) 281 { 282 case "MetaData": 283 if (target.sourceDisplayed) 284 { 285 this.insertMetaDataFormatted(fontInfo, fontObject.metadata); 286 target.textContent = Locale.$STR("fontviewer.view source"); 287 } 288 else 289 { 290 this.insertMetaDataSource(fontInfo, fontObject.metadata); 291 target.textContent = Locale.$STR("fontviewer.pretty print"); 292 } 293 target.sourceDisplayed = !target.sourceDisplayed; 294 break; 295 296 case "Preview": 297 var sample = fontInfo.getElementsByClassName("fontInfoPreviewSample").item(0); 298 var chars = fontInfo.getElementsByClassName("fontInfoPreviewCharacters").item(0); 299 if (target.lettersDisplayed) 300 { 301 sample.style.display = "block"; 302 chars.style.display = "none"; 303 target.textContent = Locale.$STR("fontviewer.view characters"); 304 } 305 else 306 { 307 sample.style.display = "none"; 308 chars.style.display = "block"; 309 target.textContent = Locale.$STR("fontviewer.view sample"); 310 } 311 target.lettersDisplayed = !target.lettersDisplayed; 312 break; 313 } 314 }, 315 316 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 317 // Meta data 318 319 /** 320 * Checks, whether the font contains meta data 321 * 322 * @param fontObject: Font related information 323 * @return True, if font contains meta data, otherwise false 324 */ 325 noMetaData: function(fontObject) 326 { 327 return fontObject.metadata == ""; 328 }, 329 330 /** 331 * Selects the corresponding Domplate template related to a given meta data property 332 * 333 * @param prop: Meta data property 334 * @return Domplate template related to the property 335 */ 336 getTag: function(prop) 337 { 338 return prop.tag ? prop.tag : Firebug.FontViewerModel.Preview.textTag; 339 }, 340 341 /** 342 * Returns the translated property name 343 * 344 * @param prop: Meta data property 345 * @return Translated name of the property 346 */ 347 getPropName: function(prop) 348 { 349 return prop.name ? prop.name : Locale.$STR("fontviewer."+prop.node.nodeName); 350 }, 351 352 /** 353 * Returns the text of a meta data property or a string 354 * 355 * @param value: Meta data property or string 356 * @return String representing the text related to the property 357 */ 358 getText: function(value) 359 { 360 if (typeof(value) == "string") 361 return value; 362 363 var name = value.getAttribute("id") || value.getAttribute("name"); 364 if (name) 365 return name; 366 367 return value.textContent; 368 }, 369 370 /** 371 * Opens a URL in a new browser tab 372 * 373 * @param event: Click event 374 */ 375 onOpenUrl: function(event) 376 { 377 Win.openNewTab(event.target.getAttribute("href")); 378 Events.cancelEvent(event); 379 }, 380 381 /** 382 * Returns either text or a linked text depending on a URL to be present of a 383 * meta data property 384 * 385 * @param node: Meta data property node 386 * @return Text or linked text 387 */ 388 getLinkedTextTag: function(node) 389 { 390 if (this.getUrl(node)) 391 return Firebug.FontViewerModel.Preview.linkTag; 392 else 393 return Firebug.FontViewerModel.Preview.textTag; 394 }, 395 396 /** 397 * Returns the URL of a meta data property 398 * 399 * @param node: Meta data property node 400 * @return URL of the meta data property 401 */ 402 getUrl: function(node) 403 { 404 return node.getAttribute("url"); 405 }, 406 407 /** 408 * Returns the name of a link related to a meta data property 409 * 410 * @param node: Meta data property node 411 * @return Link name of the meta data property 412 */ 413 getLinkName: function(node) 414 { 415 return node.getAttribute("id") || node.getAttribute("name") || node.getAttribute("url"); 416 }, 417 418 /** 419 * Returns an array of font credits 420 * 421 * @param node: Meta data property node 422 * @return Credits 423 */ 424 getCredits: function(node) 425 { 426 return Array.prototype.slice.call(node.children); 427 }, 428 429 /** 430 * Returns the role of a contributor of the font 431 * 432 * @param node: Meta data property node 433 * @return Contributor role 434 */ 435 getRole: function(node) 436 { 437 var role = node.getAttribute("role"); 438 return role ? "("+role+")" : ""; 439 }, 440 441 /** 442 * Returns the available languages of a translated meta data property text 443 * 444 * @param node: Meta data property node 445 * @return Array of languages 446 */ 447 getLanguages: function(node) 448 { 449 var texts = Array.prototype.slice.call(node.getElementsByTagName("text")); 450 var langs = []; 451 var defaultLang = this.getDefaultLang(node); 452 texts.forEach(function(e, i, a) { 453 var lang = e.getAttribute("lang"); 454 langs.push({name: lang, selected: lang == defaultLang}); 455 }); 456 457 return langs; 458 }, 459 460 /** 461 * Returns the default language of a meta data property 462 * 463 * @param node: Meta data property node 464 * @return Language 465 */ 466 getDefaultLang: function(node) 467 { 468 var localeDomain = "general.useragent"; 469 var localeName = "locale"; 470 var localeValue = Options.getPref(localeDomain, localeName); 471 472 if (node.querySelector("text[lang="+localeValue+"]")) 473 return localeValue; 474 475 if (node.querySelector("text[lang=en]")) 476 return "en"; 477 478 if (node.firstElementChild) 479 return node.firstElementChild.getAttribute("lang"); 480 481 return null; 482 }, 483 484 /** 485 * Returns the translated text of meta data property 486 * 487 * @param node: Meta data property node 488 * @param lang: Language of the text 489 * @return Translated text 490 */ 491 getTranslatedText: function(node, lang) 492 { 493 if (!lang) 494 lang = this.getDefaultLang(node); 495 496 if (lang) 497 { 498 var element = node.querySelector("text[lang="+lang+"]"); 499 if (element) 500 return element.textContent; 501 } 502 503 return ""; 504 }, 505 506 /** 507 * Displays the XML source of the meta data 508 * 509 * @param fontInfo: Font related information 510 * @param source: XML source of the meta data 511 */ 512 insertMetaDataSource: function(fontInfo, source) 513 { 514 var tbody = fontInfo.getElementsByClassName("fontInfoMetaDataBody").item(0); 515 var node = this.sourceTag.replace({}, tbody); 516 var sourceNode = node.getElementsByClassName("source").item(0); 517 Str.insertWrappedText(source, sourceNode); 518 }, 519 520 /** 521 * Displays the meta data information formatted 522 * 523 * @param fontInfo: Font related information 524 * @param source: XML source of the meta data 525 */ 526 insertMetaDataFormatted: function(fontInfo, source) 527 { 528 var tbody = fontInfo.getElementsByClassName("fontInfoMetaDataBody").item(0); 529 var parser = Xpcom.CCIN("@mozilla.org/xmlextras/domparser;1", "nsIDOMParser"); 530 var doc = parser.parseFromString(source, "text/xml"); 531 var root = doc.documentElement; 532 533 if (FBTrace.DBG_FONTS) 534 FBTrace.sysout("fontviewer.insertMetaDataFormatted; XML", doc); 535 536 Dom.clearNode(tbody); 537 538 var props = []; 539 var propValueTemplates = { 540 vendor: Firebug.FontViewerModel.Preview.vendorTag, 541 credits: Firebug.FontViewerModel.Preview.creditsTag, 542 description: Firebug.FontViewerModel.Preview.translatedInfoTag, 543 copyright: Firebug.FontViewerModel.Preview.translatedInfoTag, 544 trademark: Firebug.FontViewerModel.Preview.translatedInfoTag, 545 license: Firebug.FontViewerModel.Preview.licenseTag 546 } 547 548 for (var i=0; i<root.children.length; i++) 549 { 550 var child = root.children[i]; 551 props.push({tag: propValueTemplates[child.nodeName], node: child}); 552 } 553 554 if (FBTrace.DBG_FONTS) 555 FBTrace.sysout("fontviewer.insertMetaDataFormatted; props", props); 556 557 tbody.repObject = root; 558 Firebug.FontViewerModel.Preview.propDataTag.insertRows({props: props}, tbody); 559 }, 560 561 /** 562 * Handles text language changes 563 * 564 * @param event: Click event 565 */ 566 onTranslatedLangChange: function(event) 567 { 568 var target = event.target; 569 var translatedInfo = Dom.getAncestorByClass(target, "fontInfoTranslatedInfo"); 570 var selected = translatedInfo.getElementsByClassName("selected").item(0); 571 Css.removeClass(selected, "selected"); 572 Css.setClass(target, "selected"); 573 574 var content = translatedInfo.getElementsByClassName("fontInfoTranslatedContent").item(0); 575 content.textContent = this.getTranslatedText(Firebug.getRepObject(target), 576 target.textContent); 577 }, 578 579 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 580 // Preview 581 582 /** 583 * Returns the CSS font-face class name 584 * 585 * @param cssFamilyName: CSS name of the font family 586 * @return Name of the font-face class 587 */ 588 getFontFaceClass: function(cssFamilyName) 589 { 590 return "fontFacePreview"+cssFamilyName.replace(/[^a-z0-9_]/ig, ""); 591 }, 592 593 /** 594 * Returns the font size CSS 595 * 596 * @param size: Font size 597 * @return Font size CSS 598 */ 599 getFontSize: function(size) 600 { 601 return size+"pt"; 602 }, 603 604 /** 605 * Returns the characters used for the font preview 606 * 607 * @param charType: Type of characters to return 608 * @return Preview characters 609 */ 610 getCharacters: function(charType) 611 { 612 switch (charType) 613 { 614 case "lowercase": 615 return "abcdefghijklmnopqrstuvwxyz"; 616 617 case "uppercase": 618 return "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 619 620 case "numbersAndSpecialChars": 621 return "0123456789.:,;(\"*!?')"; 622 } 623 }, 624 625 /** 626 * Returns the CSS for the @font-face CSS 627 * 628 * @param fontObject: Font related information 629 * @return @font-face CSS 630 */ 631 getFontFaceCss: function(fontObject) 632 { 633 var fontFaceClass = this.getFontFaceClass(fontObject.CSSFamilyName); 634 return fontObject.rule.cssText.replace(/url\(.*?\)/g, "url("+fontObject.URI+")")+ 635 " ."+fontFaceClass+" {font-family: \""+fontObject.CSSFamilyName+"\";}"; 636 }, 637 638 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 639 640 /** 641 * Displays general information about the font 642 * 643 * @param body: Node containing the font information display 644 * @param fontObject: Font related information 645 */ 646 insertGeneralInfo: function(body, fontObject) 647 { 648 649 var fontInfo = body.getElementsByClassName("fontInfo").item(0); 650 var tbody = fontInfo.getElementsByClassName("fontInfoGeneralInfoBody").item(0); 651 652 Dom.clearNode(tbody); 653 654 if (fontObject) 655 { 656 var props = 657 [ 658 {name: Locale.$STR("fontviewer.Name"), node: fontObject.name}, 659 {name: Locale.$STR("fontviewer.CSS Family Name"), node: fontObject.CSSFamilyName}, 660 {name: Locale.$STR("fontviewer.Format"), node: fontObject.format} 661 ]; 662 Firebug.FontViewerModel.Preview.propDataTag.insertRows({props: props}, tbody); 663 } 664 }, 665 666 /** 667 * Renders the font display 668 * 669 * @param body: Node containing the font information display 670 * @param file: Font file to be displayed 671 * @param context: Related context 672 */ 673 render: function(body, file, context) 674 { 675 var fontObject = file.fontObject; 676 if (!fontObject) 677 return; 678 679 var styles = [10, 14, 18]; 680 var charTypes = ["lowercase", "uppercase", "numbersAndSpecialChars"]; 681 682 var node = this.bodyTag.replace({fontObject: fontObject, styles: styles, 683 charTypes: charTypes}, body, this); 684 685 var styleNode = node.getElementsByClassName("fontInfoPreviewStyle").item(0); 686 styleNode.innerHTML = this.getFontFaceCss(fontObject); 687 688 this.insertGeneralInfo(body, file.fontObject); 689 690 if (fontObject.metadata != "") 691 this.insertMetaDataFormatted(body, fontObject.metadata); 692 } 693 })}; 694 695 // ********************************************************************************************* // 696 // Registration 697 698 Firebug.registerModule(Firebug.FontViewerModel); 699 700 return Firebug.FontViewerModel; 701 702 // ********************************************************************************************* // 703 }); 704